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}