ga-dictparser 0.1.0__py3-none-any.whl

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.
dictparser/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """template package
2
+ Simple library functions for the project.
3
+ """
4
+
5
+ from ._version import __version__
6
+ from .dictparser import DictParser
7
+
8
+ __all__ = ["__version__", "DictParser"]
dictparser/__main__.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from .dictparser import DictParser
8
+
9
+
10
+ def _build_parser() -> argparse.ArgumentParser:
11
+ parser = argparse.ArgumentParser(description="Read and resolve dictionary/JSON/YAML configuration files.")
12
+ parser.add_argument("file", help="Input configuration file (.json, .yaml, .yml)")
13
+ parser.add_argument("-k", "--key", help="Dot-separated key path to resolve")
14
+ parser.add_argument("-o", "--output", help="Output file path to save the result")
15
+ parser.add_argument(
16
+ "-f",
17
+ "--format",
18
+ choices=["json", "yaml"],
19
+ help="Force output format. If omitted, infer from output file extension; default is json.",
20
+ )
21
+ return parser
22
+
23
+
24
+ def _to_text(value: Any) -> str:
25
+ if isinstance(value, (dict, list, tuple, set)):
26
+ parser = DictParser({})
27
+ return parser.to_json(value)
28
+ return str(value)
29
+
30
+
31
+ def main() -> int:
32
+ args = _build_parser().parse_args()
33
+ parser = DictParser(args.file)
34
+
35
+ value = parser.get(args.key) if args.key else parser.get_all()
36
+
37
+ if args.output:
38
+ parser.save(Path(args.output), data=value, format=args.format)
39
+ else:
40
+ print(_to_text(value))
41
+
42
+ return 0
43
+
44
+
45
+ if __name__ == "__main__":
46
+ raise SystemExit(main())
dictparser/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,152 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ from string import Formatter
4
+ import json
5
+
6
+
7
+ class DictParser:
8
+ def __init__(self, params_or_path: dict[str, Any] | str | Path):
9
+ assert isinstance(params_or_path, (dict, str, Path)), (
10
+ "params_or_path must be a dict or a file path (str or Path)"
11
+ )
12
+
13
+ if isinstance(params_or_path, dict):
14
+ self.params: dict[str, Any] = params_or_path
15
+ else:
16
+ self.params = self._load_params_from_file(params_or_path)
17
+
18
+ def _load_params_from_file(self, file_path: str | Path) -> dict[str, Any]:
19
+ file_path = Path(file_path)
20
+ if not file_path.exists():
21
+ raise FileNotFoundError(f"File not found: {file_path}")
22
+
23
+ if file_path.suffix == ".json":
24
+ import json
25
+
26
+ with open(file_path, "r") as f:
27
+ return json.load(f)
28
+ elif file_path.suffix in [".yaml", ".yml"]:
29
+ try:
30
+ import yaml
31
+ except ImportError:
32
+ raise ImportError("PyYAML is required to load YAML files. Install it with 'pip install pyyaml'")
33
+
34
+ with open(file_path, "r") as f:
35
+ return yaml.safe_load(f)
36
+ else:
37
+ raise ValueError("Unsupported file format. Use .json or .yaml/.yml.")
38
+
39
+ def get(self, path: str, *args: Any, default: Any = None, copy: bool = True) -> Any:
40
+ """
41
+ Get a parameter from the params dictionary using a dot-separated path.
42
+ """
43
+ # Build the navigation path, supporting optional additional path fragments.
44
+ keys = path.split(".")
45
+ if args:
46
+ keys = keys + [str(arg) for arg in args if arg is not None]
47
+ value: dict[str, Any] | list[Any] | tuple[Any, ...] | None = self.params
48
+ for key in keys:
49
+ # Dispatch lookup by container type: mapping keys or sequence indexes.
50
+ if isinstance(value, (dict)) and key in value:
51
+ value = value[key]
52
+ elif isinstance(value, (list, tuple)) and key.isdigit():
53
+ value = value[int(key)]
54
+ else:
55
+ return self.get_parametric_name(default)
56
+ # Return a shallow copy for mutable collections by default to prevent accidental in-place edits.
57
+ if copy and isinstance(value, (dict, list)):
58
+ if isinstance(value, dict):
59
+ value = value.copy()
60
+ else: # isinstance(value, list)
61
+ value = value.copy()
62
+ if value is None:
63
+ return self.get_parametric_name(default)
64
+ return self.get_parametric_name(value)
65
+
66
+ def _normlize_key(self, key: str) -> str:
67
+ """
68
+ Normalize a key by removing leading/trailing whitespace and converting to lowercase.
69
+ """
70
+ return key.replace(".", "_").lower()
71
+
72
+ def get_parametric_name(self, name: str | dict[str, Any] | list[Any] | tuple[Any, ...] | set[Any]) -> Any:
73
+ if isinstance(name, str):
74
+ # Start from top-level params so placeholders can reference sibling keys.
75
+ kwargs = self.params.copy()
76
+ keys = [i[1] for i in Formatter().parse(name) if i[1] is not None and i[1] not in kwargs]
77
+ # print(keys)
78
+ if keys:
79
+ for k in keys:
80
+ if k not in kwargs:
81
+ # Resolve missing placeholders recursively through the same public API.
82
+ kwargs[k] = self.get(k)
83
+ else:
84
+ kwargs[k] = k
85
+ for k in kwargs:
86
+ name = name.replace("{" + k + "}", str(kwargs[k]))
87
+ elif isinstance(name, dict):
88
+ # Recurse deeply to resolve placeholders in nested containers.
89
+ name = {k: self.get_parametric_name(v) for k, v in name.items()}
90
+ elif isinstance(name, list):
91
+ name = [self.get_parametric_name(n) for n in name]
92
+ elif isinstance(name, tuple):
93
+ name = tuple(self.get_parametric_name(n) for n in name)
94
+ elif isinstance(name, set): # pyright: ignore[reportUnnecessaryIsInstance]
95
+ results = (self.get_parametric_name(n) for n in name)
96
+ name = {n for n in results if isinstance(n, (str, int, float, bool, tuple))}
97
+ return name
98
+
99
+ def get_all(self) -> dict[str, Any]:
100
+ """
101
+ Get the entire params dictionary with all placeholders resolved.
102
+ """
103
+ return self.get_parametric_name(self.params)
104
+
105
+ def to_json(self, data: Any = None, indent: int = 2, ensure_ascii: bool = False) -> str:
106
+ """
107
+ Serialize resolved configuration to a JSON string.
108
+ """
109
+ payload = self.get_all() if data is None else data
110
+ return json.dumps(payload, indent=indent, ensure_ascii=ensure_ascii)
111
+
112
+ def to_yaml(self, data: Any = None) -> str:
113
+ """
114
+ Serialize resolved configuration to a YAML string.
115
+ """
116
+ try:
117
+ import yaml
118
+ except ImportError as exc:
119
+ raise ImportError("PyYAML is required to dump YAML. Install it with 'pip install pyyaml'") from exc
120
+
121
+ payload = self.get_all() if data is None else data
122
+ return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True)
123
+
124
+ def save(self, file_path: str | Path, data: Any = None, format: str | None = None) -> Path:
125
+ """
126
+ Save configuration (or custom data) to JSON or YAML.
127
+
128
+ If format is not provided, it is inferred from the target suffix.
129
+ Supported suffixes: .json, .yaml, .yml.
130
+ Default format is json.
131
+ """
132
+ path = Path(file_path)
133
+ output_format = (format or "").strip().lower()
134
+ if not output_format:
135
+ suffix = path.suffix.lower()
136
+ if suffix in (".yaml", ".yml"):
137
+ output_format = "yaml"
138
+ elif suffix == ".json":
139
+ output_format = "json"
140
+ else:
141
+ output_format = "json"
142
+
143
+ if output_format not in {"json", "yaml"}:
144
+ raise ValueError("Unsupported output format. Use 'json' or 'yaml'.")
145
+
146
+ payload = self.get_all() if data is None else data
147
+ path.parent.mkdir(parents=True, exist_ok=True)
148
+ if output_format == "json":
149
+ path.write_text(self.to_json(payload), encoding="utf-8")
150
+ else:
151
+ path.write_text(self.to_yaml(payload), encoding="utf-8")
152
+ return path
dictparser/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: ga-dictparser
3
+ Version: 0.1.0
4
+ Summary: Read a dictionary or JSON/YAML file with dynamic parameters
5
+ Author-email: Andrea Gemma <andrea.gemma@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://github.com/andreagemma/dictparser#readme
8
+ Project-URL: Issues, https://github.com/andreagemma/dictparser/issues
9
+ Project-URL: Source, https://github.com/andreagemma/dictparser
10
+ Keywords: configuration,dictionary,json,parser,dynamic,parameters
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Database :: Front-Ends
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pyyaml>=6.0.3
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8.0; extra == "test"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "test"
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: mypy>=1.10; extra == "dev"
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.5; extra == "dev"
35
+ Requires-Dist: twine>=5.1; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ ## DictParser
39
+
40
+ DictParser is a typed utility to read configuration values from dictionaries, JSON files, and YAML files with support for dynamic placeholders.
41
+
42
+ ### Features
43
+
44
+ - Dot-path access for nested dictionaries and lists
45
+ - Runtime placeholder resolution with `{key}` syntax
46
+ - Recursive resolution for nested containers (`dict`, `list`, `tuple`, `set`)
47
+ - Optional shallow copy for mutable return values
48
+ - Full resolved export with `get_all()`
49
+ - JSON/YAML serialization helpers: `to_json()` and `to_yaml()`
50
+ - File export helper with format inference: `save()`
51
+
52
+ ### Installation
53
+
54
+ Install from source:
55
+
56
+ ```bash
57
+ pip install -e .
58
+ ```
59
+
60
+ ### Quick Example
61
+
62
+ ```python
63
+ from dictparser import DictParser
64
+
65
+ data = {
66
+ "aa": "and",
67
+ "c": [0, 1, "{a.c.d}"],
68
+ "a": {"b": 1, "c": {"d": "{aa}", "e": "{a.b}"}},
69
+ }
70
+
71
+ parser = DictParser(data)
72
+
73
+ assert parser.get("a.b") == 1
74
+ assert parser.get("a.c.d") == "and"
75
+ assert parser.get("a.c.e") == "1"
76
+ assert parser.get("c.1") == 1
77
+ assert parser.get("c.2") == "and"
78
+ ```
79
+
80
+ ### Notes on Resolution Dispatch
81
+
82
+ The resolver internally dispatches by container type while navigating paths:
83
+
84
+ - dictionary key lookup for mappings
85
+ - integer index lookup for lists and tuples
86
+ - recursive placeholder expansion for nested containers
87
+
88
+ This behavior is now documented directly in source comments in `src/dictparser/dictparser.py`.
89
+
90
+ ### API Additions
91
+
92
+ The class exposes additional helpers for full output and persistence:
93
+
94
+ ```python
95
+ resolved = parser.get_all()
96
+ json_text = parser.to_json()
97
+ yaml_text = parser.to_yaml()
98
+
99
+ # Infer format from output suffix (.json/.yaml/.yml), fallback json.
100
+ parser.save("output.json")
101
+ parser.save("output.yaml")
102
+
103
+ # Force format regardless of output suffix.
104
+ parser.save("output.txt", format="json")
105
+ parser.save("output.txt", format="yaml")
106
+ ```
107
+
108
+ ### CLI Usage
109
+
110
+ The package exposes a CLI entry point:
111
+
112
+ ```bash
113
+ dictparser INPUT_FILE [-k KEY] [-o OUTPUT_FILE] [-f {json,yaml}]
114
+ ```
115
+
116
+ - Without `-k/--key`, the CLI resolves and returns the entire dictionary (`get_all`).
117
+ - With `-k/--key`, the CLI resolves and returns only the selected key (`get`).
118
+ - With `-o/--output`, the CLI saves the result to file.
119
+ - With `-f/--format`, the output format is forced.
120
+ - Without `-f/--format`, format is inferred from output filename and defaults to json.
121
+
122
+ ### Third-Party Licenses
123
+
124
+ Third-party licenses are archived in:
125
+
126
+ - `licenses/third_party/packages/`
127
+
128
+ Dependency summary and source links are listed in:
129
+
130
+ - `licenses/third_party/summary.tsv`
131
+ - `THIRD_PARTY_NOTICES.md`
@@ -0,0 +1,11 @@
1
+ dictparser/__init__.py,sha256=EeOtv5wzq1f4cS66vC2roh5CepGIbYHkDl-aGYKFt-w,177
2
+ dictparser/__main__.py,sha256=rnXDUYNhRzcwmwX_KLhoggK_qPqnoHC_QDrt8EEwSZM,1314
3
+ dictparser/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ dictparser/dictparser.py,sha256=Eqdb-gd6QDPQRtjcVHdNIEVmP1cAbcq1D7FS986Zp0Q,6540
5
+ dictparser/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
6
+ ga_dictparser-0.1.0.dist-info/licenses/LICENSE,sha256=qu2EXOee5U25eaDf50dc7Rmuysbz9BNo0hhJ2D9vawc,1069
7
+ ga_dictparser-0.1.0.dist-info/METADATA,sha256=n6DCIHKBQWEVlPlQQTgcmeCRlHcd6T5w-M3M_UPdV4o,4052
8
+ ga_dictparser-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ ga_dictparser-0.1.0.dist-info/entry_points.txt,sha256=9BABCbDlvZ-fq-4ejcn8D9EN_6mw3CroKFfc-R6de-k,56
10
+ ga_dictparser-0.1.0.dist-info/top_level.txt,sha256=kReEn53et-aU4kt1tRt9Tjw0OCwc1yYgKYGix28bIm8,11
11
+ ga_dictparser-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dictparser = dictparser.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrea Gemma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dictparser