Skip to content

Commit 4ec4039

Browse files
committed
Add the require_complete_init flag to control error handling during getting settings on application initialization
1 parent 0dae305 commit 4ec4039

3 files changed

Lines changed: 78 additions & 10 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "runtime-config-py"
3-
version = "0.0.7"
3+
version = "0.0.8"
44
description = "Library for runtime updating project settings."
55
license = "MIT"
66
authors = ["Aleksey Petrunnik <petrunnik.a@gmail.com>"]

src/runtime_config/runtime_config.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,36 @@
2222

2323

2424
class RuntimeConfig:
25-
def __init__(self, init_settings: SettingsType, source: BaseSource, refresh_interval: float) -> None:
25+
def __init__(
26+
self,
27+
init_settings: SettingsType,
28+
source: BaseSource,
29+
refresh_interval: float,
30+
require_complete_init: bool = True,
31+
) -> None:
2632
self._init_settings: SettingsType = copy.deepcopy(init_settings)
27-
self._settings: SettingsType = {}
33+
self._settings: SettingsType = copy.deepcopy(init_settings)
34+
self._initialized = False
2835

2936
self._source = source
3037
self._settings_merger = SettingsMerger(init_settings=init_settings)
3138
self._periodic_refresh_task: asyncio.Task[None] = periodic_task(self.refresh, callback_time=refresh_interval)
39+
self._require_complete_init = require_complete_init
3240

3341
@staticmethod
3442
async def create(
3543
init_settings: t.Dict[str, t.Any],
3644
source: BaseSource | None = None,
3745
refresh_interval: float = 10,
46+
require_complete_init: bool = True,
3847
) -> RuntimeConfig:
3948
"""
4049
Creates and initializes an instance of the class. You should always use this method to instantiate a class.
4150
:param init_settings: dictionary with default settings that you can then override.
4251
:param source: the source from which the actual values of the variables will be retrieved.
4352
:param refresh_interval: the frequency with which updates will be requested from the source.
53+
:param require_complete_init: if set to true, exceptions that occur during the first time settings are
54+
received from an external source will not be caught
4455
:return: initialized class instance.
4556
"""
4657
if 'inst' in _instance:
@@ -56,24 +67,37 @@ async def create(
5667
)
5768
source = sources.ConfigServerSrc(host=host, service_name=service_name)
5869

59-
inst = RuntimeConfig(init_settings=init_settings, source=source, refresh_interval=refresh_interval)
70+
inst = RuntimeConfig(
71+
init_settings=init_settings,
72+
source=source,
73+
refresh_interval=refresh_interval,
74+
require_complete_init=require_complete_init,
75+
)
6076
_instance['inst'] = inst
6177
await inst.refresh()
78+
inst._initialized = True
6279
return inst
6380

6481
async def refresh(self) -> None:
82+
def _check_inst_initialization(inst: RuntimeConfig, exception: Exception) -> None:
83+
if not inst._initialized and inst._require_complete_init:
84+
raise exception
85+
6586
extracted_settings = []
6687
try:
6788
extracted_settings = await self._source.get_settings()
6889
except ValidationError as exc:
69-
logger.error(str(exc), exc_info=True)
70-
except Exception:
71-
logger.error('An unexpected error occurred while fetching new settings from the server', exc_info=True)
90+
logger.error("Fetched not valid data from remote source", exc_info=True)
91+
_check_inst_initialization(self, exc)
92+
except Exception as exc:
93+
logger.error('Fetching new settings from a remote source failed', exc_info=True)
94+
_check_inst_initialization(self, exc)
7295

7396
try:
7497
self._settings = await self._settings_merger.merge(extracted_settings=extracted_settings)
75-
except Exception:
76-
logger.error('An unexpected error occurred while merge settings', exc_info=True)
98+
except Exception as exc:
99+
logger.error('Merge settings error', exc_info=True)
100+
_check_inst_initialization(self, exc)
77101

78102
def get(self, setting_name: str, default: t.Any = None) -> t.Any:
79103
return self._settings.get(setting_name, default)

tests/test_runtime_config.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,23 @@ async def test_refresh__failed_to_get_settings_from_server__config_is_initialize
211211
source_mock.get_settings.side_effect = exception
212212

213213
# act
214-
inst = await RuntimeConfig.create(init_settings=init_settings, source=source_mock)
214+
inst = await RuntimeConfig.create(init_settings=init_settings, source=source_mock, require_complete_init=False)
215215

216216
# assert
217217
assert inst._settings == init_settings
218218

219+
@pytest.mark.parametrize('exception', [ValidationError, Exception])
220+
async def test_refresh__failed_to_get_settings_from_server__raise_exc(
221+
self, mocker: MockerFixture, init_settings, source_mock, exception
222+
):
223+
# arrange
224+
mocker.patch.dict(_instance, clear=True)
225+
source_mock.get_settings.side_effect = exception
226+
227+
# act && assert
228+
with pytest.raises(exception):
229+
await RuntimeConfig.create(init_settings=init_settings, source=source_mock)
230+
219231
async def test_refresh__setting_value_cannot_be_converted_to_required_type__invalid_settings_skipped(
220232
self, mocker: MockerFixture, init_settings, source_mock
221233
):
@@ -233,6 +245,38 @@ async def test_refresh__setting_value_cannot_be_converted_to_required_type__inva
233245
# assert
234246
assert inst._settings == init_settings
235247

248+
async def test_refresh__init_instance_successful_init_required_merge_error__raise_exc(
249+
self, mocker: MockerFixture, init_settings, source_mock
250+
):
251+
# arrange
252+
expected_exception = Exception
253+
mocker.patch.dict(_instance, clear=True)
254+
source_mock.get_settings.return_value = [
255+
Setting(name='some_var', value=1, value_type=SettingValueType.int, disable=False)
256+
]
257+
mocker.patch('runtime_config.runtime_config.SettingsMerger._get_inner_dict', side_effect=expected_exception)
258+
259+
# act && assert
260+
with pytest.raises(expected_exception):
261+
await RuntimeConfig.create(init_settings=init_settings, source=source_mock)
262+
263+
async def test_refresh__init_instance_successful_init_not_required_merge_error__use_default_settings(
264+
self, mocker: MockerFixture, init_settings, source_mock
265+
):
266+
# arrange
267+
expected_exception = Exception
268+
mocker.patch.dict(_instance, clear=True)
269+
source_mock.get_settings.return_value = [
270+
Setting(name='some_var', value=1, value_type=SettingValueType.int, disable=False)
271+
]
272+
mocker.patch('runtime_config.runtime_config.SettingsMerger._get_inner_dict', side_effect=expected_exception)
273+
274+
# act
275+
inst = await RuntimeConfig.create(init_settings=init_settings, source=source_mock, require_complete_init=False)
276+
277+
# assert
278+
assert inst._settings == init_settings
279+
236280
async def test_refresh__unexpected_error_during_merge__previous_settings_are_not_changed(
237281
self, mocker: MockerFixture, init_settings, source_mock
238282
):

0 commit comments

Comments
 (0)