openframe-core 3.1.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 (149) hide show
  1. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/CHANGELOG.md +32 -0
  2. {openframe_core-3.1.0 → openframe_core-3.2.1}/PKG-INFO +1 -9
  3. {openframe_core-3.1.0 → openframe_core-3.2.1}/README.md +0 -8
  4. openframe_core-3.2.1/docs/code/modules/health.md +46 -0
  5. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/index.md +1 -1
  6. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/roadmap.md +2 -2
  7. openframe_core-3.2.1/openframe/core/schemas/__init__.py +128 -0
  8. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/__init__.py +26 -2
  9. openframe_core-3.2.1/openframe/core/testing/contracts/__init__.py +44 -0
  10. openframe_core-3.2.1/openframe/core/testing/contracts/inbound.py +221 -0
  11. openframe_core-3.2.1/openframe/core/testing/fakes/__init__.py +36 -0
  12. openframe_core-3.2.1/openframe/core/testing/fakes/inbound.py +256 -0
  13. {openframe_core-3.1.0 → openframe_core-3.2.1}/pyproject.toml +1 -1
  14. openframe_core-3.2.1/tests/test_inbound_contracts.py +164 -0
  15. openframe_core-3.2.1/tests/test_schemas.py +91 -0
  16. openframe_core-3.1.0/docs/code/modules/health.md +0 -41
  17. openframe_core-3.1.0/openframe/core/testing/contracts/__init__.py +0 -25
  18. openframe_core-3.1.0/openframe/core/testing/fakes/__init__.py +0 -22
  19. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/requirements/docs.txt +0 -0
  20. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/requirements/test.txt +0 -0
  21. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/app-test.yml +0 -0
  22. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/auto-docs.yml +0 -0
  23. {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/python-build.yml +0 -0
  24. {openframe_core-3.1.0 → openframe_core-3.2.1}/.gitignore +0 -0
  25. {openframe_core-3.1.0 → openframe_core-3.2.1}/.vscode/settings.json +0 -0
  26. {openframe_core-3.1.0 → openframe_core-3.2.1}/LICENSE +0 -0
  27. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/banner.png +0 -0
  28. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/favicon.png +0 -0
  29. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/logo-dark.png +0 -0
  30. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/logo-light.png +0 -0
  31. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/javascripts/extra.js +0 -0
  32. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/stylesheets/extra.css +0 -0
  33. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/cicd/pipeline.md +0 -0
  34. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/getting-started/index.md +0 -0
  35. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/config.md +0 -0
  36. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/exceptions.md +0 -0
  37. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/inbound.md +0 -0
  38. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/index.md +0 -0
  39. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/middleware.md +0 -0
  40. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/plugins.md +0 -0
  41. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/ports.md +0 -0
  42. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/runtime.md +0 -0
  43. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/telemetry.md +0 -0
  44. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/testing.md +0 -0
  45. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/tracing.md +0 -0
  46. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/debugging.md +0 -0
  47. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/explore.md +0 -0
  48. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/first-change.md +0 -0
  49. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/how-it-works.md +0 -0
  50. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/local-dev.md +0 -0
  51. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/quick-start/index.md +0 -0
  52. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
  53. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
  54. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
  55. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
  56. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
  57. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
  58. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/index.md +0 -0
  59. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/capability-taxonomy.md +0 -0
  60. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/component-lifecycle.md +0 -0
  61. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/data-flows.md +0 -0
  62. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/design-decisions.md +0 -0
  63. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/error-taxonomy.md +0 -0
  64. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
  65. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
  66. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/index.md +0 -0
  67. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
  68. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
  69. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/system-architecture.md +0 -0
  70. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/cicd.md +0 -0
  71. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/environments.md +0 -0
  72. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/secrets-and-config.md +0 -0
  73. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/networking.md +0 -0
  74. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/platform.md +0 -0
  75. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/storage.md +0 -0
  76. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/index.md +0 -0
  77. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/monitoring.md +0 -0
  78. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
  79. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
  80. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
  81. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
  82. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
  83. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
  84. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/glossary.md +0 -0
  85. {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/system-overview.md +0 -0
  86. {openframe_core-3.1.0 → openframe_core-3.2.1}/mkdocs.yml +0 -0
  87. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/__init__.py +0 -0
  88. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/__init__.py +0 -0
  89. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/config/__init__.py +0 -0
  90. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/config/settings.py +0 -0
  91. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/__init__.py +0 -0
  92. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/adapter.py +0 -0
  93. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/base.py +0 -0
  94. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/codes.py +0 -0
  95. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/plugin.py +0 -0
  96. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/__init__.py +0 -0
  97. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/command.py +0 -0
  98. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/context.py +0 -0
  99. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/query.py +0 -0
  100. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/usecase.py +0 -0
  101. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/__init__.py +0 -0
  102. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/telemetry.py +0 -0
  103. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/types.py +0 -0
  104. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/plugins/__init__.py +0 -0
  105. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/plugins/registry.py +0 -0
  106. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/__init__.py +0 -0
  107. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/capability.py +0 -0
  108. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/context.py +0 -0
  109. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/health.py +0 -0
  110. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/identity.py +0 -0
  111. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/lifecycle.py +0 -0
  112. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/__init__.py +0 -0
  113. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/consumer.py +0 -0
  114. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/producer.py +0 -0
  115. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/repository.py +0 -0
  116. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/port.py +0 -0
  117. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/runtime/__init__.py +0 -0
  118. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/runtime/bootstrap.py +0 -0
  119. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/telemetry/__init__.py +0 -0
  120. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/telemetry/setup.py +0 -0
  121. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/consumer.py +0 -0
  122. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/lifecycle.py +0 -0
  123. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/port.py +0 -0
  124. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/producer.py +0 -0
  125. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/repository.py +0 -0
  126. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/consumer.py +0 -0
  127. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/producer.py +0 -0
  128. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/repository.py +0 -0
  129. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fixtures.py +0 -0
  130. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/__init__.py +0 -0
  131. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/propagation.py +0 -0
  132. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/proxy.py +0 -0
  133. {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/py.typed +0 -0
  134. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/conftest.py +0 -0
  135. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_config.py +0 -0
  136. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_exceptions.py +0 -0
  137. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_inbound.py +0 -0
  138. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_middleware.py +0 -0
  139. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_middleware_types.py +0 -0
  140. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_plugins.py +0 -0
  141. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_ports.py +0 -0
  142. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_ports_primitives.py +0 -0
  143. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_record_error.py +0 -0
  144. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_runtime.py +0 -0
  145. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_shutdown_telemetry.py +0 -0
  146. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_telemetry_sidecar.py +0 -0
  147. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_testing_fakes.py +0 -0
  148. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_tracing.py +0 -0
  149. {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_tracing_propagation.py +0 -0
@@ -58,6 +58,38 @@ 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
+
74
+ ## v3.2.0
75
+
76
+ ### Added
77
+
78
+ - `openframe.core.testing.fakes.inbound` — three in-memory test
79
+ doubles for the inbound contracts: `EchoUseCase`,
80
+ `SpyCommandHandler`, `SpyQueryHandler`. Mirror of the outbound
81
+ fakes (`InMemoryRepository`, `FakeProducer`, `FakeConsumer`).
82
+ - `openframe.core.testing.contracts.inbound` — three inheritable
83
+ pytest base classes: `UseCaseContractTests`,
84
+ `CommandHandlerContractTests`, `QueryHandlerContractTests`.
85
+ Any application service or validation-framework service
86
+ implementing these protocols can prove conformance by inheriting
87
+ the relevant class.
88
+ - All six new names exported from `openframe.core.testing`
89
+ top-level for consistent import ergonomics.
90
+
91
+ ---
92
+
61
93
  ## v3.1.0
62
94
 
63
95
  ### Breaking changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openframe-core
3
- Version: 3.1.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/
@@ -73,16 +73,8 @@ Description-Content-Type: text/markdown
73
73
  <a href="https://github.com/Furious-Meteors/openframe-core/blob/production/.github/CHANGELOG.md">Changelog</a>
74
74
  </p>
75
75
 
76
- ---
77
-
78
76
  `openframe-core` is the foundation package of the OpenFrame Microservice Development Suite. It defines the structural contracts, telemetry primitives, and shared utilities that every other package in the ecosystem depends on.
79
77
 
80
- Every package in the OpenFrame ecosystem pins `openframe-core>=3.0,<4`. The major version is the stability contract for the entire ecosystem.
81
-
82
- > **v3.0.0 is a breaking redesign.** The old hardcoded, lifecycle-free ports, the standalone `HealthCheck` protocol, and the separate `OpenFramePlugin` protocol are gone. They are replaced by a single unified contract layer built on `BasePort` (`Identity` + `Lifecycle`), and the error hierarchy is consolidated under a single `OpenFrameError` root. **v3.1.0 merges `openframe.core.contracts` into `openframe.core.ports`, and moves `BaseRepository`/`BaseProducer`/`BaseConsumer` into an internal `ports/outbound/` sub-module** (mirroring `openframe.core.inbound`) — no compatibility shim at the old path, and no change to the public `from openframe.core.ports import ...` surface. See [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/) and the [Changelog](.github/CHANGELOG.md) for the full migration guide.
83
-
84
- ---
85
-
86
78
  ## What's inside
87
79
 
88
80
  | Module | Exports |
@@ -17,16 +17,8 @@
17
17
  <a href="https://github.com/Furious-Meteors/openframe-core/blob/production/.github/CHANGELOG.md">Changelog</a>
18
18
  </p>
19
19
 
20
- ---
21
-
22
20
  `openframe-core` is the foundation package of the OpenFrame Microservice Development Suite. It defines the structural contracts, telemetry primitives, and shared utilities that every other package in the ecosystem depends on.
23
21
 
24
- Every package in the OpenFrame ecosystem pins `openframe-core>=3.0,<4`. The major version is the stability contract for the entire ecosystem.
25
-
26
- > **v3.0.0 is a breaking redesign.** The old hardcoded, lifecycle-free ports, the standalone `HealthCheck` protocol, and the separate `OpenFramePlugin` protocol are gone. They are replaced by a single unified contract layer built on `BasePort` (`Identity` + `Lifecycle`), and the error hierarchy is consolidated under a single `OpenFrameError` root. **v3.1.0 merges `openframe.core.contracts` into `openframe.core.ports`, and moves `BaseRepository`/`BaseProducer`/`BaseConsumer` into an internal `ports/outbound/` sub-module** (mirroring `openframe.core.inbound`) — no compatibility shim at the old path, and no change to the public `from openframe.core.ports import ...` surface. See [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/) and the [Changelog](.github/CHANGELOG.md) for the full migration guide.
27
-
28
- ---
29
-
30
22
  ## What's inside
31
23
 
32
24
  | Module | Exports |
@@ -0,0 +1,46 @@
1
+ # Health
2
+
3
+ Health is part of the unified `Lifecycle` contract in `openframe.core.ports`. Every `BasePort` implements a single `health()` method that returns a rich `PluginHealth` snapshot — there is no separate health protocol.
4
+
5
+ ```python
6
+ from openframe.core.ports import Lifecycle, PluginHealth, PluginStatus
7
+
8
+ async def health(self) -> PluginHealth:
9
+ try:
10
+ await self._pool.fetchval("SELECT 1")
11
+ return PluginHealth(status=PluginStatus.READY)
12
+ except Exception as exc:
13
+ return PluginHealth(
14
+ status=PluginStatus.UNAVAILABLE,
15
+ message=str(exc),
16
+ )
17
+ ```
18
+
19
+ `Lifecycle.health()` is the single canonical health primitive. Liveness vs. readiness granularity — or any other reporting distinction an adapter needs — is expressed through `PluginStatus` and `PluginHealth.details`, not through separate methods:
20
+
21
+ ```python
22
+ # Minimal
23
+ PluginHealth(status=PluginStatus.READY)
24
+
25
+ # With detail
26
+ PluginHealth(
27
+ status=PluginStatus.READY,
28
+ details={"ping_ms": 2.1, "schema_ok": True},
29
+ )
30
+
31
+ # Degraded
32
+ PluginHealth(
33
+ status=PluginStatus.DEGRADED,
34
+ message="replica lag 5s",
35
+ details={"lag_s": 5},
36
+ )
37
+ ```
38
+
39
+ `PluginRegistry` calls `health()` on every registered port to build a live snapshot — see [`PluginRegistry.health_all()`](plugins.md).
40
+
41
+ ---
42
+
43
+ ## See Also
44
+
45
+ - [ports module](ports.md) — `Lifecycle`, `PluginHealth`, `PluginStatus`, `BasePort`
46
+ - [ADR-006](../../technical/architecture/adrs/adr-006-unified-port-lifecycle.md) — full design rationale
@@ -2,7 +2,7 @@
2
2
 
3
3
  The foundation package of the OpenFrame Microservice Development Suite — the unified port + lifecycle contract layer, structured exceptions, telemetry, and ASGI middleware for any hexagonal architecture Python service. Every ecosystem package pins `openframe-core>=3.0,<4`.
4
4
 
5
- > **v3.0.0 is a breaking redesign.** The old lifecycle-free ports, standalone `HealthCheck`, and separate `OpenFramePlugin` protocol are replaced by a single unified contract layer (`BasePort` = `Identity` + `Lifecycle`). The error hierarchy is consolidated under `OpenFrameError`. **v3.1.0 merges `openframe.core.contracts` into `openframe.core.ports`** and moves the outbound protocols into an internal `ports/outbound/` sub-module — no compatibility shim, no public API change. See [ADR-006](technical/architecture/adrs/adr-006-unified-port-lifecycle.md) for the full rationale.
5
+ `openframe-core` is built around a single unified contract layer: `BasePort` (`Identity` + `Lifecycle`) is the one base every outbound port and every registrable plugin extends — there is no separate health protocol and no separate plugin protocol. The error hierarchy is consolidated under a single `OpenFrameError` root. The full contract layer, including the capability-specific outbound protocols (`BaseRepository`, `BaseProducer`, `BaseConsumer`), lives in `openframe.core.ports`. See [ADR-006](technical/architecture/adrs/adr-006-unified-port-lifecycle.md) for the full design rationale.
6
6
 
7
7
  ---
8
8
 
@@ -40,7 +40,7 @@ Lower-priority items:
40
40
 
41
41
  ## v3.1.0 — contracts/ Merged into ports/, outbound/ Sub-module Introduced (breaking)
42
42
 
43
- > Ecosystem packages pin `openframe-core>=3.0,<4`.
43
+ > Ecosystem packages pinned `openframe-core>=3.0,<4`; `openframe-adapters` has migrated its imports to `openframe.core.ports`.
44
44
 
45
45
  - `openframe.core.contracts` renamed to `openframe.core.ports`. All names previously importable from `openframe.core.contracts` are now importable from `openframe.core.ports`. No compatibility shim — update all imports.
46
46
  - `openframe.core.ports` now exports both the port primitives (`BasePort`, `Capability`, `Identity`, `Lifecycle`, `PluginStatus`, `PluginHealth`, `PluginContext`, `PrincipalContext`, `TenantContext`) and the outbound protocols (`BaseRepository`, `BaseProducer`, `BaseConsumer`) from a single unified module.
@@ -50,7 +50,7 @@ Lower-priority items:
50
50
 
51
51
  ## v3.0.0 — Unified Port + Lifecycle Contract (breaking)
52
52
 
53
- > Full migration guide: [ADR-006](../architecture/adrs/adr-006-unified-port-lifecycle.md). Ecosystem packages pin `openframe-core>=3.0,<4`.
53
+ > Migration guide: [ADR-006](../architecture/adrs/adr-006-unified-port-lifecycle.md). Ecosystem packages pinned `openframe-core>=3.0,<4`; all packages have since migrated.
54
54
 
55
55
  **Removed:**
56
56
 
@@ -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,10 +4,12 @@ openframe.core.testing
4
4
  Reusable testing infrastructure for the OpenFrame ecosystem (ADR-006).
5
5
 
6
6
  Provides in-memory test doubles (fakes) and reusable pytest base classes
7
- (contract tests) that every ``openframe-adapters-*`` package uses.
7
+ (contract tests) for both the outbound (driven) side — every
8
+ ``openframe-adapters-*`` package uses these — and the inbound (driving)
9
+ side of the hexagon.
8
10
 
9
11
  Zero external dependencies — fakes import only from
10
- ``openframe.core.ports``, ``openframe.core.ports``, and
12
+ ``openframe.core.ports``, ``openframe.core.inbound``, and
11
13
  ``openframe.core.exceptions``. Contract test classes do not import pytest
12
14
  at the module level; they are only useful when pytest is installed.
13
15
 
@@ -21,15 +23,31 @@ Quick reference::
21
23
 
22
24
  repo = InMemoryRepository()
23
25
  assert isinstance(repo, BaseRepository) # BasePort + repository methods
26
+
27
+ from openframe.core.testing import EchoUseCase, SpyCommandHandler, SpyQueryHandler
28
+ from openframe.core.testing import UseCaseContractTests
29
+
30
+ use_case = EchoUseCase(response={"id": "1"})
31
+ assert isinstance(use_case, UseCase)
24
32
  """
25
33
  from __future__ import annotations
26
34
 
27
35
  from openframe.core.testing.contracts.consumer import ConsumerContractTests
36
+ from openframe.core.testing.contracts.inbound import (
37
+ CommandHandlerContractTests,
38
+ QueryHandlerContractTests,
39
+ UseCaseContractTests,
40
+ )
28
41
  from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
29
42
  from openframe.core.testing.contracts.port import PortContractTests
30
43
  from openframe.core.testing.contracts.producer import ProducerContractTests
31
44
  from openframe.core.testing.contracts.repository import RepositoryContractTests
32
45
  from openframe.core.testing.fakes.consumer import FakeConsumer
46
+ from openframe.core.testing.fakes.inbound import (
47
+ EchoUseCase,
48
+ SpyCommandHandler,
49
+ SpyQueryHandler,
50
+ )
33
51
  from openframe.core.testing.fakes.producer import FakeProducer
34
52
  from openframe.core.testing.fakes.repository import InMemoryRepository
35
53
 
@@ -37,9 +55,15 @@ __all__ = [
37
55
  "InMemoryRepository",
38
56
  "FakeProducer",
39
57
  "FakeConsumer",
58
+ "EchoUseCase",
59
+ "SpyCommandHandler",
60
+ "SpyQueryHandler",
40
61
  "LifecycleContractTests",
41
62
  "PortContractTests",
42
63
  "RepositoryContractTests",
43
64
  "ProducerContractTests",
44
65
  "ConsumerContractTests",
66
+ "UseCaseContractTests",
67
+ "CommandHandlerContractTests",
68
+ "QueryHandlerContractTests",
45
69
  ]
@@ -0,0 +1,44 @@
1
+ """
2
+ openframe.core.testing.contracts
3
+ ==================================
4
+ Reusable pytest base classes for port and inbound contract tests (ADR-006).
5
+
6
+ Outbound: ``PortContractTests``/``LifecycleContractTests`` cover the
7
+ shared ``BasePort`` identity/lifecycle suite; ``RepositoryContractTests``,
8
+ ``ProducerContractTests``, ``ConsumerContractTests`` build on
9
+ ``PortContractTests`` with domain-specific assertions.
10
+
11
+ Inbound: ``UseCaseContractTests``, ``CommandHandlerContractTests``,
12
+ ``QueryHandlerContractTests`` mirror the outbound contract classes for
13
+ the driving side of the hexagon. They do not build on any shared base
14
+ class — ``UseCase``/``CommandHandler``/``QueryHandler`` do not extend
15
+ ``BasePort``.
16
+
17
+ No top-level pytest import — all modules here can be imported without
18
+ pytest installed.
19
+
20
+ .. stability: beta
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from openframe.core.testing.contracts.consumer import ConsumerContractTests
25
+ from openframe.core.testing.contracts.inbound import (
26
+ CommandHandlerContractTests,
27
+ QueryHandlerContractTests,
28
+ UseCaseContractTests,
29
+ )
30
+ from openframe.core.testing.contracts.lifecycle import LifecycleContractTests
31
+ from openframe.core.testing.contracts.port import PortContractTests
32
+ from openframe.core.testing.contracts.producer import ProducerContractTests
33
+ from openframe.core.testing.contracts.repository import RepositoryContractTests
34
+
35
+ __all__ = [
36
+ "LifecycleContractTests",
37
+ "PortContractTests",
38
+ "RepositoryContractTests",
39
+ "ProducerContractTests",
40
+ "ConsumerContractTests",
41
+ "UseCaseContractTests",
42
+ "CommandHandlerContractTests",
43
+ "QueryHandlerContractTests",
44
+ ]
@@ -0,0 +1,221 @@
1
+ """
2
+ openframe/core/testing/contracts/inbound.py
3
+ ==============================================
4
+ UseCaseContractTests, CommandHandlerContractTests, QueryHandlerContractTests
5
+ — reusable pytest base classes for the inbound (driving) side of the
6
+ hexagon (ADR-006).
7
+
8
+ Any application service or validation-framework service implementing
9
+ :class:`~openframe.core.inbound.usecase.UseCase`,
10
+ :class:`~openframe.core.inbound.command.CommandHandler`, or
11
+ :class:`~openframe.core.inbound.query.QueryHandler` can prove protocol
12
+ conformance by inheriting the matching class here and providing its
13
+ required fixture.
14
+
15
+ Unlike :class:`~openframe.core.testing.contracts.repository.RepositoryContractTests`
16
+ (which builds on :class:`~openframe.core.testing.contracts.port.PortContractTests`
17
+ for the shared ``BasePort`` identity/lifecycle suite), there is no base
18
+ class above these three. ``UseCase``/``CommandHandler``/``QueryHandler``
19
+ do not extend ``BasePort`` — they are inbound (driving) contracts, not
20
+ outbound ports — so each class stands alone.
21
+
22
+ No top-level pytest import — this module can be imported without pytest
23
+ installed. The test methods use pytest's fixture injection mechanism
24
+ through parameter names; pytest is discovered at collection time.
25
+
26
+ .. stability: beta
27
+ Beta — API may change in minor versions with a deprecation notice.
28
+
29
+ Dependency order:
30
+ testing/contracts/inbound → inbound
31
+
32
+ Subclass usage::
33
+
34
+ class TestMyUseCase(UseCaseContractTests):
35
+ @pytest.fixture
36
+ def use_case(self) -> MyUseCase:
37
+ return MyUseCase()
38
+
39
+ class TestMyCommandHandler(CommandHandlerContractTests):
40
+ @pytest.fixture
41
+ def command_handler(self) -> MyCommandHandler:
42
+ return MyCommandHandler()
43
+
44
+ class TestMyQueryHandler(QueryHandlerContractTests):
45
+ @pytest.fixture
46
+ def query_handler(self) -> MyQueryHandler:
47
+ return MyQueryHandler()
48
+
49
+ @pytest.fixture
50
+ def make_query(self):
51
+ def _make() -> str:
52
+ return "item-1"
53
+ return _make
54
+ """
55
+ from __future__ import annotations
56
+
57
+ from openframe.core.inbound.context import RequestContext
58
+
59
+ __all__ = [
60
+ "UseCaseContractTests",
61
+ "CommandHandlerContractTests",
62
+ "QueryHandlerContractTests",
63
+ ]
64
+
65
+
66
+ class UseCaseContractTests:
67
+ """
68
+ Reusable pytest base class for :class:`~openframe.core.inbound.usecase.UseCase`
69
+ contract tests.
70
+
71
+ Subclasses must provide one pytest fixture:
72
+
73
+ ``use_case``
74
+ A fresh instance satisfying
75
+ :class:`~openframe.core.inbound.usecase.UseCase`.
76
+
77
+ Subclasses may optionally override the ``make_command`` method to
78
+ supply a custom command payload; the default is a dict ``{"id": "test"}``.
79
+ Not a pytest fixture — a plain overridable method, following the same
80
+ convention as :meth:`~openframe.core.testing.contracts.lifecycle.LifecycleContractTests._make_context`,
81
+ so this module never needs a top-level pytest import.
82
+
83
+ .. stability: beta
84
+ """
85
+
86
+ def make_command(self):
87
+ """Build a default command payload — a plain dict. Override to customise."""
88
+ return {"id": "test"}
89
+
90
+ async def test_satisfies_use_case_protocol(self, use_case) -> None:
91
+ """use_case satisfies UseCase via structural subtyping."""
92
+ from openframe.core.inbound import UseCase
93
+
94
+ assert isinstance(use_case, UseCase), (
95
+ f"{type(use_case).__name__} does not satisfy UseCase. "
96
+ "Ensure it has a matching async 'execute' method."
97
+ )
98
+
99
+ async def test_execute_without_context_returns_result(self, use_case) -> None:
100
+ """execute(command) completes without raising, given no context."""
101
+ command = self.make_command()
102
+ await use_case.execute(command)
103
+
104
+ async def test_execute_with_context_threads_correlation_id(self, use_case) -> None:
105
+ """execute(command, context) accepts a RequestContext without raising."""
106
+ command = self.make_command()
107
+ context = RequestContext(correlation_id="test-corr-1")
108
+ await use_case.execute(command, context)
109
+
110
+ async def test_execute_with_none_context_is_accepted(self, use_case) -> None:
111
+ """execute(command, None) is accepted — context is optional."""
112
+ command = self.make_command()
113
+ await use_case.execute(command, None)
114
+
115
+
116
+ class CommandHandlerContractTests:
117
+ """
118
+ Reusable pytest base class for
119
+ :class:`~openframe.core.inbound.command.CommandHandler` contract tests.
120
+
121
+ Subclasses must provide one pytest fixture:
122
+
123
+ ``command_handler``
124
+ A fresh instance satisfying
125
+ :class:`~openframe.core.inbound.command.CommandHandler`.
126
+
127
+ Subclasses may optionally override the ``make_command`` method to
128
+ supply a custom command payload; the default is a dict ``{"id": "test"}``.
129
+ Not a pytest fixture — a plain overridable method, following the same
130
+ convention as :meth:`~openframe.core.testing.contracts.lifecycle.LifecycleContractTests._make_context`,
131
+ so this module never needs a top-level pytest import.
132
+
133
+ .. stability: beta
134
+ """
135
+
136
+ def make_command(self):
137
+ """Build a default command payload — a plain dict. Override to customise."""
138
+ return {"id": "test"}
139
+
140
+ async def test_satisfies_command_handler_protocol(self, command_handler) -> None:
141
+ """command_handler satisfies CommandHandler via structural subtyping."""
142
+ from openframe.core.inbound import CommandHandler
143
+
144
+ assert isinstance(command_handler, CommandHandler), (
145
+ f"{type(command_handler).__name__} does not satisfy CommandHandler. "
146
+ "Ensure it has a matching async 'execute' method."
147
+ )
148
+
149
+ async def test_execute_returns_none(self, command_handler) -> None:
150
+ """execute(command) returns None on success — the defining CommandHandler property."""
151
+ command = self.make_command()
152
+ result = await command_handler.execute(command)
153
+ assert result is None
154
+
155
+ async def test_execute_with_context_returns_none(self, command_handler) -> None:
156
+ """execute(command, context) returns None — context does not change the return type."""
157
+ command = self.make_command()
158
+ context = RequestContext(correlation_id="test-corr-1")
159
+ result = await command_handler.execute(command, context)
160
+ assert result is None
161
+
162
+ async def test_execute_with_none_context_is_accepted(self, command_handler) -> None:
163
+ """execute(command, None) completes without raising and returns None."""
164
+ command = self.make_command()
165
+ result = await command_handler.execute(command, None)
166
+ assert result is None
167
+
168
+
169
+ class QueryHandlerContractTests:
170
+ """
171
+ Reusable pytest base class for
172
+ :class:`~openframe.core.inbound.query.QueryHandler` contract tests.
173
+
174
+ Subclasses must provide one pytest fixture:
175
+
176
+ ``query_handler``
177
+ A fresh instance satisfying
178
+ :class:`~openframe.core.inbound.query.QueryHandler`, configured
179
+ to return a non-``None`` result.
180
+
181
+ Subclasses may optionally override the ``make_query`` method to
182
+ supply a custom query payload; the default is the string
183
+ ``"test-query"``. Not a pytest fixture — a plain overridable method,
184
+ following the same convention as
185
+ :meth:`~openframe.core.testing.contracts.lifecycle.LifecycleContractTests._make_context`,
186
+ so this module never needs a top-level pytest import.
187
+
188
+ .. stability: beta
189
+ """
190
+
191
+ def make_query(self):
192
+ """Build a default query payload — a plain string. Override to customise."""
193
+ return "test-query"
194
+
195
+ async def test_satisfies_query_handler_protocol(self, query_handler) -> None:
196
+ """query_handler satisfies QueryHandler via structural subtyping."""
197
+ from openframe.core.inbound import QueryHandler
198
+
199
+ assert isinstance(query_handler, QueryHandler), (
200
+ f"{type(query_handler).__name__} does not satisfy QueryHandler. "
201
+ "Ensure it has a matching async 'execute' method."
202
+ )
203
+
204
+ async def test_execute_returns_a_result(self, query_handler) -> None:
205
+ """execute(query) returns a non-None result — distinguishes QueryHandler from CommandHandler."""
206
+ query = self.make_query()
207
+ result = await query_handler.execute(query)
208
+ assert result is not None
209
+
210
+ async def test_execute_with_context_returns_result(self, query_handler) -> None:
211
+ """execute(query, context) returns a non-None result — context does not change the return type."""
212
+ query = self.make_query()
213
+ context = RequestContext(correlation_id="test-corr-1")
214
+ result = await query_handler.execute(query, context)
215
+ assert result is not None
216
+
217
+ async def test_execute_with_none_context_is_accepted(self, query_handler) -> None:
218
+ """execute(query, None) returns a non-None result — context is optional."""
219
+ query = self.make_query()
220
+ result = await query_handler.execute(query, None)
221
+ assert result is not None
@@ -0,0 +1,36 @@
1
+ """
2
+ openframe.core.testing.fakes
3
+ ==============================
4
+ In-memory test doubles for OpenFrame ports and inbound contracts (ADR-006).
5
+
6
+ Outbound fakes (``InMemoryRepository``, ``FakeProducer``, ``FakeConsumer``)
7
+ have zero external dependencies and satisfy their respective
8
+ ``BasePort``-based port protocols via structural subtyping — including the
9
+ identity/lifecycle members every port now carries.
10
+
11
+ Inbound fakes (``EchoUseCase``, ``SpyCommandHandler``, ``SpyQueryHandler``)
12
+ mirror the outbound fakes for the driving side of the hexagon — they
13
+ satisfy ``UseCase``/``CommandHandler``/``QueryHandler`` structurally and
14
+ record every call for spy-style assertions.
15
+
16
+ .. stability: beta
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from openframe.core.testing.fakes.consumer import FakeConsumer
21
+ from openframe.core.testing.fakes.inbound import (
22
+ EchoUseCase,
23
+ SpyCommandHandler,
24
+ SpyQueryHandler,
25
+ )
26
+ from openframe.core.testing.fakes.producer import FakeProducer
27
+ from openframe.core.testing.fakes.repository import InMemoryRepository
28
+
29
+ __all__ = [
30
+ "InMemoryRepository",
31
+ "FakeProducer",
32
+ "FakeConsumer",
33
+ "EchoUseCase",
34
+ "SpyCommandHandler",
35
+ "SpyQueryHandler",
36
+ ]