openframe-core 3.2.1__tar.gz → 3.4.0__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.2.1 → openframe_core-3.4.0}/.github/CHANGELOG.md +143 -8
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.gitignore +1 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/PKG-INFO +2 -2
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/health.md +4 -3
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/plugins.md +27 -13
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/ports.md +29 -17
- openframe_core-3.4.0/docs/code/modules/runtime.md +170 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/debugging.md +12 -9
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/explore.md +2 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/how-it-works.md +45 -1
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/quick-start/index.md +21 -10
- openframe_core-3.4.0/docs/technical/architecture/adrs/adr-007-schema-contracts.md +111 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/index.md +2 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/design-decisions.md +37 -35
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/adapter-lifecycle.md +2 -2
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/adapter-connection-failure.md +4 -4
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +1 -1
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/queue-consumer-stuck.md +2 -2
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/redis-connection-lost.md +1 -1
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/overview/glossary.md +2 -2
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/overview/roadmap.md +39 -8
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/exceptions/plugin.py +1 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/plugins/__init__.py +3 -3
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/plugins/registry.py +60 -11
- openframe_core-3.4.0/openframe/core/resilience/__init__.py +44 -0
- openframe_core-3.4.0/openframe/core/resilience/circuit_breaker.py +260 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/runtime/__init__.py +4 -1
- openframe_core-3.4.0/openframe/core/runtime/bootstrap.py +299 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/tracing/proxy.py +2 -2
- {openframe_core-3.2.1 → openframe_core-3.4.0}/pyproject.toml +1 -1
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_exceptions.py +5 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_plugins.py +89 -0
- openframe_core-3.4.0/tests/test_resilience.py +233 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_runtime.py +131 -0
- openframe_core-3.2.1/docs/code/modules/runtime.md +0 -122
- openframe_core-3.2.1/openframe/core/runtime/bootstrap.py +0 -196
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.github/requirements/docs.txt +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.github/requirements/test.txt +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.github/workflows/app-test.yml +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.github/workflows/auto-docs.yml +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.github/workflows/python-build.yml +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/.vscode/settings.json +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/LICENSE +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/README.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/images/banner.png +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/images/favicon.png +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/cicd/pipeline.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/getting-started/index.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/config.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/exceptions.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/inbound.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/index.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/middleware.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/telemetry.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/testing.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/code/modules/tracing.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/first-change.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/developer-guide/local-dev.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/index.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/capability-taxonomy.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/component-lifecycle.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/error-taxonomy.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/error-propagation.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/index.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/template-wiring.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/architecture/system-architecture.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/deployment/cicd.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/infrastructure/networking.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/index.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/docs/technical/overview/system-overview.md +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/mkdocs.yml +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/config/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/config/settings.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/exceptions/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/exceptions/adapter.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/exceptions/base.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/exceptions/codes.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/inbound/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/inbound/command.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/inbound/context.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/inbound/query.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/inbound/usecase.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/middleware/telemetry.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/middleware/types.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/capability.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/context.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/health.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/identity.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/lifecycle.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/outbound/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/outbound/consumer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/outbound/producer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/outbound/repository.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/ports/port.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/schemas/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/telemetry/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/telemetry/setup.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/consumer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/inbound.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/lifecycle.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/port.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/producer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/contracts/repository.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fakes/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fakes/consumer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fakes/inbound.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fakes/producer.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fakes/repository.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/tracing/__init__.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/core/tracing/propagation.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/openframe/py.typed +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/conftest.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_config.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_inbound.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_inbound_contracts.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_middleware.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_middleware_types.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_ports.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_ports_primitives.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_record_error.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_schemas.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_shutdown_telemetry.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_telemetry_sidecar.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_testing_fakes.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_tracing.py +0 -0
- {openframe_core-3.2.1 → openframe_core-3.4.0}/tests/test_tracing_propagation.py +0 -0
|
@@ -9,17 +9,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
---
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
`PluginRegistry` direct, and `ApplicationBootstrap` now coexist without a
|
|
16
|
-
documented hierarchy. The ambiguity is better-named than before but still
|
|
17
|
-
unresolved. The next documentation pass must establish `ApplicationBootstrap`
|
|
18
|
-
as the default starting point and frame the other two as explicit alternatives
|
|
19
|
-
with stated conditions. Tracked in the roadmap.
|
|
14
|
+
## v3.4.0 — 2026-09-28
|
|
20
15
|
|
|
21
16
|
### Added
|
|
22
17
|
|
|
18
|
+
- **`openframe.core.resilience.CircuitBreakerProxy`** (new module,
|
|
19
|
+
experimental) — zero-code async circuit breaker. Wraps any object and
|
|
20
|
+
short-circuits its async methods after `failure_threshold` consecutive
|
|
21
|
+
failures, raising `AdapterConnectionError` ("circuit open", retryable)
|
|
22
|
+
immediately instead of letting every caller block until
|
|
23
|
+
`operation_timeout` on every single call during a sustained outage.
|
|
24
|
+
Standard closed → open → half-open → closed/open state machine.
|
|
25
|
+
Mirrors `TracingProxy`'s exact wrapping mechanism (`__getattr__`
|
|
26
|
+
interception, fresh `getattr` resolution of the wrapped method on every
|
|
27
|
+
call, sync methods pass through unwrapped) so the two proxies compose
|
|
28
|
+
predictably — see the module docstring for the recommended composition
|
|
29
|
+
order (`CircuitBreakerProxy(TracingProxy(repo, ...), ...)`, not the
|
|
30
|
+
reverse, so a short-circuited call never produces a misleading adapter
|
|
31
|
+
span). Addresses a real gap: none of the four `openframe-adapters`
|
|
32
|
+
packages (or anything else in the ecosystem) previously had any
|
|
33
|
+
resilience pattern beyond "block until timeout, then raise."
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Graduated `PluginRegistry` and `ApplicationBootstrap` from `experimental`
|
|
38
|
+
to `stable`** (`openframe.core.plugins.registry`, `openframe.core.runtime.bootstrap`,
|
|
39
|
+
and their respective package `__init__.py` re-exports). Graduation
|
|
40
|
+
criteria: (1) all 7 `openframe-local-validation-framework` services
|
|
41
|
+
already depend on both in production-shaped code across multiple
|
|
42
|
+
releases with no breaking change required of them; (2) v3.3.0's
|
|
43
|
+
`ApplicationBootstrap.compose()`/`get_all()`/`registry` additions landed
|
|
44
|
+
as pure additive extensions on top of the existing contract — no
|
|
45
|
+
breaking change was needed to add meaningful new functionality, which is
|
|
46
|
+
itself evidence the contract shape is right. This is the first module in
|
|
47
|
+
`openframe-core` to graduate via an explicit, written criteria set
|
|
48
|
+
(documented in each module's own docstring) rather than an unstated
|
|
49
|
+
judgment call — future modules going through the same
|
|
50
|
+
experimental→stable path should record their own criteria the same way.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## v3.3.0 — 2026-09-28
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`ApplicationBootstrap.compose(*ports)`, `.get_all()`, `.registry`** —
|
|
59
|
+
resolves the long-standing "three wiring options" ambiguity (previously
|
|
60
|
+
tracked here as an architectural watch item and in the roadmap) by
|
|
61
|
+
consolidating to one recommended class at three levels of ceremony
|
|
62
|
+
instead of three competing patterns:
|
|
63
|
+
- `compose(*ports)` (new classmethod) — zero-subclass entry point for a
|
|
64
|
+
service with one or a few ports and no per-port `config`/`init_timeout`.
|
|
65
|
+
Registers each port immediately at construction; `configure()` stays
|
|
66
|
+
the default no-op.
|
|
67
|
+
- `get_all(capability)` (new method) — mirrors `PluginRegistry.get_all()`,
|
|
68
|
+
closing the one real gap that previously forced a multi-port-per-capability
|
|
69
|
+
setup (e.g. primary + replica) off `ApplicationBootstrap` and onto
|
|
70
|
+
`PluginRegistry` directly.
|
|
71
|
+
- `registry` (new read-only property) — exposes the underlying
|
|
72
|
+
`PluginRegistry` for customization neither of the above covers (e.g.
|
|
73
|
+
`list_plugins()`), without standing up a second, parallel registry.
|
|
74
|
+
|
|
75
|
+
`PluginRegistry` direct construction and `deps.py` + `lru_cache` are no
|
|
76
|
+
longer documented as peer alternatives to `ApplicationBootstrap` — see
|
|
77
|
+
`docs/technical/architecture/design-decisions.md` and
|
|
78
|
+
`docs/developer-guide/how-it-works.md`'s new "Choosing a Wiring Pattern"
|
|
79
|
+
section for the full resolution.
|
|
80
|
+
|
|
81
|
+
- **`PluginRegistry`/`ApplicationBootstrap` per-port init timeout** —
|
|
82
|
+
`PluginRegistry(default_init_timeout=...)` and
|
|
83
|
+
`PluginRegistry.register(..., init_timeout=...)` (mirrored on
|
|
84
|
+
`ApplicationBootstrap`'s constructor and `register()`) bound how long
|
|
85
|
+
`initialize_all()` waits for a single port's `initialize()` before
|
|
86
|
+
giving up. Both default to `None` (no timeout), preserving prior
|
|
87
|
+
behaviour exactly for callers who don't opt in. Addresses a real
|
|
88
|
+
production risk: previously, one adapter's `initialize()` hanging
|
|
89
|
+
(e.g. a TCP connection that completes the handshake but never
|
|
90
|
+
responds) would block application startup indefinitely with no way
|
|
91
|
+
to bound it. A timeout surfaces as a plain `TimeoutError` and is
|
|
92
|
+
handled identically to any other `initialize()` failure — already-
|
|
93
|
+
initialized ports are rolled back (LIFO) before it propagates.
|
|
94
|
+
|
|
23
95
|
- **`openframe.core.telemetry.shutdown_telemetry()`** — flushes and shuts
|
|
24
96
|
down the OTel SDK, ensuring the `BatchSpanProcessor` exports all buffered
|
|
25
97
|
spans before process exit. Idempotent, never raises, resets `_INITIALISED`
|
|
@@ -37,6 +109,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
37
109
|
|
|
38
110
|
### Fixed
|
|
39
111
|
|
|
112
|
+
#### Code
|
|
113
|
+
|
|
114
|
+
- `TracingProxy` (`tracing/proxy.py`) — replaced the deprecated
|
|
115
|
+
`asyncio.iscoroutinefunction()` sync/async probe with
|
|
116
|
+
`inspect.iscoroutinefunction()`. `asyncio.iscoroutinefunction` is
|
|
117
|
+
slated for removal in Python 3.16; this was the only deprecation
|
|
118
|
+
warning in the test suite.
|
|
119
|
+
- `AmbiguousCapabilityError` (`exceptions/plugin.py`) — its `capability`
|
|
120
|
+
and `matches` constructor arguments are now also mirrored into
|
|
121
|
+
`self.context`, matching the pattern every other `AdapterError`/
|
|
122
|
+
`PluginError` subclass already follows (e.g. `AdapterError` mirrors
|
|
123
|
+
`adapter`/`operation` into `context`). Previously these two fields
|
|
124
|
+
were set as plain attributes but silently absent from `context`.
|
|
125
|
+
|
|
126
|
+
#### Documentation
|
|
127
|
+
|
|
128
|
+
- Corrected `PluginStatus` across `docs/technical/overview/glossary.md`,
|
|
129
|
+
`docs/code/modules/ports.md`, `docs/code/modules/health.md`, and four
|
|
130
|
+
operational runbooks — several described a `READY`/`DEGRADED`/
|
|
131
|
+
`UNAVAILABLE` enum that has never existed in source. The actual enum
|
|
132
|
+
(`ports/health.py`) is `DISCOVERED`/`REGISTERED`/`CONFIGURED`/
|
|
133
|
+
`INITIALIZED`/`READY`/`STOPPING`/`STOPPED`/`FAILED`; a failed health
|
|
134
|
+
check reports `FAILED`, with finer detail carried in
|
|
135
|
+
`PluginHealth.message`/`.details`, not a dedicated status value.
|
|
136
|
+
- Corrected the `PluginContext`, `PrincipalContext`, and `TenantContext`
|
|
137
|
+
examples in `docs/code/modules/ports.md` — the shown `PluginContext`
|
|
138
|
+
was missing the required `plugin_name` field entirely and typed
|
|
139
|
+
`config` as `dict` instead of `Mapping`; `PrincipalContext`/
|
|
140
|
+
`TenantContext` showed fields (`attributes`, `roles: frozenset[str]`)
|
|
141
|
+
that don't exist on the real dataclasses (`claims`,
|
|
142
|
+
`roles: tuple[str, ...]`, `name: str`).
|
|
143
|
+
- Added [ADR-007](../docs/technical/architecture/adrs/adr-007-schema-contracts.md)
|
|
144
|
+
documenting the `openframe.core.schemas` `@contract` marker shipped in
|
|
145
|
+
v3.2.1 — the module's own docstring already cited "ADR-007" before
|
|
146
|
+
this record existed.
|
|
147
|
+
- Corrected `docs/code/modules/plugins.md` and `docs/code/modules/runtime.md`,
|
|
148
|
+
both stale independently of the init-timeout addition above:
|
|
149
|
+
`initialize_all()` was documented as taking `principal`/`tenant`
|
|
150
|
+
parameters directly (they're actually set separately via
|
|
151
|
+
`PluginRegistry.set_context()`, undocumented until now); `ApplicationBootstrap`
|
|
152
|
+
was documented as having a `get_all()` method that did not exist on
|
|
153
|
+
that class at the time (only on `PluginRegistry` itself) — since
|
|
154
|
+
resolved by actually adding `get_all()` to `ApplicationBootstrap`, see
|
|
155
|
+
the wiring-consolidation entry above; `ApplicationBootstrap.health()`
|
|
156
|
+
— a real method — was undocumented entirely; and `start()`/`initialize_all()`
|
|
157
|
+
were both documented as raising a wrapped `PluginInitializationError`,
|
|
158
|
+
when in fact the registry re-raises whatever `initialize()` raised
|
|
159
|
+
(or a plain `TimeoutError`) completely unwrapped.
|
|
160
|
+
- Corrected `docs/developer-guide/debugging.md`'s "PluginRegistry Failures"
|
|
161
|
+
section, which repeated the same `PluginInitializationError` inaccuracy
|
|
162
|
+
above, and additionally claimed `get()` raises `PluginNotFoundError` for
|
|
163
|
+
a missing capability — it actually raises a plain `KeyError`.
|
|
164
|
+
`PluginNotFoundError` and `PluginInitializationError` both exist in the
|
|
165
|
+
public exception hierarchy (`openframe.core.exceptions`) but neither is
|
|
166
|
+
currently raised anywhere in `openframe-core`'s own source — flagged as
|
|
167
|
+
a roadmap item (dead-code exception classes), not fixed here, since
|
|
168
|
+
making them actually get raised would be a behavioral change.
|
|
169
|
+
|
|
40
170
|
#### CI/CD
|
|
41
171
|
|
|
42
172
|
- Upgraded all GitHub Actions to Node.js 24-compatible versions, resolving
|
|
@@ -295,6 +425,11 @@ Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
|
|
|
295
425
|
|
|
296
426
|
---
|
|
297
427
|
|
|
298
|
-
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.
|
|
428
|
+
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.4.0...HEAD
|
|
429
|
+
[3.4.0]: https://github.com/openframe-org/openframe-core/compare/v3.3.0...v3.4.0
|
|
430
|
+
[3.3.0]: https://github.com/openframe-org/openframe-core/compare/v3.2.1...v3.3.0
|
|
431
|
+
[3.2.1]: https://github.com/openframe-org/openframe-core/compare/v3.2.0...v3.2.1
|
|
432
|
+
[3.2.0]: https://github.com/openframe-org/openframe-core/compare/v3.1.0...v3.2.0
|
|
433
|
+
[3.1.0]: https://github.com/openframe-org/openframe-core/compare/v3.0.0...v3.1.0
|
|
299
434
|
[3.0.0]: https://github.com/openframe-org/openframe-core/compare/v1.0.0...v3.0.0
|
|
300
435
|
[1.0.0]: https://github.com/openframe-org/openframe-core/releases/tag/v1.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: openframe-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
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/
|
|
@@ -11,7 +11,7 @@ async def health(self) -> PluginHealth:
|
|
|
11
11
|
return PluginHealth(status=PluginStatus.READY)
|
|
12
12
|
except Exception as exc:
|
|
13
13
|
return PluginHealth(
|
|
14
|
-
status=PluginStatus.
|
|
14
|
+
status=PluginStatus.FAILED,
|
|
15
15
|
message=str(exc),
|
|
16
16
|
)
|
|
17
17
|
```
|
|
@@ -28,9 +28,10 @@ PluginHealth(
|
|
|
28
28
|
details={"ping_ms": 2.1, "schema_ok": True},
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
# Degraded
|
|
31
|
+
# Degraded but still serving — no separate status exists; encode the
|
|
32
|
+
# nuance in message/details while status stays READY
|
|
32
33
|
PluginHealth(
|
|
33
|
-
status=PluginStatus.
|
|
34
|
+
status=PluginStatus.READY,
|
|
34
35
|
message="replica lag 5s",
|
|
35
36
|
details={"lag_s": 5},
|
|
36
37
|
)
|
|
@@ -21,18 +21,25 @@ from openframe.core.ports import Capability
|
|
|
21
21
|
|
|
22
22
|
```python
|
|
23
23
|
class PluginRegistry:
|
|
24
|
+
def __init__(self, *, default_init_timeout: float | None = None) -> None: ...
|
|
25
|
+
|
|
24
26
|
def register(
|
|
25
27
|
self,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
plugin: BasePort,
|
|
29
|
+
*,
|
|
30
|
+
config: Mapping[str, Any] | None = None,
|
|
31
|
+
init_timeout: float | None = None,
|
|
28
32
|
) -> None: ...
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
def set_context(
|
|
31
35
|
self,
|
|
36
|
+
*,
|
|
32
37
|
principal: PrincipalContext | None = None,
|
|
33
38
|
tenant: TenantContext | None = None,
|
|
34
39
|
) -> None: ...
|
|
35
40
|
|
|
41
|
+
async def initialize_all(self) -> None: ...
|
|
42
|
+
|
|
36
43
|
async def shutdown_all(self) -> None: ...
|
|
37
44
|
|
|
38
45
|
def get(self, capability: Capability) -> BasePort: ...
|
|
@@ -42,33 +49,40 @@ class PluginRegistry:
|
|
|
42
49
|
|
|
43
50
|
---
|
|
44
51
|
|
|
45
|
-
#### `register(
|
|
52
|
+
#### `register(plugin, config, init_timeout)`
|
|
46
53
|
|
|
47
|
-
Register a `BasePort` with optional config. The config
|
|
54
|
+
Register a `BasePort` with optional config and an optional per-port init timeout. The config mapping is passed into `PluginContext.config` when `initialize_all()` is called.
|
|
48
55
|
|
|
49
56
|
**Parameters:**
|
|
50
57
|
|
|
51
58
|
| Name | Type | Default | Description |
|
|
52
59
|
|---|---|---|---|
|
|
53
|
-
| `
|
|
54
|
-
| `config` | `
|
|
60
|
+
| `plugin` | `BasePort` | — | Any `BasePort` instance (including `BaseRepository`, `BaseProducer`, `BaseConsumer`) |
|
|
61
|
+
| `config` | `Mapping[str, Any] \| None` | `None` | Port-specific configuration, e.g. `{"dsn": "postgres://..."}` |
|
|
62
|
+
| `init_timeout` | `float \| None` | `None` | Seconds to wait for this port's `initialize()` in `initialize_all()`, overriding the registry's `default_init_timeout` for this port only. `None` falls back to that default (which is itself `None` — no timeout — unless set on the constructor) |
|
|
55
63
|
|
|
56
|
-
**Raises:** `DuplicatePluginError` — if
|
|
64
|
+
**Raises:** `TypeError` (object doesn't satisfy `BasePort`) · `DuplicatePluginError` — if a port with this name is already registered.
|
|
57
65
|
|
|
58
66
|
---
|
|
59
67
|
|
|
60
|
-
#### `
|
|
68
|
+
#### `set_context(principal, tenant)`
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
Set the `principal`/`tenant` threaded through every port's `PluginContext` in `initialize_all()`. Call before `initialize_all()`, typically right after all `register()` calls.
|
|
63
71
|
|
|
64
72
|
**Parameters:**
|
|
65
73
|
|
|
66
74
|
| Name | Type | Default | Description |
|
|
67
75
|
|---|---|---|---|
|
|
68
|
-
| `principal` | `PrincipalContext \| None` | `None` | Optional caller identity threaded into `PluginContext` |
|
|
69
|
-
| `tenant` | `TenantContext \| None` | `None` | Optional tenant identity threaded into `PluginContext` |
|
|
76
|
+
| `principal` | `PrincipalContext \| None` | `None` | Optional caller identity threaded into every port's `PluginContext` |
|
|
77
|
+
| `tenant` | `TenantContext \| None` | `None` | Optional tenant identity threaded into every port's `PluginContext` |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
#### `initialize_all()`
|
|
82
|
+
|
|
83
|
+
Call `port.initialize(PluginContext)` on every registered port in registration order. Each port's config comes from its own `register()` call; `principal`/`tenant` come from `set_context()` (both default to `None` if never set). Each call is bounded by that port's `init_timeout` (or the registry's `default_init_timeout` if the port didn't specify one) — unbounded by default. Rolls back (shuts down already-initialised ports, in reverse order) if any port raises or times out, then re-raises.
|
|
70
84
|
|
|
71
|
-
**Raises:** `
|
|
85
|
+
**Raises:** The exception raised by the failing port's `initialize()` — or a plain `TimeoutError` if its timeout elapsed first. Neither is wrapped into a `PluginError` subclass; the registry re-raises whatever `initialize()` raised (or `TimeoutError`) unchanged, after rollback.
|
|
72
86
|
|
|
73
87
|
---
|
|
74
88
|
|
|
@@ -79,7 +79,7 @@ Called by `PluginRegistry.shutdown_all()` in LIFO order. Should flush, close con
|
|
|
79
79
|
|
|
80
80
|
#### `health()`
|
|
81
81
|
|
|
82
|
-
Return a liveness/readiness snapshot. Must never raise — return `PluginHealth(status=PluginStatus.
|
|
82
|
+
Return a liveness/readiness snapshot. Must never raise — return `PluginHealth(status=PluginStatus.FAILED, message=...)` on any failure.
|
|
83
83
|
|
|
84
84
|
**Returns:** `PluginHealth`
|
|
85
85
|
|
|
@@ -144,12 +144,19 @@ See [capability-taxonomy.md](../../technical/architecture/capability-taxonomy.md
|
|
|
144
144
|
### `PluginStatus`
|
|
145
145
|
|
|
146
146
|
```python
|
|
147
|
-
class PluginStatus(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
class PluginStatus(Enum):
|
|
148
|
+
DISCOVERED = auto()
|
|
149
|
+
REGISTERED = auto()
|
|
150
|
+
CONFIGURED = auto()
|
|
151
|
+
INITIALIZED = auto()
|
|
152
|
+
READY = auto()
|
|
153
|
+
STOPPING = auto()
|
|
154
|
+
STOPPED = auto()
|
|
155
|
+
FAILED = auto()
|
|
151
156
|
```
|
|
152
157
|
|
|
158
|
+
Lifecycle status values a port/plugin transitions through during application startup and shutdown; the registry tracks the last known state. There is no separate "degraded" or "unavailable" member — a health check that cannot confirm full health reports `FAILED` and encodes the nuance in `PluginHealth.message`/`.details`.
|
|
159
|
+
|
|
153
160
|
---
|
|
154
161
|
|
|
155
162
|
### `PluginHealth`
|
|
@@ -158,11 +165,11 @@ class PluginStatus(str, Enum):
|
|
|
158
165
|
@dataclass(frozen=True)
|
|
159
166
|
class PluginHealth:
|
|
160
167
|
status: PluginStatus
|
|
161
|
-
message: str
|
|
168
|
+
message: str = ""
|
|
162
169
|
details: dict[str, Any] = field(default_factory=dict)
|
|
163
170
|
```
|
|
164
171
|
|
|
165
|
-
Returned by `Lifecycle.health()`. Carries a status,
|
|
172
|
+
Returned by `Lifecycle.health()`. Carries a status, a human-readable message (empty string by default, not `None`), and optional structured detail (e.g. latency, schema checks). Frozen — safe to cache and pass across coroutines.
|
|
166
173
|
|
|
167
174
|
```python
|
|
168
175
|
# Minimal
|
|
@@ -174,12 +181,16 @@ PluginHealth(
|
|
|
174
181
|
details={"ping_ms": 2.1, "schema_ok": True},
|
|
175
182
|
)
|
|
176
183
|
|
|
177
|
-
# Degraded
|
|
184
|
+
# Degraded but still serving — no separate status exists; encode the
|
|
185
|
+
# nuance in message/details while status stays READY
|
|
178
186
|
PluginHealth(
|
|
179
|
-
status=PluginStatus.
|
|
187
|
+
status=PluginStatus.READY,
|
|
180
188
|
message="replica lag 5s",
|
|
181
189
|
details={"lag_s": 5},
|
|
182
190
|
)
|
|
191
|
+
|
|
192
|
+
# Failed
|
|
193
|
+
PluginHealth(status=PluginStatus.FAILED, message=str(exc))
|
|
183
194
|
```
|
|
184
195
|
|
|
185
196
|
---
|
|
@@ -189,12 +200,13 @@ PluginHealth(
|
|
|
189
200
|
```python
|
|
190
201
|
@dataclass(frozen=True)
|
|
191
202
|
class PluginContext:
|
|
192
|
-
config:
|
|
203
|
+
config: Mapping[str, Any]
|
|
204
|
+
plugin_name: str
|
|
193
205
|
principal: PrincipalContext | None = None
|
|
194
206
|
tenant: TenantContext | None = None
|
|
195
207
|
```
|
|
196
208
|
|
|
197
|
-
Passed to `Lifecycle.initialize()` by `PluginRegistry.initialize_all()`. Carries the port's config
|
|
209
|
+
Passed to `Lifecycle.initialize()` by `PluginRegistry.initialize_all()`. Carries the port's own validated config (a read-only mapping — an empty one when no config was registered for this port), the port's registered `name`, and optional identity/tenancy context. Intentionally narrow on cross-port dependencies — a port cannot look up other ports from this context; the registry must order registrations so dependency ports initialize first.
|
|
198
210
|
|
|
199
211
|
---
|
|
200
212
|
|
|
@@ -204,11 +216,11 @@ Passed to `Lifecycle.initialize()` by `PluginRegistry.initialize_all()`. Carries
|
|
|
204
216
|
@dataclass(frozen=True)
|
|
205
217
|
class PrincipalContext:
|
|
206
218
|
principal_id: str
|
|
207
|
-
roles:
|
|
208
|
-
|
|
219
|
+
roles: tuple[str, ...] = ()
|
|
220
|
+
claims: dict[str, object] = field(default_factory=dict)
|
|
209
221
|
```
|
|
210
222
|
|
|
211
|
-
Frozen dataclass carrying caller identity. Threaded through both `PluginContext` (outbound init) and `RequestContext` (inbound requests).
|
|
223
|
+
Frozen dataclass carrying caller identity (user, service account, API key). `roles` is empty when the caller has no roles or role information is unavailable; `claims` carries additional verified claims from the auth token/session (e.g. `{"email": "...", "org": "..."}`). Threaded through both `PluginContext` (outbound init) and `RequestContext` (inbound requests).
|
|
212
224
|
|
|
213
225
|
---
|
|
214
226
|
|
|
@@ -218,10 +230,10 @@ Frozen dataclass carrying caller identity. Threaded through both `PluginContext`
|
|
|
218
230
|
@dataclass(frozen=True)
|
|
219
231
|
class TenantContext:
|
|
220
232
|
tenant_id: str
|
|
221
|
-
|
|
233
|
+
name: str = ""
|
|
222
234
|
```
|
|
223
235
|
|
|
224
|
-
Frozen dataclass carrying tenant identity. Threaded through both `PluginContext` and `RequestContext`.
|
|
236
|
+
Frozen dataclass carrying tenant identity — `name` is empty when only the identifier is known. Threaded through both `PluginContext` and `RequestContext`.
|
|
225
237
|
|
|
226
238
|
---
|
|
227
239
|
|
|
@@ -415,7 +427,7 @@ class PostgresItemRepository:
|
|
|
415
427
|
await self._pool.fetchval("SELECT 1")
|
|
416
428
|
return PluginHealth(status=PluginStatus.READY)
|
|
417
429
|
except Exception as exc:
|
|
418
|
-
return PluginHealth(status=PluginStatus.
|
|
430
|
+
return PluginHealth(status=PluginStatus.FAILED, message=str(exc))
|
|
419
431
|
|
|
420
432
|
# Domain methods
|
|
421
433
|
async def get(self, entity_id: str) -> Item | None: ...
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# runtime
|
|
2
|
+
|
|
3
|
+
`openframe/core/runtime/` · The recommended composition root, at three levels of ceremony.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`ApplicationBootstrap` is the single recommended composition root for application code — not one of several competing wiring patterns. It manages `PluginRegistry` registration and lifecycle for you, as an async context manager: `__aenter__` calls `start()` (which calls `configure()` then `initialize_all()`), and `__aexit__` calls `stop()` (which calls `shutdown_all()` then `shutdown_telemetry()`).
|
|
10
|
+
|
|
11
|
+
It's used at three levels of ceremony, not three separate patterns — see [Choosing a Wiring Pattern](../../developer-guide/how-it-works.md#choosing-a-wiring-pattern) for the full guide:
|
|
12
|
+
|
|
13
|
+
1. `ApplicationBootstrap.compose(*ports)` — no subclass, for one or a few ports.
|
|
14
|
+
2. Subclass + `configure()` — for per-port `config`/`init_timeout` or conditional registration.
|
|
15
|
+
3. `bootstrap.registry` — the escape hatch to the underlying `PluginRegistry` for what tiers 1-2 don't cover.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Classes
|
|
24
|
+
|
|
25
|
+
### `ApplicationBootstrap`
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
class ApplicationBootstrap:
|
|
29
|
+
def __init__(self, *, default_init_timeout: float | None = None) -> None: ...
|
|
30
|
+
def configure(self) -> None: ...
|
|
31
|
+
def register(
|
|
32
|
+
self,
|
|
33
|
+
plugin: BasePort,
|
|
34
|
+
*,
|
|
35
|
+
config: Mapping[str, Any] | None = None,
|
|
36
|
+
init_timeout: float | None = None,
|
|
37
|
+
) -> None: ...
|
|
38
|
+
def get(self, capability: Capability) -> BasePort: ...
|
|
39
|
+
def get_all(self, capability: Capability) -> list[BasePort]: ...
|
|
40
|
+
@property
|
|
41
|
+
def registry(self) -> PluginRegistry: ...
|
|
42
|
+
@classmethod
|
|
43
|
+
def compose(cls, *ports: BasePort) -> "ApplicationBootstrap": ...
|
|
44
|
+
async def start(self) -> None: ...
|
|
45
|
+
async def stop(self) -> None: ...
|
|
46
|
+
async def health(self) -> dict[str, PluginHealth]: ...
|
|
47
|
+
|
|
48
|
+
async def __aenter__(self) -> "ApplicationBootstrap": ...
|
|
49
|
+
async def __aexit__(self, *args: object) -> None: ...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
#### `compose(*ports)` — classmethod
|
|
55
|
+
|
|
56
|
+
The zero-ceremony entry point. Builds an `ApplicationBootstrap` and registers each given port, in order, with no subclass. Covers the common case: settings are usually supplied at a port's own construction (e.g. `PostgresRepository(settings)`), not threaded through `PluginContext.config` — none of the four real `openframe-adapters` packages (Postgres, Mongo, Redis, Kafka) actually read `PluginContext.config` at all. Use a subclass with `configure()` instead once a port needs `config=`, `init_timeout=`, or registration order that depends on a runtime condition.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
async with ApplicationBootstrap.compose(PostgresRepository(settings)) as app:
|
|
60
|
+
repo = app.get(Capability.PERSISTENCE)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
#### `configure()`
|
|
66
|
+
|
|
67
|
+
Override this method to register ports. Called by `start()` before `initialize_all()`. The default implementation is a no-op — `compose()` never overrides it; it registers ports immediately at construction instead.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
class MyApp(ApplicationBootstrap):
|
|
71
|
+
def configure(self) -> None:
|
|
72
|
+
self.register(PostgresItemRepository(), config={"dsn": "postgres://..."})
|
|
73
|
+
self.register(RedisCache(), config={"url": "redis://..."})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
#### `start()`
|
|
79
|
+
|
|
80
|
+
Call `configure()` then `registry.initialize_all()`. If any port's `initialize()` raises (or its `init_timeout` elapses first), that exception propagates **unwrapped** after already-initialized ports are rolled back — `start()` does not wrap it into a `PluginInitializationError` (that class is currently unused anywhere in `openframe-core`).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
#### `stop()`
|
|
85
|
+
|
|
86
|
+
Call `registry.shutdown_all()`, then flush and shut down the OTel SDK via `shutdown_telemetry()`. Never raises — port and telemetry errors are both logged and shutdown continues.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
#### `register(plugin, config, init_timeout)`
|
|
91
|
+
|
|
92
|
+
Delegate to `registry.register()`. Raises `TypeError` if the object doesn't satisfy `BasePort`, or `DuplicatePluginError` if a port with this name is already registered. `init_timeout` overrides the bootstrap's own `default_init_timeout` (set on `__init__`) for this one port; `None` (the default) falls back to it.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
#### `get(capability)`
|
|
97
|
+
|
|
98
|
+
Delegate to `registry.get()` — strict, raises `AmbiguousCapabilityError` on >1 match. Only valid after `start()` has completed.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
#### `get_all(capability)`
|
|
103
|
+
|
|
104
|
+
Delegate to `registry.get_all()` — the escape hatch for the deliberate multi-port-per-capability case (e.g. primary + replica) that `get()`'s strictness rejects. Returns an empty list, never raises, when nothing matches.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
#### `registry` — property
|
|
109
|
+
|
|
110
|
+
The underlying `PluginRegistry` instance. For customization neither `compose()` nor a `configure()` subclass covers (e.g. `list_plugins()`), without standing up a second, parallel registry outside `ApplicationBootstrap`.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
#### `health()`
|
|
115
|
+
|
|
116
|
+
Delegate to `registry.health_all()` — a live health snapshot (dict of port name → `PluginHealth`) for every registered port.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Usage Patterns
|
|
121
|
+
|
|
122
|
+
### Zero-ceremony — one or a few ports
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
126
|
+
from openframe.core.ports import Capability
|
|
127
|
+
|
|
128
|
+
async with ApplicationBootstrap.compose(PostgresItemRepository()) as bootstrap:
|
|
129
|
+
repo = bootstrap.get(Capability.PERSISTENCE)
|
|
130
|
+
await serve(ItemService(repo))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Subclass — per-port config, or conditional registration
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
class MyServiceBootstrap(ApplicationBootstrap):
|
|
137
|
+
def configure(self) -> None:
|
|
138
|
+
self.register(PostgresItemRepository(), config={"dsn": "..."})
|
|
139
|
+
|
|
140
|
+
async with MyServiceBootstrap() as bootstrap:
|
|
141
|
+
repo = bootstrap.get(Capability.PERSISTENCE)
|
|
142
|
+
await serve(ItemService(repo))
|
|
143
|
+
# shutdown_all() then shutdown_telemetry() called automatically on __aexit__
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### As a FastAPI lifespan
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from contextlib import asynccontextmanager
|
|
150
|
+
from fastapi import FastAPI
|
|
151
|
+
|
|
152
|
+
bootstrap = ApplicationBootstrap.compose(PostgresItemRepository())
|
|
153
|
+
|
|
154
|
+
@asynccontextmanager
|
|
155
|
+
async def lifespan(app: FastAPI):
|
|
156
|
+
setup_telemetry()
|
|
157
|
+
await bootstrap.start()
|
|
158
|
+
yield
|
|
159
|
+
await bootstrap.stop()
|
|
160
|
+
|
|
161
|
+
app = FastAPI(lifespan=lifespan)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### The registry escape hatch — multi-port-per-capability
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
async with ApplicationBootstrap.compose(PrimaryPostgres(), ReplicaPostgres()) as bootstrap:
|
|
168
|
+
primary, replica = bootstrap.get_all(Capability.PERSISTENCE)
|
|
169
|
+
# or, equivalently: bootstrap.registry.get_all(Capability.PERSISTENCE)
|
|
170
|
+
```
|
|
@@ -45,22 +45,25 @@ except OpenFrameError:
|
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
48
|
-
## PluginRegistry Failures
|
|
48
|
+
## PluginRegistry / ApplicationBootstrap Failures
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
These apply identically whether you're using `PluginRegistry` directly or through `ApplicationBootstrap` (`app.get(...)`, `app.start()`) — the bootstrap delegates straight through.
|
|
51
51
|
|
|
52
|
-
**`
|
|
52
|
+
**`KeyError`** — no port registered for the requested `Capability`. Verify `register(port)` was called before `get(Capability.X)`. (Note: despite the exception hierarchy also defining a `PluginNotFoundError`, `get()` actually raises a plain `KeyError` — not that class.)
|
|
53
53
|
|
|
54
|
-
**`
|
|
54
|
+
**`AmbiguousCapabilityError`** — more than one port registered for the capability passed to `get()`. Use `get_all(Capability.X)` if multiple same-capability ports is intentional (`ApplicationBootstrap.get_all()` or `registry.get_all()`).
|
|
55
|
+
|
|
56
|
+
**Whatever `initialize()` raised** — `initialize_all()` does **not** wrap a failing port's exception into a `PluginInitializationError` (that class exists in the exception hierarchy but nothing in `openframe-core` currently raises it). It rolls back already-initialised ports (LIFO) and then re-raises the original exception from `initialize()` completely unchanged — catch whatever your adapter actually raises, or `OpenFrameError` for the ecosystem-wide catch point if your adapters follow that convention:
|
|
55
57
|
|
|
56
58
|
```python
|
|
57
|
-
from openframe.core.exceptions import
|
|
59
|
+
from openframe.core.exceptions import OpenFrameError
|
|
58
60
|
|
|
59
61
|
try:
|
|
60
|
-
await registry.initialize_all()
|
|
61
|
-
except
|
|
62
|
-
print(exc.
|
|
63
|
-
|
|
62
|
+
await app.start() # or: await registry.initialize_all()
|
|
63
|
+
except OpenFrameError as exc:
|
|
64
|
+
print(exc.code, exc.retryable) # whatever the failing port's initialize() raised
|
|
65
|
+
except TimeoutError:
|
|
66
|
+
print("a port's init_timeout elapsed — see PluginRegistry(default_init_timeout=...)")
|
|
64
67
|
```
|
|
65
68
|
|
|
66
69
|
---
|
|
@@ -90,6 +90,8 @@ print(isinstance(exc, OpenFrameError)) # True — single catch point for the e
|
|
|
90
90
|
|
|
91
91
|
## Explore PluginRegistry
|
|
92
92
|
|
|
93
|
+
`PluginRegistry` is the underlying mechanism — useful to poke at directly here to see how it works. In application code, use `ApplicationBootstrap` instead (`ApplicationBootstrap.compose(*ports)` for the common case) — see [How It Works § Choosing a Wiring Pattern](how-it-works.md#choosing-a-wiring-pattern).
|
|
94
|
+
|
|
93
95
|
```python
|
|
94
96
|
import asyncio
|
|
95
97
|
from openframe.core.ports import Capability
|