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.
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/CHANGELOG.md +32 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/PKG-INFO +1 -9
- {openframe_core-3.1.0 → openframe_core-3.2.1}/README.md +0 -8
- openframe_core-3.2.1/docs/code/modules/health.md +46 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/index.md +1 -1
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/roadmap.md +2 -2
- openframe_core-3.2.1/openframe/core/schemas/__init__.py +128 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/__init__.py +26 -2
- openframe_core-3.2.1/openframe/core/testing/contracts/__init__.py +44 -0
- openframe_core-3.2.1/openframe/core/testing/contracts/inbound.py +221 -0
- openframe_core-3.2.1/openframe/core/testing/fakes/__init__.py +36 -0
- openframe_core-3.2.1/openframe/core/testing/fakes/inbound.py +256 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/pyproject.toml +1 -1
- openframe_core-3.2.1/tests/test_inbound_contracts.py +164 -0
- openframe_core-3.2.1/tests/test_schemas.py +91 -0
- openframe_core-3.1.0/docs/code/modules/health.md +0 -41
- openframe_core-3.1.0/openframe/core/testing/contracts/__init__.py +0 -25
- openframe_core-3.1.0/openframe/core/testing/fakes/__init__.py +0 -22
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/requirements/test.txt +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.gitignore +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/.vscode/settings.json +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/LICENSE +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/config.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/plugins.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/ports.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/runtime.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/debugging.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/explore.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/how-it-works.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/developer-guide/quick-start/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/adrs/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/design-decisions.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/adapter-connection-failure.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/queue-consumer-stuck.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/redis-connection-lost.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/glossary.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/mkdocs.yml +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/exceptions/plugin.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/plugins/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/plugins/registry.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/outbound/repository.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/runtime/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/runtime/bootstrap.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/core/tracing/proxy.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/openframe/py.typed +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/conftest.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_config.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_exceptions.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_inbound.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_middleware.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_plugins.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_ports.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_record_error.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_runtime.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_testing_fakes.py +0 -0
- {openframe_core-3.1.0 → openframe_core-3.2.1}/tests/test_tracing.py +0 -0
- {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
|
|
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
|
-
|
|
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
|
|
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
|
-
>
|
|
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)
|
|
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.
|
|
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
|
+
]
|