extended-data 8.2.0__tar.gz → 8.3.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.
- extended_data-8.3.0/.release-please-manifest.json +3 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/CHANGELOG.md +7 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/PKG-INFO +11 -8
- {extended_data-8.2.0 → extended_data-8.3.0}/README.md +10 -7
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/containers.md +15 -6
- {extended_data-8.2.0 → extended_data-8.3.0}/pyproject.toml +1 -1
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/data.py +123 -70
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/factory.py +4 -4
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/mappings.py +10 -21
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/sequences.py +20 -9
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/strings.py +8 -1
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_containers.py +15 -2
- {extended_data-8.2.0 → extended_data-8.3.0}/uv.lock +1 -1
- extended_data-8.2.0/.release-please-manifest.json +0 -3
- {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/cd.yml +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/ci.yml +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/release.yml +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/.gitignore +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/.python-version +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/LICENSE +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/CNAME +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/PUBLISHING_CHECKLIST.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/_static/.gitkeep +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/_static/extended-data.css +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/api/index.rst +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/conf.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/primitives.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/workflows.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/core.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/index.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/inputs.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/logging.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/getting-started.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/index.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/operations/inputs.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/operations/logging.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/ownership-map.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/package-surface.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/docs/publishing.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/README.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/basic_usage.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/composed_workflows.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/file_operations.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/serialization.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/string_transformations.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/README.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/basic_usage.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/decorator_api.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/encoding_decoding.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/README.md +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/basic_logging.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/exit_run_formatting.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/markers_and_storage.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/verbosity_control.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/release-please-config.json +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/_version.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/cli.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/__main__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/decorators.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/py.typed +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/base64.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/exporters.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/files.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/importers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/const.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/handlers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/log_types.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/logging.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/py.typed +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/_normalization.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/errors.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/hcl.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/json.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/toml.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/constructors.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/dumpers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/loaders.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/representers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/tag_classes.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/introspection.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/mappings.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/matching.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/numbers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/redaction.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/sequences.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/serialization.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/splitting.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/state.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/string_transforms.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/strings.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/notation.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/words.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/strings/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/strings/inflection.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/types.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/py.typed +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/workflows/__init__.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/workflows/sync.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_base64_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_export_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_file_data_type.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_hcl2_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_import_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_integration_workflows.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_json_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_list_data_type.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_map_data_type.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_matcher_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_number_transformations.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_package_cli.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_package_surface.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_redaction.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_release_hygiene.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_serialization_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_splitter_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_stack_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_state_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_string_data_type.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_string_transformations.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_toml_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_type_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_workflows.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_yaml_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/numbers/test_notation.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/numbers/test_words.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/strings/test_inflection.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/examples/test_safe_examples.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/inputs/test_decorators.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/inputs/test_main.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/conftest.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/integration/test_lifecycle_logging.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_exit_run.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_handlers.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_logging.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_properties.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_utils.py +0 -0
- {extended_data-8.2.0 → extended_data-8.3.0}/tox.ini +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [8.3.0](https://github.com/jbcom/extended-data/compare/extended-data-v8.2.0...extended-data-v8.3.0) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* make ExtendedData the polymorphic container root ([217d8d7](https://github.com/jbcom/extended-data/commit/217d8d7694d26b75c8a0e65979f5b8162a33be9f))
|
|
9
|
+
|
|
3
10
|
## [8.2.0](https://github.com/jbcom/extended-data/compare/extended-data-v8.1.0...extended-data-v8.2.0) (2026-06-27)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: extended-data
|
|
3
|
-
Version: 8.
|
|
3
|
+
Version: 8.3.0
|
|
4
4
|
Summary: Comprehensive Python data utilities for serialization, inputs, logging, and workflows
|
|
5
5
|
Project-URL: Documentation, https://extended-data.dev
|
|
6
6
|
Project-URL: Issues, https://github.com/jbcom/extended-data/issues
|
|
@@ -82,7 +82,8 @@ tiers:
|
|
|
82
82
|
type coercion, mapping, sequence, and state utilities.
|
|
83
83
|
- Tier 2: `ExtendedData`, `ExtendedString`, `ExtendedDict`, `ExtendedList`,
|
|
84
84
|
`ExtendedTuple`, and `ExtendedSet` containers that expose Tier 1 operations
|
|
85
|
-
as methods.
|
|
85
|
+
as methods. `ExtendedData` is the common root and polymorphic constructor for
|
|
86
|
+
the shape-specific containers.
|
|
86
87
|
- Tier 3: data processors that compose the first two tiers for files, inputs,
|
|
87
88
|
logging, export/import boundaries, and workflows.
|
|
88
89
|
|
|
@@ -115,7 +116,7 @@ payload = ExtendedDict(data).deep_merge({"replicas": 3})
|
|
|
115
116
|
wrapped = ExtendedData(payload).merge({"owner": "platform"})
|
|
116
117
|
decoded_file = decode_file('{"service": {"name": "worker"}}', suffix="json")
|
|
117
118
|
artifact = DataFile.decode("service:\n name: api\n", suffix="yaml")
|
|
118
|
-
workflow = DataWorkflow.from_value(wrapped
|
|
119
|
+
workflow = DataWorkflow.from_value(wrapped).transform("unhump").result()
|
|
119
120
|
|
|
120
121
|
logger.logged_statement("prepared workflow", json_data=workflow.as_builtin(), log_level="info")
|
|
121
122
|
|
|
@@ -142,7 +143,7 @@ extended-data transform --file payload.json --step reconstruct --step unhump
|
|
|
142
143
|
|
|
143
144
|
```text
|
|
144
145
|
extended_data/
|
|
145
|
-
containers/ Tier 2 ExtendedData
|
|
146
|
+
containers/ Tier 2 ExtendedData root plus String/Dict/List/Tuple/Set containers
|
|
146
147
|
inputs/ InputProvider and decorator-based input injection
|
|
147
148
|
io/ Tier 3 file, import, export, and base64 processors
|
|
148
149
|
logging/ structured lifecycle logging
|
|
@@ -161,10 +162,12 @@ context values, such as resource IDs, emails, paths, or URLs, must be withheld
|
|
|
161
162
|
in addition to common secret fields.
|
|
162
163
|
|
|
163
164
|
Tier 2 containers inherit from standard Python collection primitives and expose
|
|
164
|
-
chainable data operations. `ExtendedData` is the
|
|
165
|
-
value
|
|
166
|
-
`
|
|
167
|
-
`
|
|
165
|
+
chainable data operations. `ExtendedData` is the polymorphic constructor for any
|
|
166
|
+
incoming value: `ExtendedData({"service": "api"})` is an `ExtendedDict`,
|
|
167
|
+
`ExtendedData(["api"])` is an `ExtendedList`, and `ExtendedData("api")` is an
|
|
168
|
+
`ExtendedString`, while all of them are also `isinstance(value, ExtendedData)`.
|
|
169
|
+
For example, `ExtendedString.decode_json()` promotes JSON into extended
|
|
170
|
+
containers, `ExtendedDict.reconstruct_special_types()` turns string scalars into
|
|
168
171
|
booleans/numbers/dates where safe, and `ExtendedList.first_non_empty()` returns
|
|
169
172
|
the first meaningful value without lowering the surrounding data boundary.
|
|
170
173
|
|
|
@@ -10,7 +10,8 @@ tiers:
|
|
|
10
10
|
type coercion, mapping, sequence, and state utilities.
|
|
11
11
|
- Tier 2: `ExtendedData`, `ExtendedString`, `ExtendedDict`, `ExtendedList`,
|
|
12
12
|
`ExtendedTuple`, and `ExtendedSet` containers that expose Tier 1 operations
|
|
13
|
-
as methods.
|
|
13
|
+
as methods. `ExtendedData` is the common root and polymorphic constructor for
|
|
14
|
+
the shape-specific containers.
|
|
14
15
|
- Tier 3: data processors that compose the first two tiers for files, inputs,
|
|
15
16
|
logging, export/import boundaries, and workflows.
|
|
16
17
|
|
|
@@ -43,7 +44,7 @@ payload = ExtendedDict(data).deep_merge({"replicas": 3})
|
|
|
43
44
|
wrapped = ExtendedData(payload).merge({"owner": "platform"})
|
|
44
45
|
decoded_file = decode_file('{"service": {"name": "worker"}}', suffix="json")
|
|
45
46
|
artifact = DataFile.decode("service:\n name: api\n", suffix="yaml")
|
|
46
|
-
workflow = DataWorkflow.from_value(wrapped
|
|
47
|
+
workflow = DataWorkflow.from_value(wrapped).transform("unhump").result()
|
|
47
48
|
|
|
48
49
|
logger.logged_statement("prepared workflow", json_data=workflow.as_builtin(), log_level="info")
|
|
49
50
|
|
|
@@ -70,7 +71,7 @@ extended-data transform --file payload.json --step reconstruct --step unhump
|
|
|
70
71
|
|
|
71
72
|
```text
|
|
72
73
|
extended_data/
|
|
73
|
-
containers/ Tier 2 ExtendedData
|
|
74
|
+
containers/ Tier 2 ExtendedData root plus String/Dict/List/Tuple/Set containers
|
|
74
75
|
inputs/ InputProvider and decorator-based input injection
|
|
75
76
|
io/ Tier 3 file, import, export, and base64 processors
|
|
76
77
|
logging/ structured lifecycle logging
|
|
@@ -89,10 +90,12 @@ context values, such as resource IDs, emails, paths, or URLs, must be withheld
|
|
|
89
90
|
in addition to common secret fields.
|
|
90
91
|
|
|
91
92
|
Tier 2 containers inherit from standard Python collection primitives and expose
|
|
92
|
-
chainable data operations. `ExtendedData` is the
|
|
93
|
-
value
|
|
94
|
-
`
|
|
95
|
-
`
|
|
93
|
+
chainable data operations. `ExtendedData` is the polymorphic constructor for any
|
|
94
|
+
incoming value: `ExtendedData({"service": "api"})` is an `ExtendedDict`,
|
|
95
|
+
`ExtendedData(["api"])` is an `ExtendedList`, and `ExtendedData("api")` is an
|
|
96
|
+
`ExtendedString`, while all of them are also `isinstance(value, ExtendedData)`.
|
|
97
|
+
For example, `ExtendedString.decode_json()` promotes JSON into extended
|
|
98
|
+
containers, `ExtendedDict.reconstruct_special_types()` turns string scalars into
|
|
96
99
|
booleans/numbers/dates where safe, and `ExtendedList.first_non_empty()` returns
|
|
97
100
|
the first meaningful value without lowering the surrounding data boundary.
|
|
98
101
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Tier 2 wraps Python data shapes with shape-specific `ExtendedString`,
|
|
4
4
|
`ExtendedDict`, `ExtendedList`, `ExtendedTuple`, and `ExtendedSet` containers.
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
`ExtendedData` is their common root and polymorphic constructor: callers can use
|
|
6
|
+
one entrypoint while still receiving the concrete extended type for the incoming
|
|
7
|
+
value.
|
|
7
8
|
|
|
8
9
|
Construction and mutation promote nested values, so method chains survive normal
|
|
9
10
|
Python literals.
|
|
@@ -20,27 +21,35 @@ print(payload["tags"].compact().deduplicate())
|
|
|
20
21
|
print(ExtendedString("HTTP Response Value").to_snake_case())
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## Polymorphic ExtendedData
|
|
24
25
|
|
|
25
26
|
Use `ExtendedData` at file, API, workflow, and vendor boundaries where the
|
|
26
27
|
payload may be a mapping today, a list tomorrow, or a scalar status value from a
|
|
27
|
-
different provider.
|
|
28
|
+
different provider. The constructor returns the concrete extended subtype when
|
|
29
|
+
one applies, so normal Python collection behavior remains native rather than
|
|
30
|
+
proxied.
|
|
28
31
|
|
|
29
32
|
```python
|
|
30
|
-
from extended_data import ExtendedData
|
|
33
|
+
from extended_data import ExtendedData, ExtendedDict, ExtendedList, ExtendedString
|
|
31
34
|
|
|
32
35
|
vendor = ExtendedData({"vendor": "google", "payload": {"names": ["alpha"]}})
|
|
33
36
|
vendor["enabled"] = "true"
|
|
34
37
|
|
|
38
|
+
assert type(vendor) is ExtendedDict
|
|
39
|
+
assert isinstance(vendor, ExtendedData)
|
|
35
40
|
assert vendor.shape == "mapping"
|
|
36
41
|
assert vendor.get("vendor").upper_first() == "Google"
|
|
37
42
|
assert vendor["payload"]["names"][0].upper_first() == "Alpha"
|
|
38
43
|
|
|
39
44
|
merged = vendor.merge({"payload": {"region": "us-east-1"}})
|
|
40
45
|
assert merged.as_builtin()["payload"]["region"] == "us-east-1"
|
|
46
|
+
|
|
47
|
+
assert type(ExtendedData([{"name": "api"}]).append({"name": "worker"})) is ExtendedList
|
|
48
|
+
assert type(ExtendedData("HTTP Response Value")) is ExtendedString
|
|
41
49
|
```
|
|
42
50
|
|
|
43
|
-
The
|
|
51
|
+
The common root exposes broad shape predicates without forcing dict-only code
|
|
52
|
+
paths:
|
|
44
53
|
|
|
45
54
|
```python
|
|
46
55
|
assert ExtendedData("HTTP Response Value").is_string
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "extended-data"
|
|
7
|
-
version = "8.
|
|
7
|
+
version = "8.3.0"
|
|
8
8
|
description = "Comprehensive Python data utilities for serialization, inputs, logging, and workflows"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,61 +1,79 @@
|
|
|
1
|
-
"""Generic Extended Data
|
|
1
|
+
"""Generic Extended Data root and factory."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections import UserString
|
|
6
5
|
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
7
6
|
from copy import deepcopy
|
|
8
7
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Self
|
|
10
|
-
|
|
11
|
-
from extended_data.containers.mappings import ExtendedDict
|
|
12
|
-
from extended_data.containers.sequences import ExtendedList, ExtendedSet, ExtendedTuple
|
|
13
|
-
from extended_data.containers.strings import ExtendedString
|
|
8
|
+
from typing import Any, Self, cast
|
|
14
9
|
|
|
15
10
|
|
|
16
11
|
class ExtendedData:
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
``
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
"""Common root and factory for any Extended Data value.
|
|
13
|
+
|
|
14
|
+
``ExtendedData(value)`` returns the most specific concrete Tier 2 container
|
|
15
|
+
whenever one exists. Mapping values become ``ExtendedDict`` instances, list
|
|
16
|
+
values become ``ExtendedList`` instances, strings become ``ExtendedString``
|
|
17
|
+
instances, and so on. Scalars and arbitrary objects remain in a small generic
|
|
18
|
+
holder so the same workflow, export, and sync helpers are available at file,
|
|
19
|
+
API, and vendor boundaries.
|
|
23
20
|
"""
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
def __new__(cls, value: Any = None) -> Self:
|
|
23
|
+
"""Return the most specific Extended Data object for ``value``."""
|
|
24
|
+
if cls is not ExtendedData:
|
|
25
|
+
return super().__new__(cls)
|
|
26
26
|
|
|
27
|
-
def __init__(self, value: Any = None) -> None:
|
|
28
|
-
"""Promote and store any supported data shape."""
|
|
29
27
|
from extended_data.containers.factory import extend_data
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
promoted = extend_data(value)
|
|
30
|
+
if isinstance(promoted, ExtendedData):
|
|
31
|
+
return cast(Self, promoted)
|
|
32
|
+
|
|
33
|
+
instance = super().__new__(cls)
|
|
34
|
+
instance._value = promoted
|
|
35
|
+
return instance
|
|
36
|
+
|
|
37
|
+
def __init__(self, value: Any = None) -> None:
|
|
38
|
+
"""Initialize scalar/object holders without touching concrete subtypes."""
|
|
39
|
+
if type(self) is ExtendedData and not hasattr(self, "_value"):
|
|
40
|
+
self._value = value
|
|
32
41
|
|
|
33
42
|
@property
|
|
34
43
|
def value(self) -> Any:
|
|
35
|
-
"""Return the
|
|
36
|
-
|
|
44
|
+
"""Return the concrete value represented by this object."""
|
|
45
|
+
if type(self) is ExtendedData:
|
|
46
|
+
return self._value
|
|
47
|
+
return self
|
|
37
48
|
|
|
38
49
|
@property
|
|
39
50
|
def data_type(self) -> str:
|
|
40
|
-
"""Return
|
|
41
|
-
return type(self.
|
|
51
|
+
"""Return this value type name."""
|
|
52
|
+
return type(self.value).__name__
|
|
42
53
|
|
|
43
54
|
@property
|
|
44
55
|
def shape(self) -> str:
|
|
45
|
-
"""Return the broad data shape represented by
|
|
46
|
-
|
|
56
|
+
"""Return the broad data shape represented by this value."""
|
|
57
|
+
from collections import UserString
|
|
58
|
+
|
|
59
|
+
from extended_data.containers.mappings import ExtendedDict
|
|
60
|
+
from extended_data.containers.sequences import ExtendedList, ExtendedSet, ExtendedTuple
|
|
61
|
+
from extended_data.containers.strings import ExtendedString
|
|
62
|
+
|
|
63
|
+
value = self.value
|
|
64
|
+
if value is None:
|
|
47
65
|
return "none"
|
|
48
|
-
if isinstance(
|
|
66
|
+
if isinstance(value, ExtendedDict | Mapping):
|
|
49
67
|
return "mapping"
|
|
50
|
-
if isinstance(
|
|
68
|
+
if isinstance(value, ExtendedList | list):
|
|
51
69
|
return "list"
|
|
52
|
-
if isinstance(
|
|
70
|
+
if isinstance(value, ExtendedTuple | tuple):
|
|
53
71
|
return "tuple"
|
|
54
|
-
if isinstance(
|
|
72
|
+
if isinstance(value, ExtendedSet | set | frozenset):
|
|
55
73
|
return "set"
|
|
56
|
-
if isinstance(
|
|
74
|
+
if isinstance(value, ExtendedString | str | UserString):
|
|
57
75
|
return "string"
|
|
58
|
-
if isinstance(
|
|
76
|
+
if isinstance(value, bool | int | float | complex | bytes | bytearray | memoryview | Path):
|
|
59
77
|
return "scalar"
|
|
60
78
|
return "object"
|
|
61
79
|
|
|
@@ -91,7 +109,7 @@ class ExtendedData:
|
|
|
91
109
|
|
|
92
110
|
@classmethod
|
|
93
111
|
def from_value(cls, value: Any = None) -> ExtendedData:
|
|
94
|
-
"""Create an
|
|
112
|
+
"""Create an Extended Data object from any value."""
|
|
95
113
|
return cls(value)
|
|
96
114
|
|
|
97
115
|
@classmethod
|
|
@@ -102,7 +120,7 @@ class ExtendedData:
|
|
|
102
120
|
file_path: str | Path | None = None,
|
|
103
121
|
suffix: str | None = None,
|
|
104
122
|
) -> ExtendedData:
|
|
105
|
-
"""Decode raw structured data and return
|
|
123
|
+
"""Decode raw structured data and return concrete Extended Data."""
|
|
106
124
|
from extended_data.io.files import decode_file
|
|
107
125
|
|
|
108
126
|
return cls(decode_file(data, file_path=file_path, suffix=suffix, as_extended=True))
|
|
@@ -118,7 +136,7 @@ class ExtendedData:
|
|
|
118
136
|
headers: Mapping[str, str] | None = None,
|
|
119
137
|
tld: Path | None = None,
|
|
120
138
|
) -> ExtendedData:
|
|
121
|
-
"""Read a local file or URL and return
|
|
139
|
+
"""Read a local file or URL and return concrete Extended Data."""
|
|
122
140
|
from extended_data.io.files import read_data_file
|
|
123
141
|
|
|
124
142
|
return cls(
|
|
@@ -137,7 +155,7 @@ class ExtendedData:
|
|
|
137
155
|
"""Return the value lowered to built-in Python containers."""
|
|
138
156
|
from extended_data.containers.factory import to_builtin
|
|
139
157
|
|
|
140
|
-
return to_builtin(self.
|
|
158
|
+
return to_builtin(self.value)
|
|
141
159
|
|
|
142
160
|
def as_extended(self) -> Any:
|
|
143
161
|
"""Return a detached promoted copy of the value."""
|
|
@@ -146,19 +164,16 @@ class ExtendedData:
|
|
|
146
164
|
return extend_data(deepcopy(self.as_builtin()))
|
|
147
165
|
|
|
148
166
|
def copy(self) -> ExtendedData:
|
|
149
|
-
"""Return a detached
|
|
150
|
-
return ExtendedData(deepcopy(self.
|
|
151
|
-
|
|
152
|
-
def replace(self, value: Any) -> Self:
|
|
153
|
-
"""Replace the underlying value and return this facade."""
|
|
154
|
-
from extended_data.containers.factory import extend_data
|
|
167
|
+
"""Return a detached Extended Data copy."""
|
|
168
|
+
return ExtendedData(deepcopy(self.as_builtin()))
|
|
155
169
|
|
|
156
|
-
|
|
157
|
-
|
|
170
|
+
def cast(self, value: Any) -> ExtendedData:
|
|
171
|
+
"""Return ``value`` promoted into the appropriate Extended Data subtype."""
|
|
172
|
+
return ExtendedData(value)
|
|
158
173
|
|
|
159
174
|
def map(self, transform: Callable[[Any], Any]) -> ExtendedData:
|
|
160
|
-
"""Apply a callable to
|
|
161
|
-
return ExtendedData(transform(self.
|
|
175
|
+
"""Apply a callable to this value and wrap the result."""
|
|
176
|
+
return ExtendedData(transform(self.value))
|
|
162
177
|
|
|
163
178
|
def map_builtin(self, transform: Callable[[Any], Any]) -> ExtendedData:
|
|
164
179
|
"""Apply a callable to lowered built-in data and wrap the result."""
|
|
@@ -168,11 +183,11 @@ class ExtendedData:
|
|
|
168
183
|
"""Apply named DataWorkflow transforms and wrap the result."""
|
|
169
184
|
from extended_data.workflows import DataWorkflow
|
|
170
185
|
|
|
171
|
-
return ExtendedData(DataWorkflow.from_value(self.
|
|
186
|
+
return ExtendedData(DataWorkflow.from_value(self.value).transform(*steps).result().value)
|
|
172
187
|
|
|
173
188
|
def merge(self, *mappings: Mapping[str, Any]) -> ExtendedData:
|
|
174
189
|
"""Deep-merge mappings when this wrapper contains mapping-shaped data."""
|
|
175
|
-
method = getattr(self.
|
|
190
|
+
method = getattr(self.value, "deep_merge", None)
|
|
176
191
|
if not callable(method):
|
|
177
192
|
raise TypeError(f"merge is not available for {self.data_type}")
|
|
178
193
|
return ExtendedData(method(*mappings))
|
|
@@ -181,7 +196,7 @@ class ExtendedData:
|
|
|
181
196
|
"""Start a DataWorkflow from this value."""
|
|
182
197
|
from extended_data.workflows import DataWorkflow
|
|
183
198
|
|
|
184
|
-
return DataWorkflow.from_value(self.
|
|
199
|
+
return DataWorkflow.from_value(self.value)
|
|
185
200
|
|
|
186
201
|
def sync_to_file(
|
|
187
202
|
self,
|
|
@@ -235,24 +250,29 @@ class ExtendedData:
|
|
|
235
250
|
"""Return this value converted to export-safe primitive data."""
|
|
236
251
|
from extended_data.io.exporters import make_raw_data_export_safe
|
|
237
252
|
|
|
238
|
-
return make_raw_data_export_safe(self.
|
|
253
|
+
return make_raw_data_export_safe(self.value, export_to_yaml=export_to_yaml)
|
|
239
254
|
|
|
240
255
|
def wrap_for_export(self, allow_encoding: bool | str = True, **format_opts: Any) -> str:
|
|
241
256
|
"""Return this value wrapped as an encoded export string."""
|
|
242
257
|
from extended_data.io.exporters import wrap_raw_data_for_export
|
|
243
258
|
|
|
244
|
-
return wrap_raw_data_for_export(self.
|
|
259
|
+
return wrap_raw_data_for_export(self.value, allow_encoding=allow_encoding, **format_opts)
|
|
245
260
|
|
|
246
261
|
def get(self, key: Any, default: Any = None) -> Any:
|
|
247
262
|
"""Return a mapping value by key, or ``default`` when unavailable."""
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
if not self.is_mapping:
|
|
264
|
+
return default
|
|
265
|
+
try:
|
|
266
|
+
return self[key]
|
|
267
|
+
except KeyError:
|
|
250
268
|
return default
|
|
251
|
-
return method(key, default)
|
|
252
269
|
|
|
253
270
|
def append(self, item: Any) -> Self:
|
|
254
271
|
"""Append to list-shaped data and return this wrapper."""
|
|
255
|
-
|
|
272
|
+
value = self.value
|
|
273
|
+
if value is self:
|
|
274
|
+
raise TypeError(f"append is not available for {self.data_type}")
|
|
275
|
+
method = getattr(value, "append", None)
|
|
256
276
|
if not callable(method):
|
|
257
277
|
raise TypeError(f"append is not available for {self.data_type}")
|
|
258
278
|
method(item)
|
|
@@ -260,7 +280,10 @@ class ExtendedData:
|
|
|
260
280
|
|
|
261
281
|
def extend(self, values: Iterable[Any]) -> Self:
|
|
262
282
|
"""Extend list-shaped data and return this wrapper."""
|
|
263
|
-
|
|
283
|
+
value = self.value
|
|
284
|
+
if value is self:
|
|
285
|
+
raise TypeError(f"extend is not available for {self.data_type}")
|
|
286
|
+
method = getattr(value, "extend", None)
|
|
264
287
|
if not callable(method):
|
|
265
288
|
raise TypeError(f"extend is not available for {self.data_type}")
|
|
266
289
|
method(values)
|
|
@@ -268,7 +291,10 @@ class ExtendedData:
|
|
|
268
291
|
|
|
269
292
|
def update(self, *args: Any, **kwargs: Any) -> Self:
|
|
270
293
|
"""Update mapping- or set-shaped data and return this wrapper."""
|
|
271
|
-
|
|
294
|
+
value = self.value
|
|
295
|
+
if value is self:
|
|
296
|
+
raise TypeError(f"update is not available for {self.data_type}")
|
|
297
|
+
method = getattr(value, "update", None)
|
|
272
298
|
if not callable(method):
|
|
273
299
|
raise TypeError(f"update is not available for {self.data_type}")
|
|
274
300
|
method(*args, **kwargs)
|
|
@@ -276,7 +302,10 @@ class ExtendedData:
|
|
|
276
302
|
|
|
277
303
|
def add(self, item: Any) -> Self:
|
|
278
304
|
"""Add to set-shaped data and return this wrapper."""
|
|
279
|
-
|
|
305
|
+
value = self.value
|
|
306
|
+
if value is self:
|
|
307
|
+
raise TypeError(f"add is not available for {self.data_type}")
|
|
308
|
+
method = getattr(value, "add", None)
|
|
280
309
|
if not callable(method):
|
|
281
310
|
raise TypeError(f"add is not available for {self.data_type}")
|
|
282
311
|
method(item)
|
|
@@ -292,47 +321,71 @@ class ExtendedData:
|
|
|
292
321
|
|
|
293
322
|
def __iter__(self) -> Iterator[Any]:
|
|
294
323
|
"""Iterate the underlying value."""
|
|
324
|
+
value = self.value
|
|
325
|
+
if value is self:
|
|
326
|
+
msg = f"iteration is not available for {self.data_type}"
|
|
327
|
+
raise TypeError(msg)
|
|
295
328
|
try:
|
|
296
|
-
return iter(
|
|
329
|
+
return iter(value)
|
|
297
330
|
except TypeError:
|
|
298
|
-
return iter([
|
|
331
|
+
return iter([value])
|
|
299
332
|
|
|
300
333
|
def __len__(self) -> int:
|
|
301
334
|
"""Return the underlying value length, or one for scalars."""
|
|
335
|
+
value = self.value
|
|
336
|
+
if value is self:
|
|
337
|
+
msg = f"length is not available for {self.data_type}"
|
|
338
|
+
raise TypeError(msg)
|
|
302
339
|
try:
|
|
303
|
-
return len(
|
|
340
|
+
return len(value)
|
|
304
341
|
except TypeError:
|
|
305
342
|
return 1
|
|
306
343
|
|
|
307
344
|
def __bool__(self) -> bool:
|
|
308
345
|
"""Mirror the truthiness of the wrapped value."""
|
|
309
|
-
|
|
346
|
+
value = self.value
|
|
347
|
+
if value is self:
|
|
348
|
+
return len(self) > 0
|
|
349
|
+
return bool(value)
|
|
310
350
|
|
|
311
351
|
def __getitem__(self, key: Any) -> Any:
|
|
312
352
|
"""Index the underlying value."""
|
|
313
|
-
|
|
353
|
+
value = self.value
|
|
354
|
+
if value is self:
|
|
355
|
+
msg = f"indexing is not available for {self.data_type}"
|
|
356
|
+
raise TypeError(msg)
|
|
357
|
+
return value[key]
|
|
314
358
|
|
|
315
359
|
def __setitem__(self, key: Any, value: Any) -> None:
|
|
316
360
|
"""Set an item on mutable underlying data."""
|
|
317
|
-
|
|
361
|
+
target = self.value
|
|
362
|
+
if target is self:
|
|
363
|
+
msg = f"item assignment is not available for {self.data_type}"
|
|
364
|
+
raise TypeError(msg)
|
|
365
|
+
target[key] = value
|
|
318
366
|
|
|
319
367
|
def __delitem__(self, key: Any) -> None:
|
|
320
368
|
"""Delete an item from mutable underlying data."""
|
|
321
|
-
|
|
369
|
+
value = self.value
|
|
370
|
+
if value is self:
|
|
371
|
+
msg = f"item deletion is not available for {self.data_type}"
|
|
372
|
+
raise TypeError(msg)
|
|
373
|
+
del value[key]
|
|
322
374
|
|
|
323
375
|
def __contains__(self, item: object) -> bool:
|
|
324
376
|
"""Return whether an item is present in the underlying value."""
|
|
377
|
+
value = self.value
|
|
378
|
+
if value is self:
|
|
379
|
+
try:
|
|
380
|
+
self[item]
|
|
381
|
+
except (IndexError, KeyError, TypeError):
|
|
382
|
+
return False
|
|
383
|
+
return True
|
|
325
384
|
try:
|
|
326
|
-
return item in
|
|
385
|
+
return item in value
|
|
327
386
|
except TypeError:
|
|
328
387
|
return False
|
|
329
388
|
|
|
330
|
-
def __eq__(self, other: object) -> bool:
|
|
331
|
-
"""Compare against another facade or raw value by built-in representation."""
|
|
332
|
-
if isinstance(other, ExtendedData):
|
|
333
|
-
return self.as_builtin() == other.as_builtin()
|
|
334
|
-
return self.as_builtin() == other
|
|
335
|
-
|
|
336
389
|
def __repr__(self) -> str:
|
|
337
390
|
"""Return a useful debugging representation."""
|
|
338
|
-
return f"ExtendedData({self.
|
|
391
|
+
return f"ExtendedData({self.value!r})"
|
|
@@ -15,12 +15,12 @@ def extend_data(value: Any) -> Any:
|
|
|
15
15
|
"""Recursively wrap built-in containers in Extended Data containers."""
|
|
16
16
|
from extended_data.containers.data import ExtendedData
|
|
17
17
|
|
|
18
|
-
if isinstance(value, ExtendedData):
|
|
19
|
-
return value.value
|
|
20
18
|
if isinstance(value, YamlTagged | YamlPairs | LiteralScalarString):
|
|
21
19
|
return value
|
|
22
20
|
if isinstance(value, ExtendedString | ExtendedDict | ExtendedList | ExtendedSet | ExtendedTuple):
|
|
23
21
|
return value
|
|
22
|
+
if isinstance(value, ExtendedData):
|
|
23
|
+
return value
|
|
24
24
|
if isinstance(value, str):
|
|
25
25
|
return ExtendedString(value)
|
|
26
26
|
if isinstance(value, Mapping):
|
|
@@ -38,8 +38,6 @@ def to_builtin(value: Any) -> Any:
|
|
|
38
38
|
"""Recursively unwrap Extended Data containers to built-in Python values."""
|
|
39
39
|
from extended_data.containers.data import ExtendedData
|
|
40
40
|
|
|
41
|
-
if isinstance(value, ExtendedData):
|
|
42
|
-
return to_builtin(value.value)
|
|
43
41
|
if isinstance(value, YamlTagged | YamlPairs | LiteralScalarString):
|
|
44
42
|
return value
|
|
45
43
|
if isinstance(value, ExtendedString):
|
|
@@ -52,6 +50,8 @@ def to_builtin(value: Any) -> Any:
|
|
|
52
50
|
return tuple(to_builtin(item) for item in value)
|
|
53
51
|
if isinstance(value, ExtendedSet):
|
|
54
52
|
return {to_builtin(item) for item in value}
|
|
53
|
+
if isinstance(value, ExtendedData):
|
|
54
|
+
return to_builtin(value.value)
|
|
55
55
|
if isinstance(value, Mapping):
|
|
56
56
|
return {to_builtin(key): to_builtin(item) for key, item in value.items()}
|
|
57
57
|
if isinstance(value, list):
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from collections import UserDict
|
|
6
|
-
from collections.abc import
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Self
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
8
8
|
|
|
9
|
+
from extended_data.containers.data import ExtendedData
|
|
9
10
|
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from _typeshed import SupportsKeysAndGetItem
|
|
12
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
13
|
from extended_data.containers.sequences import ExtendedList, ExtendedTuple
|
|
14
14
|
|
|
15
15
|
from extended_data.primitives.mappings import (
|
|
@@ -26,11 +26,13 @@ from extended_data.primitives.state import all_non_empty_in_dict, any_non_empty,
|
|
|
26
26
|
from extended_data.primitives.types import reconstruct_special_types
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class ExtendedDict(UserDict[str, Any]):
|
|
29
|
+
class ExtendedDict(UserDict[str, Any], ExtendedData):
|
|
30
30
|
"""Dictionary wrapper with chainable primitive operations."""
|
|
31
31
|
|
|
32
32
|
def __init__(self, initialdata: Mapping[str, Any] | None = None, **kwargs: Any) -> None:
|
|
33
33
|
"""Initialize the extended dictionary."""
|
|
34
|
+
if initialdata is self and not kwargs:
|
|
35
|
+
return
|
|
34
36
|
super().__init__()
|
|
35
37
|
self.update(initialdata or {}, **kwargs)
|
|
36
38
|
|
|
@@ -40,22 +42,7 @@ class ExtendedDict(UserDict[str, Any]):
|
|
|
40
42
|
|
|
41
43
|
self.data[key] = extend_data(item)
|
|
42
44
|
|
|
43
|
-
|
|
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]
|
|
45
|
+
def update(self, *args: Any, **kwargs: Any) -> Self: # type: ignore[override]
|
|
59
46
|
"""Update values while preserving extended nested containers."""
|
|
60
47
|
if len(args) > 1:
|
|
61
48
|
msg = f"update expected at most 1 argument, got {len(args)}"
|
|
@@ -77,6 +64,8 @@ class ExtendedDict(UserDict[str, Any]):
|
|
|
77
64
|
for key, value in kwargs.items():
|
|
78
65
|
self[key] = value
|
|
79
66
|
|
|
67
|
+
return self
|
|
68
|
+
|
|
80
69
|
def setdefault(self, key: str, default: Any = None) -> Any:
|
|
81
70
|
"""Insert a default while returning the promoted stored value."""
|
|
82
71
|
if key not in self.data:
|