dynamic-config-loader 2.3.0__tar.gz → 2.5.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.5.0}/PKG-INFO +14 -2
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/README.md +11 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/pyproject.toml +8 -1
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/__init__.py +9 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/__init__.py +36 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/merger.py +73 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/parser.py +86 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/__init__.py +34 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/merger.py +63 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/parser.py +62 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/__init__.py +36 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/merger.py +73 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/parser.py +86 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/__init__.py +34 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/merger.py +63 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/parser.py +48 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0/src/dynamic_config_loader.egg-info}/PKG-INFO +14 -2
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/SOURCES.txt +16 -0
- dynamic_config_loader-2.5.0/src/dynamic_config_loader.egg-info/requires.txt +4 -0
- dynamic_config_loader-2.5.0/tests/test_env_strategy.py +120 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_factory.py +25 -1
- dynamic_config_loader-2.5.0/tests/test_ini_strategy.py +118 -0
- dynamic_config_loader-2.5.0/tests/test_properties_strategy.py +120 -0
- dynamic_config_loader-2.5.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.5.0}/AUTHORS +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/LICENSE +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/NOTICE +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/setup.cfg +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/environment.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/loader.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/base_strategy.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/__init__.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/merger.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/parser.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/dependency_links.txt +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/top_level.txt +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_deep_merge.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_env_injection.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_json_strategy.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_loader.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_logging_safety.py +0 -0
- {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.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.5.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,properties,dotenv,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,24 @@ 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
|
+
* **Java Properties** (`.properties`)
|
|
251
|
+
* **Dotenv** (`.env`)
|
|
252
|
+
|
|
241
253
|
---
|
|
242
254
|
|
|
243
255
|
```text
|
|
@@ -2,6 +2,17 @@
|
|
|
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
|
+
* **Java Properties** (`.properties`)
|
|
14
|
+
* **Dotenv** (`.env`)
|
|
15
|
+
|
|
5
16
|
---
|
|
6
17
|
|
|
7
18
|
```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.5.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,12 @@ keywords = [
|
|
|
23
23
|
"strategy-pattern",
|
|
24
24
|
"yaml",
|
|
25
25
|
"json",
|
|
26
|
+
"xml",
|
|
27
|
+
"toml",
|
|
28
|
+
"ini",
|
|
29
|
+
"conf",
|
|
30
|
+
"properties",
|
|
31
|
+
"dotenv",
|
|
26
32
|
"devops"
|
|
27
33
|
]
|
|
28
34
|
|
|
@@ -46,6 +52,7 @@ classifiers = [
|
|
|
46
52
|
|
|
47
53
|
dependencies = [
|
|
48
54
|
"pyyaml>=6.0.1",
|
|
55
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
49
56
|
]
|
|
50
57
|
|
|
51
58
|
[project.urls]
|
|
@@ -17,6 +17,10 @@ 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
|
|
22
|
+
from dynamic_config_loader.strategies.env import EnvStrategy
|
|
23
|
+
from dynamic_config_loader.strategies.properties import PropertiesStrategy
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
class StrategyFactory:
|
|
@@ -34,6 +38,11 @@ class StrategyFactory:
|
|
|
34
38
|
".yml": YamlStrategy(),
|
|
35
39
|
".json": JsonStrategy(),
|
|
36
40
|
".xml": XmlStrategy(),
|
|
41
|
+
".ini": IniStrategy(),
|
|
42
|
+
".conf": IniStrategy(),
|
|
43
|
+
".toml": TomlStrategy(),
|
|
44
|
+
".env": EnvStrategy(),
|
|
45
|
+
".properties": PropertiesStrategy(),
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
def get_strategy(self, extension: str) -> Optional[BaseStrategy]:
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Env Strategy Entrypoint.
|
|
8
|
+
|
|
9
|
+
Exposes the EnvStrategy wrapper subclassing BaseStrategy.
|
|
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.env.parser import parse_env_file
|
|
16
|
+
from dynamic_config_loader.strategies.env.merger import execute_env_deep_merge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnvStrategy(BaseStrategy):
|
|
20
|
+
"""
|
|
21
|
+
Strategy implementation managing the parsing and deep-merging of dotenv (.env) assets.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def parse(self, file_path: Path) -> Dict[str, Any]:
|
|
25
|
+
return parse_env_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_env_deep_merge(
|
|
35
|
+
base_dict, update_dict, path=path, mask_secrets=mask_secrets
|
|
36
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Env Merging Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides recursive tree-merging algorithms engineered to blend cascading dotenv layers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_env_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 env structure at path: {current_path}")
|
|
33
|
+
execute_env_deep_merge(
|
|
34
|
+
base_dict[key], value, path=current_path, mask_secrets=mask_secrets
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Case B: Structural conflict or key exists
|
|
38
|
+
elif key in base_dict:
|
|
39
|
+
if isinstance(base_dict[key], dict) or isinstance(value, dict):
|
|
40
|
+
logger.warning(
|
|
41
|
+
f"Type mismatch conflict detected at path '{current_path}'. "
|
|
42
|
+
f"Overwriting base env structural type ({type(base_dict[key]).__name__}) "
|
|
43
|
+
f"with incoming type ({type(value).__name__})."
|
|
44
|
+
)
|
|
45
|
+
base_dict[key] = value
|
|
46
|
+
else:
|
|
47
|
+
if base_dict[key] != value:
|
|
48
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
49
|
+
if not mask_secrets:
|
|
50
|
+
logger.debug(
|
|
51
|
+
f"Updating env path '{current_path}': {repr(base_dict[key])} -> {repr(value)}"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
logger.debug(
|
|
55
|
+
f"Updating env path '{current_path}': [REDACTED] -> [REDACTED]"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
logger.info(
|
|
59
|
+
f"Updating configuration attribute path: {current_path} [Value Redacted]"
|
|
60
|
+
)
|
|
61
|
+
base_dict[key] = value
|
|
62
|
+
# Case C: New key
|
|
63
|
+
else:
|
|
64
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
65
|
+
if not mask_secrets:
|
|
66
|
+
logger.debug(f"Adding new env key at path '{current_path}': {repr(value)}")
|
|
67
|
+
else:
|
|
68
|
+
logger.debug(f"Adding new env key at path '{current_path}': [REDACTED]")
|
|
69
|
+
else:
|
|
70
|
+
logger.info(
|
|
71
|
+
f"Adding new configuration attribute path: {current_path} [Value Redacted]"
|
|
72
|
+
)
|
|
73
|
+
base_dict[key] = value
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Env Parsing Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides isolated, low-level file stream ingestion utilities optimized
|
|
10
|
+
for handling standard dotenv 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
|
+
|
|
20
|
+
def _parse_env_file_value(val: str) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Parses env string values into standard primitives (bool, int, float, str).
|
|
23
|
+
"""
|
|
24
|
+
val_lower = val.strip().lower()
|
|
25
|
+
if val_lower == "true":
|
|
26
|
+
return True
|
|
27
|
+
if val_lower == "false":
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
return int(val)
|
|
31
|
+
except ValueError:
|
|
32
|
+
pass
|
|
33
|
+
try:
|
|
34
|
+
return float(val)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
return val.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_env_file(file_path: Path) -> Dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Reads a dotenv configuration file asset from disk and returns a dictionary mapping.
|
|
43
|
+
"""
|
|
44
|
+
data: Dict[str, Any] = {}
|
|
45
|
+
try:
|
|
46
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
47
|
+
for line in f:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
# Ignore empty lines and comment lines starting with '#'
|
|
50
|
+
if not line or line.startswith("#"):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if "=" not in line:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
key, val = line.split("=", 1)
|
|
57
|
+
key = key.strip()
|
|
58
|
+
val = val.strip()
|
|
59
|
+
|
|
60
|
+
if not key:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Strip surrounding double or single quotes
|
|
64
|
+
if len(val) >= 2 and (
|
|
65
|
+
(val[0] == '"' and val[-1] == '"') or (val[0] == "'" and val[-1] == "'")
|
|
66
|
+
):
|
|
67
|
+
val = val[1:-1]
|
|
68
|
+
|
|
69
|
+
parsed_val = _parse_env_file_value(val)
|
|
70
|
+
|
|
71
|
+
# Tokenize key by single underscore delimiter to align with env injection track
|
|
72
|
+
parts = [p.lower() for p in key.split("_") if p]
|
|
73
|
+
if not parts:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
curr = data
|
|
77
|
+
for part in parts[:-1]:
|
|
78
|
+
if part not in curr or not isinstance(curr[part], dict):
|
|
79
|
+
curr[part] = {}
|
|
80
|
+
curr = curr[part]
|
|
81
|
+
curr[parts[-1]] = parsed_val
|
|
82
|
+
|
|
83
|
+
except (IOError, PermissionError) as e:
|
|
84
|
+
logger.exception(f"Failed to read or parse dotenv file: {file_path.name}", exc_info=e)
|
|
85
|
+
|
|
86
|
+
return data
|
|
@@ -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,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Java Properties Strategy Entrypoint.
|
|
8
|
+
|
|
9
|
+
Exposes the PropertiesStrategy wrapper subclassing BaseStrategy.
|
|
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.properties.parser import parse_properties_file
|
|
16
|
+
from dynamic_config_loader.strategies.properties.merger import execute_properties_deep_merge
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PropertiesStrategy(BaseStrategy):
|
|
20
|
+
"""
|
|
21
|
+
Strategy implementation managing the parsing and deep-merging of Java properties assets.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def parse(self, file_path: Path) -> Dict[str, Any]:
|
|
25
|
+
return parse_properties_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_properties_deep_merge(
|
|
35
|
+
base_dict, update_dict, path=path, mask_secrets=mask_secrets
|
|
36
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Properties Merging Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides recursive tree-merging algorithms engineered to blend cascading properties layers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def execute_properties_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 properties structure at path: {current_path}")
|
|
33
|
+
execute_properties_deep_merge(
|
|
34
|
+
base_dict[key], value, path=current_path, mask_secrets=mask_secrets
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Case B: Structural conflict or key exists
|
|
38
|
+
elif key in base_dict:
|
|
39
|
+
if isinstance(base_dict[key], dict) or isinstance(value, dict):
|
|
40
|
+
logger.warning(
|
|
41
|
+
f"Type mismatch conflict detected at path '{current_path}'. "
|
|
42
|
+
f"Overwriting base properties structural type ({type(base_dict[key]).__name__}) "
|
|
43
|
+
f"with incoming type ({type(value).__name__})."
|
|
44
|
+
)
|
|
45
|
+
base_dict[key] = value
|
|
46
|
+
else:
|
|
47
|
+
if base_dict[key] != value:
|
|
48
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
49
|
+
if not mask_secrets:
|
|
50
|
+
logger.debug(
|
|
51
|
+
f"Updating properties path '{current_path}': {repr(base_dict[key])} -> {repr(value)}"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
logger.debug(
|
|
55
|
+
f"Updating properties path '{current_path}': [REDACTED] -> [REDACTED]"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
logger.info(
|
|
59
|
+
f"Updating configuration attribute path: {current_path} [Value Redacted]"
|
|
60
|
+
)
|
|
61
|
+
base_dict[key] = value
|
|
62
|
+
# Case C: New key
|
|
63
|
+
else:
|
|
64
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
65
|
+
if not mask_secrets:
|
|
66
|
+
logger.debug(f"Adding new properties key at path '{current_path}': {repr(value)}")
|
|
67
|
+
else:
|
|
68
|
+
logger.debug(f"Adding new properties key at path '{current_path}': [REDACTED]")
|
|
69
|
+
else:
|
|
70
|
+
logger.info(
|
|
71
|
+
f"Adding new configuration attribute path: {current_path} [Value Redacted]"
|
|
72
|
+
)
|
|
73
|
+
base_dict[key] = value
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Copyright (C) 2026 Avinash Kumar
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Properties Parsing Strategy Module.
|
|
8
|
+
|
|
9
|
+
Provides isolated, low-level file stream ingestion utilities optimized
|
|
10
|
+
for handling standard properties 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
|
+
|
|
20
|
+
def _parse_properties_value(val: str) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Parses properties string values into standard primitives (bool, int, float, str).
|
|
23
|
+
"""
|
|
24
|
+
val_lower = val.strip().lower()
|
|
25
|
+
if val_lower == "true":
|
|
26
|
+
return True
|
|
27
|
+
if val_lower == "false":
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
return int(val)
|
|
31
|
+
except ValueError:
|
|
32
|
+
pass
|
|
33
|
+
try:
|
|
34
|
+
return float(val)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
return val.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parse_properties_file(file_path: Path) -> Dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Reads a .properties configuration file asset from disk and returns a dictionary mapping.
|
|
43
|
+
"""
|
|
44
|
+
data: Dict[str, Any] = {}
|
|
45
|
+
try:
|
|
46
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
47
|
+
for line in f:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
# Ignore empty lines and comment lines starting with '#' or '!'
|
|
50
|
+
if not line or line.startswith("#") or line.startswith("!"):
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Properties delimiter could be '=' or ':'
|
|
54
|
+
delimiter_idx = -1
|
|
55
|
+
for idx, char in enumerate(line):
|
|
56
|
+
if char in ("=", ":"):
|
|
57
|
+
delimiter_idx = idx
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
if delimiter_idx == -1:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
key = line[:delimiter_idx].strip()
|
|
64
|
+
val = line[delimiter_idx + 1:].strip()
|
|
65
|
+
|
|
66
|
+
if not key:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
parsed_val = _parse_properties_value(val)
|
|
70
|
+
|
|
71
|
+
# Tokenize key by dot delimiter to create hierarchical structure
|
|
72
|
+
parts = [p.lower() for p in key.split(".") if p]
|
|
73
|
+
if not parts:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
curr = data
|
|
77
|
+
for part in parts[:-1]:
|
|
78
|
+
if part not in curr or not isinstance(curr[part], dict):
|
|
79
|
+
curr[part] = {}
|
|
80
|
+
curr = curr[part]
|
|
81
|
+
curr[parts[-1]] = parsed_val
|
|
82
|
+
|
|
83
|
+
except (IOError, PermissionError) as e:
|
|
84
|
+
logger.exception(f"Failed to read or parse properties file: {file_path.name}", exc_info=e)
|
|
85
|
+
|
|
86
|
+
return data
|