-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add app configuration capabilities (#17)
- Loading branch information
1 parent
1218d98
commit 20b01bc
Showing
13 changed files
with
247 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -305,6 +305,9 @@ dist/* | |
.env | ||
.envs/* | ||
!.envs/.local/ | ||
config.yaml | ||
local.sh | ||
logs/* | ||
!logs/.gitkeep | ||
secrets/* | ||
cloud_sql_proxy |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from typing import Optional | ||
|
||
|
||
class IDRClientException(Exception): | ||
"""Base exception for most explicit exceptions raised by this app.""" | ||
|
||
def __init__(self, message: Optional[str] = None, *args): | ||
"""Initialize an ``IDRClientException`` with the given parameters. | ||
:param message: An optional error message. | ||
:param args: args to pass to forward to the base exception. | ||
""" | ||
self._message: Optional[str] = message | ||
super(IDRClientException, self).__init__(self._message, *args) | ||
|
||
@property | ||
def message(self) -> Optional[str]: | ||
""" | ||
Return the error message passed to this exception at initialization | ||
or ``None`` if one was not given. | ||
:return: The error message passed to this exception at initialization | ||
or None if one wasn't given. | ||
""" | ||
return self._message |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,11 @@ | ||
from .config import Config, SettingInitializer | ||
from .config import * # noqa: F401,F403 | ||
from .config import __all__ as _all_config | ||
from .tasks import * # noqa: F401,F403 | ||
from .tasks import __all__ as _all_tasks | ||
from .transports import * # noqa: F401,F403 | ||
from .transports import __all__ as _all_transports | ||
|
||
__all__ = ["Config", "SettingInitializer"] | ||
|
||
__all__ = [] | ||
__all__ += _all_config # type: ignore | ||
__all__ += _all_tasks # type: ignore | ||
__all__ += _all_transports # type: ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .config import Config, SettingInitializer | ||
from .exceptions import MissingSettingError | ||
|
||
__all__ = ["Config", "MissingSettingError", "SettingInitializer"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from typing import Optional | ||
|
||
from app.core import IDRClientException | ||
|
||
|
||
class MissingSettingError(IDRClientException, LookupError): | ||
"""Non existing setting access error.""" | ||
|
||
def __init__(self, setting: str, message: Optional[str] = None): | ||
"""Initialize a ``MissingSettingError`` with the given properties. | ||
:param setting: The missing setting. | ||
:param message: An optional message for the resulting exception. If | ||
none is provided, then a generic one is automatically generated. | ||
""" | ||
self._setting: str = setting | ||
self._message: str = ( | ||
message or 'Setting "%s" does not exist.' % self._setting | ||
) | ||
IDRClientException.__init__(self, message=self._message) | ||
|
||
@property | ||
def setting(self) -> str: | ||
""" | ||
Return the missing setting that resulted in this exception being | ||
raised. | ||
:return: The missing setting. | ||
""" | ||
return self._setting |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
from typing import Any, Mapping | ||
from unittest import TestCase | ||
|
||
import pytest | ||
|
||
from app.core import Task | ||
from app.lib import Config, MissingSettingError, SettingInitializer | ||
|
||
# ============================================================================= | ||
# HELPERS | ||
# ============================================================================= | ||
|
||
|
||
class _SettingToStringInitializer(Task[Any, str]): | ||
""" | ||
A simple initializer that takes a setting raw value and returns it's string | ||
representation. | ||
""" | ||
|
||
def execute(self, an_input: Any) -> str: | ||
return str(an_input) | ||
|
||
|
||
class _AddOneInitializer(Task[int, int]): | ||
""" | ||
A simple initializer that takes an integer setting value and returns the | ||
value + 1 or 0 if the the value is None. | ||
""" | ||
|
||
def execute(self, an_input: int) -> int: | ||
return an_input + 1 if an_input is not None else 0 | ||
|
||
|
||
# ============================================================================= | ||
# TEST CASES | ||
# ============================================================================= | ||
|
||
|
||
class TestConfig(TestCase): | ||
"""Tests for the :class:`Config` class.""" | ||
|
||
def setUp(self) -> None: | ||
super().setUp() | ||
# Settings | ||
self._setting_1_value: int = 0 | ||
self._setting_2_value: int = 0 | ||
self._settings: Mapping[str, Any] = { | ||
"SETTING_1": self._setting_1_value, | ||
"SETTING_2": self._setting_2_value, | ||
} | ||
|
||
# Setting Initializers | ||
self._setting_1_initializer = _SettingToStringInitializer() | ||
self._setting_2_initializer = _AddOneInitializer() | ||
self._initializers: Mapping[str, SettingInitializer] = { | ||
"SETTING_1": self._setting_1_initializer, | ||
"SETTING_2": self._setting_2_initializer, | ||
} | ||
|
||
def test_initializers_are_run(self) -> None: | ||
""" | ||
Assert that initializers are run and the settings value are updated. | ||
""" | ||
config = Config( | ||
settings=self._settings, settings_initializers=self._initializers | ||
) | ||
|
||
assert config.SETTING_1 == "0" | ||
assert config.SETTING_2 == 1 | ||
|
||
def test_initializers_can_set_default_values(self) -> None: | ||
""" | ||
Assert that when a setting is not provided but an initializer for the | ||
setting exists, the value returned after running the initializer | ||
becomes the new value of the setting. | ||
""" | ||
self._settings = {"SETTING_1": self._setting_1_value} | ||
config = Config( | ||
settings=self._settings, settings_initializers=self._initializers | ||
) | ||
|
||
assert config.SETTING_1 == "0" | ||
assert config.SETTING_2 == 0 # SETTING_2 now has a default | ||
|
||
def test_missing_settings_retrieval_using_dot_notation(self) -> None: | ||
""" | ||
Assert that retrieval of missing settings using the dot notation | ||
results in a :class:`MissingSettingError` being raised. | ||
""" | ||
config = Config(settings={}) | ||
with pytest.raises(MissingSettingError) as exp: | ||
_: Any = config.INVALID_SETTING | ||
|
||
assert exp.value.setting == "INVALID_SETTING" | ||
|
||
def test_settings_retrieval_using_get_method(self) -> None: | ||
""" | ||
Assert that retrieval of settings using the method returns the | ||
expected setting or the default value when the setting is missing. | ||
""" | ||
self._settings = { | ||
**self._settings, | ||
"weird::setting::name": "some value", | ||
} | ||
config = Config( | ||
settings=self._settings, settings_initializers=self._initializers | ||
) | ||
|
||
assert config.get("SETTING_1") == "0" | ||
assert config.get("weird::setting::name") == "some value" | ||
assert config.get("MISSING", default="default") == "default" | ||
assert config.get("MISSING") is None | ||
|
||
def test_settings_without_initializers_are_not_modified(self) -> None: | ||
""" | ||
Assert that settings without initializers are not modified when | ||
initializers run and are returned as is. | ||
""" | ||
setting_3_value: str = "Do not modify!" | ||
self._settings = {**self._settings, "SETTING_3": setting_3_value} | ||
config = Config( | ||
settings=self._settings, settings_initializers=self._initializers | ||
) | ||
|
||
assert config.SETTING_3 == setting_3_value |