openframe-core 3.2.0__tar.gz → 3.2.1__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 (146) hide show
  1. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/CHANGELOG.md +13 -0
  2. {openframe_core-3.2.0 → openframe_core-3.2.1}/PKG-INFO +1 -1
  3. openframe_core-3.2.1/openframe/core/schemas/__init__.py +128 -0
  4. {openframe_core-3.2.0 → openframe_core-3.2.1}/pyproject.toml +1 -1
  5. openframe_core-3.2.1/tests/test_schemas.py +91 -0
  6. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/requirements/docs.txt +0 -0
  7. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/requirements/test.txt +0 -0
  8. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/workflows/app-test.yml +0 -0
  9. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/workflows/auto-docs.yml +0 -0
  10. {openframe_core-3.2.0 → openframe_core-3.2.1}/.github/workflows/python-build.yml +0 -0
  11. {openframe_core-3.2.0 → openframe_core-3.2.1}/.gitignore +0 -0
  12. {openframe_core-3.2.0 → openframe_core-3.2.1}/.vscode/settings.json +0 -0
  13. {openframe_core-3.2.0 → openframe_core-3.2.1}/LICENSE +0 -0
  14. {openframe_core-3.2.0 → openframe_core-3.2.1}/README.md +0 -0
  15. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/images/banner.png +0 -0
  16. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/images/favicon.png +0 -0
  17. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/images/logo-dark.png +0 -0
  18. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/images/logo-light.png +0 -0
  19. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/javascripts/extra.js +0 -0
  20. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/assets/stylesheets/extra.css +0 -0
  21. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/cicd/pipeline.md +0 -0
  22. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/getting-started/index.md +0 -0
  23. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/config.md +0 -0
  24. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/exceptions.md +0 -0
  25. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/health.md +0 -0
  26. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/inbound.md +0 -0
  27. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/index.md +0 -0
  28. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/middleware.md +0 -0
  29. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/plugins.md +0 -0
  30. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/ports.md +0 -0
  31. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/runtime.md +0 -0
  32. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/telemetry.md +0 -0
  33. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/testing.md +0 -0
  34. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/code/modules/tracing.md +0 -0
  35. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/debugging.md +0 -0
  36. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/explore.md +0 -0
  37. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/first-change.md +0 -0
  38. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/how-it-works.md +0 -0
  39. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/local-dev.md +0 -0
  40. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/developer-guide/quick-start/index.md +0 -0
  41. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/index.md +0 -0
  42. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  43. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  44. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  45. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  46. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  47. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
  48. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/index.md +0 -0
  49. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/capability-taxonomy.md +0 -0
  50. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/component-lifecycle.md +0 -0
  51. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/data-flows.md +0 -0
  52. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/design-decisions.md +0 -0
  53. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/error-taxonomy.md +0 -0
  54. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
  55. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
  56. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/index.md +0 -0
  57. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
  58. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  59. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/architecture/system-architecture.md +0 -0
  60. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/deployment/cicd.md +0 -0
  61. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/deployment/environments.md +0 -0
  62. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/deployment/secrets-and-config.md +0 -0
  63. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/infrastructure/networking.md +0 -0
  64. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/infrastructure/platform.md +0 -0
  65. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/infrastructure/storage.md +0 -0
  66. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/index.md +0 -0
  67. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/monitoring.md +0 -0
  68. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
  69. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  70. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
  71. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
  72. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
  73. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  74. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/overview/glossary.md +0 -0
  75. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/overview/roadmap.md +0 -0
  76. {openframe_core-3.2.0 → openframe_core-3.2.1}/docs/technical/overview/system-overview.md +0 -0
  77. {openframe_core-3.2.0 → openframe_core-3.2.1}/mkdocs.yml +0 -0
  78. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/__init__.py +0 -0
  79. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/__init__.py +0 -0
  80. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/config/__init__.py +0 -0
  81. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/config/settings.py +0 -0
  82. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/exceptions/__init__.py +0 -0
  83. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/exceptions/adapter.py +0 -0
  84. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/exceptions/base.py +0 -0
  85. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/exceptions/codes.py +0 -0
  86. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/exceptions/plugin.py +0 -0
  87. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/inbound/__init__.py +0 -0
  88. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/inbound/command.py +0 -0
  89. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/inbound/context.py +0 -0
  90. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/inbound/query.py +0 -0
  91. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/inbound/usecase.py +0 -0
  92. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/middleware/__init__.py +0 -0
  93. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/middleware/telemetry.py +0 -0
  94. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/middleware/types.py +0 -0
  95. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/plugins/__init__.py +0 -0
  96. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/plugins/registry.py +0 -0
  97. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/__init__.py +0 -0
  98. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/capability.py +0 -0
  99. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/context.py +0 -0
  100. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/health.py +0 -0
  101. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/identity.py +0 -0
  102. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/lifecycle.py +0 -0
  103. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/__init__.py +0 -0
  104. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/consumer.py +0 -0
  105. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/producer.py +0 -0
  106. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/repository.py +0 -0
  107. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/ports/port.py +0 -0
  108. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/runtime/__init__.py +0 -0
  109. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/runtime/bootstrap.py +0 -0
  110. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/telemetry/__init__.py +0 -0
  111. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/telemetry/setup.py +0 -0
  112. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/__init__.py +0 -0
  113. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/__init__.py +0 -0
  114. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/consumer.py +0 -0
  115. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/inbound.py +0 -0
  116. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/lifecycle.py +0 -0
  117. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/port.py +0 -0
  118. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/producer.py +0 -0
  119. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/repository.py +0 -0
  120. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/__init__.py +0 -0
  121. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/consumer.py +0 -0
  122. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/inbound.py +0 -0
  123. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/producer.py +0 -0
  124. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/repository.py +0 -0
  125. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/testing/fixtures.py +0 -0
  126. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/tracing/__init__.py +0 -0
  127. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/tracing/propagation.py +0 -0
  128. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/core/tracing/proxy.py +0 -0
  129. {openframe_core-3.2.0 → openframe_core-3.2.1}/openframe/py.typed +0 -0
  130. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/conftest.py +0 -0
  131. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_config.py +0 -0
  132. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_exceptions.py +0 -0
  133. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_inbound.py +0 -0
  134. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_inbound_contracts.py +0 -0
  135. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_middleware.py +0 -0
  136. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_middleware_types.py +0 -0
  137. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_plugins.py +0 -0
  138. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_ports.py +0 -0
  139. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_ports_primitives.py +0 -0
  140. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_record_error.py +0 -0
  141. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_runtime.py +0 -0
  142. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_shutdown_telemetry.py +0 -0
  143. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_telemetry_sidecar.py +0 -0
  144. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_testing_fakes.py +0 -0
  145. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_tracing.py +0 -0
  146. {openframe_core-3.2.0 → openframe_core-3.2.1}/tests/test_tracing_propagation.py +0 -0
@@ -58,6 +58,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
58
58
 
59
59
  ---
60
60
 
61
+ ## v3.2.1
62
+
63
+ ### Added
64
+
65
+ - `openframe.core.schemas` — new experimental module providing
66
+ the `@contract(name, version)` decorator, `ContractMeta` dataclass,
67
+ and `get_contract_meta()` helper. Marks Pydantic models as
68
+ published cross-service contracts. The schema export, registry,
69
+ breaking-change diff, and codegen machinery lives in the
70
+ separate `openframe-schemas` package (openframe-tooling repo).
71
+
72
+ ---
73
+
61
74
  ## v3.2.0
62
75
 
63
76
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openframe-core
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root.
5
5
  Project-URL: Homepage, https://github.com/Furious-Meteors/openframe-core
6
6
  Project-URL: Documentation, https://furious-meteors.github.io/openframe-core/
@@ -0,0 +1,128 @@
1
+ """
2
+ openframe.core.schemas
3
+ ========================
4
+ Cross-service data shape governance (ADR-007).
5
+
6
+ The @contract decorator marks a Pydantic model as a published
7
+ cross-service contract — a shape that crosses a service boundary
8
+ (via REST, Kafka, gRPC, or any other transport) and must be
9
+ governed for breaking changes.
10
+
11
+ This module owns only the *marker*. The machinery that acts on
12
+ the marker — schema export, version registry, breaking-change diff,
13
+ codegen — lives in the separate openframe-schemas package, which
14
+ has its own dependencies and release cadence.
15
+
16
+ Dependency order:
17
+ core/schemas → stdlib only (no pydantic, no openframe imports)
18
+
19
+ Usage::
20
+
21
+ from openframe.core.schemas import contract
22
+
23
+ @contract(name="item", version="1.0")
24
+ class Item(BaseModel):
25
+ id: str
26
+ name: str
27
+ description: str | None = None
28
+ status: str = "active"
29
+
30
+ The @contract decorator does not change the class in any way that
31
+ affects Pydantic validation, serialization, or runtime behaviour.
32
+ It attaches __contract__ as a class attribute that openframe-schemas
33
+ reads at schema-export time.
34
+
35
+ .. stability: experimental
36
+ Experimental — the decorator signature (name, version) is stable;
37
+ the ContractMeta attributes may gain additional fields in future
38
+ minor versions.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ from dataclasses import dataclass
43
+
44
+ __all__ = ["ContractMeta", "contract", "get_contract_meta"]
45
+
46
+ __stability__ = "experimental"
47
+
48
+
49
+ @dataclass
50
+ class ContractMeta:
51
+ """
52
+ Metadata attached to a @contract-decorated model.
53
+
54
+ .. stability: experimental
55
+
56
+ Attributes:
57
+ name: The contract's stable name (e.g. ``"item"``). Identifies
58
+ the contract independently of the Python class name.
59
+ version: The contract's version string (e.g. ``"1.0"``). Governed
60
+ by openframe-schemas' breaking-change diff tooling.
61
+ """
62
+
63
+ name: str
64
+ version: str
65
+
66
+ __stability__ = "experimental"
67
+
68
+
69
+ def contract(name: str, version: str):
70
+ """
71
+ Mark a class as a published cross-service contract.
72
+
73
+ Attaches a :class:`ContractMeta` instance to the decorated class as
74
+ ``cls.__contract__``. Returns the class unchanged — pure decoration,
75
+ no wrapping — so Pydantic validation, serialization, and any other
76
+ runtime behaviour are unaffected.
77
+
78
+ Works with any class, not just Pydantic ``BaseModel`` subclasses, but
79
+ the intended use is always Pydantic models.
80
+
81
+ .. stability: experimental
82
+
83
+ Args:
84
+ name: The contract's stable name.
85
+ version: The contract's version string.
86
+
87
+ Returns:
88
+ A decorator that attaches ``__contract__`` to the class and
89
+ returns it unchanged.
90
+
91
+ Usage::
92
+
93
+ @contract(name="item", version="1.0")
94
+ class Item(BaseModel):
95
+ id: str
96
+ name: str
97
+ """
98
+
99
+ def _decorate(cls: type) -> type:
100
+ cls.__contract__ = ContractMeta(name=name, version=version)
101
+ return cls
102
+
103
+ return _decorate
104
+
105
+
106
+ contract.__stability__ = "experimental"
107
+
108
+
109
+ def get_contract_meta(cls: type) -> ContractMeta | None:
110
+ """
111
+ Return the ContractMeta attached to a class, if any.
112
+
113
+ Safe on any class — undecorated classes simply return ``None``
114
+ rather than raising.
115
+
116
+ .. stability: experimental
117
+
118
+ Args:
119
+ cls: The class to inspect.
120
+
121
+ Returns:
122
+ The :class:`ContractMeta` attached via :func:`contract`, or
123
+ ``None`` if the class is not @contract-decorated.
124
+ """
125
+ return getattr(cls, "__contract__", None)
126
+
127
+
128
+ get_contract_meta.__stability__ = "experimental"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openframe-core"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  description = "OpenFrame Microservice Suite - platform kernel. Port contracts, testing utilities, plugin registry, and composition root."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,91 @@
1
+ """
2
+ tests/test_schemas.py
3
+ ========================
4
+ Tests for openframe.core.schemas — the @contract marker (ADR-007).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import pytest
9
+ from pydantic import BaseModel
10
+
11
+ from openframe.core.schemas import ContractMeta, contract, get_contract_meta
12
+
13
+
14
+ def test_contract_decorator_attaches_meta() -> None:
15
+ @contract(name="item", version="1.0")
16
+ class Item(BaseModel):
17
+ id: str
18
+ name: str
19
+
20
+ assert Item.__contract__.name == "item"
21
+ assert Item.__contract__.version == "1.0"
22
+
23
+
24
+ def test_contract_decorator_returns_class_unchanged() -> None:
25
+ class RawItem(BaseModel):
26
+ id: str
27
+ name: str
28
+
29
+ DecoratedItem = contract(name="item", version="1.0")(RawItem)
30
+
31
+ assert DecoratedItem is RawItem
32
+ instance = DecoratedItem(id="1", name="widget")
33
+ assert isinstance(instance, RawItem)
34
+ assert DecoratedItem.model_json_schema() == RawItem.model_json_schema()
35
+
36
+
37
+ def test_get_contract_meta_returns_meta_for_decorated_class() -> None:
38
+ @contract(name="order", version="2.1")
39
+ class Order(BaseModel):
40
+ id: str
41
+
42
+ meta = get_contract_meta(Order)
43
+ assert meta is not None
44
+ assert meta.name == "order"
45
+ assert meta.version == "2.1"
46
+
47
+
48
+ def test_get_contract_meta_returns_none_for_undecorated_class() -> None:
49
+ class PlainModel(BaseModel):
50
+ id: str
51
+
52
+ assert get_contract_meta(PlainModel) is None
53
+
54
+
55
+ def test_contract_decorator_works_on_nested_model() -> None:
56
+ class Address(BaseModel):
57
+ street: str
58
+ city: str
59
+
60
+ @contract(name="customer", version="1.0")
61
+ class Customer(BaseModel):
62
+ id: str
63
+ address: Address
64
+
65
+ instance = Customer(id="1", address=Address(street="Main St", city="Springfield"))
66
+ assert instance.address.city == "Springfield"
67
+ assert get_contract_meta(Customer).name == "customer"
68
+
69
+
70
+ def test_contract_meta_repr() -> None:
71
+ assert repr(ContractMeta("item", "1.0")) == "ContractMeta(name='item', version='1.0')"
72
+
73
+
74
+ def test_contract_marker_does_not_affect_pydantic_validation() -> None:
75
+ class PlainModel(BaseModel):
76
+ id: str
77
+ count: int
78
+
79
+ @contract(name="counted", version="1.0")
80
+ class DecoratedModel(BaseModel):
81
+ id: str
82
+ count: int
83
+
84
+ plain = PlainModel(id="1", count=5)
85
+ decorated = DecoratedModel(id="1", count=5)
86
+ assert plain.model_dump() == decorated.model_dump()
87
+
88
+ with pytest.raises(Exception):
89
+ PlainModel(id="1", count="not-an-int")
90
+ with pytest.raises(Exception):
91
+ DecoratedModel(id="1", count="not-an-int")
File without changes
File without changes