extended-data 8.4.0__tar.gz → 8.4.2__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 (145) hide show
  1. {extended_data-8.4.0 → extended_data-8.4.2}/CHANGELOG.md +14 -0
  2. {extended_data-8.4.0 → extended_data-8.4.2}/PKG-INFO +2 -2
  3. {extended_data-8.4.0 → extended_data-8.4.2}/docs/PUBLISHING_CHECKLIST.rst +43 -13
  4. {extended_data-8.4.0 → extended_data-8.4.2}/docs/architecture.rst +30 -0
  5. {extended_data-8.4.0 → extended_data-8.4.2}/docs/conf.py +1 -1
  6. {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/containers.rst +26 -0
  7. {extended_data-8.4.0 → extended_data-8.4.2}/docs/operations/inputs.rst +1 -1
  8. {extended_data-8.4.0 → extended_data-8.4.2}/docs/ownership-map.rst +8 -4
  9. {extended_data-8.4.0 → extended_data-8.4.2}/docs/package-surface.rst +6 -2
  10. {extended_data-8.4.0 → extended_data-8.4.2}/docs/publishing.rst +31 -0
  11. {extended_data-8.4.0 → extended_data-8.4.2}/pyproject.toml +2 -2
  12. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/data.py +46 -2
  13. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/__init__.py +1 -1
  14. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/decorators.py +1 -1
  15. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/files.py +1 -1
  16. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/types.py +0 -1
  17. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_containers.py +182 -0
  18. extended_data-8.4.2/tests/core/test_decode_errors.py +76 -0
  19. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_file_data_type.py +32 -0
  20. extended_data-8.4.2/tests/core/test_normalization.py +50 -0
  21. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_package_cli.py +192 -0
  22. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_release_hygiene.py +111 -4
  23. extended_data-8.4.0/tests/inputs/test_main.py → extended_data-8.4.2/tests/inputs/test_providers.py +15 -2
  24. {extended_data-8.4.0 → extended_data-8.4.2}/.gitignore +0 -0
  25. {extended_data-8.4.0 → extended_data-8.4.2}/README.md +0 -0
  26. {extended_data-8.4.0 → extended_data-8.4.2}/docs/CNAME +0 -0
  27. {extended_data-8.4.0 → extended_data-8.4.2}/docs/__init__.py +0 -0
  28. {extended_data-8.4.0 → extended_data-8.4.2}/docs/_static/.gitkeep +0 -0
  29. {extended_data-8.4.0 → extended_data-8.4.2}/docs/_static/extended-data.css +0 -0
  30. {extended_data-8.4.0 → extended_data-8.4.2}/docs/api/index.rst +0 -0
  31. {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/primitives.rst +0 -0
  32. {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/workflows.rst +0 -0
  33. {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/core.rst +0 -0
  34. {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/index.rst +0 -0
  35. {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/inputs.rst +0 -0
  36. {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/logging.rst +0 -0
  37. {extended_data-8.4.0 → extended_data-8.4.2}/docs/getting-started.rst +0 -0
  38. {extended_data-8.4.0 → extended_data-8.4.2}/docs/index.rst +0 -0
  39. {extended_data-8.4.0 → extended_data-8.4.2}/docs/operations/logging.rst +0 -0
  40. {extended_data-8.4.0 → extended_data-8.4.2}/docs/pillars.rst +0 -0
  41. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/README.md +0 -0
  42. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/basic_usage.py +0 -0
  43. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/composed_workflows.py +0 -0
  44. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/file_operations.py +0 -0
  45. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/serialization.py +0 -0
  46. {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/string_transformations.py +0 -0
  47. {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/README.md +0 -0
  48. {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/__init__.py +0 -0
  49. {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/basic_usage.py +0 -0
  50. {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/decorator_api.py +0 -0
  51. {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/encoding_decoding.py +0 -0
  52. {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/README.md +0 -0
  53. {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/basic_logging.py +0 -0
  54. {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/exit_run_formatting.py +0 -0
  55. {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/markers_and_storage.py +0 -0
  56. {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/verbosity_control.py +0 -0
  57. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/__init__.py +0 -0
  58. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/_version.py +0 -0
  59. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/cli.py +0 -0
  60. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/__init__.py +0 -0
  61. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/factory.py +0 -0
  62. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/mappings.py +0 -0
  63. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/sequences.py +0 -0
  64. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/strings.py +0 -0
  65. /extended_data-8.4.0/src/extended_data/inputs/__main__.py → /extended_data-8.4.2/src/extended_data/inputs/providers.py +0 -0
  66. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/py.typed +0 -0
  67. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/__init__.py +0 -0
  68. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/base64.py +0 -0
  69. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/exporters.py +0 -0
  70. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/importers.py +0 -0
  71. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/__init__.py +0 -0
  72. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/const.py +0 -0
  73. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/handlers.py +0 -0
  74. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/log_types.py +0 -0
  75. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/logging.py +0 -0
  76. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/py.typed +0 -0
  77. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/utils.py +0 -0
  78. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/__init__.py +0 -0
  79. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/__init__.py +0 -0
  80. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/_normalization.py +0 -0
  81. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/errors.py +0 -0
  82. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/hcl.py +0 -0
  83. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/json.py +0 -0
  84. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/toml.py +0 -0
  85. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/__init__.py +0 -0
  86. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/constructors.py +0 -0
  87. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/dumpers.py +0 -0
  88. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/loaders.py +0 -0
  89. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/representers.py +0 -0
  90. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/tag_classes.py +0 -0
  91. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/utils.py +0 -0
  92. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/introspection.py +0 -0
  93. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/mappings.py +0 -0
  94. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/matching.py +0 -0
  95. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/numbers.py +0 -0
  96. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/redaction.py +0 -0
  97. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/sequences.py +0 -0
  98. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/serialization.py +0 -0
  99. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/splitting.py +0 -0
  100. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/state.py +0 -0
  101. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/string_transforms.py +0 -0
  102. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/strings.py +0 -0
  103. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/__init__.py +0 -0
  104. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/__init__.py +0 -0
  105. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/notation.py +0 -0
  106. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/words.py +0 -0
  107. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/__init__.py +0 -0
  108. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/inflection.py +0 -0
  109. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/py.typed +0 -0
  110. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/workflows/__init__.py +0 -0
  111. {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/workflows/sync.py +0 -0
  112. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_base64_utils.py +0 -0
  113. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_export_utils.py +0 -0
  114. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_hcl2_utils.py +0 -0
  115. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_import_utils.py +0 -0
  116. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_integration_workflows.py +0 -0
  117. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_json_utils.py +0 -0
  118. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_list_data_type.py +0 -0
  119. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_map_data_type.py +0 -0
  120. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_matcher_utils.py +0 -0
  121. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_number_transformations.py +0 -0
  122. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_package_surface.py +0 -0
  123. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_redaction.py +0 -0
  124. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_serialization_utils.py +0 -0
  125. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_splitter_utils.py +0 -0
  126. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_stack_utils.py +0 -0
  127. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_state_utils.py +0 -0
  128. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_string_data_type.py +0 -0
  129. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_string_transformations.py +0 -0
  130. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_toml_utils.py +0 -0
  131. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_type_utils.py +0 -0
  132. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_workflows.py +0 -0
  133. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_yaml_utils.py +0 -0
  134. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/numbers/test_notation.py +0 -0
  135. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/numbers/test_words.py +0 -0
  136. {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/strings/test_inflection.py +0 -0
  137. {extended_data-8.4.0 → extended_data-8.4.2}/tests/examples/test_safe_examples.py +0 -0
  138. {extended_data-8.4.0 → extended_data-8.4.2}/tests/inputs/test_decorators.py +0 -0
  139. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/conftest.py +0 -0
  140. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/integration/test_lifecycle_logging.py +0 -0
  141. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_exit_run.py +0 -0
  142. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_handlers.py +0 -0
  143. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_logging.py +0 -0
  144. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_properties.py +0 -0
  145. {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_utils.py +0 -0
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [8.4.2](https://github.com/jbcom/extended-data/compare/extended-data-v8.4.1...extended-data-v8.4.2) (2026-06-27)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * close doc, autodoc2, and test coverage gaps ([#30](https://github.com/jbcom/extended-data/issues/30)) ([9c577df](https://github.com/jbcom/extended-data/commit/9c577df4e68c0ace888566a981f6054806b0da75))
9
+
10
+ ## [8.4.1](https://github.com/jbcom/extended-data/compare/extended-data-v8.4.0...extended-data-v8.4.1) (2026-06-27)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * stabilize ExtendedData subclass factory reentry ([#28](https://github.com/jbcom/extended-data/issues/28)) ([9a0fce5](https://github.com/jbcom/extended-data/commit/9a0fce5dad766be5b4c7445c6a42cf67b4183040))
16
+
3
17
  ## [8.4.0](https://github.com/jbcom/extended-data/compare/extended-data-v8.3.1...extended-data-v8.4.0) (2026-06-27)
4
18
 
5
19
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: extended-data
3
- Version: 8.4.0
3
+ Version: 8.4.2
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
@@ -53,7 +53,7 @@ Requires-Dist: furo>=2025.12.19; extra == 'docs'
53
53
  Requires-Dist: myst-parser<6.0.0,>=4.0.1; extra == 'docs'
54
54
  Requires-Dist: sphinx-autodoc2>=0.5.0; extra == 'docs'
55
55
  Requires-Dist: sphinx-copybutton>=0.5.2; extra == 'docs'
56
- Requires-Dist: sphinx<9.0.0,>=8.2.3; extra == 'docs'
56
+ Requires-Dist: sphinx<10.0.0,>=8.2.3; extra == 'docs'
57
57
  Provides-Extra: tests
58
58
  Requires-Dist: coverage[toml]>=7.6.0; extra == 'tests'
59
59
  Requires-Dist: hypothesis>=6.100.2; extra == 'tests'
@@ -1,22 +1,22 @@
1
1
  Publishing Checklist
2
2
  ====================
3
3
 
4
- The workspace uses the standard ``ci.yml`` > ``release.yml`` > ``cd.yml``
5
- workflow shape. ``release.yml`` owns release-please. When a
4
+ The workspace uses the standard ``ci.yml`` > ``release.yml`` >
5
+ ``cd.yml`` workflow shape. ``release.yml`` owns release-please. When a
6
6
  release-please PR merge creates a release tag, it dispatches ``cd.yml``
7
- with the package name for package publication. ``extended-data`` releases
8
- also deploy the Sphinx/Furo documentation site. Do not hand-edit
9
- versions, changelog entries, release tags, or GitHub releases during the
10
- normal release path.
7
+ with the selected package name. ``extended-data`` releases also deploy
8
+ the Sphinx/Furo documentation site. Do not hand-edit versions, changelog
9
+ entries, release tags, or GitHub releases during the normal release
10
+ path.
11
11
 
12
12
  Release Model
13
13
  -------------
14
14
 
15
15
  - ``release.yml`` owns release-please version detection, changelog
16
- updates, release PRs, Git tags, and dispatching ``cd.yml`` with the
17
- selected workspace package after a release is created.
18
- - ``cd.yml`` owns tag-gated package verification and selected-package
19
- PyPI publication.
16
+ updates, release PRs, Git tags, and dispatching ``cd.yml`` after a
17
+ release is created.
18
+ - ``cd.yml`` owns tag-gated package verification, selected-package PyPI
19
+ publication, and ``extended-data`` docs deployment.
20
20
  - ``cd.yml`` deploys GitHub Pages only for ``extended-data`` releases.
21
21
  - Published package names are ``extended-data`` and
22
22
  ``pytest-extended-data``.
@@ -24,6 +24,8 @@ Release Model
24
24
  ``release.yml``.
25
25
  - The PyPI job uses OIDC trusted publishing through ``uv publish``; no
26
26
  PyPI token should be stored in repository secrets for the normal path.
27
+ - PyPI trusted publishers for this repository must use workflow
28
+ filename ``cd.yml``.
27
29
  - Do not configure package release-please entries to update the root
28
30
  ``uv.lock`` through ``extra-files``. The release PR owns package
29
31
  metadata, changelogs, and the release manifest; workspace setup can
@@ -79,9 +81,10 @@ Publishing Flow
79
81
  3. Review the release PR for the expected changelog and manifest
80
82
  updates.
81
83
  4. Merge the release PR.
82
- 5. Confirm ``release.yml`` created the GitHub release and dispatched
83
- ``cd.yml`` with the expected package name.
84
- 6. Confirm ``cd.yml`` published to PyPI through trusted publishing.
84
+ 5. Confirm ``release.yml`` created the GitHub release and dispatched the
85
+ expected package CD workflow.
86
+ 6. Confirm the package CD workflow published to PyPI through trusted
87
+ publishing.
85
88
  7. For ``extended-data`` releases, confirm ``cd.yml`` deployed the
86
89
  Sphinx site to GitHub Pages for ``extended-data.dev``.
87
90
  8. Verify the package can be installed from PyPI:
@@ -105,3 +108,30 @@ If a release workflow fails after release-please creates a tag:
105
108
  3. If a bad GitHub release was published, delete only the bad artifacts
106
109
  needed for repair.
107
110
  4. Document the repair in the PR or release notes.
111
+
112
+ First ``pytest-extended-data`` Publish
113
+ --------------------------------------
114
+
115
+ PyPI does not let a trusted publisher create a brand-new project unless
116
+ there is a matching pending publisher. Before rerunning the first
117
+ ``pytest-extended-data`` CD publish, add this pending publisher in PyPI:
118
+
119
+ +-------------------+----------------------------------+
120
+ | Field | Value |
121
+ +===================+==================================+
122
+ | PyPI project | ``pytest-extended-data`` |
123
+ +-------------------+----------------------------------+
124
+ | GitHub owner | ``jbcom`` |
125
+ +-------------------+----------------------------------+
126
+ | GitHub repository | ``extended-data`` |
127
+ +-------------------+----------------------------------+
128
+ | Workflow filename | ``cd.yml`` |
129
+ +-------------------+----------------------------------+
130
+ | Environment | leave blank |
131
+ +-------------------+----------------------------------+
132
+
133
+ Then rerun:
134
+
135
+ .. code:: bash
136
+
137
+ gh workflow run cd.yml --ref main -f tag=pytest-extended-data-v0.1.0 -f package=pytest-extended-data
@@ -100,6 +100,36 @@ Higher-order packages that must keep a stable outer identity can
100
100
  override ``cast()`` locally by storing an internal ``ExtendedData``
101
101
  value. That pattern belongs in those downstream repositories, not here.
102
102
 
103
+ Downstream Subclasses
104
+ ---------------------
105
+
106
+ Downstream packages may subclass ``ExtendedData`` when they need an
107
+ additive facade for coordination behavior. The base constructor only
108
+ performs polymorphic promotion when ``cls is ExtendedData``; constructing
109
+ a subclass keeps the subclass instance intact.
110
+
111
+ Use this pattern for stable downstream identities:
112
+
113
+ .. code:: python
114
+
115
+ class VendorLikeData(ExtendedData):
116
+ def __init__(self, value, *, provider):
117
+ self.provider = provider
118
+ self._data = ExtendedData(value)
119
+
120
+ @property
121
+ def value(self):
122
+ return self._data
123
+
124
+ def cast(self, value):
125
+ self._data = ExtendedData(value)
126
+ return self
127
+
128
+ ``ExtendedData(value)`` remains the canonical promotion path for the
129
+ stored payload. The downstream subclass owns only its additive behavior,
130
+ such as provider activation, capability dispatch, or runtime state.
131
+ It should not reimplement shape promotion or fork the container classes.
132
+
103
133
  Data Boundaries
104
134
  ---------------
105
135
 
@@ -63,6 +63,6 @@ autodoc2_packages = [
63
63
  ]
64
64
  autodoc2_render_plugin = "rst"
65
65
  autodoc2_docstring_parser_regexes = [(r".*", "myst")]
66
- autodoc2_hidden_objects = ["inherited", "dunder"]
66
+ autodoc2_hidden_objects = ["inherited", "dunder", "private"]
67
67
  autodoc2_class_docstring = "merge"
68
68
  autodoc2_module_summary = True
@@ -61,6 +61,32 @@ code paths:
61
61
  assert ExtendedData(42).is_scalar
62
62
  assert ExtendedData().is_none
63
63
 
64
+ Downstream packages can subclass ``ExtendedData`` for additive facades
65
+ without being collapsed by the root factory. Store the promoted payload
66
+ with ``ExtendedData(value)`` and keep provider or runtime behavior on the
67
+ subclass:
68
+
69
+ .. code:: python
70
+
71
+ class ProviderData(ExtendedData):
72
+ def __init__(self, value, *, provider):
73
+ self.provider = provider
74
+ self._data = ExtendedData(value)
75
+
76
+ @property
77
+ def value(self):
78
+ return self._data
79
+
80
+ provider_data = ProviderData({"service": {"name": "api"}}, provider="vendor")
81
+
82
+ assert type(provider_data) is ProviderData
83
+ assert provider_data.shape == "mapping"
84
+ assert provider_data["service"]["name"].upper_first() == "Api"
85
+
86
+ Facades that need to keep the same outer identity while changing shape
87
+ should override ``cast()`` and replace their internal ``ExtendedData``
88
+ value.
89
+
64
90
  It also provides shared data-boundary helpers:
65
91
 
66
92
  .. code:: python
@@ -13,7 +13,7 @@ The old directed inputs package is now first-class under
13
13
  from_environment=False,
14
14
  )
15
15
 
16
- print(provider.get_input("APP_DEBUG", as_type=bool))
16
+ print(provider.get_input("APP_DEBUG", is_bool=True))
17
17
  print(provider.snapshot_inputs())
18
18
 
19
19
  Decorators
@@ -35,8 +35,9 @@ Moved Out
35
35
  | External vendor API | ``jbcom/vendor-fabric`` | ``vendor-fabric`` |
36
36
  | clients | | |
37
37
  +-----------------------+-------------------------+------------------------------------+
38
- | Vendor fabric MCP and | ``jbcom/vendor-fabric`` | ``vendor-fabric[...]`` |
39
- | tool adapters | | |
38
+ | Vendor capability | ``jbcom/vendor-fabric`` | ``vendor-fabric[...]`` |
39
+ | dispatch and provider | | |
40
+ | SDK adapters | | |
40
41
  +-----------------------+-------------------------+------------------------------------+
41
42
  | Meshy, Slack, Google, | ``jbcom/vendor-fabric`` | ``vendor-fabric[...]`` |
42
43
  | GitHub, AWS, Vault, | | |
@@ -46,10 +47,10 @@ Moved Out
46
47
  | Vendor-backed Python | ``jbcom/vendor-fabric`` | ``vendor-fabric[secrets-sync]`` |
47
48
  | sync capabilities | | |
48
49
  +-----------------------+-------------------------+------------------------------------+
49
- | SecretSync agent tool | ``jbcom/vendor-fabric`` | ``vendor-fabric[ai,secrets-sync]`` |
50
+ | SecretSync agent tool | ``jbcom/agentic-fabric``| ``agentic-fabric[...]`` |
50
51
  | wrappers | | |
51
52
  +-----------------------+-------------------------+------------------------------------+
52
- | Agent framework | ``jbcom/vendor-fabric`` | ``vendor-fabric[ai]`` |
53
+ | Agent framework | ``jbcom/agentic-fabric``| ``agentic-fabric[...]`` |
53
54
  | integrations | | |
54
55
  +-----------------------+-------------------------+------------------------------------+
55
56
 
@@ -66,7 +67,10 @@ The intended layering is dependency-inward:
66
67
 
67
68
  extended-data
68
69
  <- vendor-fabric
70
+ <- agentic-fabric
69
71
 
70
72
  ``extended-data`` has no dependency on the higher layers. Higher layers
71
73
  may use ``extended-data`` primitives, containers, input handling,
72
74
  workflows, and logging without reimplementing those base concerns.
75
+ ``agentic-fabric`` depends on ``vendor-fabric`` for provider coordination;
76
+ ``vendor-fabric`` depends on ``extended-data`` for generic data behavior.
@@ -118,8 +118,12 @@ Split Packages
118
118
  --------------
119
119
 
120
120
  ``vendor-fabric`` owns external API clients, optional provider SDK
121
- dependencies, MCP/tool adapters, provider-specific examples,
122
- vendor-backed Python sync, and agent workflows.
121
+ dependencies, provider-specific examples, vendor capability dispatch,
122
+ and vendor-backed Python sync.
123
+
124
+ ``agentic-fabric`` owns agent orchestration, runtime selection,
125
+ agent-facing tool catalogs, MCP agent adapters, and framework adapters
126
+ such as LangChain, CrewAI, LangGraph, and Strands.
123
127
 
124
128
  ``extended-data`` does not preserve ``extended_data.connectors``,
125
129
  ``extended_data.secrets``, ``extended_data_types``,
@@ -26,6 +26,37 @@ Release Flow
26
26
  publishing. ``extended-data`` releases also deploy the Sphinx/Furo
27
27
  docs site.
28
28
 
29
+ PyPI Trusted Publishers
30
+ -----------------------
31
+
32
+ Each published package needs a PyPI trusted publisher matching the
33
+ workflow that uploads it. Both ``extended-data`` and
34
+ ``pytest-extended-data`` publish through the standard ``cd.yml``
35
+ workflow. If a new package needs an initial PyPI pending publisher, use
36
+ these values before rerunning CD:
37
+
38
+ .. list-table::
39
+ :header-rows: 1
40
+
41
+ * - Field
42
+ - Value
43
+ * - PyPI project
44
+ - ``pytest-extended-data``
45
+ * - GitHub owner
46
+ - ``jbcom``
47
+ * - GitHub repository
48
+ - ``extended-data``
49
+ * - Workflow filename
50
+ - ``cd.yml``
51
+ * - Environment
52
+ - leave blank
53
+
54
+ After the pending publisher exists, rerun the failed publish with:
55
+
56
+ .. code:: bash
57
+
58
+ gh workflow run cd.yml --ref main -f tag=pytest-extended-data-v0.1.0 -f package=pytest-extended-data
59
+
29
60
  Package-scoped release-please configuration owns package versions,
30
61
  package changelogs, release tags, and the release manifest. Do not add
31
62
  the root ``uv.lock`` as a package ``extra-files`` target; normal
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "extended-data"
7
- version = "8.4.0"
7
+ version = "8.4.2"
8
8
  description = "Comprehensive Python data utilities for serialization, inputs, logging, and workflows"
9
9
  requires-python = ">=3.11"
10
10
  license = { text = "MIT" }
@@ -77,7 +77,7 @@ dev = [
77
77
  docs = [
78
78
  "furo>=2025.12.19",
79
79
  "myst-parser>=4.0.1,<6.0.0",
80
- "sphinx>=8.2.3,<9.0.0",
80
+ "sphinx>=8.2.3,<10.0.0",
81
81
  "sphinx-autodoc2>=0.5.0",
82
82
  "sphinx-copybutton>=0.5.2",
83
83
  ]
@@ -5,11 +5,13 @@ from __future__ import annotations
5
5
  from collections.abc import Callable, Iterable, Iterator, Mapping
6
6
  from contextlib import suppress
7
7
  from copy import deepcopy
8
+ from functools import wraps
8
9
  from pathlib import Path
9
10
  from typing import Any, Self, cast
10
11
 
11
12
 
12
13
  _FACTORY_INITIALIZED_ATTR = "_extended_data_factory_initialized"
14
+ _DELEGATING_ATTRIBUTE_ATTR = "_extended_data_delegating_attribute"
13
15
 
14
16
 
15
17
  class ExtendedData:
@@ -23,10 +25,38 @@ class ExtendedData:
23
25
  API, and vendor boundaries.
24
26
  """
25
27
 
26
- def __new__(cls, value: Any = None) -> Self:
28
+ def __init_subclass__(cls, **kwargs: Any) -> None:
29
+ """Make factory re-entry safe for downstream subclasses."""
30
+ super().__init_subclass__(**kwargs)
31
+ original_init = getattr(cls, "__init__", None)
32
+ if (
33
+ original_init is None
34
+ or original_init is object.__init__
35
+ or getattr(original_init, "_extended_data_factory_guard", False)
36
+ ):
37
+ return
38
+
39
+ @wraps(original_init)
40
+ def guarded_init(self: Any, *args: Any, **kwargs: Any) -> None:
41
+ try:
42
+ initialized = object.__getattribute__(self, _FACTORY_INITIALIZED_ATTR)
43
+ except AttributeError:
44
+ initialized = False
45
+ if initialized:
46
+ setattr(self, _FACTORY_INITIALIZED_ATTR, False)
47
+ return
48
+ original_init(self, *args, **kwargs)
49
+
50
+ guarded_init._extended_data_factory_guard = True # type: ignore[attr-defined]
51
+ cls.__init__ = guarded_init # type: ignore[method-assign]
52
+
53
+ def __new__(cls, value: Any = None, *args: Any, **kwargs: Any) -> Self:
27
54
  """Return the most specific Extended Data object for ``value``."""
28
55
  if cls is not ExtendedData:
29
56
  return super().__new__(cls)
57
+ if args or kwargs:
58
+ msg = "ExtendedData() accepts a single optional value"
59
+ raise TypeError(msg)
30
60
 
31
61
  from extended_data.containers.factory import extend_data
32
62
 
@@ -319,10 +349,24 @@ class ExtendedData:
319
349
 
320
350
  def __getattr__(self, name: str) -> Any:
321
351
  """Delegate shape-specific operations to the promoted value."""
352
+ if name.startswith("_"):
353
+ raise AttributeError(name)
322
354
  try:
323
- value = object.__getattribute__(self, "_value")
355
+ delegating_attribute = object.__getattribute__(self, _DELEGATING_ATTRIBUTE_ATTR)
356
+ except AttributeError:
357
+ delegating_attribute = False
358
+ if delegating_attribute:
359
+ raise AttributeError(name)
360
+ try:
361
+ object.__setattr__(self, _DELEGATING_ATTRIBUTE_ATTR, True)
362
+ value = self.value
324
363
  except AttributeError as exc:
325
364
  raise AttributeError(name) from exc
365
+ finally:
366
+ with suppress(AttributeError):
367
+ object.__delattr__(self, _DELEGATING_ATTRIBUTE_ATTR)
368
+ if value is self:
369
+ raise AttributeError(name)
326
370
  return getattr(value, name)
327
371
 
328
372
  def __iter__(self) -> Iterator[Any]:
@@ -7,8 +7,8 @@ various sources like environment variables, stdin, and predefined dictionaries.
7
7
  from __future__ import annotations
8
8
 
9
9
  from extended_data._version import __version__
10
- from extended_data.inputs.__main__ import InputProvider
11
10
  from extended_data.inputs.decorators import directed_inputs, input_config
11
+ from extended_data.inputs.providers import InputProvider
12
12
 
13
13
 
14
14
  __all__ = ["InputProvider", "__version__", "directed_inputs", "input_config"]
@@ -25,7 +25,7 @@ from dataclasses import dataclass, field
25
25
  from typing import TYPE_CHECKING, Any
26
26
 
27
27
  from extended_data.containers import ExtendedDict, to_builtin
28
- from extended_data.inputs.__main__ import InputProvider
28
+ from extended_data.inputs.providers import InputProvider
29
29
 
30
30
 
31
31
  if TYPE_CHECKING:
@@ -181,7 +181,7 @@ def _data_file_metadata(
181
181
  data: Any,
182
182
  extra: Mapping[str, Any] | None = None,
183
183
  ) -> ExtendedDict:
184
- """Return promoted artifact metadata for workflow and connector handoff."""
184
+ """Return promoted artifact metadata for workflow and downstream handoff."""
185
185
  metadata = ExtendedDict(redact_sensitive_data(extra or {}))
186
186
  metadata.update(
187
187
  {
@@ -17,7 +17,6 @@ Functions:
17
17
 
18
18
  Classes:
19
19
  - ConversionError: Custom error class for handling conversion failures.
20
- - ReconstructionError: Custom error class for handling reconstruction failures.
21
20
 
22
21
  Constants:
23
22
  - DATE_PATTERN: Regex for matching ISO 8601 date strings.
@@ -555,6 +555,99 @@ def test_extended_data_factory_does_not_reinitialize_promoted_containers(monkeyp
555
555
  assert getattr(tuple_value, _FACTORY_INITIALIZED_ATTR, False) is False
556
556
 
557
557
 
558
+ def test_extended_data_factory_rejects_extra_root_constructor_arguments() -> None:
559
+ """Root factory construction should stay a single-value boundary."""
560
+ with pytest.raises(TypeError, match=r"ExtendedData\(\) accepts a single optional value"):
561
+ ExtendedData({"service": "api"}, provider="vendor") # type: ignore[call-arg]
562
+
563
+
564
+ def test_extended_data_subclasses_keep_identity_and_delegate_to_internal_value() -> None:
565
+ """Downstream facades should extend ExtendedData without being factory-collapsed."""
566
+
567
+ class CoordinatedData(ExtendedData):
568
+ def __init__(self, value: Any, *, provider: str = "test") -> None:
569
+ self.provider = provider
570
+ self._data = ExtendedData(value)
571
+
572
+ @property
573
+ def value(self) -> ExtendedData:
574
+ return self._data
575
+
576
+ def cast(self, value: Any) -> CoordinatedData:
577
+ self._data = ExtendedData(value)
578
+ return self
579
+
580
+ coordinated = CoordinatedData({"service": {"name": "api"}}, provider="vendor")
581
+
582
+ assert type(coordinated) is CoordinatedData
583
+ assert isinstance(coordinated, ExtendedData)
584
+ assert coordinated.provider == "vendor"
585
+ assert type(coordinated.value) is ExtendedDict
586
+ assert coordinated.shape == "mapping"
587
+ assert coordinated.is_mapping is True
588
+ assert list(coordinated.keys()) == ["service"]
589
+ assert coordinated["service"]["name"].upper_first() == "Api"
590
+ assert coordinated.as_builtin() == {"service": {"name": "api"}}
591
+ assert extend_data(coordinated) is coordinated
592
+ assert ExtendedData(coordinated) is coordinated
593
+ assert to_builtin(coordinated) == {"service": {"name": "api"}}
594
+
595
+ same_outer_identity = coordinated.cast([{"name": "worker"}])
596
+ same_outer_identity.append({"name": "scheduler"})
597
+
598
+ assert same_outer_identity is coordinated
599
+ assert type(coordinated.value) is ExtendedList
600
+ assert coordinated.shape == "list"
601
+ assert coordinated[0]["name"].upper_first() == "Worker"
602
+ assert coordinated[1]["name"].upper_first() == "Scheduler"
603
+ assert to_builtin(coordinated) == [{"name": "worker"}, {"name": "scheduler"}]
604
+
605
+
606
+ def test_extended_data_subclass_facades_delegate_attribute_methods() -> None:
607
+ """Facade subclasses should preserve promoted mapping and string methods."""
608
+
609
+ class CoordinatedData(ExtendedData):
610
+ def __init__(self, value: Any) -> None:
611
+ self._data = ExtendedData(value)
612
+
613
+ @property
614
+ def value(self) -> ExtendedData:
615
+ return self._data
616
+
617
+ mapping = CoordinatedData({"service": {"name": "api"}})
618
+ text = CoordinatedData("api response")
619
+
620
+ assert list(mapping.keys()) == ["service"]
621
+ assert mapping.get("service")["name"].upper_first() == "Api"
622
+ assert text.upper_first() == "Api response"
623
+ assert text.to_snake_case() == "api_response"
624
+
625
+
626
+ def test_extended_data_subclass_guard_supports_mixin_initializers() -> None:
627
+ """Factory re-entry should not rerun initializers inherited from mixins."""
628
+
629
+ class ProviderInitMixin:
630
+ def __init__(self, value: Any, *, provider: str = "mixin") -> None:
631
+ self.provider = provider
632
+ self.init_count = getattr(self, "init_count", 0) + 1
633
+ self._data = ExtendedData(value)
634
+
635
+ @property
636
+ def value(self) -> ExtendedData:
637
+ return self._data
638
+
639
+ class MixinCoordinatedData(ProviderInitMixin, ExtendedData):
640
+ pass
641
+
642
+ coordinated = MixinCoordinatedData({"service": {"name": "api"}}, provider="vendor")
643
+ promoted_again = ExtendedData(coordinated)
644
+
645
+ assert promoted_again is coordinated
646
+ assert coordinated.init_count == 1
647
+ assert coordinated.provider == "vendor"
648
+ assert coordinated["service"]["name"].upper_first() == "Api"
649
+
650
+
558
651
  def test_generic_extended_data_facade_methods_for_scalar_values(tmp_path: Path) -> None:
559
652
  """Scalar holders should expose the same explicit boundary helpers."""
560
653
  scalar = ExtendedData(42)
@@ -677,3 +770,92 @@ def test_container_classes_are_root_exports() -> None:
677
770
  assert extended_data.ExtendedTuple is ExtendedTuple
678
771
  assert extended_data.extend_data is extend_data
679
772
  assert extended_data.to_builtin is to_builtin
773
+
774
+
775
+ def test_extended_string_transform_methods_round_trip() -> None:
776
+ """ExtendedString transform bound methods should delegate to Tier 1 primitives."""
777
+ assert ExtendedString("camelCase").lower_first() == "camelCase"
778
+ assert ExtendedString("CamelCase").lower_first() == "camelCase"
779
+ assert ExtendedString("API Response").sanitize() == "API_Response"
780
+ assert ExtendedString("API Response").sanitize("-") == "API-Response"
781
+ assert ExtendedString("abcdefghijklmnop").truncate(5) == "ab..."
782
+ assert ExtendedString("abcdefghijklmnop").truncate(5, ender="!") == "abcd!"
783
+ assert ExtendedString("api_service").titleize_name() == "Api Service"
784
+ assert ExtendedString("api response").to_camel_case() == "apiResponse"
785
+ assert ExtendedString("api response").to_camel_case(uppercase_first=True) == "ApiResponse"
786
+ assert ExtendedString("api response").to_pascal_case() == "ApiResponse"
787
+ assert ExtendedString("API Response").pluralize() == "API Responses"
788
+ assert ExtendedString("API Responses").singularize() == "API Response"
789
+ assert ExtendedString("api_response").humanize() == "API response"
790
+ assert ExtendedString("api_response").titleize() == "Api Response"
791
+
792
+
793
+ def test_extended_string_is_url_bound_method() -> None:
794
+ """ExtendedString.is_url should delegate to the Tier 1 url detector."""
795
+ assert ExtendedString("https://example.com").is_url() is True
796
+ assert ExtendedString("not a url").is_url() is False
797
+
798
+
799
+ def test_extended_dict_unhump_bound_method() -> None:
800
+ """ExtendedDict.unhump should delegate to unhump_map with prefix option."""
801
+ value = ExtendedDict({"serviceName": "api", "HTTPResponse": 200})
802
+ unhumped = value.unhump()
803
+ assert unhumped == {"service_name": "api", "http_response": 200}
804
+ assert isinstance(unhumped, ExtendedDict)
805
+
806
+ prefixed = ExtendedDict({"v1Service": "api", "v2Other": "x"}).unhump(drop_without_prefix="v1")
807
+ assert prefixed == {"v1_service": "api"}
808
+
809
+
810
+ def test_extended_dict_delitem_succeeds_on_mappings() -> None:
811
+ """ExtendedData.__delitem__ should delete keys on mutable mappings."""
812
+ value = ExtendedDict({"service": "api", "region": "us-east-1"})
813
+ del value["region"]
814
+ assert "region" not in value
815
+ assert value == {"service": "api"}
816
+
817
+
818
+ def test_reconstruct_special_types_fail_silently_branches() -> None:
819
+ """reconstruct_special_types(fail_silently=True) should swallow, False should raise."""
820
+ from extended_data.primitives.types import ConversionError
821
+
822
+ bad_yaml = "---\ninvalid: [unclosed"
823
+ bad = ExtendedList([bad_yaml])
824
+
825
+ with pytest.raises(ConversionError):
826
+ bad.reconstruct_special_types(fail_silently=False)
827
+
828
+ silent = bad.reconstruct_special_types(fail_silently=True)
829
+ assert isinstance(silent, ExtendedList)
830
+
831
+ bad_tuple = ExtendedTuple((bad_yaml,))
832
+ with pytest.raises(ConversionError):
833
+ bad_tuple.reconstruct_special_types(fail_silently=False)
834
+ silent_tuple = bad_tuple.reconstruct_special_types(fail_silently=True)
835
+ assert isinstance(silent_tuple, ExtendedTuple)
836
+
837
+ bad_set = ExtendedSet({bad_yaml})
838
+ with pytest.raises(ConversionError):
839
+ bad_set.reconstruct_special_types(fail_silently=False)
840
+ silent_set = bad_set.reconstruct_special_types(fail_silently=True)
841
+ assert isinstance(silent_set, ExtendedSet)
842
+
843
+ bad_dict = ExtendedDict({"key": bad_yaml})
844
+ with pytest.raises(ConversionError):
845
+ bad_dict.reconstruct_special_types(fail_silently=False)
846
+ silent_dict = bad_dict.reconstruct_special_types(fail_silently=True)
847
+ assert isinstance(silent_dict, ExtendedDict)
848
+
849
+
850
+ def test_extended_set_discard_and_copy() -> None:
851
+ """ExtendedSet.discard and copy should mutate / duplicate as expected."""
852
+ value = ExtendedSet({"api", "worker", "gateway"})
853
+ discarded = value.discard("worker")
854
+ assert discarded is value
855
+ assert "worker" not in value
856
+ assert value == {"api", "gateway"}
857
+
858
+ copied = value.copy()
859
+ assert copied == value
860
+ assert copied is not value
861
+ assert isinstance(copied, ExtendedSet)