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.
Files changed (50) hide show
  1. {dynamic_config_loader-2.3.0/src/dynamic_config_loader.egg-info → dynamic_config_loader-2.5.0}/PKG-INFO +14 -2
  2. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/README.md +11 -0
  3. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/pyproject.toml +8 -1
  4. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/__init__.py +9 -0
  5. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/__init__.py +36 -0
  6. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/merger.py +73 -0
  7. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/env/parser.py +86 -0
  8. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/__init__.py +34 -0
  9. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/merger.py +63 -0
  10. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/ini/parser.py +62 -0
  11. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/__init__.py +36 -0
  12. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/merger.py +73 -0
  13. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/properties/parser.py +86 -0
  14. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/__init__.py +34 -0
  15. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/merger.py +63 -0
  16. dynamic_config_loader-2.5.0/src/dynamic_config_loader/strategies/toml/parser.py +48 -0
  17. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0/src/dynamic_config_loader.egg-info}/PKG-INFO +14 -2
  18. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/SOURCES.txt +16 -0
  19. dynamic_config_loader-2.5.0/src/dynamic_config_loader.egg-info/requires.txt +4 -0
  20. dynamic_config_loader-2.5.0/tests/test_env_strategy.py +120 -0
  21. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_factory.py +25 -1
  22. dynamic_config_loader-2.5.0/tests/test_ini_strategy.py +118 -0
  23. dynamic_config_loader-2.5.0/tests/test_properties_strategy.py +120 -0
  24. dynamic_config_loader-2.5.0/tests/test_toml_strategy.py +118 -0
  25. dynamic_config_loader-2.3.0/src/dynamic_config_loader.egg-info/requires.txt +0 -1
  26. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/AUTHORS +0 -0
  27. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/LICENSE +0 -0
  28. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/NOTICE +0 -0
  29. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/setup.cfg +0 -0
  30. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/__init__.py +0 -0
  31. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/environment.py +0 -0
  32. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/loader.py +0 -0
  33. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/base_strategy.py +0 -0
  34. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/__init__.py +0 -0
  35. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/merger.py +0 -0
  36. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/json/parser.py +0 -0
  37. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/__init__.py +0 -0
  38. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/merger.py +0 -0
  39. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/xml/parser.py +0 -0
  40. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/__init__.py +0 -0
  41. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/merger.py +0 -0
  42. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader/strategies/yaml/parser.py +0 -0
  43. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/dependency_links.txt +0 -0
  44. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/src/dynamic_config_loader.egg-info/top_level.txt +0 -0
  45. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_deep_merge.py +0 -0
  46. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_env_injection.py +0 -0
  47. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_json_strategy.py +0 -0
  48. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_loader.py +0 -0
  49. {dynamic_config_loader-2.3.0 → dynamic_config_loader-2.5.0}/tests/test_logging_safety.py +0 -0
  50. {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.0
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.3.0"
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