dynamic-config-loader 2.2.0__tar.gz → 2.3.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.2.0/src/dynamic_config_loader.egg-info → dynamic_config_loader-2.3.0}/PKG-INFO +8 -8
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/README.md +7 -7
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/pyproject.toml +1 -1
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/__init__.py +2 -0
- dynamic_config_loader-2.3.0/src/dynamic_config_loader/strategies/xml/__init__.py +34 -0
- dynamic_config_loader-2.3.0/src/dynamic_config_loader/strategies/xml/merger.py +63 -0
- dynamic_config_loader-2.3.0/src/dynamic_config_loader/strategies/xml/parser.py +82 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0/src/dynamic_config_loader.egg-info}/PKG-INFO +8 -8
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader.egg-info/SOURCES.txt +5 -1
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/tests/test_factory.py +5 -1
- dynamic_config_loader-2.3.0/tests/test_xml_strategy.py +114 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/AUTHORS +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/LICENSE +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/NOTICE +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/setup.cfg +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/__init__.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/environment.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/loader.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/base_strategy.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/json/__init__.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/json/merger.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/json/parser.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/yaml/__init__.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/yaml/merger.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/strategies/yaml/parser.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader.egg-info/dependency_links.txt +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader.egg-info/requires.txt +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader.egg-info/top_level.txt +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/tests/test_deep_merge.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/tests/test_env_injection.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/tests/test_json_strategy.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/tests/test_loader.py +0 -0
- {dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.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.
|
|
3
|
+
Version: 2.3.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:
|
|
@@ -308,14 +308,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
|
|
|
308
308
|
|
|
309
309
|
| Track | Target Document | Description |
|
|
310
310
|
| :--- | :--- | :--- |
|
|
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. |
|
|
311
|
+
| **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. |
|
|
312
|
+
| **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. |
|
|
313
|
+
| **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. |
|
|
314
|
+
| **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
315
|
|
|
316
316
|
---
|
|
317
317
|
|
|
318
318
|
## Repository Ledgers
|
|
319
|
-
* Version History: [CHANGELOG.md](CHANGELOG.md)
|
|
320
|
-
* Security Policy: [SECURITY.md](SECURITY.md)
|
|
321
|
-
* Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
319
|
+
* Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
|
|
320
|
+
* Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
|
|
321
|
+
* Contribution Protocols: [CONTRIBUTING.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CONTRIBUTING.md)
|
|
@@ -72,14 +72,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
|
|
|
72
72
|
|
|
73
73
|
| Track | Target Document | Description |
|
|
74
74
|
| :--- | :--- | :--- |
|
|
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. |
|
|
75
|
+
| **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. |
|
|
76
|
+
| **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. |
|
|
77
|
+
| **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. |
|
|
78
|
+
| **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
79
|
|
|
80
80
|
---
|
|
81
81
|
|
|
82
82
|
## Repository Ledgers
|
|
83
|
-
* Version History: [CHANGELOG.md](CHANGELOG.md)
|
|
84
|
-
* Security Policy: [SECURITY.md](SECURITY.md)
|
|
85
|
-
* Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
83
|
+
* Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
|
|
84
|
+
* Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
|
|
85
|
+
* 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.
|
|
7
|
+
version = "2.3.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"
|
|
@@ -16,6 +16,7 @@ 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
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class StrategyFactory:
|
|
@@ -32,6 +33,7 @@ class StrategyFactory:
|
|
|
32
33
|
".yaml": YamlStrategy(),
|
|
33
34
|
".yml": YamlStrategy(),
|
|
34
35
|
".json": JsonStrategy(),
|
|
36
|
+
".xml": XmlStrategy(),
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
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
|
+
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.
|
|
3
|
+
Version: 2.3.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:
|
|
@@ -308,14 +308,14 @@ The complete enterprise documentation is organized under the Diátaxis framework
|
|
|
308
308
|
|
|
309
309
|
| Track | Target Document | Description |
|
|
310
310
|
| :--- | :--- | :--- |
|
|
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. |
|
|
311
|
+
| **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. |
|
|
312
|
+
| **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. |
|
|
313
|
+
| **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. |
|
|
314
|
+
| **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
315
|
|
|
316
316
|
---
|
|
317
317
|
|
|
318
318
|
## Repository Ledgers
|
|
319
|
-
* Version History: [CHANGELOG.md](CHANGELOG.md)
|
|
320
|
-
* Security Policy: [SECURITY.md](SECURITY.md)
|
|
321
|
-
* Contribution Protocols: [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
319
|
+
* Version History: [CHANGELOG.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CHANGELOG.md)
|
|
320
|
+
* Security Policy: [SECURITY.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/SECURITY.md)
|
|
321
|
+
* Contribution Protocols: [CONTRIBUTING.md](https://gitlab.com/halfhiddencode/python-dynamic-config-loader/-/blob/main/CONTRIBUTING.md)
|
|
@@ -16,6 +16,9 @@ src/dynamic_config_loader/strategies/base_strategy.py
|
|
|
16
16
|
src/dynamic_config_loader/strategies/json/__init__.py
|
|
17
17
|
src/dynamic_config_loader/strategies/json/merger.py
|
|
18
18
|
src/dynamic_config_loader/strategies/json/parser.py
|
|
19
|
+
src/dynamic_config_loader/strategies/xml/__init__.py
|
|
20
|
+
src/dynamic_config_loader/strategies/xml/merger.py
|
|
21
|
+
src/dynamic_config_loader/strategies/xml/parser.py
|
|
19
22
|
src/dynamic_config_loader/strategies/yaml/__init__.py
|
|
20
23
|
src/dynamic_config_loader/strategies/yaml/merger.py
|
|
21
24
|
src/dynamic_config_loader/strategies/yaml/parser.py
|
|
@@ -24,4 +27,5 @@ tests/test_env_injection.py
|
|
|
24
27
|
tests/test_factory.py
|
|
25
28
|
tests/test_json_strategy.py
|
|
26
29
|
tests/test_loader.py
|
|
27
|
-
tests/test_logging_safety.py
|
|
30
|
+
tests/test_logging_safety.py
|
|
31
|
+
tests/test_xml_strategy.py
|
|
@@ -4,6 +4,7 @@ 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
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TestStrategyFactory(unittest.TestCase):
|
|
@@ -16,10 +17,12 @@ class TestStrategyFactory(unittest.TestCase):
|
|
|
16
17
|
strategy_yaml = factory.get_strategy(".yaml")
|
|
17
18
|
strategy_yml = factory.get_strategy(".yml")
|
|
18
19
|
strategy_json = factory.get_strategy(".json")
|
|
20
|
+
strategy_xml = factory.get_strategy(".xml")
|
|
19
21
|
|
|
20
22
|
self.assertIsInstance(strategy_yaml, YamlStrategy)
|
|
21
23
|
self.assertIsInstance(strategy_yml, YamlStrategy)
|
|
22
24
|
self.assertIsInstance(strategy_json, JsonStrategy)
|
|
25
|
+
self.assertIsInstance(strategy_xml, XmlStrategy)
|
|
23
26
|
|
|
24
27
|
def test_factory_case_insensitivity(self):
|
|
25
28
|
"""
|
|
@@ -28,16 +31,17 @@ class TestStrategyFactory(unittest.TestCase):
|
|
|
28
31
|
strategy_mixed = factory.get_strategy(".YaMl")
|
|
29
32
|
strategy_upper = factory.get_strategy(".YML")
|
|
30
33
|
strategy_json_upper = factory.get_strategy(".JSON")
|
|
34
|
+
strategy_xml_upper = factory.get_strategy(".XML")
|
|
31
35
|
|
|
32
36
|
self.assertIsInstance(strategy_mixed, YamlStrategy)
|
|
33
37
|
self.assertIsInstance(strategy_upper, YamlStrategy)
|
|
34
38
|
self.assertIsInstance(strategy_json_upper, JsonStrategy)
|
|
39
|
+
self.assertIsInstance(strategy_xml_upper, XmlStrategy)
|
|
35
40
|
|
|
36
41
|
def test_factory_unsupported_extensions(self):
|
|
37
42
|
"""
|
|
38
43
|
Verifies that StrategyFactory returns None for unregistered extensions.
|
|
39
44
|
"""
|
|
40
|
-
self.assertIsNone(factory.get_strategy(".xml"))
|
|
41
45
|
self.assertIsNone(factory.get_strategy(".ini"))
|
|
42
46
|
|
|
43
47
|
|
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/__init__.py
RENAMED
|
File without changes
|
{dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.0}/src/dynamic_config_loader/environment.py
RENAMED
|
File without changes
|
{dynamic_config_loader-2.2.0 → dynamic_config_loader-2.3.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
|