102 timestamp: datetime.datetime
103 commited_entries: typing.List[_ChangelogEntry]
104 staged_entry: _ChangelogEntry
108 0, datetime.timezone.utc,
115 def service_timestamp(self) -> str:
118 def next_timestamp(self) -> str:
123 """Commit staged changed if any and return last commited entry."""
132 def get_updated_since(
136 ids: typing.Optional[typing.List[str]] =
None,
141 values = {name: values[name]
for name
in ids
if name
in values}
142 removed = [name
for name
in removed
if name
in ids]
144 timestamp=entry.timestamp, values=values, removed=removed,
147 def _get_updated_since(
148 self, values: ConfigDict, updated_since: str,
149 ) -> typing.Tuple[ConfigDict, typing.List[str]]:
150 if not updated_since:
153 last_known_state = {}
155 if entry.timestamp > updated_since:
156 dirty_keys.update(entry.dirty_keys)
158 if entry.timestamp == updated_since:
159 last_known_state = entry.state
164 for key
in dirty_keys:
165 value = values.get(key, RemoveKey)
166 if last_known_state.get(key, Missing) != value:
167 if value
is RemoveKey:
171 return result, removed
173 def add_entries(self, values: ConfigDict):
176 @contextlib.contextmanager
177 def rollback(self, defaults: ConfigDict):
183 def _do_rollback(self, defaults: ConfigDict):
189 maybe_dirty.update(entry.dirty_keys)
192 last_state = last.state
195 for key
in maybe_dirty:
196 original = defaults.get(key, RemoveKey)
197 if last_state[key] != original:
199 reverted[key] = original
202 timestamp=last.timestamp,
204 dirty_keys=dirty_keys,
210 dirty_keys=dirty_keys.copy(),
212 prev_state=entry.state,
217 """Simple dynamic config backend."""
222 initial_values: ConfigDict,
223 config_cache_components: typing.Iterable[str],
225 changelog: _Changelog,
227 self.
_values = initial_values.copy()
232 def set_values(self, values: ConfigDict):
235 def set_values_unsafe(self, values: ConfigDict):
240 def set(self, **values):
243 def get_values_unsafe(self) -> ConfigDict:
246 def get(self, key: str, default: typing.Any =
None) -> typing.Any:
248 if default
is not None:
251 return copy.deepcopy(self.
_values[key])
253 def remove_values(self, keys):
254 extra_keys = set(keys).difference(self.
_values.keys())
257 f
'Attempting to remove nonexistent configs: {extra_keys}',
261 self.
_changelog.add_entries({key: RemoveKey
for key
in keys})
264 def remove(self, key):
267 @contextlib.contextmanager
268 def modify(self, key: str) -> typing.Any:
269 value = self.
get(key)
273 @contextlib.contextmanager
275 self, *keys: typing.Tuple[str, ...],
276 ) -> typing.Tuple[typing.Any, ...]:
277 values = tuple(self.
get(key)
for key
in keys)
281 def _sync_with_service(self):
292 cache_invalidation_state,
293 _config_service_defaults_updated,
294 dynamic_config_changelog,
295 _dynconfig_load_json_cached,
299 Fixture that allows to control dynamic config values used by the service.
301 After change to the config, be sure to call:
303 await service_client.update_server_state()
306 HTTP client requests call it automatically before each request.
308 @ingroup userver_testsuite_fixtures
311 initial_values=_config_service_defaults_updated.snapshot,
312 config_cache_components=dynconf_cache_names,
313 cache_invalidation_state=cache_invalidation_state,
314 changelog=dynamic_config_changelog,
317 with dynamic_config_changelog.rollback(
318 _config_service_defaults_updated.snapshot,
320 for path
in reversed(list(search_path(
'config.json'))):
321 values = _dynconfig_load_json_cached(path)
322 updates.update(values)
323 for marker
in request.node.iter_markers(
'config'):
324 marker_json = object_substitute(marker.kwargs)
325 updates.update(marker_json)
326 config.set_values_unsafe(updates)
330@pytest.fixture(scope='session')
377 config_fallback_path, dynamic_config_fallback_patch,
380 Fixture that returns default values for dynamic config. You may override
381 it in your local conftest.py or fixture:
384 @pytest.fixture(scope='session')
385 def config_service_defaults():
386 with open('defaults.json') as fp:
390 @ingroup userver_testsuite_fixtures
392 if not config_fallback_path:
393 return dynamic_config_fallback_patch
395 if pathlib.Path(config_fallback_path).exists():
396 with open(config_fallback_path,
'r', encoding=
'utf-8')
as file:
397 fallback = json.load(file)
398 fallback.update(dynamic_config_fallback_patch)
402 'Either provide the path to dynamic config defaults file using '
403 '--config-fallback pytest option, or override '
404 f
'{config_service_defaults.__name__} fixture to provide custom '
405 'dynamic config loading behavior.',
409@dataclasses.dataclass(frozen=False)
413 async def update(self, client, dynamic_config) -> None:
415 values = await client.get_dynamic_config_defaults()
416 if not isinstance(values, dict):
419 values.update(dynamic_config.get_values_unsafe())
427@pytest.fixture(scope='package')
454 Returns a function that adjusts the static configuration file for
456 Sets `dynamic-config.defaults-path` according to `config_service_defaults`.
457 Updates `dynamic-config.defaults` with `config_service_defaults`.
459 @ingroup userver_testsuite_fixtures
462 def extract_defaults_dict(component_config, config_vars) -> dict:
463 defaults_field = component_config.get(
'defaults',
None)
or {}
464 if isinstance(defaults_field, dict):
465 return defaults_field
466 elif isinstance(defaults_field, str):
467 if defaults_field.startswith(
'$'):
468 return config_vars.get(defaults_field[1:], {})
470 f
'Unexpected static config option '
471 f
'`dynamic-config.defaults`: {defaults_field!r}'
474 def _patch_config(config_yaml, config_vars):
475 components = config_yaml[
'components_manager'][
'components']
476 if components.get(
'dynamic-config',
None)
is None:
477 components[
'dynamic-config'] = {}
478 dynconf_component = components[
'dynamic-config']
480 dynconf_component.pop(
'defaults-path',
None)
482 extract_defaults_dict(dynconf_component, config_vars),
483 **config_service_defaults,
485 dynconf_component[
'defaults'] = defaults
490@pytest.fixture(scope='session')
523 dynamic_config: DynamicConfig,
524 dynamic_config_changelog: _Changelog,
527 Adds a mockserver handler that forwards dynamic_config to service's
528 `dynamic-config-client` component.
530 @ingroup userver_testsuite_fixtures
533 @mockserver.json_handler('/configs-service/configs/values')
534 def _mock_configs(request):
535 updates = dynamic_config_changelog.get_updated_since(
536 dynamic_config.get_values_unsafe(),
537 request.json.get(
'updated_since',
''),
538 request.json.get(
'ids'),
540 response = {
'configs': updates.values,
'updated_at': updates.timestamp}
542 response[
'removed'] = updates.removed
545 @mockserver.json_handler('/configs-service/configs/status')
546 def _mock_configs_status(_request):
547 return {
'updated_at': dynamic_config_changelog.timestamp}