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 +8 -0
- dictparser/__main__.py +46 -0
- dictparser/_version.py +1 -0
- dictparser/dictparser.py +152 -0
- dictparser/py.typed +1 -0
- ga_dictparser-0.1.0.dist-info/METADATA +131 -0
- ga_dictparser-0.1.0.dist-info/RECORD +11 -0
- ga_dictparser-0.1.0.dist-info/WHEEL +5 -0
- ga_dictparser-0.1.0.dist-info/entry_points.txt +2 -0
- ga_dictparser-0.1.0.dist-info/licenses/LICENSE +21 -0
- ga_dictparser-0.1.0.dist-info/top_level.txt +1 -0
dictparser/__init__.py
ADDED
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"
|
dictparser/dictparser.py
ADDED
|
@@ -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,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
|