dynamic-config-loader 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dynamic_config_loader-2.3.0/src/dynamic_config_loader.egg-info → dynamic_config_loader-2.4.0}/PKG-INFO +12 -2
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/README.md +9 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/pyproject.toml +6 -1
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/__init__.py +5 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/__init__.py +34 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/merger.py +63 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/parser.py +62 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/__init__.py +34 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/merger.py +63 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/parser.py +48 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0/src/dynamic_config_loader.egg-info}/PKG-INFO +12 -2
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/SOURCES.txt +8 -0
- dynamic_config_loader-2.4.0/src/dynamic_config_loader.egg-info/requires.txt +4 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_factory.py +15 -1
- dynamic_config_loader-2.4.0/tests/test_ini_strategy.py +118 -0
- dynamic_config_loader-2.4.0/tests/test_toml_strategy.py +118 -0
- dynamic_config_loader-2.3.0/src/dynamic_config_loader.egg-info/requires.txt +0 -1
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/AUTHORS +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/LICENSE +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/NOTICE +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/setup.cfg +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/environment.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/loader.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/base_strategy.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/xml/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/xml/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/xml/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/dependency_links.txt +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/top_level.txt +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_deep_merge.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_env_injection.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_json_strategy.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_loader.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_logging_safety.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/tests/test_xml_strategy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamic-config-loader
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: An enterprise-grade configuration orchestration engine designed to ingest, cascade, and deeply merge multiple configuration file layers.
|
|
5
5
|
Author-email: Avinash Kumar <halfhiddencode@gmail.com>
|
|
6
6
|
License:
|
|
@@ -210,7 +210,7 @@ Project-URL: Repository, https://gitlab.com/halfhiddencode/python-dynamic-config
|
|
|
210
210
|
Project-URL: Issues, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/issues
|
|
211
211
|
Project-URL: Documentation, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/tree/main/docs
|
|
212
212
|
Project-URL: Changelog, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md
|
|
213
|
-
Keywords: config,configuration,dynamic-loader,cascading-config,deep-merge,strategy-pattern,yaml,json,devops
|
|
213
|
+
Keywords: config,configuration,dynamic-loader,cascading-config,deep-merge,strategy-pattern,yaml,json,xml,toml,ini,conf,devops
|
|
214
214
|
Classifier: Development Status :: 5 - Production/Stable
|
|
215
215
|
Classifier: Intended Audience :: Developers
|
|
216
216
|
Classifier: Intended Audience :: System Administrators
|
|
@@ -232,12 +232,22 @@ License-File: LICENSE
|
|
|
232
232
|
License-File: NOTICE
|
|
233
233
|
License-File: AUTHORS
|
|
234
234
|
Requires-Dist: pyyaml>=6.0.1
|
|
235
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
235
236
|
Dynamic: license-file
|
|
236
237
|
|
|
237
238
|
# Dynamic Configuration Loader
|
|
238
239
|
|
|
239
240
|
A lightweight, production-grade, strategy-driven configuration orchestration engine designed to dynamically discover, alphabetically sort, and recursively deep-merge multi-layered configuration files.
|
|
240
241
|
|
|
242
|
+
## Supported Formats
|
|
243
|
+
|
|
244
|
+
The orchestration engine natively parses and cascades the following configuration formats:
|
|
245
|
+
* **YAML** (`.yaml`, `.yml`)
|
|
246
|
+
* **JSON** (`.json`)
|
|
247
|
+
* **XML** (`.xml`)
|
|
248
|
+
* **TOML** (`.toml`)
|
|
249
|
+
* **INI / Conf** (`.ini`, `.conf`)
|
|
250
|
+
|
|
241
251
|
---
|
|
242
252
|
|
|
243
253
|
```text
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, production-grade, strategy-driven configuration orchestration engine designed to dynamically discover, alphabetically sort, and recursively deep-merge multi-layered configuration files.
|
|
4
4
|
|
|
5
|
+
## Supported Formats
|
|
6
|
+
|
|
7
|
+
The orchestration engine natively parses and cascades the following configuration formats:
|
|
8
|
+
* **YAML** (`.yaml`, `.yml`)
|
|
9
|
+
* **JSON** (`.json`)
|
|
10
|
+
* **XML** (`.xml`)
|
|
11
|
+
* **TOML** (`.toml`)
|
|
12
|
+
* **INI / Conf** (`.ini`, `.conf`)
|
|
13
|
+
|
|
5
14
|
---
|
|
6
15
|
|
|
7
16
|
```text
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dynamic-config-loader"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "An enterprise-grade configuration orchestration engine designed to ingest, cascade, and deeply merge multiple configuration file layers."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -23,6 +23,10 @@ keywords = [
|
|
|
23
23
|
"strategy-pattern",
|
|
24
24
|
"yaml",
|
|
25
25
|
"json",
|
|
26
|
+
"xml",
|
|
27
|
+
"toml",
|
|
28
|
+
"ini",
|
|
29
|
+
"conf",
|
|
26
30
|
"devops"
|
|
27
31
|
]
|
|
28
32
|
|
|
@@ -46,6 +50,7 @@ classifiers = [
|
|
|
46
50
|
|
|
47
51
|
dependencies = [
|
|
48
52
|
"pyyaml>=6.0.1",
|
|
53
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
49
54
|
]
|
|
50
55
|
|
|
51
56
|
[project.urls]
|
|
@@ -17,6 +17,8 @@ from dynamic_config_loader.strategies.base_strategy import BaseStrategy
|
|
|
17
17
|
from dynamic_config_loader.strategies.yaml import YamlStrategy
|
|
18
18
|
from dynamic_config_loader.strategies.json import JsonStrategy
|
|
19
19
|
from dynamic_config_loader.strategies.xml import XmlStrategy
|
|
20
|
+
from dynamic_config_loader.strategies.ini import IniStrategy
|
|
21
|
+
from dynamic_config_loader.strategies.toml import TomlStrategy
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class StrategyFactory:
|
|
@@ -34,6 +36,9 @@ class StrategyFactory:
|
|
|
34
36
|
".yml": YamlStrategy(),
|
|
35
37
|
".json": JsonStrategy(),
|
|
36
38
|
".xml": XmlStrategy(),
|
|
39
|
+
".ini": IniStrategy(),
|
|
40
|
+
".conf": IniStrategy(),
|
|
41
|
+
".toml": TomlStrategy(),
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
def get_strategy(self, extension: str) -> Optional[BaseStrategy]:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
INI Strategy Package Initializer.
|
|
8
|
+
|
|
9
|
+
Exposes the concrete IniStrategy engine implementation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
from dynamic_config_loader.strategies.base_strategy import BaseStrategy
|
|
15
|
+
from dynamic_config_loader.strategies.ini.parser import parse_ini_file
|
|
16
|
+
from dynamic_config_loader.strategies.ini.merger import execute_ini_deep_merge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IniStrategy(BaseStrategy):
|
|
20
|
+
"""
|
|
21
|
+
Concrete Strategy implementation managing the processing life cycle of INI configurations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def parse(self, file_path: Path) -> Dict[str, Any]:
|
|
25
|
+
return parse_ini_file(file_path)
|
|
26
|
+
|
|
27
|
+
def merge(
|
|
28
|
+
self,
|
|
29
|
+
base_dict: Dict[str, Any],
|
|
30
|
+
update_dict: Dict[str, Any],
|
|
31
|
+
path: str = "",
|
|
32
|
+
mask_secrets: bool = True,
|
|
33
|
+
) -> None:
|
|
34
|
+
execute_ini_deep_merge(base_dict, update_dict, path=path, mask_secrets=mask_secrets)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
INI Merging Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides recursive tree-merging algorithms engineered to blend cascading INI layers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_ini_deep_merge(
|
|
19
|
+
base_dict: Dict[str, Any],
|
|
20
|
+
update_dict: Dict[str, Any],
|
|
21
|
+
path: str = "",
|
|
22
|
+
mask_secrets: bool = True,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Recursively deep-merges update_dict into base_dict in-place.
|
|
26
|
+
"""
|
|
27
|
+
for key, value in update_dict.items():
|
|
28
|
+
current_path = f"{path}.{key}" if path else key
|
|
29
|
+
|
|
30
|
+
# Case A: Both values are dictionaries -> recurse
|
|
31
|
+
if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict):
|
|
32
|
+
logger.debug(f"Deep merging nested INI structure at path: {current_path}")
|
|
33
|
+
execute_ini_deep_merge(base_dict[key], value, path=current_path, mask_secrets=mask_secrets)
|
|
34
|
+
|
|
35
|
+
# Case B: Structural conflict or key exists
|
|
36
|
+
elif key in base_dict:
|
|
37
|
+
if isinstance(base_dict[key], dict) or isinstance(value, dict):
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Type mismatch conflict detected at path '{current_path}'. "
|
|
40
|
+
f"Overwriting base INI structural type ({type(base_dict[key]).__name__}) "
|
|
41
|
+
f"with incoming type ({type(value).__name__})."
|
|
42
|
+
)
|
|
43
|
+
base_dict[key] = value
|
|
44
|
+
else:
|
|
45
|
+
if base_dict[key] != value:
|
|
46
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
47
|
+
if not mask_secrets:
|
|
48
|
+
logger.debug(f"Updating INI path '{current_path}': {repr(base_dict[key])} -> {repr(value)}")
|
|
49
|
+
else:
|
|
50
|
+
logger.debug(f"Updating INI path '{current_path}': [REDACTED] -> [REDACTED]")
|
|
51
|
+
else:
|
|
52
|
+
logger.info(f"Updating configuration attribute path: {current_path} [Value Redacted]")
|
|
53
|
+
base_dict[key] = value
|
|
54
|
+
# Case C: New key
|
|
55
|
+
else:
|
|
56
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
57
|
+
if not mask_secrets:
|
|
58
|
+
logger.debug(f"Adding new INI key at path '{current_path}': {repr(value)}")
|
|
59
|
+
else:
|
|
60
|
+
logger.debug(f"Adding new INI key at path '{current_path}': [REDACTED]")
|
|
61
|
+
else:
|
|
62
|
+
logger.info(f"Adding new configuration attribute path: {current_path} [Value Redacted]")
|
|
63
|
+
base_dict[key] = value
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
INI Parsing Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides isolated, low-level file stream ingestion utilities optimized
|
|
10
|
+
for handling standard INI configuration assets.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import configparser
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_primitive(val: str) -> Any:
|
|
22
|
+
"""
|
|
23
|
+
Parses a string value into standard Python primitives (bool, int, float, str).
|
|
24
|
+
"""
|
|
25
|
+
val_lower = val.strip().lower()
|
|
26
|
+
if val_lower == "true":
|
|
27
|
+
return True
|
|
28
|
+
if val_lower == "false":
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
return int(val)
|
|
32
|
+
except ValueError:
|
|
33
|
+
pass
|
|
34
|
+
try:
|
|
35
|
+
return float(val)
|
|
36
|
+
except ValueError:
|
|
37
|
+
pass
|
|
38
|
+
return val.strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_ini_file(file_path: Path) -> Dict[str, Any]:
|
|
42
|
+
"""
|
|
43
|
+
Reads an INI configuration file asset from disk and returns a dictionary mapping.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
if file_path.stat().st_size == 0:
|
|
47
|
+
logger.info(f"Target INI file context is empty or unassigned: {file_path.name}")
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
config = configparser.ConfigParser()
|
|
51
|
+
config.read(file_path, encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
result = {}
|
|
54
|
+
for section in config.sections():
|
|
55
|
+
result[section] = {}
|
|
56
|
+
for key, val in config.items(section):
|
|
57
|
+
result[section][key] = parse_primitive(val)
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
except (configparser.Error, IOError, PermissionError) as e:
|
|
61
|
+
logger.exception(f"Failed to read or parse INI file: {file_path.name}", exc_info=e)
|
|
62
|
+
return {}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
TOML Strategy Package Initializer.
|
|
8
|
+
|
|
9
|
+
Exposes the concrete TomlStrategy engine implementation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
from dynamic_config_loader.strategies.base_strategy import BaseStrategy
|
|
15
|
+
from dynamic_config_loader.strategies.toml.parser import parse_toml_file
|
|
16
|
+
from dynamic_config_loader.strategies.toml.merger import execute_toml_deep_merge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TomlStrategy(BaseStrategy):
|
|
20
|
+
"""
|
|
21
|
+
Concrete Strategy implementation managing the processing life cycle of TOML configurations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def parse(self, file_path: Path) -> Dict[str, Any]:
|
|
25
|
+
return parse_toml_file(file_path)
|
|
26
|
+
|
|
27
|
+
def merge(
|
|
28
|
+
self,
|
|
29
|
+
base_dict: Dict[str, Any],
|
|
30
|
+
update_dict: Dict[str, Any],
|
|
31
|
+
path: str = "",
|
|
32
|
+
mask_secrets: bool = True,
|
|
33
|
+
) -> None:
|
|
34
|
+
execute_toml_deep_merge(base_dict, update_dict, path=path, mask_secrets=mask_secrets)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
TOML Merging Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides recursive tree-merging algorithms engineered to blend cascading TOML layers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_toml_deep_merge(
|
|
19
|
+
base_dict: Dict[str, Any],
|
|
20
|
+
update_dict: Dict[str, Any],
|
|
21
|
+
path: str = "",
|
|
22
|
+
mask_secrets: bool = True,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Recursively deep-merges update_dict into base_dict in-place.
|
|
26
|
+
"""
|
|
27
|
+
for key, value in update_dict.items():
|
|
28
|
+
current_path = f"{path}.{key}" if path else key
|
|
29
|
+
|
|
30
|
+
# Case A: Both values are dictionaries -> recurse
|
|
31
|
+
if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict):
|
|
32
|
+
logger.debug(f"Deep merging nested TOML structure at path: {current_path}")
|
|
33
|
+
execute_toml_deep_merge(base_dict[key], value, path=current_path, mask_secrets=mask_secrets)
|
|
34
|
+
|
|
35
|
+
# Case B: Structural conflict or key exists
|
|
36
|
+
elif key in base_dict:
|
|
37
|
+
if isinstance(base_dict[key], dict) or isinstance(value, dict):
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Type mismatch conflict detected at path '{current_path}'. "
|
|
40
|
+
f"Overwriting base TOML structural type ({type(base_dict[key]).__name__}) "
|
|
41
|
+
f"with incoming type ({type(value).__name__})."
|
|
42
|
+
)
|
|
43
|
+
base_dict[key] = value
|
|
44
|
+
else:
|
|
45
|
+
if base_dict[key] != value:
|
|
46
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
47
|
+
if not mask_secrets:
|
|
48
|
+
logger.debug(f"Updating TOML path '{current_path}': {repr(base_dict[key])} -> {repr(value)}")
|
|
49
|
+
else:
|
|
50
|
+
logger.debug(f"Updating TOML path '{current_path}': [REDACTED] -> [REDACTED]")
|
|
51
|
+
else:
|
|
52
|
+
logger.info(f"Updating configuration attribute path: {current_path} [Value Redacted]")
|
|
53
|
+
base_dict[key] = value
|
|
54
|
+
# Case C: New key
|
|
55
|
+
else:
|
|
56
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
57
|
+
if not mask_secrets:
|
|
58
|
+
logger.debug(f"Adding new TOML key at path '{current_path}': {repr(value)}")
|
|
59
|
+
else:
|
|
60
|
+
logger.debug(f"Adding new TOML key at path '{current_path}': [REDACTED]")
|
|
61
|
+
else:
|
|
62
|
+
logger.info(f"Adding new configuration attribute path: {current_path} [Value Redacted]")
|
|
63
|
+
base_dict[key] = value
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
TOML Parsing Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides isolated, low-level file stream ingestion utilities optimized
|
|
10
|
+
for handling standard TOML configuration assets.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import tomllib
|
|
21
|
+
except ImportError:
|
|
22
|
+
# Fallback compatibility layer for Python < 3.11
|
|
23
|
+
import tomli as tomllib
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_toml_file(file_path: Path) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Reads a TOML configuration file asset from disk and returns a dictionary mapping.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
if file_path.stat().st_size == 0:
|
|
32
|
+
logger.info(f"Target TOML file context is empty or unassigned: {file_path.name}")
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
with open(file_path, "rb") as f:
|
|
36
|
+
data = tomllib.load(f)
|
|
37
|
+
if data is None:
|
|
38
|
+
return {}
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
return data
|
|
41
|
+
logger.warning(
|
|
42
|
+
f"Invalid structural layout detected inside file: {file_path.name}. "
|
|
43
|
+
f"Expected key-value schema tree map, but received root type: {type(data).__name__}."
|
|
44
|
+
)
|
|
45
|
+
return {}
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.exception(f"Failed to read or parse TOML file: {file_path.name}", exc_info=e)
|
|
48
|
+
return {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamic-config-loader
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: An enterprise-grade configuration orchestration engine designed to ingest, cascade, and deeply merge multiple configuration file layers.
|
|
5
5
|
Author-email: Avinash Kumar <halfhiddencode@gmail.com>
|
|
6
6
|
License:
|
|
@@ -210,7 +210,7 @@ Project-URL: Repository, https://gitlab.com/halfhiddencode/python-dynamic-config
|
|
|
210
210
|
Project-URL: Issues, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/issues
|
|
211
211
|
Project-URL: Documentation, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/tree/main/docs
|
|
212
212
|
Project-URL: Changelog, https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md
|
|
213
|
-
Keywords: config,configuration,dynamic-loader,cascading-config,deep-merge,strategy-pattern,yaml,json,devops
|
|
213
|
+
Keywords: config,configuration,dynamic-loader,cascading-config,deep-merge,strategy-pattern,yaml,json,xml,toml,ini,conf,devops
|
|
214
214
|
Classifier: Development Status :: 5 - Production/Stable
|
|
215
215
|
Classifier: Intended Audience :: Developers
|
|
216
216
|
Classifier: Intended Audience :: System Administrators
|
|
@@ -232,12 +232,22 @@ License-File: LICENSE
|
|
|
232
232
|
License-File: NOTICE
|
|
233
233
|
License-File: AUTHORS
|
|
234
234
|
Requires-Dist: pyyaml>=6.0.1
|
|
235
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
235
236
|
Dynamic: license-file
|
|
236
237
|
|
|
237
238
|
# Dynamic Configuration Loader
|
|
238
239
|
|
|
239
240
|
A lightweight, production-grade, strategy-driven configuration orchestration engine designed to dynamically discover, alphabetically sort, and recursively deep-merge multi-layered configuration files.
|
|
240
241
|
|
|
242
|
+
## Supported Formats
|
|
243
|
+
|
|
244
|
+
The orchestration engine natively parses and cascades the following configuration formats:
|
|
245
|
+
* **YAML** (`.yaml`, `.yml`)
|
|
246
|
+
* **JSON** (`.json`)
|
|
247
|
+
* **XML** (`.xml`)
|
|
248
|
+
* **TOML** (`.toml`)
|
|
249
|
+
* **INI / Conf** (`.ini`, `.conf`)
|
|
250
|
+
|
|
241
251
|
---
|
|
242
252
|
|
|
243
253
|
```text
|
|
@@ -13,9 +13,15 @@ src/dynamic_config_loader.egg-info/requires.txt
|
|
|
13
13
|
src/dynamic_config_loader.egg-info/top_level.txt
|
|
14
14
|
src/dynamic_config_loader/strategies/__init__.py
|
|
15
15
|
src/dynamic_config_loader/strategies/base_strategy.py
|
|
16
|
+
src/dynamic_config_loader/strategies/ini/__init__.py
|
|
17
|
+
src/dynamic_config_loader/strategies/ini/merger.py
|
|
18
|
+
src/dynamic_config_loader/strategies/ini/parser.py
|
|
16
19
|
src/dynamic_config_loader/strategies/json/__init__.py
|
|
17
20
|
src/dynamic_config_loader/strategies/json/merger.py
|
|
18
21
|
src/dynamic_config_loader/strategies/json/parser.py
|
|
22
|
+
src/dynamic_config_loader/strategies/toml/__init__.py
|
|
23
|
+
src/dynamic_config_loader/strategies/toml/merger.py
|
|
24
|
+
src/dynamic_config_loader/strategies/toml/parser.py
|
|
19
25
|
src/dynamic_config_loader/strategies/xml/__init__.py
|
|
20
26
|
src/dynamic_config_loader/strategies/xml/merger.py
|
|
21
27
|
src/dynamic_config_loader/strategies/xml/parser.py
|
|
@@ -25,7 +31,9 @@ src/dynamic_config_loader/strategies/yaml/parser.py
|
|
|
25
31
|
tests/test_deep_merge.py
|
|
26
32
|
tests/test_env_injection.py
|
|
27
33
|
tests/test_factory.py
|
|
34
|
+
tests/test_ini_strategy.py
|
|
28
35
|
tests/test_json_strategy.py
|
|
29
36
|
tests/test_loader.py
|
|
30
37
|
tests/test_logging_safety.py
|
|
38
|
+
tests/test_toml_strategy.py
|
|
31
39
|
tests/test_xml_strategy.py
|
|
@@ -5,6 +5,8 @@ from dynamic_config_loader.strategies import factory
|
|
|
5
5
|
from dynamic_config_loader.strategies.yaml import YamlStrategy
|
|
6
6
|
from dynamic_config_loader.strategies.json import JsonStrategy
|
|
7
7
|
from dynamic_config_loader.strategies.xml import XmlStrategy
|
|
8
|
+
from dynamic_config_loader.strategies.ini import IniStrategy
|
|
9
|
+
from dynamic_config_loader.strategies.toml import TomlStrategy
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class TestStrategyFactory(unittest.TestCase):
|
|
@@ -18,11 +20,17 @@ class TestStrategyFactory(unittest.TestCase):
|
|
|
18
20
|
strategy_yml = factory.get_strategy(".yml")
|
|
19
21
|
strategy_json = factory.get_strategy(".json")
|
|
20
22
|
strategy_xml = factory.get_strategy(".xml")
|
|
23
|
+
strategy_ini = factory.get_strategy(".ini")
|
|
24
|
+
strategy_conf = factory.get_strategy(".conf")
|
|
25
|
+
strategy_toml = factory.get_strategy(".toml")
|
|
21
26
|
|
|
22
27
|
self.assertIsInstance(strategy_yaml, YamlStrategy)
|
|
23
28
|
self.assertIsInstance(strategy_yml, YamlStrategy)
|
|
24
29
|
self.assertIsInstance(strategy_json, JsonStrategy)
|
|
25
30
|
self.assertIsInstance(strategy_xml, XmlStrategy)
|
|
31
|
+
self.assertIsInstance(strategy_ini, IniStrategy)
|
|
32
|
+
self.assertIsInstance(strategy_conf, IniStrategy)
|
|
33
|
+
self.assertIsInstance(strategy_toml, TomlStrategy)
|
|
26
34
|
|
|
27
35
|
def test_factory_case_insensitivity(self):
|
|
28
36
|
"""
|
|
@@ -32,17 +40,23 @@ class TestStrategyFactory(unittest.TestCase):
|
|
|
32
40
|
strategy_upper = factory.get_strategy(".YML")
|
|
33
41
|
strategy_json_upper = factory.get_strategy(".JSON")
|
|
34
42
|
strategy_xml_upper = factory.get_strategy(".XML")
|
|
43
|
+
strategy_ini_upper = factory.get_strategy(".INI")
|
|
44
|
+
strategy_conf_upper = factory.get_strategy(".CONF")
|
|
45
|
+
strategy_toml_upper = factory.get_strategy(".TOML")
|
|
35
46
|
|
|
36
47
|
self.assertIsInstance(strategy_mixed, YamlStrategy)
|
|
37
48
|
self.assertIsInstance(strategy_upper, YamlStrategy)
|
|
38
49
|
self.assertIsInstance(strategy_json_upper, JsonStrategy)
|
|
39
50
|
self.assertIsInstance(strategy_xml_upper, XmlStrategy)
|
|
51
|
+
self.assertIsInstance(strategy_ini_upper, IniStrategy)
|
|
52
|
+
self.assertIsInstance(strategy_conf_upper, IniStrategy)
|
|
53
|
+
self.assertIsInstance(strategy_toml_upper, TomlStrategy)
|
|
40
54
|
|
|
41
55
|
def test_factory_unsupported_extensions(self):
|
|
42
56
|
"""
|
|
43
57
|
Verifies that StrategyFactory returns None for unregistered extensions.
|
|
44
58
|
"""
|
|
45
|
-
self.assertIsNone(factory.get_strategy(".
|
|
59
|
+
self.assertIsNone(factory.get_strategy(".txt"))
|
|
46
60
|
|
|
47
61
|
|
|
48
62
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dynamic_config_loader.loader import DynamicConfigLoader
|
|
8
|
+
from dynamic_config_loader.strategies.ini.parser import parse_ini_file
|
|
9
|
+
from dynamic_config_loader.strategies.ini.merger import execute_ini_deep_merge
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestIniStrategy(unittest.TestCase):
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.TEST_DIR = Path(__file__).parent
|
|
16
|
+
self.base_file = self.TEST_DIR / "test_configs" / "base_config.ini"
|
|
17
|
+
self.overrides_dir = self.TEST_DIR / "test_configs" / "overrides"
|
|
18
|
+
|
|
19
|
+
# Clear env variables
|
|
20
|
+
if "PYTHON_ADDITIONAL_CONFIG" in os.environ:
|
|
21
|
+
del os.environ["PYTHON_ADDITIONAL_CONFIG"]
|
|
22
|
+
if "ENABLE_UNSAFE_LOGGING" in os.environ:
|
|
23
|
+
del os.environ["ENABLE_UNSAFE_LOGGING"]
|
|
24
|
+
|
|
25
|
+
def tearDown(self):
|
|
26
|
+
if "PYTHON_ADDITIONAL_CONFIG" in os.environ:
|
|
27
|
+
del os.environ["PYTHON_ADDITIONAL_CONFIG"]
|
|
28
|
+
if "ENABLE_UNSAFE_LOGGING" in os.environ:
|
|
29
|
+
del os.environ["ENABLE_UNSAFE_LOGGING"]
|
|
30
|
+
|
|
31
|
+
def test_ini_parsing(self):
|
|
32
|
+
"""
|
|
33
|
+
Verifies that parse_ini_file correctly parses INI files and translates primitive types.
|
|
34
|
+
"""
|
|
35
|
+
data = parse_ini_file(self.base_file)
|
|
36
|
+
self.assertEqual(data["app"]["name"], "Test INI Gateway")
|
|
37
|
+
self.assertFalse(data["app"]["debug_mode"])
|
|
38
|
+
self.assertEqual(data["database"]["port"], 5432)
|
|
39
|
+
self.assertEqual(data["database"]["host"], "127.0.0.1")
|
|
40
|
+
|
|
41
|
+
def test_ini_deep_merging(self):
|
|
42
|
+
"""
|
|
43
|
+
Verifies recursive deep merges and type mismatch resolution for INI structures.
|
|
44
|
+
"""
|
|
45
|
+
base = {
|
|
46
|
+
"app": {
|
|
47
|
+
"debug": False
|
|
48
|
+
},
|
|
49
|
+
"database": {
|
|
50
|
+
"host": "localhost",
|
|
51
|
+
"settings": {"pool": 10}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
update = {
|
|
55
|
+
"app": {
|
|
56
|
+
"debug": True
|
|
57
|
+
},
|
|
58
|
+
"database": {
|
|
59
|
+
"settings": {"pool": 20, "timeout": 30}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
execute_ini_deep_merge(base, update)
|
|
64
|
+
|
|
65
|
+
self.assertTrue(base["app"]["debug"])
|
|
66
|
+
self.assertEqual(base["database"]["host"], "localhost")
|
|
67
|
+
self.assertEqual(base["database"]["settings"]["pool"], 20)
|
|
68
|
+
self.assertEqual(base["database"]["settings"]["timeout"], 30)
|
|
69
|
+
|
|
70
|
+
def test_ini_loader_integration(self):
|
|
71
|
+
"""
|
|
72
|
+
Verifies that DynamicConfigLoader correctly orchestrates baseline INI and directory overrides INI.
|
|
73
|
+
"""
|
|
74
|
+
# Set overrides path in env
|
|
75
|
+
os.environ["PYTHON_ADDITIONAL_CONFIG"] = str(self.overrides_dir)
|
|
76
|
+
|
|
77
|
+
loader = DynamicConfigLoader(default_config_path=str(self.base_file))
|
|
78
|
+
result = loader.load()
|
|
79
|
+
|
|
80
|
+
# baseline INI: Test INI Gateway, overrides folder has 01_dev_patch.ini overriding database host
|
|
81
|
+
self.assertEqual(result["app"]["name"], "Test INI Gateway")
|
|
82
|
+
self.assertTrue(result["app"]["debug_mode"])
|
|
83
|
+
self.assertEqual(result["database"]["host"], "ini-dev-cluster.local")
|
|
84
|
+
self.assertEqual(result["database"]["port"], 5432)
|
|
85
|
+
|
|
86
|
+
def test_ini_log_masking(self):
|
|
87
|
+
"""
|
|
88
|
+
Verifies that logging of updates/additions for INI conforms to mask_secrets checks.
|
|
89
|
+
"""
|
|
90
|
+
base = {"db_pass": "old_secret"}
|
|
91
|
+
update = {"db_pass": "new_secret"}
|
|
92
|
+
|
|
93
|
+
# Safe logs (default mask_secrets=True)
|
|
94
|
+
with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture:
|
|
95
|
+
execute_ini_deep_merge(base, update, mask_secrets=True)
|
|
96
|
+
|
|
97
|
+
log_messages = "\n".join(log_capture.output)
|
|
98
|
+
self.assertIn("[REDACTED]", log_messages)
|
|
99
|
+
self.assertNotIn("new_secret", log_messages)
|
|
100
|
+
|
|
101
|
+
# Reset base to ensure the second execution performs an update and triggers log outputs
|
|
102
|
+
base = {"db_pass": "old_secret"}
|
|
103
|
+
|
|
104
|
+
# Unsafe logs (mask_secrets=False via ENABLE_UNSAFE_LOGGING setting)
|
|
105
|
+
os.environ["ENABLE_UNSAFE_LOGGING"] = "true"
|
|
106
|
+
loader_unsafe = DynamicConfigLoader(default_config_path=str(self.base_file))
|
|
107
|
+
loader_unsafe.load() # Resolves self.mask_secrets to False
|
|
108
|
+
|
|
109
|
+
with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture_unsafe:
|
|
110
|
+
execute_ini_deep_merge(base, update, mask_secrets=loader_unsafe.mask_secrets)
|
|
111
|
+
|
|
112
|
+
log_messages_unsafe = "\n".join(log_capture_unsafe.output)
|
|
113
|
+
self.assertIn("new_secret", log_messages_unsafe)
|
|
114
|
+
self.assertNotIn("[REDACTED]", log_messages_unsafe)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
unittest.main()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import unittest
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dynamic_config_loader.loader import DynamicConfigLoader
|
|
8
|
+
from dynamic_config_loader.strategies.toml.parser import parse_toml_file
|
|
9
|
+
from dynamic_config_loader.strategies.toml.merger import execute_toml_deep_merge
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTomlStrategy(unittest.TestCase):
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.TEST_DIR = Path(__file__).parent
|
|
16
|
+
self.base_file = self.TEST_DIR / "test_configs" / "base_config.toml"
|
|
17
|
+
self.overrides_dir = self.TEST_DIR / "test_configs" / "overrides"
|
|
18
|
+
|
|
19
|
+
# Clear env variables
|
|
20
|
+
if "PYTHON_ADDITIONAL_CONFIG" in os.environ:
|
|
21
|
+
del os.environ["PYTHON_ADDITIONAL_CONFIG"]
|
|
22
|
+
if "ENABLE_UNSAFE_LOGGING" in os.environ:
|
|
23
|
+
del os.environ["ENABLE_UNSAFE_LOGGING"]
|
|
24
|
+
|
|
25
|
+
def tearDown(self):
|
|
26
|
+
if "PYTHON_ADDITIONAL_CONFIG" in os.environ:
|
|
27
|
+
del os.environ["PYTHON_ADDITIONAL_CONFIG"]
|
|
28
|
+
if "ENABLE_UNSAFE_LOGGING" in os.environ:
|
|
29
|
+
del os.environ["ENABLE_UNSAFE_LOGGING"]
|
|
30
|
+
|
|
31
|
+
def test_toml_parsing(self):
|
|
32
|
+
"""
|
|
33
|
+
Verifies that parse_toml_file correctly parses TOML files.
|
|
34
|
+
"""
|
|
35
|
+
data = parse_toml_file(self.base_file)
|
|
36
|
+
self.assertEqual(data["app"]["name"], "Test TOML Gateway")
|
|
37
|
+
self.assertFalse(data["app"]["debug_mode"])
|
|
38
|
+
self.assertEqual(data["database"]["port"], 5432)
|
|
39
|
+
self.assertEqual(data["database"]["host"], "127.0.0.1")
|
|
40
|
+
|
|
41
|
+
def test_toml_deep_merging(self):
|
|
42
|
+
"""
|
|
43
|
+
Verifies recursive deep merges and type mismatch resolution for TOML structures.
|
|
44
|
+
"""
|
|
45
|
+
base = {
|
|
46
|
+
"app": {
|
|
47
|
+
"debug": False
|
|
48
|
+
},
|
|
49
|
+
"database": {
|
|
50
|
+
"host": "localhost",
|
|
51
|
+
"settings": {"pool": 10}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
update = {
|
|
55
|
+
"app": {
|
|
56
|
+
"debug": True
|
|
57
|
+
},
|
|
58
|
+
"database": {
|
|
59
|
+
"settings": {"pool": 20, "timeout": 30}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
execute_toml_deep_merge(base, update)
|
|
64
|
+
|
|
65
|
+
self.assertTrue(base["app"]["debug"])
|
|
66
|
+
self.assertEqual(base["database"]["host"], "localhost")
|
|
67
|
+
self.assertEqual(base["database"]["settings"]["pool"], 20)
|
|
68
|
+
self.assertEqual(base["database"]["settings"]["timeout"], 30)
|
|
69
|
+
|
|
70
|
+
def test_toml_loader_integration(self):
|
|
71
|
+
"""
|
|
72
|
+
Verifies that DynamicConfigLoader correctly orchestrates baseline TOML and directory overrides TOML.
|
|
73
|
+
"""
|
|
74
|
+
# Set overrides path in env
|
|
75
|
+
os.environ["PYTHON_ADDITIONAL_CONFIG"] = str(self.overrides_dir)
|
|
76
|
+
|
|
77
|
+
loader = DynamicConfigLoader(default_config_path=str(self.base_file))
|
|
78
|
+
result = loader.load()
|
|
79
|
+
|
|
80
|
+
# baseline TOML: Test TOML Gateway, overrides folder has 01_dev_patch.toml overriding database host
|
|
81
|
+
self.assertEqual(result["app"]["name"], "Test TOML Gateway")
|
|
82
|
+
self.assertTrue(result["app"]["debug_mode"])
|
|
83
|
+
self.assertEqual(result["database"]["host"], "toml-dev-cluster.local")
|
|
84
|
+
self.assertEqual(result["database"]["port"], 5432)
|
|
85
|
+
|
|
86
|
+
def test_toml_log_masking(self):
|
|
87
|
+
"""
|
|
88
|
+
Verifies that logging of updates/additions for TOML conforms to mask_secrets checks.
|
|
89
|
+
"""
|
|
90
|
+
base = {"db_pass": "old_secret"}
|
|
91
|
+
update = {"db_pass": "new_secret"}
|
|
92
|
+
|
|
93
|
+
# Safe logs (default mask_secrets=True)
|
|
94
|
+
with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture:
|
|
95
|
+
execute_toml_deep_merge(base, update, mask_secrets=True)
|
|
96
|
+
|
|
97
|
+
log_messages = "\n".join(log_capture.output)
|
|
98
|
+
self.assertIn("[REDACTED]", log_messages)
|
|
99
|
+
self.assertNotIn("new_secret", log_messages)
|
|
100
|
+
|
|
101
|
+
# Reset base to ensure the second execution performs an update and triggers log outputs
|
|
102
|
+
base = {"db_pass": "old_secret"}
|
|
103
|
+
|
|
104
|
+
# Unsafe logs (mask_secrets=False via ENABLE_UNSAFE_LOGGING setting)
|
|
105
|
+
os.environ["ENABLE_UNSAFE_LOGGING"] = "true"
|
|
106
|
+
loader_unsafe = DynamicConfigLoader(default_config_path=str(self.base_file))
|
|
107
|
+
loader_unsafe.load() # Resolves self.mask_secrets to False
|
|
108
|
+
|
|
109
|
+
with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture_unsafe:
|
|
110
|
+
execute_toml_deep_merge(base, update, mask_secrets=loader_unsafe.mask_secrets)
|
|
111
|
+
|
|
112
|
+
log_messages_unsafe = "\n".join(log_capture_unsafe.output)
|
|
113
|
+
self.assertIn("new_secret", log_messages_unsafe)
|
|
114
|
+
self.assertNotIn("[REDACTED]", log_messages_unsafe)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pyyaml>=6.0.1
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/__init__.py
RENAMED
|
File without changes
|
{dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/environment.py
RENAMED
|
File without changes
|
{dynamic_config_loader-2.3.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|