3
0
Fork 0

feat: add WuttaConfigProfile base class

convenience for use with various configurable features/services etc.
This commit is contained in:
Lance Edgar 2025-08-08 22:41:33 -05:00
parent a721e63275
commit ad6a3377ab
2 changed files with 105 additions and 1 deletions

View file

@ -1013,3 +1013,89 @@ def make_config(
extension.startup(config) extension.startup(config)
return config return config
class WuttaConfigProfile:
"""
Base class to represent a configured "profile" in the context of
some service etc.
:param config: App :term:`config object`.
:param key: Config key for the profile.
Generally each subclass will represent a certain type of config
profile, and each instance will represent a single profile
(identified by the ``key``).
"""
def __init__(self, config, key):
self.config = config
self.app = self.config.get_app()
self.key = key
self.load()
@property
def section(self):
"""
The primary config section under which profiles may be
defined.
There is no default; each subclass must declare it.
This corresponds to the typical INI file section, for instance
a section of ``wutta.telemetry`` assumes file contents like:
.. code-block:: ini
[wutta.telemetry]
default.submit_url = /nodes/telemetry
special.submit_url = /nodes/telemetry-special
"""
raise NotImplementedError
def load(self):
"""
Read all relevant settings from config, and assign attributes
on the profile instance accordingly.
There is no default logic but subclass will generally override.
While a caller can use :meth:`get_str()` to obtain arbitrary
config values dynamically, it is often useful for the profile
to pre-load some config values. This allows "smarter"
interpretation of config values in some cases, and at least
ensures common/shared logic.
There is no constraint or other guidance in terms of which
profile attributes might be set by this method. Subclass
should document if necessary.
"""
def get_str(self, option, **kwargs):
"""
Get a string value for the profile, from config.
:param option: Name of config option for which to return value.
This just calls :meth:`~WuttaConfig.get()` on the config
object, but for a particular setting name which it composes
dynamically.
Assuming a config file like:
.. code-block:: ini
[wutta.telemetry]
default.submit_url = /nodes/telemetry
Then a ``default`` profile under the ``wutta.telemetry``
section would effectively have a ``submit_url`` option::
class TelemetryProfile(WuttaConfigProfile):
section = "wutta.telemetry"
profile = TelemetryProfile("default")
url = profile.get_str("submit_url")
"""
return self.config.get(f'{self.section}.{self.key}.{option}', **kwargs)

View file

@ -12,7 +12,7 @@ from wuttjamaican import conf as mod
from wuttjamaican import conf from wuttjamaican import conf
from wuttjamaican.exc import ConfigurationError from wuttjamaican.exc import ConfigurationError
from wuttjamaican.app import AppHandler from wuttjamaican.app import AppHandler
from wuttjamaican.testing import FileTestCase from wuttjamaican.testing import FileTestCase, ConfigTestCase
class TestWuttaConfig(FileTestCase): class TestWuttaConfig(FileTestCase):
@ -867,3 +867,21 @@ class TestMakeConfig(FileTestCase):
foo_cls.assert_called_once_with() foo_cls.assert_called_once_with()
foo_obj.configure.assert_called_once_with(testconfig) foo_obj.configure.assert_called_once_with(testconfig)
foo_obj.startup.assert_called_once_with(testconfig) foo_obj.startup.assert_called_once_with(testconfig)
class TestWuttaConfigProfile(ConfigTestCase):
def make_profile(self, key):
return mod.WuttaConfigProfile(self.config, key)
def test_section(self):
profile = self.make_profile('default')
self.assertRaises(NotImplementedError, getattr, profile, 'section')
def test_get_str(self):
self.config.setdefault('wutta.telemetry.default.submit_url', '/nodes/telemetry')
with patch.object(mod.WuttaConfigProfile, 'section', new='wutta.telemetry'):
profile = self.make_profile('default')
self.assertEqual(profile.section, 'wutta.telemetry')
url = profile.get_str('submit_url')
self.assertEqual(url, '/nodes/telemetry')