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.
Files changed (151) hide show
  1. extended_data-8.3.0/.release-please-manifest.json +3 -0
  2. {extended_data-8.2.0 → extended_data-8.3.0}/CHANGELOG.md +7 -0
  3. {extended_data-8.2.0 → extended_data-8.3.0}/PKG-INFO +11 -8
  4. {extended_data-8.2.0 → extended_data-8.3.0}/README.md +10 -7
  5. {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/containers.md +15 -6
  6. {extended_data-8.2.0 → extended_data-8.3.0}/pyproject.toml +1 -1
  7. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/data.py +123 -70
  8. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/factory.py +4 -4
  9. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/mappings.py +10 -21
  10. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/sequences.py +20 -9
  11. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/strings.py +8 -1
  12. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_containers.py +15 -2
  13. {extended_data-8.2.0 → extended_data-8.3.0}/uv.lock +1 -1
  14. extended_data-8.2.0/.release-please-manifest.json +0 -3
  15. {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/cd.yml +0 -0
  16. {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/ci.yml +0 -0
  17. {extended_data-8.2.0 → extended_data-8.3.0}/.github/workflows/release.yml +0 -0
  18. {extended_data-8.2.0 → extended_data-8.3.0}/.gitignore +0 -0
  19. {extended_data-8.2.0 → extended_data-8.3.0}/.python-version +0 -0
  20. {extended_data-8.2.0 → extended_data-8.3.0}/LICENSE +0 -0
  21. {extended_data-8.2.0 → extended_data-8.3.0}/docs/CNAME +0 -0
  22. {extended_data-8.2.0 → extended_data-8.3.0}/docs/PUBLISHING_CHECKLIST.md +0 -0
  23. {extended_data-8.2.0 → extended_data-8.3.0}/docs/__init__.py +0 -0
  24. {extended_data-8.2.0 → extended_data-8.3.0}/docs/_static/.gitkeep +0 -0
  25. {extended_data-8.2.0 → extended_data-8.3.0}/docs/_static/extended-data.css +0 -0
  26. {extended_data-8.2.0 → extended_data-8.3.0}/docs/api/index.rst +0 -0
  27. {extended_data-8.2.0 → extended_data-8.3.0}/docs/conf.py +0 -0
  28. {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/primitives.md +0 -0
  29. {extended_data-8.2.0 → extended_data-8.3.0}/docs/core/workflows.md +0 -0
  30. {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/core.md +0 -0
  31. {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/index.md +0 -0
  32. {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/inputs.md +0 -0
  33. {extended_data-8.2.0 → extended_data-8.3.0}/docs/examples/logging.md +0 -0
  34. {extended_data-8.2.0 → extended_data-8.3.0}/docs/getting-started.md +0 -0
  35. {extended_data-8.2.0 → extended_data-8.3.0}/docs/index.md +0 -0
  36. {extended_data-8.2.0 → extended_data-8.3.0}/docs/operations/inputs.md +0 -0
  37. {extended_data-8.2.0 → extended_data-8.3.0}/docs/operations/logging.md +0 -0
  38. {extended_data-8.2.0 → extended_data-8.3.0}/docs/ownership-map.md +0 -0
  39. {extended_data-8.2.0 → extended_data-8.3.0}/docs/package-surface.md +0 -0
  40. {extended_data-8.2.0 → extended_data-8.3.0}/docs/publishing.md +0 -0
  41. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/README.md +0 -0
  42. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/basic_usage.py +0 -0
  43. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/composed_workflows.py +0 -0
  44. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/file_operations.py +0 -0
  45. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/serialization.py +0 -0
  46. {extended_data-8.2.0 → extended_data-8.3.0}/examples/core/string_transformations.py +0 -0
  47. {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/README.md +0 -0
  48. {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/__init__.py +0 -0
  49. {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/basic_usage.py +0 -0
  50. {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/decorator_api.py +0 -0
  51. {extended_data-8.2.0 → extended_data-8.3.0}/examples/inputs/encoding_decoding.py +0 -0
  52. {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/README.md +0 -0
  53. {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/basic_logging.py +0 -0
  54. {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/exit_run_formatting.py +0 -0
  55. {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/markers_and_storage.py +0 -0
  56. {extended_data-8.2.0 → extended_data-8.3.0}/examples/logging/verbosity_control.py +0 -0
  57. {extended_data-8.2.0 → extended_data-8.3.0}/release-please-config.json +0 -0
  58. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/__init__.py +0 -0
  59. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/_version.py +0 -0
  60. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/cli.py +0 -0
  61. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/containers/__init__.py +0 -0
  62. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/__init__.py +0 -0
  63. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/__main__.py +0 -0
  64. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/decorators.py +0 -0
  65. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/inputs/py.typed +0 -0
  66. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/__init__.py +0 -0
  67. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/base64.py +0 -0
  68. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/exporters.py +0 -0
  69. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/files.py +0 -0
  70. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/io/importers.py +0 -0
  71. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/__init__.py +0 -0
  72. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/const.py +0 -0
  73. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/handlers.py +0 -0
  74. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/log_types.py +0 -0
  75. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/logging.py +0 -0
  76. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/py.typed +0 -0
  77. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/logging/utils.py +0 -0
  78. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/__init__.py +0 -0
  79. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/__init__.py +0 -0
  80. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/_normalization.py +0 -0
  81. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/errors.py +0 -0
  82. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/hcl.py +0 -0
  83. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/json.py +0 -0
  84. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/toml.py +0 -0
  85. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/__init__.py +0 -0
  86. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/constructors.py +0 -0
  87. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/dumpers.py +0 -0
  88. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/loaders.py +0 -0
  89. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/representers.py +0 -0
  90. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/tag_classes.py +0 -0
  91. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/formats/yaml/utils.py +0 -0
  92. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/introspection.py +0 -0
  93. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/mappings.py +0 -0
  94. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/matching.py +0 -0
  95. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/numbers.py +0 -0
  96. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/redaction.py +0 -0
  97. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/sequences.py +0 -0
  98. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/serialization.py +0 -0
  99. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/splitting.py +0 -0
  100. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/state.py +0 -0
  101. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/string_transforms.py +0 -0
  102. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/strings.py +0 -0
  103. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/__init__.py +0 -0
  104. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/__init__.py +0 -0
  105. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/notation.py +0 -0
  106. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/numbers/words.py +0 -0
  107. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/strings/__init__.py +0 -0
  108. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/transformations/strings/inflection.py +0 -0
  109. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/primitives/types.py +0 -0
  110. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/py.typed +0 -0
  111. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/workflows/__init__.py +0 -0
  112. {extended_data-8.2.0 → extended_data-8.3.0}/src/extended_data/workflows/sync.py +0 -0
  113. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_base64_utils.py +0 -0
  114. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_export_utils.py +0 -0
  115. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_file_data_type.py +0 -0
  116. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_hcl2_utils.py +0 -0
  117. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_import_utils.py +0 -0
  118. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_integration_workflows.py +0 -0
  119. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_json_utils.py +0 -0
  120. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_list_data_type.py +0 -0
  121. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_map_data_type.py +0 -0
  122. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_matcher_utils.py +0 -0
  123. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_number_transformations.py +0 -0
  124. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_package_cli.py +0 -0
  125. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_package_surface.py +0 -0
  126. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_redaction.py +0 -0
  127. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_release_hygiene.py +0 -0
  128. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_serialization_utils.py +0 -0
  129. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_splitter_utils.py +0 -0
  130. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_stack_utils.py +0 -0
  131. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_state_utils.py +0 -0
  132. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_string_data_type.py +0 -0
  133. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_string_transformations.py +0 -0
  134. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_toml_utils.py +0 -0
  135. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_type_utils.py +0 -0
  136. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_workflows.py +0 -0
  137. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/test_yaml_utils.py +0 -0
  138. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/numbers/test_notation.py +0 -0
  139. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/numbers/test_words.py +0 -0
  140. {extended_data-8.2.0 → extended_data-8.3.0}/tests/core/transformations/strings/test_inflection.py +0 -0
  141. {extended_data-8.2.0 → extended_data-8.3.0}/tests/examples/test_safe_examples.py +0 -0
  142. {extended_data-8.2.0 → extended_data-8.3.0}/tests/inputs/test_decorators.py +0 -0
  143. {extended_data-8.2.0 → extended_data-8.3.0}/tests/inputs/test_main.py +0 -0
  144. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/conftest.py +0 -0
  145. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/integration/test_lifecycle_logging.py +0 -0
  146. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_exit_run.py +0 -0
  147. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_handlers.py +0 -0
  148. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_logging.py +0 -0
  149. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_properties.py +0 -0
  150. {extended_data-8.2.0 → extended_data-8.3.0}/tests/logging/test_utils.py +0 -0
  151. {extended_data-8.2.0 → extended_data-8.3.0}/tox.ini +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "8.3.0"
3
+ }
@@ -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.2.0
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.value).transform("unhump").result()
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 and ExtendedString/Dict/List/Tuple/Set wrappers
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 generic facade for any incoming
165
- value and delegates to shape-specific containers where possible. For example,
166
- `ExtendedString.decode_json()` promotes JSON into extended containers,
167
- `ExtendedDict.reconstruct_special_types()` turns string scalars into
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.value).transform("unhump").result()
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 and ExtendedString/Dict/List/Tuple/Set wrappers
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 generic facade for any incoming
93
- value and delegates to shape-specific containers where possible. For example,
94
- `ExtendedString.decode_json()` promotes JSON into extended containers,
95
- `ExtendedDict.reconstruct_special_types()` turns string scalars into
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
- Tier 3 starts with `ExtendedData`, a stable facade for callers that do not know
6
- or should not care which shape is currently inside a payload.
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
- ## Generic ExtendedData
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 facade exposes broad shape predicates without forcing dict-only code paths:
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.2.0"
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 container facade."""
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
- """Stable holder for any promoted Extended Data value.
18
-
19
- ``extend_data`` returns the most specific Tier 2 container for a value. This
20
- facade is for higher-level code that cannot or should not care whether the
21
- current payload is mapping-, sequence-, string-, set-, scalar-, or object-
22
- shaped.
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
- __hash__ = None # type: ignore[assignment]
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
- self._value = value.value if isinstance(value, ExtendedData) else extend_data(value)
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 promoted underlying value."""
36
- return self._value
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 the promoted value type name."""
41
- return type(self._value).__name__
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 the current value."""
46
- if self._value is None:
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(self._value, ExtendedDict | Mapping):
66
+ if isinstance(value, ExtendedDict | Mapping):
49
67
  return "mapping"
50
- if isinstance(self._value, ExtendedList | list):
68
+ if isinstance(value, ExtendedList | list):
51
69
  return "list"
52
- if isinstance(self._value, ExtendedTuple | tuple):
70
+ if isinstance(value, ExtendedTuple | tuple):
53
71
  return "tuple"
54
- if isinstance(self._value, ExtendedSet | set | frozenset):
72
+ if isinstance(value, ExtendedSet | set | frozenset):
55
73
  return "set"
56
- if isinstance(self._value, ExtendedString | str | UserString):
74
+ if isinstance(value, ExtendedString | str | UserString):
57
75
  return "string"
58
- if isinstance(self._value, bool | int | float | complex | bytes | bytearray | memoryview | Path):
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 ``ExtendedData`` wrapper from any value."""
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 a generic wrapper."""
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 decoded generic data."""
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._value)
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 ``ExtendedData`` copy."""
150
- return ExtendedData(deepcopy(self._value))
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
- self._value = extend_data(value)
157
- return self
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 the promoted value and wrap the result."""
161
- return ExtendedData(transform(self._value))
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._value).transform(*steps).result().value)
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._value, "deep_merge", None)
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._value)
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._value, export_to_yaml=export_to_yaml)
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._value, allow_encoding=allow_encoding, **format_opts)
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
- method = getattr(self._value, "get", None)
249
- if not callable(method):
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
- method = getattr(self._value, "append", None)
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
- method = getattr(self._value, "extend", None)
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
- method = getattr(self._value, "update", None)
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
- method = getattr(self._value, "add", None)
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(self._value)
329
+ return iter(value)
297
330
  except TypeError:
298
- return iter([self._value])
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(self._value)
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
- return bool(self._value)
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
- return self._value[key]
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
- self._value[key] = value
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
- del self._value[key]
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 self._value
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._value!r})"
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 Iterable, Mapping
7
- from typing import TYPE_CHECKING, Any, Self, overload
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
- @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]
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: