openframe-core 1.0.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1 @@
1
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,17 @@
1
+ """
2
+ openframe/core/config/
3
+ =======================
4
+ Base configuration primitives for all OpenFrame adapter packages.
5
+
6
+ Every adapter config class subclasses ``BaseAdapterSettings``, which reads
7
+ field values from environment variables via Pydantic Settings.
8
+
9
+ Usage::
10
+
11
+ from openframe.core.config import BaseAdapterSettings
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from openframe.core.config.settings import BaseAdapterSettings
16
+
17
+ __all__ = ["BaseAdapterSettings"]
@@ -0,0 +1,70 @@
1
+ """
2
+ openframe/core/config/settings.py
3
+ ====================================
4
+ Base settings class for all OpenFrame adapter packages.
5
+
6
+ Every adapter config (e.g. ``PostgresSettings``, ``RedisSettings``) must
7
+ subclass ``BaseAdapterSettings`` and declare its adapter-specific fields.
8
+ Pydantic Settings reads field values from environment variables automatically
9
+ — the field name maps directly to the env var name (case-insensitive).
10
+
11
+ Missing required fields raise ``pydantic_core.ValidationError`` at
12
+ instantiation time, not at first use. This means misconfigured deployments
13
+ fail fast on startup rather than at the first request.
14
+
15
+ Dependency order: this module imports only from pydantic-settings (external).
16
+ No openframe.core imports.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from pydantic_settings import BaseSettings, SettingsConfigDict
21
+
22
+ __all__ = ["BaseAdapterSettings"]
23
+
24
+
25
+ class BaseAdapterSettings(BaseSettings):
26
+ """
27
+ Base settings for all OpenFrame adapter packages.
28
+
29
+ Subclass in each adapter's ``config/settings.py`` and declare
30
+ adapter-specific fields. All fields are read from environment variables
31
+ automatically by Pydantic Settings.
32
+
33
+ Configuration:
34
+ - ``env_prefix = ""`` — field names map directly to env var names.
35
+ - ``case_sensitive = False`` — ``DATABASE_URL`` maps to ``database_url``.
36
+ - ``extra = "ignore"`` — unknown env vars are silently ignored.
37
+
38
+ Example::
39
+
40
+ class PostgresSettings(BaseAdapterSettings):
41
+ database_url: str # reads DATABASE_URL — required
42
+ pool_size: int = 10 # reads POOL_SIZE — optional, default 10
43
+ pool_max_overflow: int = 5 # reads POOL_MAX_OVERFLOW
44
+
45
+ # Missing DATABASE_URL raises ValidationError immediately:
46
+ settings = PostgresSettings()
47
+
48
+ Common fields available on every adapter settings instance:
49
+
50
+ Attributes:
51
+ adapter_name: Identifies the adapter in logs and traces.
52
+ Reads ``ADAPTER_NAME`` env var.
53
+ connection_timeout: Seconds to wait when establishing a connection.
54
+ Reads ``CONNECTION_TIMEOUT``. Default: 30.0.
55
+ operation_timeout: Seconds to wait for a single operation to complete.
56
+ Reads ``OPERATION_TIMEOUT``. Default: 10.0.
57
+ max_retries: Maximum number of retry attempts on transient failure.
58
+ Reads ``MAX_RETRIES``. Default: 3.
59
+ """
60
+
61
+ model_config = SettingsConfigDict(
62
+ env_prefix="",
63
+ case_sensitive=False,
64
+ extra="ignore",
65
+ )
66
+
67
+ adapter_name: str = "openframe"
68
+ connection_timeout: float = 30.0
69
+ operation_timeout: float = 10.0
70
+ max_retries: int = 3
@@ -0,0 +1,39 @@
1
+ """
2
+ openframe/core/exceptions/
3
+ ============================
4
+ Structured exception hierarchy for all OpenFrame adapter packages.
5
+
6
+ All adapter packages raise only these exceptions — never raw driver
7
+ exceptions. Services catch ``AdapterError`` as a single catch point
8
+ regardless of which adapter is wired in.
9
+
10
+ Usage::
11
+
12
+ from openframe.core.exceptions import (
13
+ AdapterError,
14
+ AdapterConnectionError,
15
+ AdapterQueryError,
16
+ AdapterNotFoundError,
17
+ AdapterConfigurationError,
18
+ AdapterTimeoutError,
19
+ )
20
+ """
21
+ from __future__ import annotations
22
+
23
+ from openframe.core.exceptions.errors import (
24
+ AdapterConfigurationError,
25
+ AdapterConnectionError,
26
+ AdapterError,
27
+ AdapterNotFoundError,
28
+ AdapterQueryError,
29
+ AdapterTimeoutError,
30
+ )
31
+
32
+ __all__ = [
33
+ "AdapterError",
34
+ "AdapterConnectionError",
35
+ "AdapterQueryError",
36
+ "AdapterNotFoundError",
37
+ "AdapterConfigurationError",
38
+ "AdapterTimeoutError",
39
+ ]
@@ -0,0 +1,189 @@
1
+ """
2
+ openframe/core/exceptions/errors.py
3
+ =====================================
4
+ Structured exception hierarchy for all OpenFrame adapter packages.
5
+
6
+ Every adapter package raises only AdapterError subclasses — never raw
7
+ driver exceptions (psycopg errors, aioredis errors, aiokafka errors, etc.).
8
+ Services catch AdapterError at a single point regardless of which adapter
9
+ is wired in.
10
+
11
+ Raise with cause chaining at the raise site::
12
+
13
+ try:
14
+ result = await conn.execute(query)
15
+ except SomeDriverError as exc:
16
+ raise AdapterQueryError(
17
+ message="Query failed",
18
+ adapter="postgres",
19
+ operation="get",
20
+ cause=exc,
21
+ ) from exc
22
+
23
+ Dependency order: this module has no imports from openframe.core.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ __all__ = [
28
+ "AdapterError",
29
+ "AdapterConnectionError",
30
+ "AdapterQueryError",
31
+ "AdapterNotFoundError",
32
+ "AdapterConfigurationError",
33
+ "AdapterTimeoutError",
34
+ ]
35
+
36
+
37
+ class AdapterError(Exception):
38
+ """
39
+ Base exception for all OpenFrame adapter packages.
40
+
41
+ All adapter packages raise only AdapterError subclasses — never raw
42
+ driver exceptions. Services catch AdapterError for a single catch
43
+ point regardless of which adapter is wired in.
44
+
45
+ Attributes:
46
+ message: Human-readable description of what went wrong.
47
+ adapter: Adapter identifier, e.g. "postgres", "redis", "kafka".
48
+ operation: Operation that failed, e.g. "get", "create", "publish".
49
+ cause: Underlying driver exception, if any. Always chain at
50
+ the raise site: ``raise AdapterXError(...) from exc``.
51
+
52
+ The ``__str__`` format is::
53
+
54
+ [adapter.operation] message
55
+ [adapter.operation] message — caused by: <cause>
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ message: str,
61
+ adapter: str,
62
+ operation: str,
63
+ cause: Exception | None = None,
64
+ ) -> None:
65
+ """
66
+ Initialise the exception.
67
+
68
+ Args:
69
+ message: Human-readable description of the failure.
70
+ adapter: Adapter identifier (e.g. "postgres", "redis").
71
+ operation: Operation that failed (e.g. "get", "create").
72
+ cause: Underlying driver exception, or None.
73
+ """
74
+ super().__init__(message) # only message → self.args = (message,)
75
+ self.message = message
76
+ self.adapter = adapter
77
+ self.operation = operation
78
+ self.cause = cause
79
+
80
+ def __str__(self) -> str:
81
+ """
82
+ Return a human-readable string including adapter, operation, and message.
83
+
84
+ If a cause is present it is appended after \" — caused by: \".
85
+ """
86
+ base = f"[{self.adapter}.{self.operation}] {self.message}"
87
+ if self.cause:
88
+ return f"{base} — caused by: {self.cause}"
89
+ return base
90
+
91
+ def __repr__(self) -> str:
92
+ return (
93
+ f"{type(self).__name__}("
94
+ f"message={self.message!r}, "
95
+ f"adapter={self.adapter!r}, "
96
+ f"operation={self.operation!r}, "
97
+ f"cause={self.cause!r})"
98
+ )
99
+
100
+
101
+ class AdapterConnectionError(AdapterError):
102
+ """
103
+ Raised when the adapter cannot establish a connection to its backend.
104
+
105
+ Typical causes: network unreachable, wrong host/port, TLS handshake
106
+ failure, authentication rejected before a connection is established.
107
+
108
+ Raise with cause chaining::
109
+
110
+ raise AdapterConnectionError(
111
+ "Cannot reach Postgres at localhost:5432",
112
+ adapter="postgres",
113
+ operation="connect",
114
+ cause=original_exc,
115
+ ) from original_exc
116
+ """
117
+
118
+
119
+ class AdapterQueryError(AdapterError):
120
+ """
121
+ Raised when an operation fails after a connection is established.
122
+
123
+ Typical causes: constraint violation, syntax error, permission denied
124
+ on a specific table or topic, partial failure mid-batch.
125
+
126
+ Raise with cause chaining::
127
+
128
+ raise AdapterQueryError(
129
+ "INSERT violated unique constraint on items.slug",
130
+ adapter="postgres",
131
+ operation="create",
132
+ cause=original_exc,
133
+ ) from original_exc
134
+ """
135
+
136
+
137
+ class AdapterNotFoundError(AdapterError):
138
+ """
139
+ Raised when the requested entity does not exist in the backend.
140
+
141
+ This is a semantic "not found" — the adapter successfully queried
142
+ the backend but the entity is absent. Distinct from AdapterQueryError
143
+ (which means the query itself failed).
144
+
145
+ Raise without a cause when the backend returns a definitive empty
146
+ result::
147
+
148
+ raise AdapterNotFoundError(
149
+ f"Item {item_id!r} not found",
150
+ adapter="postgres",
151
+ operation="get",
152
+ )
153
+ """
154
+
155
+
156
+ class AdapterConfigurationError(AdapterError):
157
+ """
158
+ Raised when required configuration is missing or invalid.
159
+
160
+ Typical causes: required env var absent, value fails validation
161
+ (wrong type, out of range), conflicting options.
162
+
163
+ Raise at adapter initialisation time (not at first query) so
164
+ misconfigured deployments fail fast on startup::
165
+
166
+ raise AdapterConfigurationError(
167
+ "DATABASE_URL must be set",
168
+ adapter="postgres",
169
+ operation="init",
170
+ )
171
+ """
172
+
173
+
174
+ class AdapterTimeoutError(AdapterError):
175
+ """
176
+ Raised when an operation exceeds its configured timeout.
177
+
178
+ Distinct from AdapterConnectionError — the connection was established
179
+ but the operation did not complete within the allowed window.
180
+
181
+ Raise with cause chaining when wrapping an asyncio.TimeoutError::
182
+
183
+ raise AdapterTimeoutError(
184
+ "Query exceeded 10s operation_timeout",
185
+ adapter="postgres",
186
+ operation="get",
187
+ cause=original_exc,
188
+ ) from original_exc
189
+ """
@@ -0,0 +1,19 @@
1
+ """
2
+ openframe/core/health/
3
+ =======================
4
+ Health check port for all OpenFrame adapter packages.
5
+
6
+ Every adapter implements ``HealthCheck`` so the application layer can
7
+ verify liveness and readiness without knowing which adapter is wired in.
8
+
9
+ Usage::
10
+
11
+ from openframe.core.health import HealthCheck
12
+
13
+ assert isinstance(my_adapter, HealthCheck)
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from openframe.core.health.protocol import HealthCheck
18
+
19
+ __all__ = ["HealthCheck"]
@@ -0,0 +1,94 @@
1
+ """
2
+ openframe/core/health/protocol.py
3
+ ====================================
4
+ Health check port for all OpenFrame adapter packages.
5
+
6
+ Every adapter implements ``HealthCheck`` so the application layer can verify
7
+ adapter liveness and readiness without knowing which adapter is wired in.
8
+
9
+ Two levels of check:
10
+ - ``ping()`` — cheap liveness. Used by load balancers and watchdogs. Should
11
+ complete in milliseconds (TCP connect, ``SELECT 1``).
12
+ - ``is_ready()`` — full readiness. Used at startup. More expensive — verifies
13
+ schema migrations applied, connection pool healthy, etc.
14
+
15
+ Runtime isinstance check::
16
+
17
+ isinstance(adapter, HealthCheck) # ✓ works
18
+ isinstance(obj, HealthCheck) # checks for ping and is_ready methods
19
+
20
+ Dependency order: this module imports only from Python stdlib typing.
21
+ No openframe.core imports.
22
+ """
23
+ from __future__ import annotations
24
+
25
+ from typing import Protocol, runtime_checkable
26
+
27
+ __all__ = ["HealthCheck"]
28
+
29
+
30
+ @runtime_checkable
31
+ class HealthCheck(Protocol):
32
+ """
33
+ Health check port. Every adapter implements ``ping()`` and ``is_ready()``.
34
+
35
+ Adapters satisfy this protocol structurally — no inheritance needed.
36
+ Any class with both async method signatures satisfies ``HealthCheck``.
37
+
38
+ ``ping()`` vs ``is_ready()``:
39
+
40
+ - ``ping()`` is a low-cost liveness check. Verify a TCP connection can be
41
+ opened, or run ``SELECT 1``. Should complete within milliseconds.
42
+ Called frequently by load balancers and watchdog processes.
43
+
44
+ - ``is_ready()`` is a full readiness check. Verify the connection pool is
45
+ healthy, required schemas exist, pending migrations have been applied,
46
+ etc. More expensive — called once at application startup and on
47
+ scheduled readiness probes.
48
+
49
+ Usage::
50
+
51
+ class PostgresRepository:
52
+ async def ping(self) -> bool:
53
+ try:
54
+ await self._pool.fetchval("SELECT 1")
55
+ return True
56
+ except Exception:
57
+ return False
58
+
59
+ async def is_ready(self) -> bool:
60
+ try:
61
+ await self._pool.fetchval("SELECT COUNT(*) FROM pg_tables")
62
+ return True
63
+ except Exception:
64
+ return False
65
+
66
+ assert isinstance(PostgresRepository(), HealthCheck)
67
+ """
68
+
69
+ async def ping(self) -> bool:
70
+ """
71
+ Low-cost liveness check.
72
+
73
+ Verify the adapter can reach its backend. Should be fast and cheap —
74
+ a TCP connect attempt or ``SELECT 1`` is appropriate.
75
+
76
+ Returns:
77
+ True if the backend is reachable, False otherwise.
78
+ Must not raise — return False on any failure.
79
+ """
80
+ ...
81
+
82
+ async def is_ready(self) -> bool:
83
+ """
84
+ Full readiness check.
85
+
86
+ Verify the adapter is fully ready to serve requests. More thorough
87
+ than ``ping()`` — check that the schema is correct, migrations are
88
+ applied, and the connection pool has healthy connections.
89
+
90
+ Returns:
91
+ True if the adapter is ready to serve, False otherwise.
92
+ Must not raise — return False on any failure.
93
+ """
94
+ ...
@@ -0,0 +1,55 @@
1
+ """
2
+ openframe/core/middleware/
3
+ ===========================
4
+ Pure ASGI telemetry middleware and shared ASGI type aliases for the
5
+ OpenFrame ecosystem.
6
+
7
+ The middleware is framework-agnostic — it works with FastAPI, Starlette,
8
+ Litestar, or any bare ASGI application.
9
+
10
+ Usage::
11
+
12
+ from openframe.core.middleware import TelemetryMiddleware
13
+
14
+ app = TelemetryMiddleware(app)
15
+
16
+ ASGI type aliases::
17
+
18
+ from openframe.core.middleware import ASGIApp, ASGIScope, ASGIMessage, Receive, Send
19
+
20
+ def my_middleware(app: ASGIApp) -> ASGIApp:
21
+ async def wrapped(scope: ASGIScope, receive: Receive, send: Send) -> None:
22
+ ...
23
+ return wrapped
24
+
25
+ Note on ``isinstance`` with type aliases:
26
+ ``ASGIScope = MutableMapping[str, Any]`` is a subscripted generic alias.
27
+ ``isinstance({}, ASGIScope)`` raises ``TypeError`` — use the origin type::
28
+
29
+ from collections.abc import MutableMapping
30
+ isinstance({}, MutableMapping) # ✓ works
31
+
32
+ To inspect the alias::
33
+
34
+ from typing import get_origin
35
+ get_origin(ASGIScope) is MutableMapping # ✓ True
36
+ """
37
+ from __future__ import annotations
38
+
39
+ from openframe.core.middleware.telemetry import TelemetryMiddleware
40
+ from openframe.core.middleware.types import (
41
+ ASGIApp,
42
+ ASGIMessage,
43
+ ASGIScope,
44
+ Receive,
45
+ Send,
46
+ )
47
+
48
+ __all__ = [
49
+ "TelemetryMiddleware",
50
+ "ASGIApp",
51
+ "ASGIScope",
52
+ "ASGIMessage",
53
+ "Receive",
54
+ "Send",
55
+ ]