openframe-core 2.0.0__tar.gz → 3.0.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.0.1/.github/CHANGELOG.md +222 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/requirements/docs.txt +1 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/app-test.yml +2 -2
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/auto-docs.yml +4 -4
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/workflows/python-build.yml +6 -6
- openframe_core-3.0.1/.vscode/settings.json +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/PKG-INFO +142 -24
- openframe_core-3.0.1/README.md +367 -0
- openframe_core-3.0.1/docs/assets/images/banner.png +0 -0
- openframe_core-3.0.1/docs/code/cicd/pipeline.md +100 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/getting-started/index.md +11 -3
- openframe_core-3.0.1/docs/code/modules/contracts.md +219 -0
- openframe_core-3.0.1/docs/code/modules/exceptions.md +268 -0
- openframe_core-3.0.1/docs/code/modules/health.md +41 -0
- openframe_core-3.0.1/docs/code/modules/inbound.md +153 -0
- openframe_core-3.0.1/docs/code/modules/index.md +20 -0
- openframe_core-3.0.1/docs/code/modules/plugins.md +165 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/ports.md +66 -5
- openframe_core-3.0.1/docs/code/modules/runtime.md +122 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/telemetry.md +40 -0
- openframe_core-3.0.1/docs/code/modules/testing.md +194 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/tracing.md +2 -2
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/debugging.md +41 -4
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/explore.md +61 -10
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/first-change.md +12 -5
- openframe_core-3.0.1/docs/developer-guide/how-it-works.md +113 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/local-dev.md +1 -1
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/developer-guide/quick-start/index.md +45 -25
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/index.md +11 -7
- openframe_core-3.0.1/docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md +108 -0
- openframe_core-3.0.1/docs/technical/architecture/adrs/index.md +22 -0
- openframe_core-3.0.1/docs/technical/architecture/capability-taxonomy.md +78 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/component-lifecycle.md +26 -5
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/design-decisions.md +68 -1
- openframe_core-3.0.1/docs/technical/architecture/error-taxonomy.md +142 -0
- openframe_core-3.0.1/docs/technical/architecture/package-journey/adapter-lifecycle.md +70 -0
- openframe_core-3.0.1/docs/technical/architecture/package-journey/error-propagation.md +101 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/package-journey/index.md +6 -6
- openframe_core-3.0.1/docs/technical/architecture/package-journey/template-wiring.md +60 -0
- openframe_core-3.0.1/docs/technical/architecture/system-architecture.md +194 -0
- openframe_core-3.0.1/docs/technical/deployment/cicd.md +98 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/networking.md +1 -1
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/index.md +2 -2
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/adapter-connection-failure.md +18 -7
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/postgres-pool-exhausted.md +1 -1
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/queue-consumer-stuck.md +7 -3
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/redis-connection-lost.md +1 -1
- openframe_core-3.0.1/docs/technical/overview/glossary.md +117 -0
- openframe_core-3.0.1/docs/technical/overview/roadmap.md +89 -0
- openframe_core-3.0.1/docs/technical/overview/system-overview.md +111 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/mkdocs.yml +49 -12
- openframe_core-3.0.1/openframe/__init__.py +4 -0
- openframe_core-3.0.1/openframe/core/contracts/__init__.py +61 -0
- openframe_core-3.0.1/openframe/core/contracts/capability.py +61 -0
- openframe_core-3.0.1/openframe/core/contracts/context.py +64 -0
- openframe_core-3.0.1/openframe/core/contracts/health.py +96 -0
- openframe_core-3.0.1/openframe/core/contracts/identity.py +47 -0
- openframe_core-3.0.1/openframe/core/contracts/lifecycle.py +81 -0
- openframe_core-3.0.1/openframe/core/contracts/port.py +47 -0
- openframe_core-3.0.1/openframe/core/exceptions/__init__.py +72 -0
- openframe_core-2.0.0/openframe/core/exceptions/errors.py → openframe_core-3.0.1/openframe/core/exceptions/adapter.py +55 -14
- openframe_core-3.0.1/openframe/core/exceptions/base.py +120 -0
- openframe_core-3.0.1/openframe/core/exceptions/codes.py +79 -0
- openframe_core-3.0.1/openframe/core/exceptions/plugin.py +142 -0
- openframe_core-3.0.1/openframe/core/inbound/__init__.py +32 -0
- openframe_core-3.0.1/openframe/core/inbound/command.py +59 -0
- openframe_core-3.0.1/openframe/core/inbound/context.py +48 -0
- openframe_core-3.0.1/openframe/core/inbound/query.py +60 -0
- openframe_core-3.0.1/openframe/core/inbound/usecase.py +72 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/telemetry.py +32 -4
- openframe_core-3.0.1/openframe/core/plugins/__init__.py +35 -0
- openframe_core-3.0.1/openframe/core/plugins/registry.py +364 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/__init__.py +7 -4
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/consumer.py +21 -10
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/producer.py +22 -11
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/ports/repository.py +26 -11
- openframe_core-3.0.1/openframe/core/runtime/__init__.py +21 -0
- openframe_core-3.0.1/openframe/core/runtime/bootstrap.py +196 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/telemetry/__init__.py +10 -1
- openframe_core-3.0.1/openframe/core/telemetry/setup.py +400 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/__init__.py +7 -4
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/__init__.py +5 -1
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/consumer.py +25 -5
- openframe_core-3.0.1/openframe/core/testing/contracts/lifecycle.py +118 -0
- openframe_core-3.0.1/openframe/core/testing/contracts/port.py +85 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/producer.py +25 -5
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/contracts/repository.py +31 -51
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/__init__.py +3 -2
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/consumer.py +51 -7
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/producer.py +46 -6
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fakes/repository.py +67 -33
- openframe_core-3.0.1/openframe/core/tracing/__init__.py +43 -0
- openframe_core-3.0.1/openframe/core/tracing/propagation.py +77 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/tracing/proxy.py +9 -3
- openframe_core-3.0.1/openframe/py.typed +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/pyproject.toml +1 -1
- openframe_core-3.0.1/tests/test_contracts.py +245 -0
- openframe_core-3.0.1/tests/test_exceptions.py +399 -0
- openframe_core-3.0.1/tests/test_inbound.py +158 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_plugins.py +86 -38
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_ports.py +59 -10
- openframe_core-3.0.1/tests/test_record_error.py +143 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_runtime.py +30 -16
- openframe_core-3.0.1/tests/test_shutdown_telemetry.py +144 -0
- openframe_core-3.0.1/tests/test_telemetry_sidecar.py +181 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_testing_fakes.py +58 -15
- openframe_core-3.0.1/tests/test_tracing_propagation.py +148 -0
- openframe_core-2.0.0/.github/CHANGELOG.md +0 -91
- openframe_core-2.0.0/README.md +0 -249
- openframe_core-2.0.0/docs/assets/images/banner.png +0 -0
- openframe_core-2.0.0/docs/code/cicd/pipeline.md +0 -57
- openframe_core-2.0.0/docs/code/modules/exceptions.md +0 -97
- openframe_core-2.0.0/docs/code/modules/health.md +0 -55
- openframe_core-2.0.0/docs/code/modules/index.md +0 -13
- openframe_core-2.0.0/docs/developer-guide/how-it-works.md +0 -81
- openframe_core-2.0.0/docs/technical/architecture/package-journey/adapter-lifecycle.md +0 -58
- openframe_core-2.0.0/docs/technical/architecture/package-journey/error-propagation.md +0 -66
- openframe_core-2.0.0/docs/technical/architecture/package-journey/template-wiring.md +0 -54
- openframe_core-2.0.0/docs/technical/architecture/system-architecture.md +0 -100
- openframe_core-2.0.0/docs/technical/deployment/cicd.md +0 -46
- openframe_core-2.0.0/docs/technical/overview/glossary.md +0 -71
- openframe_core-2.0.0/docs/technical/overview/roadmap.md +0 -42
- openframe_core-2.0.0/docs/technical/overview/system-overview.md +0 -77
- openframe_core-2.0.0/openframe/core/errors/__init__.py +0 -25
- openframe_core-2.0.0/openframe/core/errors/plugin.py +0 -83
- openframe_core-2.0.0/openframe/core/exceptions/__init__.py +0 -39
- openframe_core-2.0.0/openframe/core/health/__init__.py +0 -19
- openframe_core-2.0.0/openframe/core/health/protocol.py +0 -94
- openframe_core-2.0.0/openframe/core/plugins/__init__.py +0 -27
- openframe_core-2.0.0/openframe/core/plugins/contracts.py +0 -148
- openframe_core-2.0.0/openframe/core/plugins/registry.py +0 -266
- openframe_core-2.0.0/openframe/core/runtime/__init__.py +0 -14
- openframe_core-2.0.0/openframe/core/runtime/bootstrap.py +0 -148
- openframe_core-2.0.0/openframe/core/telemetry/setup.py +0 -230
- openframe_core-2.0.0/openframe/core/tracing/__init__.py +0 -24
- openframe_core-2.0.0/tests/test_exceptions.py +0 -218
- openframe_core-2.0.0/tests/test_health.py +0 -118
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.github/requirements/test.txt +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/.gitignore +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/LICENSE +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/favicon.png +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/logo-dark.png +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/images/logo-light.png +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/javascripts/extra.js +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/assets/stylesheets/extra.css +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/config.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/code/modules/middleware.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-001-namespace.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-002-hexagonal.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-003-async.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-004-pydantic.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/adrs/adr-005-middleware.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/data-flows.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/architecture/package-journey/tracing-flow.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/deployment/environments.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/deployment/secrets-and-config.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/platform.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/infrastructure/storage.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/monitoring.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/modal-cold-start-spike.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/docs/technical/operations/runbooks/telemetry-export-failing.md +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/__init__.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/config/__init__.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/config/settings.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/__init__.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/middleware/types.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/openframe/core/testing/fixtures.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/conftest.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_config.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_middleware.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_middleware_types.py +0 -0
- {openframe_core-2.0.0 → openframe_core-3.0.1}/tests/test_tracing.py +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Watch items (architectural)
|
|
13
|
+
|
|
14
|
+
- **Three application wiring options** — `deps.py` + `lru_cache`,
|
|
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.
|
|
20
|
+
|
|
21
|
+
- **`contracts/` five-module depth** — the `capability → context → health →
|
|
22
|
+
identity → lifecycle → port` chain is clean today. The risk is a sixth or
|
|
23
|
+
seventh module that doesn't stay within the single-concept-per-file rule or
|
|
24
|
+
that imports from non-adjacent layers. Consolidate before expanding if either
|
|
25
|
+
signal appears.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **`openframe.core.telemetry.shutdown_telemetry()`** — flushes and shuts
|
|
30
|
+
down the OTel SDK, ensuring the `BatchSpanProcessor` exports all buffered
|
|
31
|
+
spans before process exit. Idempotent, never raises, resets `_INITIALISED`
|
|
32
|
+
so `setup_telemetry()` can run again (useful in tests).
|
|
33
|
+
`ApplicationBootstrap.stop()` calls it automatically after port shutdown.
|
|
34
|
+
Templates that do not use `ApplicationBootstrap` should call it in the
|
|
35
|
+
`lifespan` teardown path.
|
|
36
|
+
|
|
37
|
+
- **`openframe.core.tracing.propagation`** — W3C TraceContext inject/extract
|
|
38
|
+
helpers for message-broker adapters: `inject(carrier)` writes
|
|
39
|
+
`traceparent` into a header dict; `extract(carrier)` reads it back and
|
|
40
|
+
returns an OTel `Context`. Centralises what was previously private inside
|
|
41
|
+
the Kafka adapter so every broker adapter shares one implementation.
|
|
42
|
+
Re-exported as `openframe.core.tracing.propagation`.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
#### CI/CD
|
|
47
|
+
|
|
48
|
+
- Upgraded all GitHub Actions to Node.js 24-compatible versions, resolving
|
|
49
|
+
Node.js 20 deprecation warnings across all three workflows:
|
|
50
|
+
- `actions/checkout` v4 → **v5**
|
|
51
|
+
- `actions/setup-python` v5 → **v6**
|
|
52
|
+
- `actions/upload-artifact` v4 → **v6**
|
|
53
|
+
- `actions/download-artifact` v4 → **v7**
|
|
54
|
+
- Removed invalid `fetch-depth` input from the `actions/upload-artifact`
|
|
55
|
+
step in `python-build.yml` — this parameter belongs on `actions/checkout`,
|
|
56
|
+
not on the artifact upload action.
|
|
57
|
+
- Renamed `skip_existing` → `skip-existing` in `pypa/gh-action-pypi-publish`
|
|
58
|
+
to comply with the action's current kebab-case input convention.
|
|
59
|
+
- Added `attestations: false` to the PyPI publish step; token-based
|
|
60
|
+
authentication disables Trusted Publishing, which also disables provenance
|
|
61
|
+
attestation generation — the flag must be set explicitly to suppress the
|
|
62
|
+
spurious "attestations input ignored" warning. Remove this flag (or set
|
|
63
|
+
`attestations: true`) once Trusted Publishing (OIDC) is configured on PyPI.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## [3.0.0] — 2026-07-07
|
|
68
|
+
|
|
69
|
+
### Architecture scorecard
|
|
70
|
+
|
|
71
|
+
| Dimension | Score | Notes |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| Contract design | **9.5 / 10** | `Capability` enum closes the open-string gap; `BasePort` unification removes the dual-mechanism ambiguity. |
|
|
74
|
+
| Error model | **9.5 / 10** | `StrEnum` + decentralised `domain.kind` convention is the correct extensibility model for a multi-package ecosystem. |
|
|
75
|
+
| Testing infrastructure | **9.0 / 10** | `testing/contracts/` + `testing/fakes/` centralise what every adapter was duplicating — highest-leverage addition for the ecosystem. |
|
|
76
|
+
| Composition / wiring | **7.0 / 10** | `ApplicationBootstrap` names the third option, but `shutdown_telemetry()` is still missing — lifecycle is not complete. |
|
|
77
|
+
| Telemetry | **8.0 / 10** | Sidecar architecture, bounded-queue export, `record_error` seam — no shutdown flush hook yet.
|
|
78
|
+
|
|
79
|
+
### BREAKING CHANGE
|
|
80
|
+
|
|
81
|
+
Full clean-break redesign of the port/health/plugin contract layer. There
|
|
82
|
+
are no deprecated aliases and no backward-compatible shims for anything
|
|
83
|
+
removed below — see
|
|
84
|
+
[ADR-006](../docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md)
|
|
85
|
+
for the complete rationale.
|
|
86
|
+
|
|
87
|
+
#### Added
|
|
88
|
+
|
|
89
|
+
- **`openframe.core.contracts`** (new, apex module) — the single, unified port + lifecycle contract layer:
|
|
90
|
+
- `Identity` — `runtime_checkable` Protocol: `name`, `version`, `capability`.
|
|
91
|
+
- `Lifecycle` — `runtime_checkable` Protocol: `async initialize(context)`, `async shutdown()`, `async health()`.
|
|
92
|
+
- `BasePort(Identity, Lifecycle, Protocol)` — `runtime_checkable`. The single base every outbound port extends.
|
|
93
|
+
- `Capability(str, Enum)` — closed taxonomy: `PERSISTENCE`, `CACHE`, `QUEUE`, `SECRETS`, `FLAGS`, `STORAGE`, `TRANSPORT`, `INFERENCE`, `EMBEDDING`, `SCHEDULE`, `SEARCH`.
|
|
94
|
+
- `PluginStatus`, `PluginHealth`, `PluginContext` — moved here from the deleted `plugins/contracts.py`; the one canonical home.
|
|
95
|
+
- `PrincipalContext`, `TenantContext` — new frozen dataclasses threaded through `PluginContext` and (on the inbound side) `RequestContext`.
|
|
96
|
+
- **`openframe.core.inbound`** (new module) — the driving side of the hexagon:
|
|
97
|
+
- `UseCase[TInput, TOutput]`, `CommandHandler[TInput]`, `QueryHandler[TInput, TOutput]` — `runtime_checkable` Protocols.
|
|
98
|
+
- `RequestContext` — frozen dataclass: correlation id + optional `PrincipalContext`/`TenantContext`.
|
|
99
|
+
- **`openframe.core.exceptions`** — unified error hierarchy (consolidated; see Changed/Removed below):
|
|
100
|
+
- `OpenFrameError` — the single root every ecosystem error derives from. Carries `code`, `message`, `severity`, `retryable`, `correlation_id`, `context`, and `cause`. A single `except OpenFrameError` is a catch point for the whole platform.
|
|
101
|
+
- `ErrorCode`, `Severity` — `StrEnum` taxonomy. Codes follow a decentralised `domain.kind` convention; downstream packages declare their own codes rather than extending the enum.
|
|
102
|
+
- `AmbiguousCapabilityError` — raised by `PluginRegistry.get()` when more than one port shares a capability.
|
|
103
|
+
- **`openframe.core.telemetry.record_error`** — the single seam through which errors flow into telemetry: sets `error.code`/`error.severity`/`error.retryable` span attributes, records the exception, stamps `correlation_id` back onto the error, and increments the `openframe.error.count` metric exactly once per error (deduped across seams). Called at the `TracingProxy`, `TelemetryMiddleware`, and `PluginRegistry` lifecycle boundaries. `exceptions` never imports telemetry — telemetry reads the error's data.
|
|
104
|
+
- **`docs/technical/architecture/adrs/adr-006-unified-port-lifecycle.md`** — binding design record for this release; supersedes the port/health/plugin portions of ADR-002. Includes the unified-error-hierarchy addendum.
|
|
105
|
+
- **`docs/technical/architecture/capability-taxonomy.md`** — human-readable reference for the `Capability` enum.
|
|
106
|
+
- **`docs/technical/architecture/error-taxonomy.md`** — human-readable reference for the `OpenFrameError` hierarchy and the `domain.kind` code convention.
|
|
107
|
+
|
|
108
|
+
#### Changed
|
|
109
|
+
|
|
110
|
+
- **`openframe.core.ports`** — `BaseRepository[T]`, `BaseProducer[T]`, `BaseConsumer[T]` are rebuilt in place on `BasePort`. Every port is now lifecycle-aware by definition (`name`/`version`/`capability`/`initialize`/`shutdown`/`health` in addition to its domain methods). There is no `Managed*` twin and no lifecycle-free variant.
|
|
111
|
+
- **`openframe.core.plugins.registry.PluginRegistry`**
|
|
112
|
+
- `register()` now accepts any `BasePort` (previously `OpenFramePlugin`) and an optional `config` mapping threaded through to `PluginContext` at initialize time.
|
|
113
|
+
- `get()`/`get_all()` now take a `Capability` enum member instead of a raw `str`.
|
|
114
|
+
- `get()` is now strict — raises `AmbiguousCapabilityError` when more than one port shares the requested capability, instead of silently returning the first match. Use `get_all()` for the deliberate multi-port case.
|
|
115
|
+
- New `set_context(principal=..., tenant=...)` — threads a real `PrincipalContext`/`TenantContext` through every port's `PluginContext` in `initialize_all()` (previously always passed `config={}` with no identity).
|
|
116
|
+
- **`openframe.core.runtime.ApplicationBootstrap`** — `register()`/`get()` updated to the `BasePort`/`Capability` signatures above.
|
|
117
|
+
- **`openframe.core.testing.contracts`** — `RepositoryContractTests`, `ProducerContractTests`, `ConsumerContractTests` now build on the new `PortContractTests` (identity + lifecycle checks for any `BasePort`), which itself builds on `LifecycleContractTests` (initialize → health → idempotent shutdown → shutdown-before-initialize safety).
|
|
118
|
+
- **`openframe.core.testing.fakes`** — `InMemoryRepository`, `FakeProducer`, `FakeConsumer` now satisfy `BasePort`: each gained `name`/`version`/`capability` and `initialize`/`shutdown`/`health`.
|
|
119
|
+
- **`openframe.core.exceptions`** — `AdapterError` and `PluginError` are now both re-parented under the new `OpenFrameError` root. Their constructors (`AdapterError(message, adapter, operation, cause)`, `PluginError(message, plugin_name)`) and `__str__` output are unchanged, so existing raise sites and `except` clauses keep working. Each subclass now also carries a `domain.kind` `code` and a `retryable` default (`adapter.connection`/`adapter.timeout` are retryable; `adapter.not_found` is not).
|
|
120
|
+
|
|
121
|
+
#### Removed
|
|
122
|
+
|
|
123
|
+
- **`openframe.core.health`** (entire module deleted) — `HealthCheck` (`ping()`/`is_ready()`) no longer exists. Health is exclusively `Lifecycle.health() -> PluginHealth`.
|
|
124
|
+
- **`OpenFramePlugin`** (`openframe.core.plugins.contracts`, module deleted) — no longer exists as a separate protocol. A "plugin" is just a registered `BasePort`.
|
|
125
|
+
- The pre-v3 hardcoded, lifecycle-free `BaseRepository`/`BaseProducer`/`BaseConsumer` Protocols (domain methods only, no identity/lifecycle) — replaced in place, not additively.
|
|
126
|
+
- `InMemoryRepository.ping()`/`.is_ready()` (`ping_healthy=`/`ready_healthy=` constructor args) — replaced by `health()` (`healthy=` constructor arg).
|
|
127
|
+
- **`openframe.core.errors`** (entire module deleted) — the `PluginError` family (`PluginError`, `PluginInitializationError`, `PluginNotFoundError`, `DuplicatePluginError`, `AmbiguousCapabilityError`) moved into the unified `openframe.core.exceptions` package. Import from `openframe.core.exceptions` instead of `openframe.core.errors`.
|
|
128
|
+
|
|
129
|
+
### Migration notes
|
|
130
|
+
|
|
131
|
+
`openframe-adapters` packages pinned `openframe-core>=2.0,<3` continue
|
|
132
|
+
resolving to 2.x and are unaffected until explicitly migrated. To migrate
|
|
133
|
+
an adapter to v3:
|
|
134
|
+
|
|
135
|
+
1. Add `name: str`, `version: str`, `capability: Capability` attributes to every port implementation.
|
|
136
|
+
2. Add `async initialize(self, context: PluginContext) -> None` and `async shutdown(self) -> None`.
|
|
137
|
+
3. Replace `ping()`/`is_ready()` with a single `async health(self) -> PluginHealth`, encoding the liveness/readiness distinction (if you need to keep it) in `PluginStatus`/`PluginHealth.details` rather than two methods.
|
|
138
|
+
4. Replace any hand-written `OpenFramePlugin` wrapper class with the port object itself — it already satisfies `BasePort` once steps 1–3 are done, so no wrapper is needed.
|
|
139
|
+
5. Replace string capability keys (`"persistence"`) with the `Capability` enum (`Capability.PERSISTENCE`) everywhere they're used to register with or query a `PluginRegistry`.
|
|
140
|
+
6. Update any `from openframe.core.errors import ...` to `from openframe.core.exceptions import ...` (the `errors` package was consolidated into `exceptions`). Adapter raise sites for `AdapterError` and its subclasses need no changes — the constructors are unchanged. Optionally, catch `OpenFrameError` for a single ecosystem-wide catch point, and read `err.code`/`err.retryable`/`err.severity` instead of branching on concrete exception classes.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## [1.0.0] — 2026-06-13
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
#### `openframe.core.exceptions`
|
|
149
|
+
- `AdapterError` — base exception class for all OpenFrame adapter packages. Carries `message`, `adapter`, `operation`, and `cause` fields. Implements `__str__` to produce `[adapter.operation] message — caused by: <cause>` rather than a raw tuple repr.
|
|
150
|
+
- `AdapterConnectionError` — raised when an adapter cannot establish a connection to its backend.
|
|
151
|
+
- `AdapterQueryError` — raised when an operation fails after a connection is established.
|
|
152
|
+
- `AdapterNotFoundError` — raised when a requested entity does not exist in the backend.
|
|
153
|
+
- `AdapterConfigurationError` — raised when required configuration (env vars, settings) is missing or invalid.
|
|
154
|
+
- `AdapterTimeoutError` — raised when an operation exceeds its configured timeout.
|
|
155
|
+
|
|
156
|
+
#### `openframe.core.config`
|
|
157
|
+
- `BaseAdapterSettings` — Pydantic `BaseSettings` subclass that all adapter config classes inherit from. Reads fields from environment variables automatically (case-insensitive, no prefix). Ships with four common fields: `adapter_name`, `connection_timeout` (30 s), `operation_timeout` (10 s), and `max_retries` (3). Missing required fields raise `ValidationError` at instantiation time.
|
|
158
|
+
|
|
159
|
+
#### `openframe.core.ports`
|
|
160
|
+
- `BaseRepository[T]` — generic `runtime_checkable` Protocol for persistence adapters. Methods: `get`, `list`, `create`, `update`, `delete`.
|
|
161
|
+
- `BaseProducer[T]` — generic `runtime_checkable` Protocol for message producers. Methods: `publish`, `publish_batch`, `close`.
|
|
162
|
+
- `BaseConsumer[T]` — generic `runtime_checkable` Protocol for message consumers. Methods: `subscribe`, `ack`, `nack`, `close`. Uses a push-based handler model via `Callable[[T], Awaitable[None]]`.
|
|
163
|
+
|
|
164
|
+
#### `openframe.core.health`
|
|
165
|
+
- `HealthCheck` — `runtime_checkable` Protocol with two async methods: `ping()` (low-cost liveness, e.g. `SELECT 1`) and `is_ready()` (full readiness — schema migrations, pool health).
|
|
166
|
+
|
|
167
|
+
#### `openframe.core.telemetry`
|
|
168
|
+
- `setup_telemetry()` — idempotent OTel SDK bootstrap. Configures OTLP trace and metric exporters when `OTEL_EXPORTER_OTLP_ENDPOINT` is set; falls back to no-op providers otherwise. Applies `LoggingInstrumentor` for automatic trace-id injection into log records.
|
|
169
|
+
- `get_tracer(name)` — `@lru_cache` accessor for the globally-configured `Tracer`.
|
|
170
|
+
- `get_meter(name)` — `@lru_cache` accessor for the globally-configured `Meter`.
|
|
171
|
+
- `record_lifecycle_event(event_name, attributes)` — platform-agnostic lifecycle counter. Replaces the template's `record_cold_start()`; callers pass `event_name="cold_start"`. Core remains platform-agnostic.
|
|
172
|
+
- Reads environment variables: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, `OTEL_METRIC_EXPORT_INTERVAL_MS`, `OPENFRAME_ENV`.
|
|
173
|
+
|
|
174
|
+
#### `openframe.core.tracing`
|
|
175
|
+
- `TracingProxy` — zero-code async telemetry sidecar. Wraps any object and intercepts every async method call with an OTel child span. Span name: `{prefix}.{method_name}`. Sync methods pass through unwrapped. Uses an explicit `_cache` dict for closure reuse without method snapshotting — safe for reconnecting adapters (Postgres pools, Redis clients) that replace their own methods after reconnect.
|
|
176
|
+
|
|
177
|
+
#### `openframe.core.middleware`
|
|
178
|
+
- `TelemetryMiddleware` — pure ASGI telemetry middleware, compatible with FastAPI, Starlette, Litestar, and bare ASGI. Records one OTel span and five HTTP metric instruments per request. Injects `x-session-id` response header. Sets `StatusCode.ERROR` for HTTP ≥ 400, `StatusCode.OK` otherwise. Emits a structured log line per request.
|
|
179
|
+
- Metric instruments: `http.server.request.count` (`1`), `http.server.request.duration` (`s`), `http.server.active_requests` (`1`), `http.server.error.count` (`1`), `http.server.response.size` (`By`). Duration uses unit `"s"` per OTel HTTP semantic conventions.
|
|
180
|
+
- Span attributes: `http.method`, `http.target`, `http.route`, `http.scheme`, `http.status_code`, `http.user_agent`, `net.host.name`, `http.client_ip`, `app.session_id`.
|
|
181
|
+
- Route template extraction: raw ASGI uses `scope["path"]`; Starlette/FastAPI users get template grouping automatically via `scope["route"].path`.
|
|
182
|
+
|
|
183
|
+
#### `openframe.core.middleware.types`
|
|
184
|
+
- `ASGIScope`, `ASGIMessage`, `Receive`, `Send`, `ASGIApp` — stdlib-only ASGI type aliases shared across the OpenFrame ecosystem. No framework imports required.
|
|
185
|
+
|
|
186
|
+
#### Tests
|
|
187
|
+
- 112 tests across 8 test modules: `test_exceptions`, `test_config`, `test_ports`, `test_health`, `test_tracing`, `test_middleware`, `test_middleware_types`, `test_config`.
|
|
188
|
+
- `conftest.py` resets all OTel SDK global state between tests (the `Once._done` guard on `set_tracer_provider` / `set_meter_provider`, `lru_cache` entries, and the `_INITIALISED` flag) so every test receives a clean, isolated tracer and meter.
|
|
189
|
+
|
|
190
|
+
#### Package
|
|
191
|
+
- `pyproject.toml` with `hatchling` build backend, `requires-python = ">=3.11"`.
|
|
192
|
+
- `pytest.ini` with `asyncio_mode = auto` for `pytest-asyncio`.
|
|
193
|
+
- Namespace package declaration in `openframe/__init__.py` via `pkgutil.extend_path`.
|
|
194
|
+
|
|
195
|
+
### Design decisions
|
|
196
|
+
|
|
197
|
+
- **Exception naming** — all exception subclasses carry the `Adapter` prefix (`AdapterConnectionError`, `AdapterTimeoutError`) to avoid shadowing the Python stdlib built-ins `ConnectionError` and `TimeoutError`.
|
|
198
|
+
- **No Grafana-specific auth** — `_grafana_headers()` from the production template is removed. The OTel SDK handles authentication natively via `OTEL_EXPORTER_OTLP_HEADERS`.
|
|
199
|
+
- **`OPENFRAME_ENV` replaces `MODAL_ENV`** — the environment tag is platform-agnostic. Modal users map `MODAL_ENV → OPENFRAME_ENV` in their `configure_env_vars()`.
|
|
200
|
+
- **Middleware does not call `setup_telemetry()`** — it calls `get_tracer()` and `get_meter()` directly. `setup_telemetry()` is an application startup concern (called once in a `lifespan` handler). This design makes the middleware safely composable with test fixtures that install their own providers.
|
|
201
|
+
- **`TracingProxy` closure safety** — the original template captured the resolved method in the closure at first access (a snapshot). This implementation resolves the method fresh on every invocation inside `_traced`, making the proxy safe for adapters that replace their own methods after reconnect.
|
|
202
|
+
- **Pure ASGI middleware** — uses `__call__(scope, receive, send)` rather than Starlette's `BaseHTTPMiddleware`, which has documented streaming and buffering limitations.
|
|
203
|
+
|
|
204
|
+
### Dependencies
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
opentelemetry-api>=1.24
|
|
208
|
+
opentelemetry-sdk>=1.24
|
|
209
|
+
opentelemetry-exporter-otlp-proto-http>=1.24
|
|
210
|
+
opentelemetry-semantic-conventions>=0.45b0
|
|
211
|
+
opentelemetry-instrumentation-logging>=0.45b0
|
|
212
|
+
pydantic>=2.0
|
|
213
|
+
pydantic-settings>=2.0
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Dev dependencies: `pytest>=8.0`, `pytest-asyncio>=0.23`, `httpx>=0.27`.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
[Unreleased]: https://github.com/openframe-org/openframe-core/compare/v3.0.0...HEAD
|
|
221
|
+
[3.0.0]: https://github.com/openframe-org/openframe-core/compare/v1.0.0...v3.0.0
|
|
222
|
+
[1.0.0]: https://github.com/openframe-org/openframe-core/releases/tag/v1.0.0
|
|
@@ -22,10 +22,10 @@ jobs:
|
|
|
22
22
|
|
|
23
23
|
steps:
|
|
24
24
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
25
|
+
uses: actions/checkout@v5
|
|
26
26
|
|
|
27
27
|
- name: Setup Python ${{ matrix.python-version }}
|
|
28
|
-
uses: actions/setup-python@
|
|
28
|
+
uses: actions/setup-python@v6
|
|
29
29
|
with:
|
|
30
30
|
python-version: ${{ matrix.python-version }}
|
|
31
31
|
allow-prereleases: true # required if 3.14 is still RC on the runner
|
|
@@ -20,10 +20,10 @@ jobs:
|
|
|
20
20
|
|
|
21
21
|
steps:
|
|
22
22
|
- name: Checkout
|
|
23
|
-
uses: actions/checkout@
|
|
23
|
+
uses: actions/checkout@v5
|
|
24
24
|
|
|
25
25
|
- name: Setup Python
|
|
26
|
-
uses: actions/setup-python@
|
|
26
|
+
uses: actions/setup-python@v6
|
|
27
27
|
with:
|
|
28
28
|
python-version: "3.11"
|
|
29
29
|
cache: pip
|
|
@@ -50,12 +50,12 @@ jobs:
|
|
|
50
50
|
|
|
51
51
|
steps:
|
|
52
52
|
- name: Checkout
|
|
53
|
-
uses: actions/checkout@
|
|
53
|
+
uses: actions/checkout@v5
|
|
54
54
|
with:
|
|
55
55
|
fetch-depth: 0 # full history needed for mkdocs git-revision-date plugins
|
|
56
56
|
|
|
57
57
|
- name: Setup Python
|
|
58
|
-
uses: actions/setup-python@
|
|
58
|
+
uses: actions/setup-python@v6
|
|
59
59
|
with:
|
|
60
60
|
python-version: "3.11"
|
|
61
61
|
cache: pip
|
|
@@ -21,10 +21,10 @@ jobs:
|
|
|
21
21
|
|
|
22
22
|
steps:
|
|
23
23
|
- name: Checkout
|
|
24
|
-
uses: actions/checkout@
|
|
24
|
+
uses: actions/checkout@v5
|
|
25
25
|
|
|
26
26
|
- name: Setup Python
|
|
27
|
-
uses: actions/setup-python@
|
|
27
|
+
uses: actions/setup-python@v6
|
|
28
28
|
with:
|
|
29
29
|
python-version: "3.11"
|
|
30
30
|
cache: pip
|
|
@@ -38,12 +38,11 @@ jobs:
|
|
|
38
38
|
run: python -m build
|
|
39
39
|
|
|
40
40
|
- name: Upload distribution artifacts
|
|
41
|
-
uses: actions/upload-artifact@
|
|
41
|
+
uses: actions/upload-artifact@v6
|
|
42
42
|
with:
|
|
43
43
|
name: dist
|
|
44
44
|
path: dist/
|
|
45
45
|
retention-days: 7
|
|
46
|
-
fetch-depth: 2 # needs at least 2 commits for HEAD~1 to exist
|
|
47
46
|
|
|
48
47
|
# ── 3. Publish to PyPI ──────────────────────────────────────────────────
|
|
49
48
|
publish:
|
|
@@ -71,7 +70,7 @@ jobs:
|
|
|
71
70
|
|
|
72
71
|
steps:
|
|
73
72
|
- name: Download distribution artifacts
|
|
74
|
-
uses: actions/download-artifact@
|
|
73
|
+
uses: actions/download-artifact@v7
|
|
75
74
|
with:
|
|
76
75
|
name: dist
|
|
77
76
|
path: dist/
|
|
@@ -84,4 +83,5 @@ jobs:
|
|
|
84
83
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
85
84
|
with:
|
|
86
85
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
87
|
-
|
|
86
|
+
skip-existing: true
|
|
87
|
+
attestations: false
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openframe-core
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: OpenFrame Microservice Suite - platform kernel. 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/
|
|
@@ -55,13 +55,13 @@ Requires-Dist: ruff<1,>=0.11; extra == 'docs-dev'
|
|
|
55
55
|
Description-Content-Type: text/markdown
|
|
56
56
|
|
|
57
57
|
<p align="center">
|
|
58
|
-
<img src="docs/assets/images/banner.png" alt="openframe-core" width="100%" />
|
|
58
|
+
<img src="https://raw.githubusercontent.com/Furious-Meteors/openframe-core/production/docs/assets/images/banner.png" alt="openframe-core" width="100%" />
|
|
59
59
|
</p>
|
|
60
60
|
|
|
61
61
|
<p align="center">
|
|
62
62
|
<a href="https://pypi.org/project/openframe-core/"><img src="https://img.shields.io/pypi/v/openframe-core?color=6DB33F&labelColor=1a1a1a&label=PyPI" alt="PyPI version"/></a>
|
|
63
63
|
<a href="https://pypi.org/project/openframe-core/"><img src="https://img.shields.io/pypi/pyversions/openframe-core?color=6DB33F&labelColor=1a1a1a" alt="Python versions"/></a>
|
|
64
|
-
<a href="https://github.com/Furious-Meteors/openframe-core/actions/workflows/
|
|
64
|
+
<a href="https://github.com/Furious-Meteors/openframe-core/actions/workflows/python-build.yml"><img src="https://img.shields.io/github/actions/workflow/status/Furious-Meteors/openframe-core/python-build.yml?branch=production&color=6DB33F&labelColor=1a1a1a&label=tests" alt="Tests"/></a>
|
|
65
65
|
<a href="https://github.com/Furious-Meteors/openframe-core/blob/production/LICENSE"><img src="https://img.shields.io/badge/license-MIT-6DB33F?labelColor=1a1a1a" alt="License"/></a>
|
|
66
66
|
<a href="https://furious-meteors.github.io/openframe-core/"><img src="https://img.shields.io/badge/docs-live-6DB33F?labelColor=1a1a1a" alt="Docs"/></a>
|
|
67
67
|
</p>
|
|
@@ -77,7 +77,9 @@ Description-Content-Type: text/markdown
|
|
|
77
77
|
|
|
78
78
|
`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
79
|
|
|
80
|
-
Every package in the OpenFrame ecosystem pins `openframe-core>=
|
|
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. 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.
|
|
81
83
|
|
|
82
84
|
---
|
|
83
85
|
|
|
@@ -85,14 +87,18 @@ Every package in the OpenFrame ecosystem pins `openframe-core>=1.0,<2`. The majo
|
|
|
85
87
|
|
|
86
88
|
| Module | Exports |
|
|
87
89
|
|---|---|
|
|
88
|
-
| `openframe.core.
|
|
90
|
+
| `openframe.core.contracts` | `BasePort` · `Identity` · `Lifecycle` · `Capability` · `PluginStatus` · `PluginHealth` · `PluginContext` · `PrincipalContext` · `TenantContext` |
|
|
91
|
+
| `openframe.core.exceptions` | `OpenFrameError` (root) · `ErrorCode` · `Severity` · `AdapterError` (+5) · `PluginError` (+4, incl. `AmbiguousCapabilityError`) |
|
|
89
92
|
| `openframe.core.config` | `BaseAdapterSettings` — Pydantic `BaseSettings` subclass all adapters inherit |
|
|
90
|
-
| `openframe.core.ports` | `BaseRepository[T]` · `BaseProducer[T]` · `BaseConsumer[T]` — `
|
|
91
|
-
| `openframe.core.
|
|
92
|
-
| `openframe.core.
|
|
93
|
+
| `openframe.core.ports` | `BaseRepository[T]` · `BaseProducer[T]` · `BaseConsumer[T]` — `BasePort` + domain methods (outbound side) |
|
|
94
|
+
| `openframe.core.inbound` | `UseCase[TIn, TOut]` · `CommandHandler[TIn]` · `QueryHandler[TIn, TOut]` · `RequestContext` (driving side) |
|
|
95
|
+
| `openframe.core.plugins` | `PluginRegistry` — explicit registration, ordered init, LIFO shutdown, capability lookup |
|
|
96
|
+
| `openframe.core.runtime` | `ApplicationBootstrap` — optional composition-root base class |
|
|
97
|
+
| `openframe.core.telemetry` | `setup_telemetry()` · `get_tracer()` · `get_meter()` · `record_lifecycle_event()` · `record_error()` |
|
|
93
98
|
| `openframe.core.tracing` | `TracingProxy` — zero-code async telemetry sidecar |
|
|
94
99
|
| `openframe.core.middleware` | `TelemetryMiddleware` — pure ASGI middleware |
|
|
95
100
|
| `openframe.core.middleware.types` | `ASGIScope` · `ASGIMessage` · `Receive` · `Send` · `ASGIApp` |
|
|
101
|
+
| `openframe.core.testing` | `InMemoryRepository` · `FakeProducer` · `FakeConsumer` · `PortContractTests` · `LifecycleContractTests` · `RepositoryContractTests` · `ProducerContractTests` · `ConsumerContractTests` |
|
|
96
102
|
|
|
97
103
|
---
|
|
98
104
|
|
|
@@ -111,10 +117,14 @@ pip install "openframe-core[dev]"
|
|
|
111
117
|
|
|
112
118
|
## Quick start
|
|
113
119
|
|
|
114
|
-
### Exceptions —
|
|
120
|
+
### Exceptions — one catch point for the whole ecosystem
|
|
121
|
+
|
|
122
|
+
Every error derives from `OpenFrameError`, and semantics (`code`, `retryable`, `severity`) are carried as data:
|
|
115
123
|
|
|
116
124
|
```python
|
|
117
|
-
from openframe.core.exceptions import
|
|
125
|
+
from openframe.core.exceptions import (
|
|
126
|
+
OpenFrameError, AdapterNotFoundError, AdapterQueryError,
|
|
127
|
+
)
|
|
118
128
|
|
|
119
129
|
# In an adapter — wrap driver exceptions before they leave
|
|
120
130
|
try:
|
|
@@ -127,16 +137,20 @@ except SomeDriverError as exc:
|
|
|
127
137
|
cause=exc,
|
|
128
138
|
) from exc
|
|
129
139
|
|
|
130
|
-
# In a service — one catch point
|
|
140
|
+
# In a service — one catch point for anything the ecosystem raises
|
|
131
141
|
try:
|
|
132
142
|
entity = await repo.get(entity_id)
|
|
133
143
|
except AdapterNotFoundError:
|
|
134
144
|
return None
|
|
135
|
-
except
|
|
136
|
-
|
|
145
|
+
except OpenFrameError as exc:
|
|
146
|
+
if exc.retryable: # act on data, not an isinstance ladder
|
|
147
|
+
await backoff_and_retry()
|
|
148
|
+
logger.error("failure code=%s: %s", exc.code, exc) # [postgres.get] ... — caused by: ...
|
|
137
149
|
raise
|
|
138
150
|
```
|
|
139
151
|
|
|
152
|
+
Error codes follow a decentralised `domain.kind` convention (`adapter.connection`, `plugin.duplicate`); downstream packages declare their own codes and still catch cleanly under `OpenFrameError`.
|
|
153
|
+
|
|
140
154
|
### Config — env vars validated at startup, not first request
|
|
141
155
|
|
|
142
156
|
```python
|
|
@@ -150,12 +164,27 @@ class PostgresSettings(BaseAdapterSettings):
|
|
|
150
164
|
settings = PostgresSettings()
|
|
151
165
|
```
|
|
152
166
|
|
|
153
|
-
### Ports —
|
|
167
|
+
### Ports — one lifecycle-aware base for every adapter
|
|
168
|
+
|
|
169
|
+
Every port is a `BasePort`: `Identity` (`name`/`version`/`capability`) + `Lifecycle` (`initialize`/`shutdown`/`health`) plus its own domain methods. Adapters satisfy it structurally — no inheritance required.
|
|
154
170
|
|
|
155
171
|
```python
|
|
172
|
+
from openframe.core.contracts import Capability, PluginContext, PluginHealth, PluginStatus
|
|
156
173
|
from openframe.core.ports import BaseRepository
|
|
157
174
|
|
|
158
175
|
class PostgresItemRepository:
|
|
176
|
+
# Identity
|
|
177
|
+
name = "postgres-items"
|
|
178
|
+
version = "1.0.0"
|
|
179
|
+
capability = Capability.PERSISTENCE
|
|
180
|
+
|
|
181
|
+
# Lifecycle
|
|
182
|
+
async def initialize(self, context: PluginContext) -> None: ...
|
|
183
|
+
async def shutdown(self) -> None: ...
|
|
184
|
+
async def health(self) -> PluginHealth:
|
|
185
|
+
return PluginHealth(status=PluginStatus.READY)
|
|
186
|
+
|
|
187
|
+
# Domain methods
|
|
159
188
|
async def get(self, entity_id: str) -> Item | None: ...
|
|
160
189
|
async def list(self, limit: int, offset: int) -> tuple[list[Item], int]: ...
|
|
161
190
|
async def create(self, entity: Item) -> Item: ...
|
|
@@ -166,6 +195,58 @@ class PostgresItemRepository:
|
|
|
166
195
|
assert isinstance(PostgresItemRepository(), BaseRepository) # True
|
|
167
196
|
```
|
|
168
197
|
|
|
198
|
+
### Plugins — register any port, managed lifecycle
|
|
199
|
+
|
|
200
|
+
A "plugin" is just a registered `BasePort`. The registry initializes in order, shuts down LIFO, and looks up by the typed `Capability` enum:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from openframe.core.contracts import Capability
|
|
204
|
+
from openframe.core.plugins import PluginRegistry
|
|
205
|
+
|
|
206
|
+
registry = PluginRegistry()
|
|
207
|
+
registry.register(PostgresItemRepository(), config={"dsn": "postgres://..."})
|
|
208
|
+
registry.register(RedisCache(), config={"url": "redis://..."})
|
|
209
|
+
|
|
210
|
+
await registry.initialize_all() # forward order; rolls back on failure
|
|
211
|
+
repo = registry.get(Capability.PERSISTENCE) # strict — raises if >1 match
|
|
212
|
+
# ... serve traffic ...
|
|
213
|
+
await registry.shutdown_all() # reverse order; never raises
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`get()` is strict: it raises `AmbiguousCapabilityError` when more than one port shares a capability. Use `get_all(Capability.PERSISTENCE)` for the intended multi-port case (e.g. primary + replica).
|
|
217
|
+
|
|
218
|
+
### Runtime — optional composition root
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from openframe.core.contracts import Capability
|
|
222
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
223
|
+
|
|
224
|
+
class MyServiceBootstrap(ApplicationBootstrap):
|
|
225
|
+
def configure(self) -> None:
|
|
226
|
+
self.register(PostgresItemRepository(), config={"dsn": "..."})
|
|
227
|
+
|
|
228
|
+
async with MyServiceBootstrap() as bootstrap: # start() on enter, stop() on exit
|
|
229
|
+
repo = bootstrap.get(Capability.PERSISTENCE)
|
|
230
|
+
await serve(ItemService(repo))
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Inbound ports — the driving side of the hexagon
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from openframe.core.inbound import UseCase, RequestContext
|
|
237
|
+
|
|
238
|
+
class CreateItem:
|
|
239
|
+
async def execute(self, command: CreateItemCommand,
|
|
240
|
+
context: RequestContext | None = None) -> Item:
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
assert isinstance(CreateItem(), UseCase) # structural — no inheritance
|
|
244
|
+
|
|
245
|
+
# An inbound adapter (HTTP route, message handler, CLI) builds the context:
|
|
246
|
+
ctx = RequestContext(correlation_id="trace-abc")
|
|
247
|
+
item = await CreateItem().execute(command, ctx)
|
|
248
|
+
```
|
|
249
|
+
|
|
169
250
|
### Telemetry — OTel bootstrap in one call
|
|
170
251
|
|
|
171
252
|
```python
|
|
@@ -222,6 +303,37 @@ app = TelemetryMiddleware(my_asgi_app)
|
|
|
222
303
|
|
|
223
304
|
Instruments five HTTP metrics per request: `http.server.request.count`, `http.server.request.duration` (`s`), `http.server.active_requests`, `http.server.error.count`, `http.server.response.size`. Injects `x-session-id` on every response.
|
|
224
305
|
|
|
306
|
+
### Errors flow into telemetry automatically
|
|
307
|
+
|
|
308
|
+
`OpenFrameError`s are recorded into telemetry at every boundary seam — `TracingProxy` (outbound adapter calls), `TelemetryMiddleware` (inbound HTTP), and `PluginRegistry` lifecycle (startup/shutdown). Each records the exception on the active span, sets the `error.code` / `error.severity` / `error.retryable` span attributes, stamps the trace id back onto `err.correlation_id`, and increments the `openframe.error.count` metric exactly once per error. The errors layer never imports telemetry — telemetry reads the error's data — so the dependency DAG stays intact. You can also call it directly:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from openframe.core.telemetry import record_error
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
...
|
|
315
|
+
except OpenFrameError as exc:
|
|
316
|
+
record_error(exc) # span attributes + one metric increment (deduped)
|
|
317
|
+
raise
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Testing utilities — shared fakes and contract tests
|
|
321
|
+
|
|
322
|
+
`openframe-core` ships test doubles and reusable pytest base classes that every adapter package uses, so adapters get behavioral + lifecycle conformance for free:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from openframe.core.testing import InMemoryRepository, RepositoryContractTests
|
|
326
|
+
|
|
327
|
+
# In-memory BasePort implementation for fast unit tests / local dev
|
|
328
|
+
repo = InMemoryRepository[Item]()
|
|
329
|
+
|
|
330
|
+
# Inherit the full contract suite (CRUD + BasePort lifecycle) for your adapter
|
|
331
|
+
class TestPostgresRepository(RepositoryContractTests):
|
|
332
|
+
@pytest.fixture
|
|
333
|
+
def port(self):
|
|
334
|
+
return PostgresItemRepository()
|
|
335
|
+
```
|
|
336
|
+
|
|
225
337
|
---
|
|
226
338
|
|
|
227
339
|
## Environment variables
|
|
@@ -244,23 +356,29 @@ pip install -e ".[dev]"
|
|
|
244
356
|
pytest tests/ -v
|
|
245
357
|
```
|
|
246
358
|
|
|
247
|
-
|
|
359
|
+
284 tests. All run in under 1 second — no network calls, no external services.
|
|
248
360
|
|
|
249
361
|
```bash
|
|
250
362
|
# Smoke test
|
|
251
|
-
python -c "from openframe.core.
|
|
363
|
+
python -c "from openframe.core.contracts import BasePort; print('openframe-core OK')"
|
|
252
364
|
```
|
|
253
365
|
|
|
254
366
|
---
|
|
255
367
|
|
|
256
368
|
## Design decisions
|
|
257
369
|
|
|
370
|
+
**Why one `BasePort` instead of separate port / health / plugin protocols?**
|
|
371
|
+
Before v3 an adapter had to satisfy three unrelated protocols (a port, `HealthCheck`, and `OpenFramePlugin`) and often hand-write a wrapper class to be registrable. `BasePort` (`Identity` + `Lifecycle`) unifies them: every port is lifecycle-aware and registry-ready with zero wrapper code. See [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/).
|
|
372
|
+
|
|
373
|
+
**Why a single `OpenFrameError` root?**
|
|
374
|
+
So a service (or gateway, or middleware) can catch anything the ecosystem raises at one point, and act on `code` / `retryable` / `severity` as data rather than importing and branching on every concrete error class. Downstream packages add their own families under the same root without widening anyone's `except`.
|
|
375
|
+
|
|
376
|
+
**Why is `Capability` a closed enum?**
|
|
377
|
+
Registry lookups (`registry.get(Capability.PERSISTENCE)`) are type-checked, and the duplicate-capability guard and "which capabilities exist" analysis are only possible with a closed vocabulary — no adapters inventing ad hoc capability strings.
|
|
378
|
+
|
|
258
379
|
**Why `Adapter` prefix on exceptions?**
|
|
259
380
|
`ConnectionError` and `TimeoutError` are Python stdlib built-ins. Using the same names would shadow them silently. `AdapterConnectionError` and `AdapterTimeoutError` are unambiguous.
|
|
260
381
|
|
|
261
|
-
**Why `super().__init__(message)` only?**
|
|
262
|
-
`Exception.__str__` returns a tuple repr when multiple args are passed. Passing only the message keeps `str(exc)` readable: `[postgres.get] entity not found`.
|
|
263
|
-
|
|
264
382
|
**Why does `TelemetryMiddleware` not call `setup_telemetry()`?**
|
|
265
383
|
The OTel SDK's `set_tracer_provider()` is protected by a once-guard — subsequent calls are silently rejected. Calling `setup_telemetry()` inside the middleware overwrites the provider set at startup (including test fixture providers), causing spans to be dropped silently.
|
|
266
384
|
|
|
@@ -279,7 +397,7 @@ The closure resolves the wrapped method via `getattr(wrapped, name)` on every in
|
|
|
279
397
|
| [`openframe-ai`](https://github.com/Furious-Meteors/openframe-ai) | `pip install openframe-ai[serving]` | LangChain · LlamaIndex · CrewAI · model serving · training |
|
|
280
398
|
| [`openframe-suite`](https://github.com/Furious-Meteors/openframe-suite) | `pip install openframe-suite[all]` | Full platform — installs everything |
|
|
281
399
|
|
|
282
|
-
All packages pin `openframe-core>=
|
|
400
|
+
All packages pin `openframe-core>=3.0,<4`.
|
|
283
401
|
|
|
284
402
|
---
|
|
285
403
|
|
|
@@ -291,11 +409,11 @@ Full documentation — architecture, module reference, runbooks, developer guide
|
|
|
291
409
|
|
|
292
410
|
| Section | Contents |
|
|
293
411
|
|---|---|
|
|
294
|
-
| [System
|
|
295
|
-
| [
|
|
296
|
-
| [
|
|
412
|
+
| [System Architecture](https://furious-meteors.github.io/openframe-core/technical/architecture/system-architecture/) | Unified contract layer, module dependency DAG, the hexagon's two sides |
|
|
413
|
+
| [ADR-006](https://furious-meteors.github.io/openframe-core/technical/architecture/adrs/adr-006-unified-port-lifecycle/) | The unified port + lifecycle contract, and the unified error hierarchy |
|
|
414
|
+
| [Capability Taxonomy](https://furious-meteors.github.io/openframe-core/technical/architecture/capability-taxonomy/) | The `Capability` enum reference |
|
|
415
|
+
| [Error Taxonomy](https://furious-meteors.github.io/openframe-core/technical/architecture/error-taxonomy/) | The `OpenFrameError` hierarchy and `domain.kind` code convention |
|
|
297
416
|
| [Module Reference](https://furious-meteors.github.io/openframe-core/code/modules/) | Every public class, method, parameter, return type, and raises |
|
|
298
|
-
| [Runbooks](https://furious-meteors.github.io/openframe-core/technical/operations/) | Six operational failure scenarios with recovery steps |
|
|
299
417
|
| [Developer Guide](https://furious-meteors.github.io/openframe-core/developer-guide/quick-start/) | Quick start, how it works, first code change, debugging |
|
|
300
418
|
|
|
301
419
|
---
|