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.
- {extended_data-8.4.0 → extended_data-8.4.2}/CHANGELOG.md +14 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/PKG-INFO +2 -2
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/PUBLISHING_CHECKLIST.rst +43 -13
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/architecture.rst +30 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/conf.py +1 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/containers.rst +26 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/operations/inputs.rst +1 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/ownership-map.rst +8 -4
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/package-surface.rst +6 -2
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/publishing.rst +31 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/pyproject.toml +2 -2
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/data.py +46 -2
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/__init__.py +1 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/decorators.py +1 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/files.py +1 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/types.py +0 -1
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_containers.py +182 -0
- extended_data-8.4.2/tests/core/test_decode_errors.py +76 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_file_data_type.py +32 -0
- extended_data-8.4.2/tests/core/test_normalization.py +50 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_package_cli.py +192 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_release_hygiene.py +111 -4
- extended_data-8.4.0/tests/inputs/test_main.py → extended_data-8.4.2/tests/inputs/test_providers.py +15 -2
- {extended_data-8.4.0 → extended_data-8.4.2}/.gitignore +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/README.md +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/CNAME +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/_static/.gitkeep +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/_static/extended-data.css +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/api/index.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/primitives.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/core/workflows.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/core.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/index.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/inputs.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/examples/logging.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/getting-started.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/index.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/operations/logging.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/docs/pillars.rst +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/README.md +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/basic_usage.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/composed_workflows.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/file_operations.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/serialization.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/core/string_transformations.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/README.md +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/basic_usage.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/decorator_api.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/inputs/encoding_decoding.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/README.md +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/basic_logging.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/exit_run_formatting.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/markers_and_storage.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/examples/logging/verbosity_control.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/_version.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/cli.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/factory.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/mappings.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/sequences.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/containers/strings.py +0 -0
- /extended_data-8.4.0/src/extended_data/inputs/__main__.py → /extended_data-8.4.2/src/extended_data/inputs/providers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/inputs/py.typed +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/base64.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/exporters.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/io/importers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/const.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/handlers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/log_types.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/logging.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/py.typed +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/logging/utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/_normalization.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/errors.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/hcl.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/json.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/toml.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/constructors.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/dumpers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/loaders.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/representers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/tag_classes.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/introspection.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/mappings.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/matching.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/numbers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/redaction.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/sequences.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/serialization.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/splitting.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/state.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/string_transforms.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/strings.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/notation.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/words.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/inflection.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/py.typed +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/workflows/__init__.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/src/extended_data/workflows/sync.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_base64_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_export_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_hcl2_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_import_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_integration_workflows.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_json_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_list_data_type.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_map_data_type.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_matcher_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_number_transformations.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_package_surface.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_redaction.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_serialization_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_splitter_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_stack_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_state_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_string_data_type.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_string_transformations.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_toml_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_type_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_workflows.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/test_yaml_utils.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/numbers/test_notation.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/numbers/test_words.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/core/transformations/strings/test_inflection.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/examples/test_safe_examples.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/inputs/test_decorators.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/conftest.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/integration/test_lifecycle_logging.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_exit_run.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_handlers.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_logging.py +0 -0
- {extended_data-8.4.0 → extended_data-8.4.2}/tests/logging/test_properties.py +0 -0
- {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.
|
|
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<
|
|
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`` >
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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``
|
|
17
|
-
|
|
18
|
-
- ``cd.yml`` owns tag-gated package verification
|
|
19
|
-
|
|
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
|
-
|
|
84
|
-
6. Confirm
|
|
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
|
|
@@ -35,8 +35,9 @@ Moved Out
|
|
|
35
35
|
| External vendor API | ``jbcom/vendor-fabric`` | ``vendor-fabric`` |
|
|
36
36
|
| clients | | |
|
|
37
37
|
+-----------------------+-------------------------+------------------------------------+
|
|
38
|
-
| Vendor
|
|
39
|
-
|
|
|
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/
|
|
50
|
+
| SecretSync agent tool | ``jbcom/agentic-fabric``| ``agentic-fabric[...]`` |
|
|
50
51
|
| wrappers | | |
|
|
51
52
|
+-----------------------+-------------------------+------------------------------------+
|
|
52
|
-
| Agent framework | ``jbcom/
|
|
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,
|
|
122
|
-
vendor-backed Python sync
|
|
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.
|
|
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,<
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
{
|
|
@@ -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)
|