def apply_config_changes(before_editing: str, data: Dict[str, Any], kvpairs: List[str]) -> Tuple[str, Dict[str, Any]]:
    """Apply config changes specified as a list of key-value pairs.

    Keys are interpreted as dotted paths into the configuration data structure. Except for paths beginning with
    ``postgresql.parameters`` where rest of the path is used directly to allow for PostgreSQL GUCs containing dots.
    Values are interpreted as YAML values.

    :param before_editing: human representation before editing.
    :param data: configuration data structure.
    :param kvpairs: list of strings containing key value pairs separated by ``=``.

    :returns: tuple of human-readable, parsed data structure after changes.

    :raises:
        :class:`PatroniCtlException`: if any entry in *kvpairs* is ``None`` or not in the expected format.
    """
    changed_data = copy.deepcopy(data)

    def set_path_value(config: Dict[str, Any], path: List[str], value: Any, prefix: Tuple[str, ...] = ()) -> None:
        """Recursively walk through *config* and update setting specified by *path* with *value*.

        :param config: configuration data structure with all settings found under *prefix* path.
        :param path: dotted path split by dot as delimiter into a list. Used to control the recursive calls and identify
            when a leaf node is reached.
        :param value: value for configuration described by *path*. If ``None`` the configuration key is removed from
            *config*.
        :param prefix: previous parts of *path* that have already been opened by parent recursive calls. Used to know
            if we are changing a Postgres related setting or not. *prefix* plus *path* compose the original *path* given
            on the root call.
        """
        # Postgresql GUCs can't be nested, but can contain dots so we re-flatten the structure for this case
        if prefix == ('postgresql', 'parameters'):
            path = ['.'.join(path)]

        key = path[0]
        # When *path* contains a single item it means we reached a leaf node in the configuration, so we can remove or
        # update the configuration based on what has been requested by the user.
        if len(path) == 1:
            if value is None:
                config.pop(key, None)
            else:
                config[key] = value
        # Otherwise we need to keep navigating down in the configuration structure.
        else:
            if not isinstance(config.get(key), dict):
                config[key] = {}
            set_path_value(config[key], path[1:], value, prefix + (key,))
            if config[key] == {}:
                del config[key]

    for pair in kvpairs:
        if not pair or "=" not in pair:
            raise PatroniCtlException("Invalid parameter setting {0}".format(pair))
        key_path, value = pair.split("=", 1)
        set_path_value(changed_data, key_path.strip().split("."), yaml.safe_load(value))

    return format_config_for_editing(changed_data), changed_data
Apply config changes specified as a list of key-value pairs.

    Keys are interpreted as dotted paths into the configuration data structure. Except for paths beginning with
    ``postgresql.parameters`` where rest of the path is used directly to allow for PostgreSQL GUCs containing dots.
    Values are interpreted as YAML values.

    :param before_editing: human representation before editing.
    :param data: configuration data structure.
    :param kvpairs: list of strings containing key value pairs separated by ``=``.

    :returns: tuple of human-readable, parsed data structure after changes.

    :raises:
        :class:`PatroniCtlException`: if any entry in *kvpairs* is ``None`` or not in the expected format.
    
patroni/ctl.py