Documentation · Quick Start · PyPI · Changelog
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.
Every package in the OpenFrame ecosystem pins openframe-core>=3.0,<4. The major version is the stability contract for the entire ecosystem.
v3.0.0 is a breaking redesign. The old hardcoded, lifecycle-free ports, the standalone
HealthCheckprotocol, and the separateOpenFramePluginprotocol are gone. They are replaced by a single unified contract layer built onBasePort(Identity+Lifecycle), and the error hierarchy is consolidated under a singleOpenFrameErrorroot. See ADR-006 and the Changelog for the full migration guide.
What's inside
| Module | Exports |
|---|---|
openframe.core.contracts |
BasePort · Identity · Lifecycle · Capability · PluginStatus · PluginHealth · PluginContext · PrincipalContext · TenantContext |
openframe.core.exceptions |
OpenFrameError (root) · ErrorCode · Severity · AdapterError (+5) · PluginError (+4, incl. AmbiguousCapabilityError) |
openframe.core.config |
BaseAdapterSettings — Pydantic BaseSettings subclass all adapters inherit |
openframe.core.ports |
BaseRepository[T] · BaseProducer[T] · BaseConsumer[T] — BasePort + domain methods (outbound side) |
openframe.core.inbound |
UseCase[TIn, TOut] · CommandHandler[TIn] · QueryHandler[TIn, TOut] · RequestContext (driving side) |
openframe.core.plugins |
PluginRegistry — explicit registration, ordered init, LIFO shutdown, capability lookup |
openframe.core.runtime |
ApplicationBootstrap — optional composition-root base class |
openframe.core.telemetry |
setup_telemetry() · get_tracer() · get_meter() · record_lifecycle_event() · record_error() |
openframe.core.tracing |
TracingProxy — zero-code async telemetry sidecar |
openframe.core.middleware |
TelemetryMiddleware — pure ASGI middleware |
openframe.core.middleware.types |
ASGIScope · ASGIMessage · Receive · Send · ASGIApp |
openframe.core.testing |
InMemoryRepository · FakeProducer · FakeConsumer · PortContractTests · LifecycleContractTests · RepositoryContractTests · ProducerContractTests · ConsumerContractTests |
Installation
pip install openframe-core
# With dev dependencies (pytest, pytest-asyncio, httpx)
pip install "openframe-core[dev]"
Quick start
Exceptions — one catch point for the whole ecosystem
Every error derives from OpenFrameError, and semantics (code, retryable, severity) are carried as data:
from openframe.core.exceptions import (
OpenFrameError, AdapterNotFoundError, AdapterQueryError,
)
# In an adapter — wrap driver exceptions before they leave
try:
row = await conn.fetchrow(query, entity_id)
except SomeDriverError as exc:
raise AdapterQueryError(
"Query failed",
adapter="postgres",
operation="get",
cause=exc,
) from exc
# In a service — one catch point for anything the ecosystem raises
try:
entity = await repo.get(entity_id)
except AdapterNotFoundError:
return None
except OpenFrameError as exc:
if exc.retryable: # act on data, not an isinstance ladder
await backoff_and_retry()
logger.error("failure code=%s: %s", exc.code, exc) # [postgres.get] ... — caused by: ...
raise
Error codes follow a decentralised domain.kind convention (adapter.connection, plugin.duplicate); downstream packages declare their own codes and still catch cleanly under OpenFrameError.
Config — env vars validated at startup, not first request
from openframe.core.config import BaseAdapterSettings
class PostgresSettings(BaseAdapterSettings):
database_url: str # reads DATABASE_URL — required
pool_size: int = 10 # reads POOL_SIZE — optional, default 10
# Raises pydantic_core.ValidationError immediately if DATABASE_URL is not set
settings = PostgresSettings()
Ports — one lifecycle-aware base for every adapter
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.
from openframe.core.contracts import Capability, PluginContext, PluginHealth, PluginStatus
from openframe.core.ports import BaseRepository
class PostgresItemRepository:
# Identity
name = "postgres-items"
version = "1.0.0"
capability = Capability.PERSISTENCE
# Lifecycle
async def initialize(self, context: PluginContext) -> None: ...
async def shutdown(self) -> None: ...
async def health(self) -> PluginHealth:
return PluginHealth(status=PluginStatus.READY)
# Domain methods
async def get(self, entity_id: str) -> Item | None: ...
async def list(self, limit: int, offset: int) -> tuple[list[Item], int]: ...
async def create(self, entity: Item) -> Item: ...
async def update(self, entity: Item) -> Item | None: ...
async def delete(self, entity_id: str) -> bool: ...
# Structural — no BaseRepository inheritance needed
assert isinstance(PostgresItemRepository(), BaseRepository) # True
Plugins — register any port, managed lifecycle
A "plugin" is just a registered BasePort. The registry initializes in order, shuts down LIFO, and looks up by the typed Capability enum:
from openframe.core.contracts import Capability
from openframe.core.plugins import PluginRegistry
registry = PluginRegistry()
registry.register(PostgresItemRepository(), config={"dsn": "postgres://..."})
registry.register(RedisCache(), config={"url": "redis://..."})
await registry.initialize_all() # forward order; rolls back on failure
repo = registry.get(Capability.PERSISTENCE) # strict — raises if >1 match
# ... serve traffic ...
await registry.shutdown_all() # reverse order; never raises
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).
Runtime — optional composition root
from openframe.core.contracts import Capability
from openframe.core.runtime import ApplicationBootstrap
class MyServiceBootstrap(ApplicationBootstrap):
def configure(self) -> None:
self.register(PostgresItemRepository(), config={"dsn": "..."})
async with MyServiceBootstrap() as bootstrap: # start() on enter, stop() on exit
repo = bootstrap.get(Capability.PERSISTENCE)
await serve(ItemService(repo))
Inbound ports — the driving side of the hexagon
from openframe.core.inbound import UseCase, RequestContext
class CreateItem:
async def execute(self, command: CreateItemCommand,
context: RequestContext | None = None) -> Item:
...
assert isinstance(CreateItem(), UseCase) # structural — no inheritance
# An inbound adapter (HTTP route, message handler, CLI) builds the context:
ctx = RequestContext(correlation_id="trace-abc")
item = await CreateItem().execute(command, ctx)
Telemetry — OTel bootstrap in one call
from contextlib import asynccontextmanager
from openframe.core.telemetry import setup_telemetry, record_lifecycle_event
@asynccontextmanager
async def lifespan(app):
setup_telemetry() # idempotent — safe to call multiple times
record_lifecycle_event("cold_start") # platform-agnostic lifecycle counter
yield
Configure via environment variables — no vendor lock-in:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp.example.com"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64_token>"
export OTEL_SERVICE_NAME="my-service"
export OPENFRAME_ENV="prod"
Modal users: map
MODAL_ENV→OPENFRAME_ENVin yourconfigure_env_vars()helper.
TracingProxy — spans without touching service code
from openframe.core.tracing import TracingProxy
raw_repo = PostgresItemRepository(settings)
traced_repo = TracingProxy(raw_repo, prefix="repository.item")
# traced_repo.get(...) → span "repository.item.get"
# traced_repo.create(...) → span "repository.item.create"
# The service layer never knows telemetry exists.
Safe for reconnecting adapters — the wrapped method is resolved fresh on every call, never snapshotted.
Middleware — pure ASGI, framework-agnostic
from openframe.core.middleware import TelemetryMiddleware
# FastAPI
app = FastAPI(lifespan=lifespan)
app.add_middleware(TelemetryMiddleware)
# Bare ASGI
app = TelemetryMiddleware(my_asgi_app)
Important: do not call
setup_telemetry()inside the middleware. Call it once in yourlifespanhandler. The middleware usesget_tracer()andget_meter()directly against whatever providers are currently set.
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.
Errors flow into telemetry automatically
OpenFrameErrors 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:
from openframe.core.telemetry import record_error
try:
...
except OpenFrameError as exc:
record_error(exc) # span attributes + one metric increment (deduped)
raise
Testing utilities — shared fakes and contract tests
openframe-core ships test doubles and reusable pytest base classes that every adapter package uses, so adapters get behavioral + lifecycle conformance for free:
from openframe.core.testing import InMemoryRepository, RepositoryContractTests
# In-memory BasePort implementation for fast unit tests / local dev
repo = InMemoryRepository[Item]()
# Inherit the full contract suite (CRUD + BasePort lifecycle) for your adapter
class TestPostgresRepository(RepositoryContractTests):
@pytest.fixture
def port(self):
return PostgresItemRepository()
Environment variables
| Variable | Default | Description |
|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
— | OTLP base URL. When absent, no-op providers are used — no errors, no export. |
OTEL_EXPORTER_OTLP_HEADERS |
— | Auth headers. Format: Authorization=Basic <token> |
OTEL_SERVICE_NAME |
openframe |
Service name tag on all telemetry |
OTEL_SERVICE_VERSION |
1.0.0 |
Service version tag |
OTEL_METRIC_EXPORT_INTERVAL_MS |
15000 |
Metric export interval in milliseconds |
OPENFRAME_ENV |
dev |
Deployment environment: dev · feat · prod |
Running tests
pip install -e ".[dev]"
pytest tests/ -v
284 tests. All run in under 1 second — no network calls, no external services.
# Smoke test
python -c "from openframe.core.contracts import BasePort; print('openframe-core OK')"
Design decisions
Why one BasePort instead of separate port / health / plugin protocols?
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.
Why a single OpenFrameError root?
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.
Why is Capability a closed enum?
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.
Why Adapter prefix on exceptions?
ConnectionError and TimeoutError are Python stdlib built-ins. Using the same names would shadow them silently. AdapterConnectionError and AdapterTimeoutError are unambiguous.
Why does TelemetryMiddleware not call setup_telemetry()?
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.
Why is TracingProxy safe for reconnecting adapters?
The closure resolves the wrapped method via getattr(wrapped, name) on every invocation — never from a snapshot captured at first access. When a Postgres pool or Redis client replaces its own methods after reconnect, the proxy calls the new methods automatically.
Ecosystem
| Package | Install | Description |
|---|---|---|
openframe-adapters |
pip install openframe-adapters[postgres] |
DB + queue adapters — Postgres, Redis, Mongo, Kafka, NATS, and more |
openframe-protocol |
pip install openframe-protocol[sse] |
WebSocket · SSE · gRPC · MCP · webhooks |
openframe-infra |
pip install openframe-infra[storage] |
Storage · auth · secrets · observability · feature flags |
openframe-ai |
pip install openframe-ai[serving] |
LangChain · LlamaIndex · CrewAI · model serving · training |
openframe-suite |
pip install openframe-suite[all] |
Full platform — installs everything |
All packages pin openframe-core>=3.0,<4.
Documentation
Full documentation — architecture, module reference, runbooks, developer guide — at:
furious-meteors.github.io/openframe-core
| Section | Contents |
|---|---|
| System Architecture | Unified contract layer, module dependency DAG, the hexagon's two sides |
| ADR-006 | The unified port + lifecycle contract, and the unified error hierarchy |
| Capability Taxonomy | The Capability enum reference |
| Error Taxonomy | The OpenFrameError hierarchy and domain.kind code convention |
| Module Reference | Every public class, method, parameter, return type, and raises |
| Developer Guide | Quick start, how it works, first code change, debugging |
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openframe_core-3.0.1.tar.gz.
File metadata
- Download URL: openframe_core-3.0.1.tar.gz
- Upload date:
- Size: 800.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bb788fdc5e343e4c84af6dbbea226fa3160fc955ee70f1c505e3493c9f0e4d8
|
|
| MD5 |
0c99c8c6099dd013209250997f2fad4e
|
|
| BLAKE2b-256 |
84302d8a0d07f966692b2ec929ea69ec325432266333a041eb8c4977a8f3e492
|
File details
Details for the file openframe_core-3.0.1-py3-none-any.whl.
File metadata
- Download URL: openframe_core-3.0.1-py3-none-any.whl
- Upload date:
- Size: 82.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a272e55d710786b249739c10a8df8d77c9605ad587e40ee49555adf4f165b28c
|
|
| MD5 |
d20f2e5ed0e26e741354a0926d0e6dbf
|
|
| BLAKE2b-256 |
dea5509fbbdd16ce282f7e23391c68da32ea0c6ade4b201e0ca45e64c289e482
|