extended-data 8.0.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.
- extended_data/__init__.py +105 -0
- extended_data/_version.py +16 -0
- extended_data/cli.py +204 -0
- extended_data/containers/__init__.py +17 -0
- extended_data/containers/factory.py +57 -0
- extended_data/containers/mappings.py +181 -0
- extended_data/containers/sequences.py +403 -0
- extended_data/containers/strings.py +276 -0
- extended_data/inputs/__init__.py +14 -0
- extended_data/inputs/__main__.py +412 -0
- extended_data/inputs/decorators.py +346 -0
- extended_data/inputs/py.typed +0 -0
- extended_data/io/__init__.py +50 -0
- extended_data/io/base64.py +65 -0
- extended_data/io/exporters.py +151 -0
- extended_data/io/files.py +619 -0
- extended_data/io/importers.py +53 -0
- extended_data/logging/__init__.py +13 -0
- extended_data/logging/const.py +15 -0
- extended_data/logging/handlers.py +61 -0
- extended_data/logging/log_types.py +18 -0
- extended_data/logging/logging.py +663 -0
- extended_data/logging/py.typed +0 -0
- extended_data/logging/utils.py +167 -0
- extended_data/primitives/__init__.py +172 -0
- extended_data/primitives/formats/__init__.py +47 -0
- extended_data/primitives/formats/_normalization.py +15 -0
- extended_data/primitives/formats/errors.py +72 -0
- extended_data/primitives/formats/hcl.py +264 -0
- extended_data/primitives/formats/json.py +100 -0
- extended_data/primitives/formats/toml.py +48 -0
- extended_data/primitives/formats/yaml/__init__.py +43 -0
- extended_data/primitives/formats/yaml/constructors.py +58 -0
- extended_data/primitives/formats/yaml/dumpers.py +75 -0
- extended_data/primitives/formats/yaml/loaders.py +31 -0
- extended_data/primitives/formats/yaml/representers.py +82 -0
- extended_data/primitives/formats/yaml/tag_classes.py +79 -0
- extended_data/primitives/formats/yaml/utils.py +75 -0
- extended_data/primitives/introspection.py +168 -0
- extended_data/primitives/mappings.py +399 -0
- extended_data/primitives/matching.py +82 -0
- extended_data/primitives/numbers.py +236 -0
- extended_data/primitives/redaction.py +126 -0
- extended_data/primitives/sequences.py +85 -0
- extended_data/primitives/serialization.py +37 -0
- extended_data/primitives/splitting.py +56 -0
- extended_data/primitives/state.py +162 -0
- extended_data/primitives/string_transforms.py +74 -0
- extended_data/primitives/strings.py +127 -0
- extended_data/primitives/transformations/__init__.py +13 -0
- extended_data/primitives/transformations/numbers/__init__.py +40 -0
- extended_data/primitives/transformations/numbers/notation.py +181 -0
- extended_data/primitives/transformations/numbers/words.py +403 -0
- extended_data/primitives/transformations/strings/__init__.py +26 -0
- extended_data/primitives/transformations/strings/inflection.py +95 -0
- extended_data/primitives/types.py +532 -0
- extended_data/py.typed +0 -0
- extended_data/workflows/__init__.py +394 -0
- extended_data-8.0.0.dist-info/METADATA +189 -0
- extended_data-8.0.0.dist-info/RECORD +63 -0
- extended_data-8.0.0.dist-info/WHEEL +4 -0
- extended_data-8.0.0.dist-info/entry_points.txt +2 -0
- extended_data-8.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Extended Data.
|
|
2
|
+
|
|
3
|
+
This package provides Python utilities for structured data primitives, inputs,
|
|
4
|
+
logging, file processing, and workflow-oriented data operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from extended_data._version import __version__
|
|
10
|
+
from extended_data.containers import (
|
|
11
|
+
ExtendedDict,
|
|
12
|
+
ExtendedList,
|
|
13
|
+
ExtendedSet,
|
|
14
|
+
ExtendedString,
|
|
15
|
+
ExtendedTuple,
|
|
16
|
+
extend_data,
|
|
17
|
+
to_builtin,
|
|
18
|
+
)
|
|
19
|
+
from extended_data.inputs import InputProvider, directed_inputs, input_config
|
|
20
|
+
from extended_data.io.base64 import base64_decode, base64_encode
|
|
21
|
+
from extended_data.io.exporters import (
|
|
22
|
+
make_raw_data_export_safe,
|
|
23
|
+
wrap_raw_data_for_export,
|
|
24
|
+
)
|
|
25
|
+
from extended_data.io.files import (
|
|
26
|
+
DataFile,
|
|
27
|
+
FilePath,
|
|
28
|
+
clone_repository_to_temp,
|
|
29
|
+
decode_file,
|
|
30
|
+
delete_file,
|
|
31
|
+
file_path_depth,
|
|
32
|
+
file_path_rel_to_root,
|
|
33
|
+
get_encoding_for_file_path,
|
|
34
|
+
get_parent_repository,
|
|
35
|
+
get_repository_name,
|
|
36
|
+
get_tld,
|
|
37
|
+
is_url,
|
|
38
|
+
match_file_extensions,
|
|
39
|
+
read_data_file,
|
|
40
|
+
read_file,
|
|
41
|
+
resolve_local_path,
|
|
42
|
+
write_file,
|
|
43
|
+
)
|
|
44
|
+
from extended_data.io.importers import unwrap_raw_data_from_import
|
|
45
|
+
from extended_data.logging import ExitRunError, KeyTransform, Logging
|
|
46
|
+
from extended_data.primitives.formats.errors import DataDecodeError
|
|
47
|
+
from extended_data.workflows import (
|
|
48
|
+
DATA_TRANSFORM_STEPS,
|
|
49
|
+
DataWorkflow,
|
|
50
|
+
StepLike,
|
|
51
|
+
WorkflowAction,
|
|
52
|
+
WorkflowResult,
|
|
53
|
+
WorkflowStep,
|
|
54
|
+
data_transform_action,
|
|
55
|
+
list_data_transform_steps,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"DATA_TRANSFORM_STEPS",
|
|
61
|
+
"DataDecodeError",
|
|
62
|
+
"DataFile",
|
|
63
|
+
"DataWorkflow",
|
|
64
|
+
"ExitRunError",
|
|
65
|
+
"ExtendedDict",
|
|
66
|
+
"ExtendedList",
|
|
67
|
+
"ExtendedSet",
|
|
68
|
+
"ExtendedString",
|
|
69
|
+
"ExtendedTuple",
|
|
70
|
+
"FilePath",
|
|
71
|
+
"InputProvider",
|
|
72
|
+
"KeyTransform",
|
|
73
|
+
"Logging",
|
|
74
|
+
"StepLike",
|
|
75
|
+
"WorkflowAction",
|
|
76
|
+
"WorkflowResult",
|
|
77
|
+
"WorkflowStep",
|
|
78
|
+
"__version__",
|
|
79
|
+
"base64_decode",
|
|
80
|
+
"base64_encode",
|
|
81
|
+
"clone_repository_to_temp",
|
|
82
|
+
"data_transform_action",
|
|
83
|
+
"decode_file",
|
|
84
|
+
"delete_file",
|
|
85
|
+
"directed_inputs",
|
|
86
|
+
"extend_data",
|
|
87
|
+
"file_path_depth",
|
|
88
|
+
"file_path_rel_to_root",
|
|
89
|
+
"get_encoding_for_file_path",
|
|
90
|
+
"get_parent_repository",
|
|
91
|
+
"get_repository_name",
|
|
92
|
+
"get_tld",
|
|
93
|
+
"input_config",
|
|
94
|
+
"is_url",
|
|
95
|
+
"list_data_transform_steps",
|
|
96
|
+
"make_raw_data_export_safe",
|
|
97
|
+
"match_file_extensions",
|
|
98
|
+
"read_data_file",
|
|
99
|
+
"read_file",
|
|
100
|
+
"resolve_local_path",
|
|
101
|
+
"to_builtin",
|
|
102
|
+
"unwrap_raw_data_from_import",
|
|
103
|
+
"wrap_raw_data_for_export",
|
|
104
|
+
"write_file",
|
|
105
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Package version helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_version() -> str:
|
|
9
|
+
"""Return the installed extended-data distribution version."""
|
|
10
|
+
try:
|
|
11
|
+
return version("extended-data")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
return "0+unknown"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__version__ = get_version()
|
extended_data/cli.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Top-level command line interface for Extended Data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import Any, cast
|
|
10
|
+
|
|
11
|
+
from extended_data.io import DataFile
|
|
12
|
+
from extended_data.primitives.redaction import redact_sensitive_text
|
|
13
|
+
from extended_data.workflows import DataWorkflow, WorkflowResult, list_data_transform_steps
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
OUTPUT_ENCODINGS = ("json", "yaml", "toml", "hcl", "raw")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _write_stdout(message: str) -> None:
|
|
20
|
+
"""Write one CLI output line."""
|
|
21
|
+
sys.stdout.write(f"{message}\n")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write_stderr(message: str) -> None:
|
|
25
|
+
"""Write one CLI error line."""
|
|
26
|
+
sys.stderr.write(f"{redact_sensitive_text(message)}\n")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _decode_artifact(args: argparse.Namespace) -> DataFile:
|
|
30
|
+
"""Decode an inline payload or file path into a DataFile artifact."""
|
|
31
|
+
value = getattr(args, "value", None)
|
|
32
|
+
file_path = getattr(args, "file_path", None)
|
|
33
|
+
|
|
34
|
+
if value is not None and file_path is not None:
|
|
35
|
+
raise ValueError("pass either VALUE or --file, not both")
|
|
36
|
+
if value is None and file_path is None:
|
|
37
|
+
raise ValueError("pass VALUE or --file")
|
|
38
|
+
if file_path is not None:
|
|
39
|
+
return DataFile.read(file_path, suffix=args.suffix)
|
|
40
|
+
return DataFile.decode(cast(str, value), suffix=args.suffix)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_decode(args: argparse.Namespace) -> int:
|
|
44
|
+
"""Decode structured data and write it through the shared export boundary."""
|
|
45
|
+
try:
|
|
46
|
+
artifact = _decode_artifact(args)
|
|
47
|
+
_write_stdout(artifact.wrap_for_export(allow_encoding=args.output, **_json_format_opts(args)))
|
|
48
|
+
return 0
|
|
49
|
+
except Exception as e:
|
|
50
|
+
_write_stderr(str(e))
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
|
55
|
+
"""Decode structured data and write its DataFile metadata."""
|
|
56
|
+
try:
|
|
57
|
+
artifact = _decode_artifact(args)
|
|
58
|
+
_write_stdout(artifact.metadata.wrap_for_export(allow_encoding=args.output, **_json_format_opts(args)))
|
|
59
|
+
return 0
|
|
60
|
+
except Exception as e:
|
|
61
|
+
_write_stderr(str(e))
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _json_format_opts(args: argparse.Namespace) -> dict[str, Any]:
|
|
66
|
+
"""Return common JSON formatting options for CLI export commands."""
|
|
67
|
+
if args.output == "json" and not args.compact:
|
|
68
|
+
return {"indent_2": True}
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _merge_workflow(args: argparse.Namespace) -> DataWorkflow:
|
|
73
|
+
"""Build a layered merge workflow from CLI arguments."""
|
|
74
|
+
file_paths = args.file_paths
|
|
75
|
+
if len(file_paths) < 2:
|
|
76
|
+
raise ValueError("merge requires at least two files")
|
|
77
|
+
|
|
78
|
+
workflow = DataWorkflow.from_file(file_paths[0], suffix=args.suffix)
|
|
79
|
+
for file_path in file_paths[1:]:
|
|
80
|
+
workflow = workflow.merge_file(file_path, suffix=args.suffix)
|
|
81
|
+
return workflow
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def cmd_merge(args: argparse.Namespace) -> int:
|
|
85
|
+
"""Merge structured files through DataWorkflow and write or print the result."""
|
|
86
|
+
try:
|
|
87
|
+
workflow = _merge_workflow(args)
|
|
88
|
+
result: WorkflowResult
|
|
89
|
+
if args.write:
|
|
90
|
+
result = workflow.write(args.write, encoding=args.output, allow_empty=args.allow_empty)
|
|
91
|
+
else:
|
|
92
|
+
result = workflow.result()
|
|
93
|
+
_write_stdout(result.wrap_for_export(allow_encoding=args.output, **_json_format_opts(args)))
|
|
94
|
+
return 0
|
|
95
|
+
except Exception as e:
|
|
96
|
+
_write_stderr(str(e))
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _transform_workflow(args: argparse.Namespace) -> DataWorkflow:
|
|
101
|
+
"""Build a workflow that applies named Tier 2 transforms."""
|
|
102
|
+
steps = args.steps or []
|
|
103
|
+
if not steps:
|
|
104
|
+
raise ValueError("transform requires at least one --step")
|
|
105
|
+
|
|
106
|
+
return _decode_artifact(args).workflow().transform(*steps)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def cmd_transform(args: argparse.Namespace) -> int:
|
|
110
|
+
"""Apply named Tier 2 transforms through DataWorkflow."""
|
|
111
|
+
try:
|
|
112
|
+
workflow = _transform_workflow(args)
|
|
113
|
+
result: WorkflowResult
|
|
114
|
+
if args.write:
|
|
115
|
+
result = workflow.write(args.write, encoding=args.output, allow_empty=args.allow_empty)
|
|
116
|
+
else:
|
|
117
|
+
result = workflow.result()
|
|
118
|
+
_write_stdout(result.wrap_for_export(allow_encoding=args.output, **_json_format_opts(args)))
|
|
119
|
+
return 0
|
|
120
|
+
except Exception as e:
|
|
121
|
+
_write_stderr(str(e))
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
126
|
+
"""Build the top-level Extended Data argument parser."""
|
|
127
|
+
parser = argparse.ArgumentParser(
|
|
128
|
+
prog="extended-data",
|
|
129
|
+
description="CLI for Extended Data primitives, files, and workflows",
|
|
130
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
131
|
+
epilog="""
|
|
132
|
+
Examples:
|
|
133
|
+
extended-data decode '{"service": {"name": "api"}}' --suffix json
|
|
134
|
+
extended-data decode --file config.yaml --output json
|
|
135
|
+
extended-data inspect --file config.yaml
|
|
136
|
+
extended-data merge base.yaml env.yaml --output yaml
|
|
137
|
+
extended-data transform --file payload.json --step reconstruct --step unhump
|
|
138
|
+
""",
|
|
139
|
+
)
|
|
140
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
141
|
+
|
|
142
|
+
decode_parser = subparsers.add_parser("decode", help="Decode inline data or a file")
|
|
143
|
+
decode_parser.add_argument("value", nargs="?", help="Inline payload to decode")
|
|
144
|
+
decode_parser.add_argument("--file", dest="file_path", help="File path or URL to decode")
|
|
145
|
+
decode_parser.add_argument("--suffix", help="Input format override")
|
|
146
|
+
decode_parser.add_argument("--output", choices=OUTPUT_ENCODINGS, default="json", help="Output encoding")
|
|
147
|
+
decode_parser.add_argument("--compact", action="store_true", help="Compact JSON output")
|
|
148
|
+
decode_parser.set_defaults(func=cmd_decode)
|
|
149
|
+
|
|
150
|
+
inspect_parser = subparsers.add_parser("inspect", help="Decode data and print artifact metadata")
|
|
151
|
+
inspect_parser.add_argument("value", nargs="?", help="Inline payload to inspect")
|
|
152
|
+
inspect_parser.add_argument("--file", dest="file_path", help="File path or URL to inspect")
|
|
153
|
+
inspect_parser.add_argument("--suffix", help="Input format override")
|
|
154
|
+
inspect_parser.add_argument("--output", choices=OUTPUT_ENCODINGS, default="json", help="Output encoding")
|
|
155
|
+
inspect_parser.add_argument("--compact", action="store_true", help="Compact JSON output")
|
|
156
|
+
inspect_parser.set_defaults(func=cmd_inspect)
|
|
157
|
+
|
|
158
|
+
merge_parser = subparsers.add_parser("merge", help="Deep merge structured files")
|
|
159
|
+
merge_parser.add_argument("file_paths", nargs="+", help="Structured files to merge in order")
|
|
160
|
+
merge_parser.add_argument("--suffix", help="Input format override for all files")
|
|
161
|
+
merge_parser.add_argument("--output", choices=OUTPUT_ENCODINGS, default="json", help="Output encoding")
|
|
162
|
+
merge_parser.add_argument("--compact", action="store_true", help="Compact JSON output")
|
|
163
|
+
merge_parser.add_argument("--write", help="Write merged output to this file")
|
|
164
|
+
merge_parser.add_argument("--allow-empty", action="store_true", help="Allow writing empty merged output")
|
|
165
|
+
merge_parser.set_defaults(func=cmd_merge)
|
|
166
|
+
|
|
167
|
+
transform_parser = subparsers.add_parser("transform", help="Apply named Extended Data transforms")
|
|
168
|
+
transform_parser.add_argument("value", nargs="?", help="Inline payload to transform")
|
|
169
|
+
transform_parser.add_argument("--file", dest="file_path", help="File path or URL to transform")
|
|
170
|
+
transform_parser.add_argument("--suffix", help="Input format override")
|
|
171
|
+
transform_parser.add_argument(
|
|
172
|
+
"--step",
|
|
173
|
+
dest="steps",
|
|
174
|
+
action="append",
|
|
175
|
+
choices=list_data_transform_steps(),
|
|
176
|
+
help="Transform step to apply in order",
|
|
177
|
+
)
|
|
178
|
+
transform_parser.add_argument("--output", choices=OUTPUT_ENCODINGS, default="json", help="Output encoding")
|
|
179
|
+
transform_parser.add_argument("--compact", action="store_true", help="Compact JSON output")
|
|
180
|
+
transform_parser.add_argument("--write", help="Write transformed output to this file")
|
|
181
|
+
transform_parser.add_argument("--allow-empty", action="store_true", help="Allow writing empty transformed output")
|
|
182
|
+
transform_parser.set_defaults(func=cmd_transform)
|
|
183
|
+
|
|
184
|
+
return parser
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
188
|
+
"""Run the Extended Data CLI."""
|
|
189
|
+
args = list(argv) if argv is not None else sys.argv[1:]
|
|
190
|
+
parser = _build_parser()
|
|
191
|
+
parsed = parser.parse_args(args)
|
|
192
|
+
|
|
193
|
+
if not parsed.command:
|
|
194
|
+
parser.print_help()
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
return parsed.func(parsed)
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
return 130
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
sys.exit(main())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Tier 2 extended container classes."""
|
|
2
|
+
|
|
3
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
4
|
+
from extended_data.containers.mappings import ExtendedDict
|
|
5
|
+
from extended_data.containers.sequences import ExtendedList, ExtendedSet, ExtendedTuple
|
|
6
|
+
from extended_data.containers.strings import ExtendedString
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ExtendedDict",
|
|
11
|
+
"ExtendedList",
|
|
12
|
+
"ExtendedSet",
|
|
13
|
+
"ExtendedString",
|
|
14
|
+
"ExtendedTuple",
|
|
15
|
+
"extend_data",
|
|
16
|
+
"to_builtin",
|
|
17
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Factories for moving between plain data and extended containers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from extended_data.containers.mappings import ExtendedDict
|
|
9
|
+
from extended_data.containers.sequences import ExtendedList, ExtendedSet, ExtendedTuple
|
|
10
|
+
from extended_data.containers.strings import ExtendedString
|
|
11
|
+
from extended_data.primitives.formats.yaml import LiteralScalarString, YamlPairs, YamlTagged
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extend_data(value: Any) -> Any:
|
|
15
|
+
"""Recursively wrap built-in containers in Extended Data containers."""
|
|
16
|
+
if isinstance(value, YamlTagged | YamlPairs | LiteralScalarString):
|
|
17
|
+
return value
|
|
18
|
+
if isinstance(value, ExtendedString | ExtendedDict | ExtendedList | ExtendedSet | ExtendedTuple):
|
|
19
|
+
return value
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
return ExtendedString(value)
|
|
22
|
+
if isinstance(value, Mapping):
|
|
23
|
+
return ExtendedDict({key: extend_data(item) for key, item in value.items()})
|
|
24
|
+
if isinstance(value, list):
|
|
25
|
+
return ExtendedList(extend_data(item) for item in value)
|
|
26
|
+
if isinstance(value, tuple):
|
|
27
|
+
return ExtendedTuple(extend_data(item) for item in value)
|
|
28
|
+
if isinstance(value, set | frozenset):
|
|
29
|
+
return ExtendedSet(extend_data(item) for item in value)
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_builtin(value: Any) -> Any:
|
|
34
|
+
"""Recursively unwrap Extended Data containers to built-in Python values."""
|
|
35
|
+
if isinstance(value, YamlTagged | YamlPairs | LiteralScalarString):
|
|
36
|
+
return value
|
|
37
|
+
if isinstance(value, ExtendedString):
|
|
38
|
+
return str(value)
|
|
39
|
+
if isinstance(value, ExtendedDict):
|
|
40
|
+
return {to_builtin(key): to_builtin(item) for key, item in value.items()}
|
|
41
|
+
if isinstance(value, ExtendedList):
|
|
42
|
+
return [to_builtin(item) for item in value]
|
|
43
|
+
if isinstance(value, ExtendedTuple):
|
|
44
|
+
return tuple(to_builtin(item) for item in value)
|
|
45
|
+
if isinstance(value, ExtendedSet):
|
|
46
|
+
return {to_builtin(item) for item in value}
|
|
47
|
+
if isinstance(value, Mapping):
|
|
48
|
+
return {to_builtin(key): to_builtin(item) for key, item in value.items()}
|
|
49
|
+
if isinstance(value, list):
|
|
50
|
+
return [to_builtin(item) for item in value]
|
|
51
|
+
if isinstance(value, tuple):
|
|
52
|
+
return tuple(to_builtin(item) for item in value)
|
|
53
|
+
if isinstance(value, set):
|
|
54
|
+
return {to_builtin(item) for item in value}
|
|
55
|
+
if isinstance(value, frozenset):
|
|
56
|
+
return frozenset(to_builtin(item) for item in value)
|
|
57
|
+
return value
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Extended mapping container built on Tier 1 primitives."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import UserDict
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self, overload
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from _typeshed import SupportsKeysAndGetItem
|
|
12
|
+
|
|
13
|
+
from extended_data.containers.sequences import ExtendedList, ExtendedTuple
|
|
14
|
+
|
|
15
|
+
from extended_data.primitives.mappings import (
|
|
16
|
+
all_values_from_map,
|
|
17
|
+
deduplicate_map,
|
|
18
|
+
deep_merge,
|
|
19
|
+
filter_map,
|
|
20
|
+
first_non_empty_value_from_map,
|
|
21
|
+
flatten_map,
|
|
22
|
+
unhump_map,
|
|
23
|
+
)
|
|
24
|
+
from extended_data.primitives.splitting import split_dict_by_type
|
|
25
|
+
from extended_data.primitives.state import all_non_empty_in_dict, any_non_empty, yield_non_empty
|
|
26
|
+
from extended_data.primitives.types import reconstruct_special_types
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExtendedDict(UserDict[str, Any]):
|
|
30
|
+
"""Dictionary wrapper with chainable primitive operations."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, initialdata: Mapping[str, Any] | None = None, **kwargs: Any) -> None:
|
|
33
|
+
"""Initialize the extended dictionary."""
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.update(initialdata or {}, **kwargs)
|
|
36
|
+
|
|
37
|
+
def __setitem__(self, key: str, item: Any) -> None:
|
|
38
|
+
"""Set a value while preserving extended nested containers."""
|
|
39
|
+
from extended_data.containers.factory import extend_data
|
|
40
|
+
|
|
41
|
+
self.data[key] = extend_data(item)
|
|
42
|
+
|
|
43
|
+
@overload
|
|
44
|
+
def update(self, other: SupportsKeysAndGetItem[str, Any], /) -> None: ...
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
def update(self, other: SupportsKeysAndGetItem[str, Any], /, **kwargs: Any) -> None: ...
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def update(self, other: Iterable[tuple[str, Any]], /) -> None: ...
|
|
51
|
+
|
|
52
|
+
@overload
|
|
53
|
+
def update(self, other: Iterable[tuple[str, Any]], /, **kwargs: Any) -> None: ...
|
|
54
|
+
|
|
55
|
+
@overload
|
|
56
|
+
def update(self, **kwargs: Any) -> None: ...
|
|
57
|
+
|
|
58
|
+
def update(self, *args: Any, **kwargs: Any) -> None: # type: ignore[misc]
|
|
59
|
+
"""Update values while preserving extended nested containers."""
|
|
60
|
+
if len(args) > 1:
|
|
61
|
+
msg = f"update expected at most 1 argument, got {len(args)}"
|
|
62
|
+
raise TypeError(msg)
|
|
63
|
+
|
|
64
|
+
if args:
|
|
65
|
+
other = args[0]
|
|
66
|
+
if hasattr(other, "items"):
|
|
67
|
+
for key, value in other.items():
|
|
68
|
+
self[key] = value
|
|
69
|
+
elif hasattr(other, "keys") and hasattr(other, "__getitem__"):
|
|
70
|
+
keys = other.keys()
|
|
71
|
+
for key in keys:
|
|
72
|
+
self[key] = other[key]
|
|
73
|
+
else:
|
|
74
|
+
for key, value in other:
|
|
75
|
+
self[key] = value
|
|
76
|
+
|
|
77
|
+
for key, value in kwargs.items():
|
|
78
|
+
self[key] = value
|
|
79
|
+
|
|
80
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
|
81
|
+
"""Insert a default while returning the promoted stored value."""
|
|
82
|
+
if key not in self.data:
|
|
83
|
+
self[key] = default
|
|
84
|
+
return self.data[key]
|
|
85
|
+
|
|
86
|
+
def __ior__(self, other: Any) -> Self: # type: ignore[override,misc]
|
|
87
|
+
"""Update from a mapping or item iterable while preserving extended containers."""
|
|
88
|
+
self.update(other)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def deep_merge(self, *mappings: Mapping[str, Any]) -> ExtendedDict:
|
|
92
|
+
"""Return a deeply merged copy."""
|
|
93
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
94
|
+
|
|
95
|
+
return extend_data(deep_merge(to_builtin(self.data), *(to_builtin(mapping) for mapping in mappings)))
|
|
96
|
+
|
|
97
|
+
def flatten(self, *, separator: str = ".") -> ExtendedDict:
|
|
98
|
+
"""Return a flattened copy."""
|
|
99
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
100
|
+
|
|
101
|
+
return extend_data(flatten_map(to_builtin(self.data), separator=separator))
|
|
102
|
+
|
|
103
|
+
def filter(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
allowlist: list[str] | None = None,
|
|
107
|
+
denylist: list[str] | None = None,
|
|
108
|
+
) -> ExtendedTuple[ExtendedDict]:
|
|
109
|
+
"""Return accepted and rejected mapping entries."""
|
|
110
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
111
|
+
from extended_data.containers.sequences import ExtendedTuple
|
|
112
|
+
|
|
113
|
+
accepted, rejected = filter_map(to_builtin(self.data), allowlist=allowlist, denylist=denylist)
|
|
114
|
+
return ExtendedTuple((extend_data(accepted), extend_data(rejected)))
|
|
115
|
+
|
|
116
|
+
def compact(self) -> ExtendedDict:
|
|
117
|
+
"""Return a copy without values considered empty."""
|
|
118
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
119
|
+
|
|
120
|
+
return extend_data(all_non_empty_in_dict(to_builtin(self.data)))
|
|
121
|
+
|
|
122
|
+
def deduplicate(self) -> ExtendedDict:
|
|
123
|
+
"""Return a copy with nested duplicate list values removed."""
|
|
124
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
125
|
+
|
|
126
|
+
return extend_data(deduplicate_map(to_builtin(self.data)))
|
|
127
|
+
|
|
128
|
+
def unhump(self, *, drop_without_prefix: str | None = None) -> ExtendedDict:
|
|
129
|
+
"""Return a copy with camelCase keys converted to snake_case."""
|
|
130
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
131
|
+
|
|
132
|
+
return extend_data(unhump_map(to_builtin(self.data), drop_without_prefix=drop_without_prefix))
|
|
133
|
+
|
|
134
|
+
def all_values(self) -> ExtendedList[Any]:
|
|
135
|
+
"""Return all values from the nested mapping."""
|
|
136
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
137
|
+
|
|
138
|
+
return extend_data(all_values_from_map(to_builtin(self.data)))
|
|
139
|
+
|
|
140
|
+
def split_by_type(self, *, primitive_only: bool = False) -> ExtendedDict:
|
|
141
|
+
"""Return mapping entries grouped by value type name."""
|
|
142
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
143
|
+
|
|
144
|
+
grouped = split_dict_by_type(to_builtin(self.data), primitive_only=primitive_only)
|
|
145
|
+
return extend_data({type_key.__name__: values for type_key, values in grouped.items()})
|
|
146
|
+
|
|
147
|
+
def first_non_empty_value(self, *keys: str) -> Any:
|
|
148
|
+
"""Return the first non-empty value for the provided keys."""
|
|
149
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
150
|
+
|
|
151
|
+
return extend_data(first_non_empty_value_from_map(to_builtin(self.data), *keys))
|
|
152
|
+
|
|
153
|
+
def first_non_empty_entry(self, *keys: str) -> ExtendedDict:
|
|
154
|
+
"""Return the first non-empty keyed entry for the provided keys."""
|
|
155
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
156
|
+
|
|
157
|
+
return extend_data(any_non_empty(to_builtin(self.data), *keys))
|
|
158
|
+
|
|
159
|
+
def non_empty_entries(self, *keys: str) -> ExtendedList[ExtendedDict]:
|
|
160
|
+
"""Return all non-empty keyed entries for the provided keys."""
|
|
161
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
162
|
+
|
|
163
|
+
return extend_data(list(yield_non_empty(to_builtin(self.data), *keys)))
|
|
164
|
+
|
|
165
|
+
def reconstruct_special_types(self, *, fail_silently: bool = False) -> ExtendedDict:
|
|
166
|
+
"""Return a copy with string-like special values reconstructed."""
|
|
167
|
+
from extended_data.containers.factory import extend_data, to_builtin
|
|
168
|
+
|
|
169
|
+
return extend_data(reconstruct_special_types(to_builtin(self.data), fail_silently=fail_silently))
|
|
170
|
+
|
|
171
|
+
def to_export_safe(self, *, export_to_yaml: bool = False) -> Any:
|
|
172
|
+
"""Return this mapping converted to export-safe primitive data."""
|
|
173
|
+
from extended_data.io.exporters import make_raw_data_export_safe
|
|
174
|
+
|
|
175
|
+
return make_raw_data_export_safe(self.data, export_to_yaml=export_to_yaml)
|
|
176
|
+
|
|
177
|
+
def wrap_for_export(self, allow_encoding: bool | str = True, **format_opts: Any) -> str:
|
|
178
|
+
"""Return this mapping wrapped as an encoded export string."""
|
|
179
|
+
from extended_data.io.exporters import wrap_raw_data_for_export
|
|
180
|
+
|
|
181
|
+
return wrap_raw_data_for_export(self.data, allow_encoding=allow_encoding, **format_opts)
|