dynamic-config-loader 2.2.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.
Files changed (42) hide show
  1. {dynamic_config_loader-2.2.0/src/dynamic_config_loader.egg-info → dynamic_config_loader-2.4.0}/PKG-INFO +19 -9
  2. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/README.md +16 -7
  3. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/pyproject.toml +6 -1
  4. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/__init__.py +7 -0
  5. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/__init__.py +34 -0
  6. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/merger.py +63 -0
  7. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/ini/parser.py +62 -0
  8. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/__init__.py +34 -0
  9. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/merger.py +63 -0
  10. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/toml/parser.py +48 -0
  11. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/xml/__init__.py +34 -0
  12. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/xml/merger.py +63 -0
  13. dynamic_config_loader-2.4.0/src/dynamic_config_loader/strategies/xml/parser.py +82 -0
  14. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0/src/dynamic_config_loader.egg-info}/PKG-INFO +19 -9
  15. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/SOURCES.txt +13 -1
  16. dynamic_config_loader-2.4.0/src/dynamic_config_loader.egg-info/requires.txt +4 -0
  17. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_factory.py +20 -2
  18. dynamic_config_loader-2.4.0/tests/test_ini_strategy.py +118 -0
  19. dynamic_config_loader-2.4.0/tests/test_toml_strategy.py +118 -0
  20. dynamic_config_loader-2.4.0/tests/test_xml_strategy.py +114 -0
  21. dynamic_config_loader-2.2.0/src/dynamic_config_loader.egg-info/requires.txt +0 -1
  22. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/AUTHORS +0 -0
  23. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/LICENSE +0 -0
  24. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/NOTICE +0 -0
  25. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/setup.cfg +0 -0
  26. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/__init__.py +0 -0
  27. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/environment.py +0 -0
  28. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/loader.py +0 -0
  29. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/base_strategy.py +0 -0
  30. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/__init__.py +0 -0
  31. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/merger.py +0 -0
  32. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/json/parser.py +0 -0
  33. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/__init__.py +0 -0
  34. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/merger.py +0 -0
  35. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader/strategies/yaml/parser.py +0 -0
  36. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/dependency_links.txt +0 -0
  37. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/src/dynamic_config_loader.egg-info/top_level.txt +0 -0
  38. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_deep_merge.py +0 -0
  39. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_env_injection.py +0 -0
  40. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_json_strategy.py +0 -0
  41. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_loader.py +0 -0
  42. {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.4.0}/tests/test_logging_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamic-config-loader
3
- Version: 2.2.0
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
@@ -308,14 +318,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
308
318
 
309
319
  | Track | Target Document | Description |
310
320
  | :--- | :--- | :--- |
311
- | **Getting Started** | [Getting Started Tutorial](docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
312
- | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
313
- | **Contributor Guide** | [Format Strategy Contribution Guide](docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
314
- | **Technical Reference** | [Core Architecture Reference](docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
321
+ | **Getting Started** | [Getting Started Tutorial](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
322
+ | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
323
+ | **Contributor Guide** | [Format Strategy Contribution Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
324
+ | **Technical Reference** | [Core Architecture Reference](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
315
325
 
316
326
  ---
317
327
 
318
328
  ## Repository Ledgers
319
- * Version History: [CHANGELOG.md](CHANGELOG.md)
320
- * Security Policy: [SECURITY.md](SECURITY.md)
321
- * Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
329
+ * Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
330
+ * Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
331
+ * Contribution Protocols: [CONTRIBUTING.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CONTRIBUTING.md)
@@ -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
@@ -72,14 +81,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
72
81
 
73
82
  | Track | Target Document | Description |
74
83
  | :--- | :--- | :--- |
75
- | **Getting Started** | [Getting Started Tutorial](docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
76
- | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
77
- | **Contributor Guide** | [Format Strategy Contribution Guide](docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
78
- | **Technical Reference** | [Core Architecture Reference](docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
84
+ | **Getting Started** | [Getting Started Tutorial](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
85
+ | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
86
+ | **Contributor Guide** | [Format Strategy Contribution Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
87
+ | **Technical Reference** | [Core Architecture Reference](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
79
88
 
80
89
  ---
81
90
 
82
91
  ## Repository Ledgers
83
- * Version History: [CHANGELOG.md](CHANGELOG.md)
84
- * Security Policy: [SECURITY.md](SECURITY.md)
85
- * Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
92
+ * Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
93
+ * Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
94
+ * Contribution Protocols: [CONTRIBUTING.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CONTRIBUTING.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dynamic-config-loader"
7
- version = "2.2.0"
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]
@@ -16,6 +16,9 @@ from dynamic_config_loader.strategies.base_strategy import BaseStrategy
16
16
  # Import the strategy worker class from the underlying yaml package
17
17
  from dynamic_config_loader.strategies.yaml import YamlStrategy
18
18
  from dynamic_config_loader.strategies.json import JsonStrategy
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
19
22
 
20
23
 
21
24
  class StrategyFactory:
@@ -32,6 +35,10 @@ class StrategyFactory:
32
35
  ".yaml": YamlStrategy(),
33
36
  ".yml": YamlStrategy(),
34
37
  ".json": JsonStrategy(),
38
+ ".xml": XmlStrategy(),
39
+ ".ini": IniStrategy(),
40
+ ".conf": IniStrategy(),
41
+ ".toml": TomlStrategy(),
35
42
  }
36
43
 
37
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 {}
@@ -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
+ XML Strategy Package Initializer.
8
+
9
+ Exposes the concrete XmlStrategy 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.xml.parser import parse_xml_file
16
+ from dynamic_config_loader.strategies.xml.merger import execute_xml_deep_merge
17
+
18
+
19
+ class XmlStrategy(BaseStrategy):
20
+ """
21
+ Concrete Strategy implementation managing the processing life cycle of XML configurations.
22
+ """
23
+
24
+ def parse(self, file_path: Path) -> Dict[str, Any]:
25
+ return parse_xml_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_xml_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
+ XML Merging Strategy Module.
8
+
9
+ Provides recursive tree-merging algorithms engineered to blend cascading XML layers.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any, Dict
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def execute_xml_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 XML structure at path: {current_path}")
33
+ execute_xml_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 XML 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 XML path '{current_path}': {repr(base_dict[key])} -> {repr(value)}")
49
+ else:
50
+ logger.debug(f"Updating XML 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 XML key at path '{current_path}': {repr(value)}")
59
+ else:
60
+ logger.debug(f"Adding new XML 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,82 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ # Copyright (C) 2026 Avinash Kumar
5
+
6
+ """
7
+ XML Parsing Strategy Module.
8
+
9
+ Provides isolated, low-level file stream ingestion utilities optimized
10
+ for handling standard XML configuration assets.
11
+ """
12
+
13
+ import logging
14
+ import xml.etree.ElementTree as ET
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 _xml_element_to_dict(element: ET.Element) -> Any:
42
+ """
43
+ Recursively translates XML elements into Python dictionaries.
44
+ """
45
+ children = list(element)
46
+ if not children:
47
+ text = element.text
48
+ if text is None:
49
+ return ""
50
+ return parse_primitive(text)
51
+
52
+ result = {}
53
+ for child in children:
54
+ result[child.tag] = _xml_element_to_dict(child)
55
+ return result
56
+
57
+
58
+ def parse_xml_file(file_path: Path) -> Dict[str, Any]:
59
+ """
60
+ Reads an XML configuration file asset from disk and returns a dictionary mapping.
61
+ """
62
+ try:
63
+ if file_path.stat().st_size == 0:
64
+ logger.info(f"Target XML file context is empty or unassigned: {file_path.name}")
65
+ return {}
66
+
67
+ tree = ET.parse(file_path)
68
+ root = tree.getroot()
69
+ data = _xml_element_to_dict(root)
70
+
71
+ if data is None:
72
+ return {}
73
+ if isinstance(data, dict):
74
+ return data
75
+ logger.warning(
76
+ f"Invalid structural layout detected inside file: {file_path.name}. "
77
+ f"Expected key-value schema tree map, but received root type: {type(data).__name__}."
78
+ )
79
+ return {}
80
+ except (ET.ParseError, IOError, PermissionError) as e:
81
+ logger.exception(f"Failed to read or parse XML file: {file_path.name}", exc_info=e)
82
+ return {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dynamic-config-loader
3
- Version: 2.2.0
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
@@ -308,14 +318,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
308
318
 
309
319
  | Track | Target Document | Description |
310
320
  | :--- | :--- | :--- |
311
- | **Getting Started** | [Getting Started Tutorial](docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
312
- | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
313
- | **Contributor Guide** | [Format Strategy Contribution Guide](docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
314
- | **Technical Reference** | [Core Architecture Reference](docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
321
+ | **Getting Started** | [Getting Started Tutorial](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/tutorials/getting_started.md) | Onboarding sequence, repeatable format tracks, and installation via GitLab Package Registry. |
322
+ | **Operations Guide** | [Advanced Ingestion and Path Traversal Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/user_usage.md) | Path parsing matrix, edge-case refusals, Central Config Manager pattern, and log masking safety details. |
323
+ | **Contributor Guide** | [Format Strategy Contribution Guide](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/guides/contributor_guide.md) | Abstract base strategy class signatures, factory registration hooks, and copy-pasteable JSON blueprints. |
324
+ | **Technical Reference** | [Core Architecture Reference](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/docs/reference/architecture.md) | System design architecture, UML mapping, execution pipelines, and logging safety compliance matrices. |
315
325
 
316
326
  ---
317
327
 
318
328
  ## Repository Ledgers
319
- * Version History: [CHANGELOG.md](CHANGELOG.md)
320
- * Security Policy: [SECURITY.md](SECURITY.md)
321
- * Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
329
+ * Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
330
+ * Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
331
+ * Contribution Protocols: [CONTRIBUTING.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CONTRIBUTING.md)
@@ -13,15 +13,27 @@ 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
25
+ src/dynamic_config_loader/strategies/xml/__init__.py
26
+ src/dynamic_config_loader/strategies/xml/merger.py
27
+ src/dynamic_config_loader/strategies/xml/parser.py
19
28
  src/dynamic_config_loader/strategies/yaml/__init__.py
20
29
  src/dynamic_config_loader/strategies/yaml/merger.py
21
30
  src/dynamic_config_loader/strategies/yaml/parser.py
22
31
  tests/test_deep_merge.py
23
32
  tests/test_env_injection.py
24
33
  tests/test_factory.py
34
+ tests/test_ini_strategy.py
25
35
  tests/test_json_strategy.py
26
36
  tests/test_loader.py
27
- tests/test_logging_safety.py
37
+ tests/test_logging_safety.py
38
+ tests/test_toml_strategy.py
39
+ tests/test_xml_strategy.py
@@ -0,0 +1,4 @@
1
+ pyyaml>=6.0.1
2
+
3
+ [:python_version < "3.11"]
4
+ tomli>=2.0.1
@@ -4,6 +4,9 @@ import unittest
4
4
  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
+ 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
7
10
 
8
11
 
9
12
  class TestStrategyFactory(unittest.TestCase):
@@ -16,10 +19,18 @@ class TestStrategyFactory(unittest.TestCase):
16
19
  strategy_yaml = factory.get_strategy(".yaml")
17
20
  strategy_yml = factory.get_strategy(".yml")
18
21
  strategy_json = factory.get_strategy(".json")
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")
19
26
 
20
27
  self.assertIsInstance(strategy_yaml, YamlStrategy)
21
28
  self.assertIsInstance(strategy_yml, YamlStrategy)
22
29
  self.assertIsInstance(strategy_json, JsonStrategy)
30
+ self.assertIsInstance(strategy_xml, XmlStrategy)
31
+ self.assertIsInstance(strategy_ini, IniStrategy)
32
+ self.assertIsInstance(strategy_conf, IniStrategy)
33
+ self.assertIsInstance(strategy_toml, TomlStrategy)
23
34
 
24
35
  def test_factory_case_insensitivity(self):
25
36
  """
@@ -28,17 +39,24 @@ class TestStrategyFactory(unittest.TestCase):
28
39
  strategy_mixed = factory.get_strategy(".YaMl")
29
40
  strategy_upper = factory.get_strategy(".YML")
30
41
  strategy_json_upper = factory.get_strategy(".JSON")
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")
31
46
 
32
47
  self.assertIsInstance(strategy_mixed, YamlStrategy)
33
48
  self.assertIsInstance(strategy_upper, YamlStrategy)
34
49
  self.assertIsInstance(strategy_json_upper, JsonStrategy)
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)
35
54
 
36
55
  def test_factory_unsupported_extensions(self):
37
56
  """
38
57
  Verifies that StrategyFactory returns None for unregistered extensions.
39
58
  """
40
- self.assertIsNone(factory.get_strategy(".xml"))
41
- self.assertIsNone(factory.get_strategy(".ini"))
59
+ self.assertIsNone(factory.get_strategy(".txt"))
42
60
 
43
61
 
44
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()
@@ -0,0 +1,114 @@
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.xml.parser import parse_xml_file
9
+ from dynamic_config_loader.strategies.xml.merger import execute_xml_deep_merge
10
+
11
+
12
+ class TestXmlStrategy(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.xml"
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_xml_parsing(self):
32
+ """
33
+ Verifies that parse_xml_file correctly parses XML files and translates primitive types.
34
+ """
35
+ data = parse_xml_file(self.base_file)
36
+ self.assertEqual(data["app_name"], "Test XML Gateway")
37
+ self.assertFalse(data["debug_mode"])
38
+ self.assertEqual(data["database"]["port"], 5432)
39
+ self.assertEqual(data["database"]["host"], "127.0.0.1")
40
+
41
+ def test_xml_deep_merging(self):
42
+ """
43
+ Verifies recursive deep merges and type mismatch resolution for XML structures.
44
+ """
45
+ base = {
46
+ "debug": False,
47
+ "database": {
48
+ "host": "localhost",
49
+ "settings": {"pool": 10}
50
+ }
51
+ }
52
+ update = {
53
+ "debug": True,
54
+ "database": {
55
+ "settings": {"pool": 20, "timeout": 30}
56
+ }
57
+ }
58
+
59
+ execute_xml_deep_merge(base, update)
60
+
61
+ self.assertTrue(base["debug"])
62
+ self.assertEqual(base["database"]["host"], "localhost")
63
+ self.assertEqual(base["database"]["settings"]["pool"], 20)
64
+ self.assertEqual(base["database"]["settings"]["timeout"], 30)
65
+
66
+ def test_xml_loader_integration(self):
67
+ """
68
+ Verifies that DynamicConfigLoader correctly orchestrates baseline XML and directory overrides XML.
69
+ """
70
+ # Set overrides path in env
71
+ os.environ["PYTHON_ADDITIONAL_CONFIG"] = str(self.overrides_dir)
72
+
73
+ loader = DynamicConfigLoader(default_config_path=str(self.base_file))
74
+ result = loader.load()
75
+
76
+ # baseline XML: Test XML Gateway, overrides folder has 01_dev_patch.xml overriding database host
77
+ self.assertEqual(result["app_name"], "Test XML Gateway")
78
+ self.assertTrue(result["debug_mode"])
79
+ self.assertEqual(result["database"]["host"], "xml-dev-cluster.local")
80
+ self.assertEqual(result["database"]["port"], 5432)
81
+
82
+ def test_xml_log_masking(self):
83
+ """
84
+ Verifies that logging of updates/additions for XML conforms to mask_secrets checks.
85
+ """
86
+ base = {"db_pass": "old_secret"}
87
+ update = {"db_pass": "new_secret"}
88
+
89
+ # Safe logs (default mask_secrets=True)
90
+ with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture:
91
+ execute_xml_deep_merge(base, update, mask_secrets=True)
92
+
93
+ log_messages = "\n".join(log_capture.output)
94
+ self.assertIn("[REDACTED]", log_messages)
95
+ self.assertNotIn("new_secret", log_messages)
96
+
97
+ # Reset base to ensure the second execution performs an update and triggers log outputs
98
+ base = {"db_pass": "old_secret"}
99
+
100
+ # Unsafe logs (mask_secrets=False via ENABLE_UNSAFE_LOGGING setting)
101
+ os.environ["ENABLE_UNSAFE_LOGGING"] = "true"
102
+ loader_unsafe = DynamicConfigLoader(default_config_path=str(self.base_file))
103
+ loader_unsafe.load() # Resolves self.mask_secrets to False
104
+
105
+ with self.assertLogs("dynamic_config_loader", level=logging.DEBUG) as log_capture_unsafe:
106
+ execute_xml_deep_merge(base, update, mask_secrets=loader_unsafe.mask_secrets)
107
+
108
+ log_messages_unsafe = "\n".join(log_capture_unsafe.output)
109
+ self.assertIn("new_secret", log_messages_unsafe)
110
+ self.assertNotIn("[REDACTED]", log_messages_unsafe)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ unittest.main()