extended-data 8.4.1__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.1 → extended_data-8.4.2}/CHANGELOG.md +7 -0
  2. {extended_data-8.4.1 → extended_data-8.4.2}/PKG-INFO +1 -1
  3. {extended_data-8.4.1 → extended_data-8.4.2}/docs/conf.py +1 -1
  4. {extended_data-8.4.1 → extended_data-8.4.2}/docs/operations/inputs.rst +1 -1
  5. {extended_data-8.4.1 → extended_data-8.4.2}/pyproject.toml +1 -1
  6. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/inputs/__init__.py +1 -1
  7. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/inputs/decorators.py +1 -1
  8. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/types.py +0 -1
  9. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_containers.py +89 -0
  10. extended_data-8.4.2/tests/core/test_decode_errors.py +76 -0
  11. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_file_data_type.py +32 -0
  12. extended_data-8.4.2/tests/core/test_normalization.py +50 -0
  13. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_package_cli.py +192 -0
  14. extended_data-8.4.1/tests/inputs/test_main.py → extended_data-8.4.2/tests/inputs/test_providers.py +15 -2
  15. {extended_data-8.4.1 → extended_data-8.4.2}/.gitignore +0 -0
  16. {extended_data-8.4.1 → extended_data-8.4.2}/README.md +0 -0
  17. {extended_data-8.4.1 → extended_data-8.4.2}/docs/CNAME +0 -0
  18. {extended_data-8.4.1 → extended_data-8.4.2}/docs/PUBLISHING_CHECKLIST.rst +0 -0
  19. {extended_data-8.4.1 → extended_data-8.4.2}/docs/__init__.py +0 -0
  20. {extended_data-8.4.1 → extended_data-8.4.2}/docs/_static/.gitkeep +0 -0
  21. {extended_data-8.4.1 → extended_data-8.4.2}/docs/_static/extended-data.css +0 -0
  22. {extended_data-8.4.1 → extended_data-8.4.2}/docs/api/index.rst +0 -0
  23. {extended_data-8.4.1 → extended_data-8.4.2}/docs/architecture.rst +0 -0
  24. {extended_data-8.4.1 → extended_data-8.4.2}/docs/core/containers.rst +0 -0
  25. {extended_data-8.4.1 → extended_data-8.4.2}/docs/core/primitives.rst +0 -0
  26. {extended_data-8.4.1 → extended_data-8.4.2}/docs/core/workflows.rst +0 -0
  27. {extended_data-8.4.1 → extended_data-8.4.2}/docs/examples/core.rst +0 -0
  28. {extended_data-8.4.1 → extended_data-8.4.2}/docs/examples/index.rst +0 -0
  29. {extended_data-8.4.1 → extended_data-8.4.2}/docs/examples/inputs.rst +0 -0
  30. {extended_data-8.4.1 → extended_data-8.4.2}/docs/examples/logging.rst +0 -0
  31. {extended_data-8.4.1 → extended_data-8.4.2}/docs/getting-started.rst +0 -0
  32. {extended_data-8.4.1 → extended_data-8.4.2}/docs/index.rst +0 -0
  33. {extended_data-8.4.1 → extended_data-8.4.2}/docs/operations/logging.rst +0 -0
  34. {extended_data-8.4.1 → extended_data-8.4.2}/docs/ownership-map.rst +0 -0
  35. {extended_data-8.4.1 → extended_data-8.4.2}/docs/package-surface.rst +0 -0
  36. {extended_data-8.4.1 → extended_data-8.4.2}/docs/pillars.rst +0 -0
  37. {extended_data-8.4.1 → extended_data-8.4.2}/docs/publishing.rst +0 -0
  38. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/README.md +0 -0
  39. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/basic_usage.py +0 -0
  40. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/composed_workflows.py +0 -0
  41. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/file_operations.py +0 -0
  42. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/serialization.py +0 -0
  43. {extended_data-8.4.1 → extended_data-8.4.2}/examples/core/string_transformations.py +0 -0
  44. {extended_data-8.4.1 → extended_data-8.4.2}/examples/inputs/README.md +0 -0
  45. {extended_data-8.4.1 → extended_data-8.4.2}/examples/inputs/__init__.py +0 -0
  46. {extended_data-8.4.1 → extended_data-8.4.2}/examples/inputs/basic_usage.py +0 -0
  47. {extended_data-8.4.1 → extended_data-8.4.2}/examples/inputs/decorator_api.py +0 -0
  48. {extended_data-8.4.1 → extended_data-8.4.2}/examples/inputs/encoding_decoding.py +0 -0
  49. {extended_data-8.4.1 → extended_data-8.4.2}/examples/logging/README.md +0 -0
  50. {extended_data-8.4.1 → extended_data-8.4.2}/examples/logging/basic_logging.py +0 -0
  51. {extended_data-8.4.1 → extended_data-8.4.2}/examples/logging/exit_run_formatting.py +0 -0
  52. {extended_data-8.4.1 → extended_data-8.4.2}/examples/logging/markers_and_storage.py +0 -0
  53. {extended_data-8.4.1 → extended_data-8.4.2}/examples/logging/verbosity_control.py +0 -0
  54. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/__init__.py +0 -0
  55. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/_version.py +0 -0
  56. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/cli.py +0 -0
  57. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/__init__.py +0 -0
  58. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/data.py +0 -0
  59. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/factory.py +0 -0
  60. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/mappings.py +0 -0
  61. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/sequences.py +0 -0
  62. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/containers/strings.py +0 -0
  63. /extended_data-8.4.1/src/extended_data/inputs/__main__.py → /extended_data-8.4.2/src/extended_data/inputs/providers.py +0 -0
  64. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/inputs/py.typed +0 -0
  65. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/io/__init__.py +0 -0
  66. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/io/base64.py +0 -0
  67. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/io/exporters.py +0 -0
  68. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/io/files.py +0 -0
  69. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/io/importers.py +0 -0
  70. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/__init__.py +0 -0
  71. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/const.py +0 -0
  72. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/handlers.py +0 -0
  73. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/log_types.py +0 -0
  74. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/logging.py +0 -0
  75. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/py.typed +0 -0
  76. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/logging/utils.py +0 -0
  77. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/__init__.py +0 -0
  78. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/__init__.py +0 -0
  79. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/_normalization.py +0 -0
  80. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/errors.py +0 -0
  81. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/hcl.py +0 -0
  82. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/json.py +0 -0
  83. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/toml.py +0 -0
  84. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/__init__.py +0 -0
  85. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/constructors.py +0 -0
  86. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/dumpers.py +0 -0
  87. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/loaders.py +0 -0
  88. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/representers.py +0 -0
  89. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/tag_classes.py +0 -0
  90. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/formats/yaml/utils.py +0 -0
  91. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/introspection.py +0 -0
  92. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/mappings.py +0 -0
  93. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/matching.py +0 -0
  94. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/numbers.py +0 -0
  95. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/redaction.py +0 -0
  96. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/sequences.py +0 -0
  97. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/serialization.py +0 -0
  98. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/splitting.py +0 -0
  99. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/state.py +0 -0
  100. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/string_transforms.py +0 -0
  101. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/strings.py +0 -0
  102. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/__init__.py +0 -0
  103. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/__init__.py +0 -0
  104. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/notation.py +0 -0
  105. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/numbers/words.py +0 -0
  106. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/__init__.py +0 -0
  107. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/primitives/transformations/strings/inflection.py +0 -0
  108. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/py.typed +0 -0
  109. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/workflows/__init__.py +0 -0
  110. {extended_data-8.4.1 → extended_data-8.4.2}/src/extended_data/workflows/sync.py +0 -0
  111. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_base64_utils.py +0 -0
  112. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_export_utils.py +0 -0
  113. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_hcl2_utils.py +0 -0
  114. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_import_utils.py +0 -0
  115. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_integration_workflows.py +0 -0
  116. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_json_utils.py +0 -0
  117. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_list_data_type.py +0 -0
  118. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_map_data_type.py +0 -0
  119. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_matcher_utils.py +0 -0
  120. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_number_transformations.py +0 -0
  121. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_package_surface.py +0 -0
  122. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_redaction.py +0 -0
  123. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_release_hygiene.py +0 -0
  124. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_serialization_utils.py +0 -0
  125. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_splitter_utils.py +0 -0
  126. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_stack_utils.py +0 -0
  127. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_state_utils.py +0 -0
  128. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_string_data_type.py +0 -0
  129. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_string_transformations.py +0 -0
  130. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_toml_utils.py +0 -0
  131. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_type_utils.py +0 -0
  132. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_workflows.py +0 -0
  133. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/test_yaml_utils.py +0 -0
  134. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/transformations/numbers/test_notation.py +0 -0
  135. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/transformations/numbers/test_words.py +0 -0
  136. {extended_data-8.4.1 → extended_data-8.4.2}/tests/core/transformations/strings/test_inflection.py +0 -0
  137. {extended_data-8.4.1 → extended_data-8.4.2}/tests/examples/test_safe_examples.py +0 -0
  138. {extended_data-8.4.1 → extended_data-8.4.2}/tests/inputs/test_decorators.py +0 -0
  139. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/conftest.py +0 -0
  140. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/integration/test_lifecycle_logging.py +0 -0
  141. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/test_exit_run.py +0 -0
  142. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/test_handlers.py +0 -0
  143. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/test_logging.py +0 -0
  144. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/test_properties.py +0 -0
  145. {extended_data-8.4.1 → extended_data-8.4.2}/tests/logging/test_utils.py +0 -0
@@ -1,5 +1,12 @@
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
+
3
10
  ## [8.4.1](https://github.com/jbcom/extended-data/compare/extended-data-v8.4.0...extended-data-v8.4.1) (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.4.1
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
@@ -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
@@ -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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "extended-data"
7
- version = "8.4.1"
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" }
@@ -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:
@@ -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.
@@ -770,3 +770,92 @@ def test_container_classes_are_root_exports() -> None:
770
770
  assert extended_data.ExtendedTuple is ExtendedTuple
771
771
  assert extended_data.extend_data is extend_data
772
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)
@@ -0,0 +1,76 @@
1
+ """Direct tests for the shared decode-error helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from extended_data.primitives.formats.errors import DataDecodeError, invalid_utf8_error
6
+
7
+
8
+ def test_from_exception_builds_sanitized_error() -> None:
9
+ """DataDecodeError.from_exception should translate parser attributes without source snippets."""
10
+
11
+ class FakeParserError(Exception):
12
+ msg = "unexpected token"
13
+ lineno = 3
14
+ colno = 12
15
+
16
+ err = DataDecodeError.from_exception("YAML", FakeParserError("whatever source text"))
17
+
18
+ assert err.format_name == "YAML"
19
+ assert err.reason == "unexpected token"
20
+ assert err.line == 3
21
+ assert err.column == 12
22
+ message = str(err)
23
+ assert "Failed to decode YAML data" in message
24
+ assert "unexpected token" in message
25
+ assert "line 3" in message
26
+ assert "column 12" in message
27
+ assert "whatever source text" not in message
28
+
29
+
30
+ def test_from_exception_falls_back_to_type_name_when_no_msg() -> None:
31
+ """When a parser exposes no msg/problem attr, the reason should be the exception type name."""
32
+
33
+ class BareError(Exception):
34
+ pass
35
+
36
+ err = DataDecodeError.from_exception("JSON", BareError())
37
+
38
+ assert err.reason == "BareError"
39
+ assert err.line is None
40
+ assert err.column is None
41
+
42
+
43
+ def test_from_exception_extracts_yaml_problem_mark() -> None:
44
+ """The YAML parser exposes problem_mark; from_exception should honor it."""
45
+
46
+ class Mark:
47
+ line = 5
48
+ column = 9
49
+
50
+ class YamlishError(Exception):
51
+ problem_mark = Mark()
52
+
53
+ err = DataDecodeError.from_exception("YAML", YamlishError())
54
+
55
+ assert err.line == 6
56
+ assert err.column == 10
57
+
58
+
59
+ def test_invalid_utf8_error_returns_data_decode_error() -> None:
60
+ """invalid_utf8_error should return a DataDecodeError with the canonical reason text."""
61
+ err = invalid_utf8_error("TOML")
62
+
63
+ assert isinstance(err, DataDecodeError)
64
+ assert err.format_name == "TOML"
65
+ assert err.reason == "input bytes are not valid UTF-8"
66
+ assert "input bytes are not valid UTF-8" in str(err)
67
+
68
+
69
+ def test_data_decode_error_message_without_reason_or_position() -> None:
70
+ """A bare DataDecodeError should still render a valid message."""
71
+ err = DataDecodeError("HCL")
72
+
73
+ assert str(err) == "Failed to decode HCL data."
74
+ assert err.reason is None
75
+ assert err.line is None
76
+ assert err.column is None
@@ -739,6 +739,38 @@ def test_write_file_tfvars_infers_hcl(tmp_path: Path) -> None:
739
739
  assert decode_file(content, file_path=test_file) == data
740
740
 
741
741
 
742
+ def test_write_file_toml(tmp_path: Path) -> None:
743
+ """TOML encoding should round-trip through write_file and decode_file."""
744
+ test_file = tmp_path / "config.toml"
745
+ data = {"service": "api", "replicas": 3, "enabled": True}
746
+
747
+ result = write_file(test_file, data, tld=tmp_path)
748
+
749
+ assert result == test_file.resolve()
750
+ assert test_file.exists()
751
+ content = test_file.read_text()
752
+ assert 'service = "api"' in content
753
+ assert "replicas = 3" in content
754
+ assert decode_file(content, file_path=test_file) == data
755
+
756
+
757
+ def test_data_file_toml_round_trip(tmp_path: Path) -> None:
758
+ """DataFile should preserve TOML data across write then read."""
759
+ source = tmp_path / "source.toml"
760
+ source.write_text('service = "api"\nreplicas = 3\n', encoding="utf-8")
761
+
762
+ artifact = DataFile.read(source)
763
+ assert artifact.data == {"service": "api", "replicas": 3}
764
+ assert artifact.encoding == "toml"
765
+
766
+ target = tmp_path / "build" / "out.toml"
767
+ artifact.write(target, tld=tmp_path)
768
+
769
+ reread = DataFile.read(target)
770
+ assert reread.data == artifact.data
771
+ assert reread.encoding == "toml"
772
+
773
+
742
774
  def test_write_file_bytes(tmp_path: Path) -> None:
743
775
  """Write raw bytes without re-encoding."""
744
776
  test_file = tmp_path / "payload.bin"
@@ -0,0 +1,50 @@
1
+ """Tests for the internal data normalization helper."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from extended_data.containers import ExtendedDict, ExtendedList, ExtendedSet, ExtendedString, ExtendedTuple
6
+ from extended_data.primitives.formats._normalization import lower_extended_data
7
+
8
+
9
+ def test_lower_extended_data_unwraps_containers() -> None:
10
+ """lower_extended_data should lower Tier 2 containers to plain Python values."""
11
+ value = ExtendedDict(
12
+ {
13
+ "service": ExtendedString("api"),
14
+ "ports": ExtendedList([80, 443]),
15
+ "tags": ExtendedSet({"prod", "api"}),
16
+ "aliases": ExtendedTuple(("api", "gateway")),
17
+ }
18
+ )
19
+
20
+ lowered = lower_extended_data(value)
21
+
22
+ assert lowered == {
23
+ "service": "api",
24
+ "ports": [80, 443],
25
+ "tags": {"prod", "api"},
26
+ "aliases": ("api", "gateway"),
27
+ }
28
+ assert type(lowered) is dict
29
+ assert type(lowered["ports"]) is list
30
+ assert type(lowered["tags"]) is set
31
+ assert type(lowered["aliases"]) is tuple
32
+
33
+
34
+ def test_lower_extended_data_passes_through_plain_values() -> None:
35
+ """lower_extended_data should leave plain Python values untouched."""
36
+ assert lower_extended_data(None) is None
37
+ assert lower_extended_data(42) == 42
38
+ assert lower_extended_data("api") == "api"
39
+ assert lower_extended_data({"service": "api"}) == {"service": "api"}
40
+
41
+
42
+ def test_lower_extended_data_unwraps_nested_extended_containers() -> None:
43
+ """lower_extended_data should recursively lower nested Tier 2 containers."""
44
+ value = ExtendedDict({"outer": ExtendedDict({"inner": ExtendedList([ExtendedString("api")])})})
45
+
46
+ lowered = lower_extended_data(value)
47
+
48
+ assert lowered == {"outer": {"inner": ["api"]}}
49
+ assert type(lowered["outer"]) is dict
50
+ assert type(lowered["outer"]["inner"]) is list
@@ -269,3 +269,195 @@ def test_transform_can_write_output_artifact(tmp_path) -> None:
269
269
  output_text = output.read_text(encoding="utf-8")
270
270
  assert _stdout_text(mock_write) == f"{output_text}\n"
271
271
  assert "http_response_code: 200" in output_text
272
+
273
+
274
+ def test_decode_compact_json_drops_indent() -> None:
275
+ """--compact should produce single-line JSON without indent."""
276
+ with patch("sys.stdout.write") as mock_write:
277
+ exit_code = cli_module.main(["decode", '{"service": "api"}', "--suffix", "json", "--compact"])
278
+
279
+ assert exit_code == 0
280
+ output = _stdout_text(mock_write)
281
+ assert json.loads(output) == {"service": "api"}
282
+ assert "\n" not in output.strip()
283
+
284
+
285
+ def test_decode_non_json_output_ignores_compact() -> None:
286
+ """--compact only affects JSON; YAML output should remain unchanged."""
287
+ with patch("sys.stdout.write") as mock_write:
288
+ exit_code = cli_module.main(["decode", '{"service": "api"}', "--suffix", "json", "--output", "yaml", "--compact"])
289
+
290
+ assert exit_code == 0
291
+ output = _stdout_text(mock_write)
292
+ assert "service:" in output
293
+ assert "api" in output
294
+
295
+
296
+ def test_decode_toml_output() -> None:
297
+ """decode should emit TOML when --output toml is requested."""
298
+ with patch("sys.stdout.write") as mock_write:
299
+ exit_code = cli_module.main(["decode", '{"service": "api", "replicas": 3}', "--suffix", "json", "--output", "toml"])
300
+
301
+ assert exit_code == 0
302
+ output = _stdout_text(mock_write)
303
+ assert 'service = "api"' in output
304
+ assert "replicas = 3" in output
305
+
306
+
307
+ def test_decode_hcl_output() -> None:
308
+ """decode should emit HCL when --output hcl is requested."""
309
+ with patch("sys.stdout.write") as mock_write:
310
+ exit_code = cli_module.main(["decode", '{"locals": [{"region": "us-east-1"}]}', "--suffix", "json", "--output", "hcl"])
311
+
312
+ assert exit_code == 0
313
+ output = _stdout_text(mock_write)
314
+ assert "locals {" in output
315
+ assert 'region = "us-east-1"' in output
316
+
317
+
318
+ def test_inspect_toml_output(tmp_path) -> None:
319
+ """inspect should emit TOML metadata when --output toml is requested."""
320
+ config = tmp_path / "service.json"
321
+ config.write_text('{"service": "api"}', encoding="utf-8")
322
+
323
+ with patch("sys.stdout.write") as mock_write:
324
+ exit_code = cli_module.main(["inspect", "--file", str(config), "--output", "toml"])
325
+
326
+ assert exit_code == 0
327
+ output = _stdout_text(mock_write)
328
+ assert "source" in output
329
+ assert "json" in output
330
+
331
+
332
+ def test_inspect_hcl_output() -> None:
333
+ """inspect should emit HCL metadata when --output hcl is requested."""
334
+ with patch("sys.stdout.write") as mock_write:
335
+ exit_code = cli_module.main(["inspect", '{"service": "api"}', "--suffix", "json", "--output", "hcl"])
336
+
337
+ assert exit_code == 0
338
+ output = _stdout_text(mock_write)
339
+ assert "source" in output
340
+ assert "memory" in output
341
+
342
+
343
+ def test_merge_toml_output(tmp_path) -> None:
344
+ """merge should emit TOML when --output toml is requested."""
345
+ base = tmp_path / "base.json"
346
+ env = tmp_path / "env.json"
347
+ base.write_text('{"service": "api", "replicas": 1}', encoding="utf-8")
348
+ env.write_text('{"replicas": 3}', encoding="utf-8")
349
+
350
+ with patch("sys.stdout.write") as mock_write:
351
+ exit_code = cli_module.main(["merge", str(base), str(env), "--output", "toml"])
352
+
353
+ assert exit_code == 0
354
+ output = _stdout_text(mock_write)
355
+ assert 'service = "api"' in output
356
+ assert "replicas = 3" in output
357
+
358
+
359
+ def test_merge_hcl_output(tmp_path) -> None:
360
+ """merge should emit HCL when --output hcl is requested."""
361
+ base = tmp_path / "base.json"
362
+ env = tmp_path / "env.json"
363
+ base.write_text('{"locals": [{"region": "us-east-1"}]}', encoding="utf-8")
364
+ env.write_text('{"locals": [{"zone": "a"}]}', encoding="utf-8")
365
+
366
+ with patch("sys.stdout.write") as mock_write:
367
+ exit_code = cli_module.main(["merge", str(base), str(env), "--output", "hcl"])
368
+
369
+ assert exit_code == 0
370
+ output = _stdout_text(mock_write)
371
+ assert "locals {" in output
372
+
373
+
374
+ def test_transform_toml_output(tmp_path) -> None:
375
+ """transform should emit TOML when --output toml is requested."""
376
+ payload = tmp_path / "payload.json"
377
+ payload.write_text('{"HTTPResponseCode": "200"}', encoding="utf-8")
378
+
379
+ with patch("sys.stdout.write") as mock_write:
380
+ exit_code = cli_module.main(
381
+ ["transform", "--file", str(payload), "--step", "reconstruct", "--step", "unhump", "--output", "toml"]
382
+ )
383
+
384
+ assert exit_code == 0
385
+ output = _stdout_text(mock_write)
386
+ assert "http_response_code" in output
387
+ assert "200" in output
388
+
389
+
390
+ def test_transform_hcl_output(tmp_path) -> None:
391
+ """transform should emit HCL when --output hcl is requested."""
392
+ payload = tmp_path / "payload.json"
393
+ payload.write_text('{"locals": [{"region": "us-east-1"}]}', encoding="utf-8")
394
+
395
+ with patch("sys.stdout.write") as mock_write:
396
+ exit_code = cli_module.main(
397
+ ["transform", "--file", str(payload), "--step", "reconstruct", "--output", "hcl"]
398
+ )
399
+
400
+ assert exit_code == 0
401
+ output = _stdout_text(mock_write)
402
+ assert "locals {" in output
403
+
404
+
405
+ def test_merge_allow_empty_writes_empty_output(tmp_path) -> None:
406
+ """--allow-empty should let merge write an empty result without error."""
407
+ base = tmp_path / "base.json"
408
+ env = tmp_path / "env.json"
409
+ output = tmp_path / "out.json"
410
+ base.write_text("{}", encoding="utf-8")
411
+ env.write_text("{}", encoding="utf-8")
412
+
413
+ with patch("sys.stdout.write"):
414
+ exit_code = cli_module.main(
415
+ ["merge", str(base), str(env), "--output", "json", "--write", str(output), "--allow-empty"]
416
+ )
417
+
418
+ assert exit_code == 0
419
+ assert output.exists()
420
+
421
+
422
+ def test_transform_allow_empty_writes_empty_output(tmp_path) -> None:
423
+ """--allow-empty should let transform write an empty result without error."""
424
+ payload = tmp_path / "payload.json"
425
+ output = tmp_path / "out.json"
426
+ payload.write_text("{}", encoding="utf-8")
427
+
428
+ with patch("sys.stdout.write"):
429
+ exit_code = cli_module.main(
430
+ ["transform", "--file", str(payload), "--step", "compact", "--output", "json", "--write", str(output), "--allow-empty"]
431
+ )
432
+
433
+ assert exit_code == 0
434
+ assert output.exists()
435
+
436
+
437
+ def test_no_command_prints_help_and_returns_zero() -> None:
438
+ """Calling the CLI with no command should print help and exit 0."""
439
+ with patch("sys.stdout.write") as mock_write:
440
+ exit_code = cli_module.main([])
441
+
442
+ assert exit_code == 0
443
+ output = _stdout_text(mock_write)
444
+ assert "CLI for Extended Data" in output or "usage:" in output.lower()
445
+
446
+
447
+ def test_keyboard_interrupt_returns_130() -> None:
448
+ """KeyboardInterrupt during a command should return exit code 130."""
449
+ with patch("extended_data.cli.cmd_decode", side_effect=KeyboardInterrupt):
450
+ exit_code = cli_module.main(["decode", '{"service": "api"}', "--suffix", "json"])
451
+
452
+ assert exit_code == 130
453
+
454
+
455
+ def test_main_argv_none_reads_sys_argv(monkeypatch) -> None:
456
+ """main(argv=None) should fall back to sys.argv[1:]."""
457
+ monkeypatch.setattr("sys.argv", ["extended-data", "decode", '{"x": 1}', "--suffix", "json"])
458
+
459
+ with patch("sys.stdout.write") as mock_write:
460
+ exit_code = cli_module.main()
461
+
462
+ assert exit_code == 0
463
+ assert json.loads(_stdout_text(mock_write)) == {"x": 1}
@@ -38,8 +38,8 @@ import pytest
38
38
 
39
39
  from extended_data import base64_encode
40
40
  from extended_data.containers import ExtendedDict, ExtendedString
41
- from extended_data.inputs import __main__ as inputs_module
42
- from extended_data.inputs.__main__ import InputProvider
41
+ from extended_data.inputs import providers as inputs_module
42
+ from extended_data.inputs.providers import InputProvider
43
43
 
44
44
 
45
45
  @pytest.fixture
@@ -288,6 +288,19 @@ def test_decode_input_base64():
288
288
  assert decoded == {"name": "test"}
289
289
 
290
290
 
291
+ def test_decode_input_base64_then_yaml():
292
+ """Base64 payload decoded then re-decoded as YAML should produce a mapping."""
293
+ import yaml
294
+
295
+ yaml_payload = yaml.safe_dump({"name": "test", "replicas": 3})
296
+ encoded_value = base64_encode(yaml_payload.encode())
297
+ dic = InputProvider(inputs={"base64_key": encoded_value})
298
+
299
+ decoded = dic.decode_input("base64_key", decode_from_base64=True, decode_from_yaml=True)
300
+
301
+ assert decoded == {"name": "test", "replicas": 3}
302
+
303
+
291
304
  def test_decode_input_base64_from_bytes():
292
305
  """Base64 encoded bytes can be decoded and parsed."""
293
306
  encoded_value = base64_encode(json.dumps({"name": "test"}).encode())
File without changes
File without changes
File without changes