config_ninja.controller
Define a controller class for operating on a config_ninja.settings.ObjectSpec.
1"""Define a controller class for operating on a `config_ninja.settings.ObjectSpec`.""" 2 3from __future__ import annotations 4 5import logging 6import typing 7from typing import TypeAlias 8 9import jinja2 10 11from config_ninja import settings, systemd 12from config_ninja.backend import FormatT, dumps, loads 13from config_ninja.settings import ObjectSpec 14 15__all__ = ['BackendController', 'ErrorHandler'] 16 17logger = logging.getLogger(__name__) 18 19ActionType: TypeAlias = typing.Callable[[str], typing.Any] 20ErrorHandler: TypeAlias = typing.Callable[[dict[typing.Any, typing.Any]], typing.ContextManager[None]] 21 22 23class BackendController: 24 """Define logic for operating a configuration `ObjectSpec`.""" 25 26 key: str 27 """The key of the configuration object in the settings file.""" 28 29 logger: logging.Logger 30 """Each `BackendController` has its own logger (named `"config_ninja.controller:{key}"`).""" 31 32 spec: ObjectSpec 33 """Full specification for the configuration object.""" 34 35 def __init__(self, spec: ObjectSpec, key: str) -> None: 36 """Ensure the parent directory of the destination path exists; initialize a logger.""" 37 self.logger = logging.getLogger(f'{__name__}:{key}') 38 self.key = key 39 self.spec = spec 40 41 spec.dest.path.parent.mkdir(parents=True, exist_ok=True) 42 43 def __str__(self) -> str: 44 """Represent the controller as its backend populating its destination.""" 45 return f'{self.spec.source.backend} ({self.spec.source.format}) -> {self.spec.dest}' 46 47 def _do(self, action: ActionType, data: dict[str, typing.Any]) -> None: 48 if self.spec.dest.is_template: 49 assert isinstance(self.spec.dest.format, jinja2.Template) # noqa: S101 # 👈 for static analysis 50 action(self.spec.dest.format.render(data)) 51 else: 52 fmt: FormatT = self.spec.dest.format # type: ignore[assignment] 53 action(dumps(fmt, data)) 54 55 @classmethod 56 def from_settings( 57 cls, conf_settings: settings.Config, key: str, handle_key_errors: ErrorHandler 58 ) -> BackendController: 59 """Create a `BackendController` instance from the given settings.""" 60 cfg_obj = conf_settings.settings.OBJECTS[key] 61 with handle_key_errors(cfg_obj): # type: ignore[arg-type] 62 spec = ObjectSpec.from_primitives(cfg_obj, conf_settings.engine) 63 return cls(spec, key) 64 65 def get(self, do_print: typing.Callable[[str], typing.Any]) -> None: 66 """Retrieve and print the value of the configuration object. 67 68 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 69 and `BackendController.awrite()` methods. 70 """ 71 data = loads(self.spec.source.format, self.spec.source.backend.get()) 72 self._do(do_print, data) 73 for hook in self.spec.hooks: 74 self.logger.debug('would execute: %s', hook) 75 76 async def aget(self, do_print: typing.Callable[[str], typing.Any]) -> None: 77 """Poll to retrieve the latest configuration object, and print on each update. 78 79 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 80 and `BackendController.awrite()` methods. 81 """ 82 if systemd.AVAILABLE: # pragma: no cover 83 systemd.notify() 84 85 async for content in self.spec.source.backend.poll(): 86 data = loads(self.spec.source.format, content) 87 self._do(do_print, data) 88 for hook in self.spec.hooks: 89 self.logger.debug('would execute: %s', hook) 90 91 def write(self) -> None: 92 """Retrieve the latest value of the configuration object, and write to file. 93 94 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 95 after writing the configuration object to the destination file. 96 """ 97 data = loads(self.spec.source.format, self.spec.source.backend.get()) 98 self._do(self.spec.dest.path.write_text, data) 99 for hook in self.spec.hooks: 100 hook() 101 102 async def awrite(self) -> None: 103 """Poll to retrieve the latest configuration object, and write to file on each update. 104 105 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 106 after writing the configuration object to the destination file. 107 """ 108 if systemd.AVAILABLE: # pragma: no cover 109 systemd.notify() 110 111 async for content in self.spec.source.backend.poll(): 112 data = loads(self.spec.source.format, content) 113 self._do(self.spec.dest.path.write_text, data) 114 for hook in self.spec.hooks: 115 hook() 116 117 118logger.debug('successfully imported %s', __name__)
24class BackendController: 25 """Define logic for operating a configuration `ObjectSpec`.""" 26 27 key: str 28 """The key of the configuration object in the settings file.""" 29 30 logger: logging.Logger 31 """Each `BackendController` has its own logger (named `"config_ninja.controller:{key}"`).""" 32 33 spec: ObjectSpec 34 """Full specification for the configuration object.""" 35 36 def __init__(self, spec: ObjectSpec, key: str) -> None: 37 """Ensure the parent directory of the destination path exists; initialize a logger.""" 38 self.logger = logging.getLogger(f'{__name__}:{key}') 39 self.key = key 40 self.spec = spec 41 42 spec.dest.path.parent.mkdir(parents=True, exist_ok=True) 43 44 def __str__(self) -> str: 45 """Represent the controller as its backend populating its destination.""" 46 return f'{self.spec.source.backend} ({self.spec.source.format}) -> {self.spec.dest}' 47 48 def _do(self, action: ActionType, data: dict[str, typing.Any]) -> None: 49 if self.spec.dest.is_template: 50 assert isinstance(self.spec.dest.format, jinja2.Template) # noqa: S101 # 👈 for static analysis 51 action(self.spec.dest.format.render(data)) 52 else: 53 fmt: FormatT = self.spec.dest.format # type: ignore[assignment] 54 action(dumps(fmt, data)) 55 56 @classmethod 57 def from_settings( 58 cls, conf_settings: settings.Config, key: str, handle_key_errors: ErrorHandler 59 ) -> BackendController: 60 """Create a `BackendController` instance from the given settings.""" 61 cfg_obj = conf_settings.settings.OBJECTS[key] 62 with handle_key_errors(cfg_obj): # type: ignore[arg-type] 63 spec = ObjectSpec.from_primitives(cfg_obj, conf_settings.engine) 64 return cls(spec, key) 65 66 def get(self, do_print: typing.Callable[[str], typing.Any]) -> None: 67 """Retrieve and print the value of the configuration object. 68 69 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 70 and `BackendController.awrite()` methods. 71 """ 72 data = loads(self.spec.source.format, self.spec.source.backend.get()) 73 self._do(do_print, data) 74 for hook in self.spec.hooks: 75 self.logger.debug('would execute: %s', hook) 76 77 async def aget(self, do_print: typing.Callable[[str], typing.Any]) -> None: 78 """Poll to retrieve the latest configuration object, and print on each update. 79 80 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 81 and `BackendController.awrite()` methods. 82 """ 83 if systemd.AVAILABLE: # pragma: no cover 84 systemd.notify() 85 86 async for content in self.spec.source.backend.poll(): 87 data = loads(self.spec.source.format, content) 88 self._do(do_print, data) 89 for hook in self.spec.hooks: 90 self.logger.debug('would execute: %s', hook) 91 92 def write(self) -> None: 93 """Retrieve the latest value of the configuration object, and write to file. 94 95 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 96 after writing the configuration object to the destination file. 97 """ 98 data = loads(self.spec.source.format, self.spec.source.backend.get()) 99 self._do(self.spec.dest.path.write_text, data) 100 for hook in self.spec.hooks: 101 hook() 102 103 async def awrite(self) -> None: 104 """Poll to retrieve the latest configuration object, and write to file on each update. 105 106 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 107 after writing the configuration object to the destination file. 108 """ 109 if systemd.AVAILABLE: # pragma: no cover 110 systemd.notify() 111 112 async for content in self.spec.source.backend.poll(): 113 data = loads(self.spec.source.format, content) 114 self._do(self.spec.dest.path.write_text, data) 115 for hook in self.spec.hooks: 116 hook()
Define logic for operating a configuration ObjectSpec.
36 def __init__(self, spec: ObjectSpec, key: str) -> None: 37 """Ensure the parent directory of the destination path exists; initialize a logger.""" 38 self.logger = logging.getLogger(f'{__name__}:{key}') 39 self.key = key 40 self.spec = spec 41 42 spec.dest.path.parent.mkdir(parents=True, exist_ok=True)
Ensure the parent directory of the destination path exists; initialize a logger.
Each BackendController has its own logger (named "config_ninja.controller:{key}").
56 @classmethod 57 def from_settings( 58 cls, conf_settings: settings.Config, key: str, handle_key_errors: ErrorHandler 59 ) -> BackendController: 60 """Create a `BackendController` instance from the given settings.""" 61 cfg_obj = conf_settings.settings.OBJECTS[key] 62 with handle_key_errors(cfg_obj): # type: ignore[arg-type] 63 spec = ObjectSpec.from_primitives(cfg_obj, conf_settings.engine) 64 return cls(spec, key)
Create a BackendController instance from the given settings.
66 def get(self, do_print: typing.Callable[[str], typing.Any]) -> None: 67 """Retrieve and print the value of the configuration object. 68 69 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 70 and `BackendController.awrite()` methods. 71 """ 72 data = loads(self.spec.source.format, self.spec.source.backend.get()) 73 self._do(do_print, data) 74 for hook in self.spec.hooks: 75 self.logger.debug('would execute: %s', hook)
Retrieve and print the value of the configuration object.
Any config_ninja.settings.poe.Hooks will be skipped; they are only executed by the BackendController.write()
and BackendController.awrite() methods.
77 async def aget(self, do_print: typing.Callable[[str], typing.Any]) -> None: 78 """Poll to retrieve the latest configuration object, and print on each update. 79 80 Any `config_ninja.settings.poe.Hook`s will be skipped; they are only executed by the `BackendController.write()` 81 and `BackendController.awrite()` methods. 82 """ 83 if systemd.AVAILABLE: # pragma: no cover 84 systemd.notify() 85 86 async for content in self.spec.source.backend.poll(): 87 data = loads(self.spec.source.format, content) 88 self._do(do_print, data) 89 for hook in self.spec.hooks: 90 self.logger.debug('would execute: %s', hook)
Poll to retrieve the latest configuration object, and print on each update.
Any config_ninja.settings.poe.Hooks will be skipped; they are only executed by the BackendController.write()
and BackendController.awrite() methods.
92 def write(self) -> None: 93 """Retrieve the latest value of the configuration object, and write to file. 94 95 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 96 after writing the configuration object to the destination file. 97 """ 98 data = loads(self.spec.source.format, self.spec.source.backend.get()) 99 self._do(self.spec.dest.path.write_text, data) 100 for hook in self.spec.hooks: 101 hook()
Retrieve the latest value of the configuration object, and write to file.
If the config_ninja.settings.ObjectSpec provides any config_ninja.settings.poe.Hooks, they will be executed
after writing the configuration object to the destination file.
103 async def awrite(self) -> None: 104 """Poll to retrieve the latest configuration object, and write to file on each update. 105 106 If the `config_ninja.settings.ObjectSpec` provides any `config_ninja.settings.poe.Hook`s, they will be executed 107 after writing the configuration object to the destination file. 108 """ 109 if systemd.AVAILABLE: # pragma: no cover 110 systemd.notify() 111 112 async for content in self.spec.source.backend.poll(): 113 data = loads(self.spec.source.format, content) 114 self._do(self.spec.dest.path.write_text, data) 115 for hook in self.spec.hooks: 116 hook()
Poll to retrieve the latest configuration object, and write to file on each update.
If the config_ninja.settings.ObjectSpec provides any config_ninja.settings.poe.Hooks, they will be executed
after writing the configuration object to the destination file.