openframe-adapters 2.0.2__tar.gz → 2.0.3__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_adapters-2.0.2 → openframe_adapters-2.0.3}/PKG-INFO +85 -60
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/README.md +84 -59
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/PKG-INFO +85 -60
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/pyproject.toml +1 -1
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/SOURCES.txt +0 -0
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/dependency_links.txt +0 -0
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/requires.txt +0 -0
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/top_level.txt +0 -0
- {openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openframe-adapters
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: OpenFrame Microservice Suite - adapter meta-package. Install adapters by name or group.
|
|
5
5
|
Author-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
6
6
|
Maintainer-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
@@ -184,112 +184,134 @@ Every adapter wires into your service through a single file — `deps.py` or
|
|
|
184
184
|
that knows which adapter is active. Routes and services never import adapters
|
|
185
185
|
directly.
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Wiring goes through `openframe-core`'s `ApplicationBootstrap`
|
|
188
|
+
(requires `openframe-core>=3.3`) — one recommended class, at two levels of
|
|
189
|
+
ceremony:
|
|
188
190
|
|
|
189
|
-
> **One adapter** —
|
|
190
|
-
> **
|
|
191
|
-
>
|
|
191
|
+
> **One or a few adapters, no per-adapter config needed** — `ApplicationBootstrap.compose(*plugins)`. No subclass.
|
|
192
|
+
> **An adapter needs its own `config=`/`init_timeout=`, or registration order depends on a runtime condition** — subclass with `configure()`.
|
|
193
|
+
> Either way, `ApplicationBootstrap` manages startup ordering, health aggregation, and graceful shutdown (including telemetry flush) across all adapters — you never hand-roll that part.
|
|
192
194
|
|
|
193
|
-
###
|
|
194
|
-
|
|
195
|
-
Install one adapter and wire it directly. Four lines. No registry needed.
|
|
195
|
+
### One adapter — `compose()`, no subclass
|
|
196
196
|
|
|
197
197
|
```python
|
|
198
198
|
# bootstrap/dependencies.py
|
|
199
|
-
from
|
|
200
|
-
from openframe.
|
|
199
|
+
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
200
|
+
from openframe.core.ports import Capability
|
|
201
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
201
202
|
from openframe.core.tracing import TracingProxy
|
|
202
203
|
from src.adapters.item_repository import ItemPostgresRepository
|
|
203
204
|
from src.application.services.item_service import ItemService
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
app = ApplicationBootstrap.compose(
|
|
207
|
+
PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
208
|
+
)
|
|
208
209
|
|
|
209
210
|
def get_item_service() -> ItemService:
|
|
210
|
-
|
|
211
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
212
|
+
return ItemService(repo)
|
|
211
213
|
```
|
|
212
214
|
|
|
213
215
|
`PostgresSettings()` reads `DATABASE_URL` from env at startup.
|
|
214
|
-
`
|
|
215
|
-
`TracingProxy` wraps it for automatic OTel spans on every call.
|
|
216
|
+
`TracingProxy` wraps the repository for automatic OTel spans on every call.
|
|
216
217
|
|
|
217
|
-
###
|
|
218
|
+
### Two or more adapters — still `compose()`, or subclass if you need per-adapter config
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
The registry handles startup ordering, health aggregation, and graceful
|
|
221
|
-
shutdown across all adapters.
|
|
220
|
+
`compose()` accepts any number of ports — pass them all in one call:
|
|
222
221
|
|
|
223
222
|
```python
|
|
224
223
|
# bootstrap/dependencies.py
|
|
225
224
|
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
226
225
|
from openframe.adapters.db.redis import RedisPlugin, RedisSettings
|
|
227
|
-
from openframe.core.
|
|
226
|
+
from openframe.core.ports import Capability
|
|
227
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
228
228
|
from openframe.core.tracing import TracingProxy
|
|
229
229
|
from src.application.services.item_service import ItemService
|
|
230
230
|
from src.application.services.session_service import SessionService
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
_registry = PluginRegistry()
|
|
237
|
-
_registry.register(PostgresPlugin(PostgresSettings())) # capability: Capability.PERSISTENCE
|
|
238
|
-
_registry.register(RedisPlugin(RedisSettings())) # capability: Capability.CACHE
|
|
239
|
-
await _registry.initialize_all() # fails fast if any backend unreachable
|
|
240
|
-
|
|
241
|
-
async def shutdown() -> None:
|
|
242
|
-
if _registry:
|
|
243
|
-
await _registry.shutdown_all() # never raises
|
|
232
|
+
app = ApplicationBootstrap.compose(
|
|
233
|
+
PostgresPlugin(PostgresSettings()), # capability: Capability.PERSISTENCE
|
|
234
|
+
RedisPlugin(RedisSettings()), # capability: Capability.CACHE
|
|
235
|
+
)
|
|
244
236
|
|
|
245
237
|
def get_item_service() -> ItemService:
|
|
246
|
-
repo = TracingProxy(
|
|
247
|
-
_registry.get("persistence").get_repository(),
|
|
248
|
-
prefix="repository.item",
|
|
249
|
-
)
|
|
238
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
250
239
|
return ItemService(repo)
|
|
251
240
|
|
|
252
241
|
def get_session_service() -> SessionService:
|
|
253
|
-
cache = TracingProxy(
|
|
254
|
-
_registry.get("cache").get_repository(),
|
|
255
|
-
prefix="cache.session",
|
|
256
|
-
)
|
|
242
|
+
cache = TracingProxy(app.get(Capability.CACHE).get_repository(), prefix="cache.session")
|
|
257
243
|
return SessionService(cache)
|
|
258
244
|
```
|
|
259
245
|
|
|
260
|
-
|
|
246
|
+
Reach for a subclass instead once an adapter needs its own `config=` mapping,
|
|
247
|
+
a per-adapter `init_timeout=`, or registration order that depends on a
|
|
248
|
+
runtime condition:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class AppBootstrap(ApplicationBootstrap):
|
|
252
|
+
def configure(self) -> None:
|
|
253
|
+
self.register(PostgresPlugin(PostgresSettings()), config={"pool_hint": "primary"})
|
|
254
|
+
self.register(RedisPlugin(RedisSettings()), init_timeout=5.0)
|
|
255
|
+
|
|
256
|
+
app = AppBootstrap()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
For the deliberate multi-port-per-capability case (e.g. primary + replica
|
|
260
|
+
Postgres), use `app.get_all(Capability.PERSISTENCE)` or, for anything neither
|
|
261
|
+
covers, the underlying registry directly via `app.registry`.
|
|
262
|
+
|
|
263
|
+
Wire `start()` and `stop()` in your FastAPI lifespan:
|
|
261
264
|
|
|
262
265
|
```python
|
|
263
266
|
from contextlib import asynccontextmanager
|
|
264
267
|
from fastapi import FastAPI
|
|
265
268
|
from openframe.core.middleware import TelemetryMiddleware
|
|
266
269
|
from openframe.core.telemetry import setup_telemetry
|
|
267
|
-
from src.bootstrap import
|
|
270
|
+
from src.bootstrap.dependencies import app as bootstrap
|
|
268
271
|
|
|
269
272
|
@asynccontextmanager
|
|
270
273
|
async def lifespan(app: FastAPI):
|
|
271
274
|
setup_telemetry()
|
|
272
|
-
await
|
|
275
|
+
await bootstrap.start() # initializes every registered adapter — fails fast if any backend unreachable
|
|
273
276
|
yield
|
|
274
|
-
await
|
|
277
|
+
await bootstrap.stop() # shuts down every adapter, then flushes telemetry — never raises
|
|
275
278
|
|
|
276
279
|
app = FastAPI(lifespan=lifespan)
|
|
277
280
|
app.add_middleware(TelemetryMiddleware)
|
|
278
281
|
```
|
|
279
282
|
|
|
283
|
+
### Domain subclass registration
|
|
284
|
+
|
|
285
|
+
Every `*Plugin` supports registering a domain-specific subclass instead of
|
|
286
|
+
the plain base adapter class — `repository_class` (Postgres, Mongo, Redis),
|
|
287
|
+
`producer_class` and `consumer_class` (Kafka):
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
app = ApplicationBootstrap.compose(
|
|
291
|
+
PostgresPlugin(PostgresSettings(), table="items", repository_class=ItemRepository),
|
|
292
|
+
RedisPlugin(RedisSettings(), repository_class=SessionRepository),
|
|
293
|
+
KafkaPlugin(KafkaSettings(), producer_class=ArtifactEventProducer, consumer_class=OrderEventConsumer),
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`get_repository()` / `get_producer()` / `make_consumer()` then return an
|
|
298
|
+
instance of the subclass you passed in, not the plain base class — so
|
|
299
|
+
overridden entity mapping (`_doc_to_entity`, `_entity_to_doc`, `_serialise`,
|
|
300
|
+
`_deserialise`, etc.) is preserved end-to-end through the registry.
|
|
301
|
+
|
|
280
302
|
### Plugin capabilities
|
|
281
303
|
|
|
282
304
|
Every `*Plugin` class declares a `capability` attribute — a typed
|
|
283
305
|
`openframe.core.ports.Capability` enum member (not a raw string as of
|
|
284
306
|
`openframe-core` v3.0; the module was `openframe.core.contracts` before
|
|
285
307
|
v3.1, which merged it into `openframe.core.ports`). This is the key used
|
|
286
|
-
by `
|
|
308
|
+
by `ApplicationBootstrap.get()` / `PluginRegistry.get()`:
|
|
287
309
|
|
|
288
310
|
```python
|
|
289
311
|
from openframe.core.ports import Capability
|
|
290
312
|
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
app.get(Capability.PERSISTENCE) # → PostgresPlugin / MongoPlugin
|
|
314
|
+
app.get(Capability.CACHE) # → RedisPlugin
|
|
293
315
|
```
|
|
294
316
|
|
|
295
317
|
The capability taxonomy is a closed enum shared across the entire OpenFrame
|
|
@@ -314,29 +336,32 @@ keep working, but new code should key on the enum member directly.
|
|
|
314
336
|
### The env-var swap exception
|
|
315
337
|
|
|
316
338
|
Switching between adapters via an environment variable (`PERSISTENCE_BACKEND=postgres`
|
|
317
|
-
vs `PERSISTENCE_BACKEND=mongo`)
|
|
318
|
-
|
|
339
|
+
vs `PERSISTENCE_BACKEND=mongo`) still only ever has **one** adapter active at
|
|
340
|
+
a time — `compose()` still applies, just with the chosen plugin decided
|
|
341
|
+
before the call:
|
|
319
342
|
|
|
320
343
|
```python
|
|
321
|
-
|
|
322
|
-
def _get_repository():
|
|
344
|
+
def _make_plugin():
|
|
323
345
|
backend = os.environ.get("PERSISTENCE_BACKEND", "postgres")
|
|
324
346
|
if backend == "postgres":
|
|
325
|
-
return
|
|
347
|
+
return PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
326
348
|
elif backend == "mongo":
|
|
327
|
-
return
|
|
349
|
+
return MongoPlugin(MongoSettings(), repository_class=ItemMongoRepository)
|
|
328
350
|
raise ValueError(f"Unknown PERSISTENCE_BACKEND: {backend!r}")
|
|
351
|
+
|
|
352
|
+
app = ApplicationBootstrap.compose(_make_plugin())
|
|
329
353
|
```
|
|
330
354
|
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
Both backends still register under the same `Capability.PERSISTENCE` — the
|
|
356
|
+
rest of your service (`app.get(Capability.PERSISTENCE)`) never knows which
|
|
357
|
+
one is active.
|
|
333
358
|
|
|
334
359
|
### Full wiring reference
|
|
335
360
|
|
|
336
|
-
For the complete guide —
|
|
337
|
-
wiring, and architecture
|
|
338
|
-
[
|
|
339
|
-
|
|
361
|
+
For the complete guide — the three-tier model (`compose()` → `configure()`
|
|
362
|
+
subclass → `.registry` escape hatch), lifespan wiring, and architecture
|
|
363
|
+
diagrams — see [How It Works § Choosing a Wiring Pattern](https://furious-meteors.github.io/openframe-core/developer-guide/how-it-works/#choosing-a-wiring-pattern)
|
|
364
|
+
in the `openframe-core` documentation.
|
|
340
365
|
|
|
341
366
|
---
|
|
342
367
|
|
|
@@ -93,112 +93,134 @@ Every adapter wires into your service through a single file — `deps.py` or
|
|
|
93
93
|
that knows which adapter is active. Routes and services never import adapters
|
|
94
94
|
directly.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Wiring goes through `openframe-core`'s `ApplicationBootstrap`
|
|
97
|
+
(requires `openframe-core>=3.3`) — one recommended class, at two levels of
|
|
98
|
+
ceremony:
|
|
97
99
|
|
|
98
|
-
> **One adapter** —
|
|
99
|
-
> **
|
|
100
|
-
>
|
|
100
|
+
> **One or a few adapters, no per-adapter config needed** — `ApplicationBootstrap.compose(*plugins)`. No subclass.
|
|
101
|
+
> **An adapter needs its own `config=`/`init_timeout=`, or registration order depends on a runtime condition** — subclass with `configure()`.
|
|
102
|
+
> Either way, `ApplicationBootstrap` manages startup ordering, health aggregation, and graceful shutdown (including telemetry flush) across all adapters — you never hand-roll that part.
|
|
101
103
|
|
|
102
|
-
###
|
|
103
|
-
|
|
104
|
-
Install one adapter and wire it directly. Four lines. No registry needed.
|
|
104
|
+
### One adapter — `compose()`, no subclass
|
|
105
105
|
|
|
106
106
|
```python
|
|
107
107
|
# bootstrap/dependencies.py
|
|
108
|
-
from
|
|
109
|
-
from openframe.
|
|
108
|
+
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
109
|
+
from openframe.core.ports import Capability
|
|
110
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
110
111
|
from openframe.core.tracing import TracingProxy
|
|
111
112
|
from src.adapters.item_repository import ItemPostgresRepository
|
|
112
113
|
from src.application.services.item_service import ItemService
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
app = ApplicationBootstrap.compose(
|
|
116
|
+
PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
117
|
+
)
|
|
117
118
|
|
|
118
119
|
def get_item_service() -> ItemService:
|
|
119
|
-
|
|
120
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
121
|
+
return ItemService(repo)
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
`PostgresSettings()` reads `DATABASE_URL` from env at startup.
|
|
123
|
-
`
|
|
124
|
-
`TracingProxy` wraps it for automatic OTel spans on every call.
|
|
125
|
+
`TracingProxy` wraps the repository for automatic OTel spans on every call.
|
|
125
126
|
|
|
126
|
-
###
|
|
127
|
+
### Two or more adapters — still `compose()`, or subclass if you need per-adapter config
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
The registry handles startup ordering, health aggregation, and graceful
|
|
130
|
-
shutdown across all adapters.
|
|
129
|
+
`compose()` accepts any number of ports — pass them all in one call:
|
|
131
130
|
|
|
132
131
|
```python
|
|
133
132
|
# bootstrap/dependencies.py
|
|
134
133
|
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
135
134
|
from openframe.adapters.db.redis import RedisPlugin, RedisSettings
|
|
136
|
-
from openframe.core.
|
|
135
|
+
from openframe.core.ports import Capability
|
|
136
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
137
137
|
from openframe.core.tracing import TracingProxy
|
|
138
138
|
from src.application.services.item_service import ItemService
|
|
139
139
|
from src.application.services.session_service import SessionService
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
_registry = PluginRegistry()
|
|
146
|
-
_registry.register(PostgresPlugin(PostgresSettings())) # capability: Capability.PERSISTENCE
|
|
147
|
-
_registry.register(RedisPlugin(RedisSettings())) # capability: Capability.CACHE
|
|
148
|
-
await _registry.initialize_all() # fails fast if any backend unreachable
|
|
149
|
-
|
|
150
|
-
async def shutdown() -> None:
|
|
151
|
-
if _registry:
|
|
152
|
-
await _registry.shutdown_all() # never raises
|
|
141
|
+
app = ApplicationBootstrap.compose(
|
|
142
|
+
PostgresPlugin(PostgresSettings()), # capability: Capability.PERSISTENCE
|
|
143
|
+
RedisPlugin(RedisSettings()), # capability: Capability.CACHE
|
|
144
|
+
)
|
|
153
145
|
|
|
154
146
|
def get_item_service() -> ItemService:
|
|
155
|
-
repo = TracingProxy(
|
|
156
|
-
_registry.get("persistence").get_repository(),
|
|
157
|
-
prefix="repository.item",
|
|
158
|
-
)
|
|
147
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
159
148
|
return ItemService(repo)
|
|
160
149
|
|
|
161
150
|
def get_session_service() -> SessionService:
|
|
162
|
-
cache = TracingProxy(
|
|
163
|
-
_registry.get("cache").get_repository(),
|
|
164
|
-
prefix="cache.session",
|
|
165
|
-
)
|
|
151
|
+
cache = TracingProxy(app.get(Capability.CACHE).get_repository(), prefix="cache.session")
|
|
166
152
|
return SessionService(cache)
|
|
167
153
|
```
|
|
168
154
|
|
|
169
|
-
|
|
155
|
+
Reach for a subclass instead once an adapter needs its own `config=` mapping,
|
|
156
|
+
a per-adapter `init_timeout=`, or registration order that depends on a
|
|
157
|
+
runtime condition:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
class AppBootstrap(ApplicationBootstrap):
|
|
161
|
+
def configure(self) -> None:
|
|
162
|
+
self.register(PostgresPlugin(PostgresSettings()), config={"pool_hint": "primary"})
|
|
163
|
+
self.register(RedisPlugin(RedisSettings()), init_timeout=5.0)
|
|
164
|
+
|
|
165
|
+
app = AppBootstrap()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For the deliberate multi-port-per-capability case (e.g. primary + replica
|
|
169
|
+
Postgres), use `app.get_all(Capability.PERSISTENCE)` or, for anything neither
|
|
170
|
+
covers, the underlying registry directly via `app.registry`.
|
|
171
|
+
|
|
172
|
+
Wire `start()` and `stop()` in your FastAPI lifespan:
|
|
170
173
|
|
|
171
174
|
```python
|
|
172
175
|
from contextlib import asynccontextmanager
|
|
173
176
|
from fastapi import FastAPI
|
|
174
177
|
from openframe.core.middleware import TelemetryMiddleware
|
|
175
178
|
from openframe.core.telemetry import setup_telemetry
|
|
176
|
-
from src.bootstrap import
|
|
179
|
+
from src.bootstrap.dependencies import app as bootstrap
|
|
177
180
|
|
|
178
181
|
@asynccontextmanager
|
|
179
182
|
async def lifespan(app: FastAPI):
|
|
180
183
|
setup_telemetry()
|
|
181
|
-
await
|
|
184
|
+
await bootstrap.start() # initializes every registered adapter — fails fast if any backend unreachable
|
|
182
185
|
yield
|
|
183
|
-
await
|
|
186
|
+
await bootstrap.stop() # shuts down every adapter, then flushes telemetry — never raises
|
|
184
187
|
|
|
185
188
|
app = FastAPI(lifespan=lifespan)
|
|
186
189
|
app.add_middleware(TelemetryMiddleware)
|
|
187
190
|
```
|
|
188
191
|
|
|
192
|
+
### Domain subclass registration
|
|
193
|
+
|
|
194
|
+
Every `*Plugin` supports registering a domain-specific subclass instead of
|
|
195
|
+
the plain base adapter class — `repository_class` (Postgres, Mongo, Redis),
|
|
196
|
+
`producer_class` and `consumer_class` (Kafka):
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
app = ApplicationBootstrap.compose(
|
|
200
|
+
PostgresPlugin(PostgresSettings(), table="items", repository_class=ItemRepository),
|
|
201
|
+
RedisPlugin(RedisSettings(), repository_class=SessionRepository),
|
|
202
|
+
KafkaPlugin(KafkaSettings(), producer_class=ArtifactEventProducer, consumer_class=OrderEventConsumer),
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`get_repository()` / `get_producer()` / `make_consumer()` then return an
|
|
207
|
+
instance of the subclass you passed in, not the plain base class — so
|
|
208
|
+
overridden entity mapping (`_doc_to_entity`, `_entity_to_doc`, `_serialise`,
|
|
209
|
+
`_deserialise`, etc.) is preserved end-to-end through the registry.
|
|
210
|
+
|
|
189
211
|
### Plugin capabilities
|
|
190
212
|
|
|
191
213
|
Every `*Plugin` class declares a `capability` attribute — a typed
|
|
192
214
|
`openframe.core.ports.Capability` enum member (not a raw string as of
|
|
193
215
|
`openframe-core` v3.0; the module was `openframe.core.contracts` before
|
|
194
216
|
v3.1, which merged it into `openframe.core.ports`). This is the key used
|
|
195
|
-
by `
|
|
217
|
+
by `ApplicationBootstrap.get()` / `PluginRegistry.get()`:
|
|
196
218
|
|
|
197
219
|
```python
|
|
198
220
|
from openframe.core.ports import Capability
|
|
199
221
|
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
app.get(Capability.PERSISTENCE) # → PostgresPlugin / MongoPlugin
|
|
223
|
+
app.get(Capability.CACHE) # → RedisPlugin
|
|
202
224
|
```
|
|
203
225
|
|
|
204
226
|
The capability taxonomy is a closed enum shared across the entire OpenFrame
|
|
@@ -223,29 +245,32 @@ keep working, but new code should key on the enum member directly.
|
|
|
223
245
|
### The env-var swap exception
|
|
224
246
|
|
|
225
247
|
Switching between adapters via an environment variable (`PERSISTENCE_BACKEND=postgres`
|
|
226
|
-
vs `PERSISTENCE_BACKEND=mongo`)
|
|
227
|
-
|
|
248
|
+
vs `PERSISTENCE_BACKEND=mongo`) still only ever has **one** adapter active at
|
|
249
|
+
a time — `compose()` still applies, just with the chosen plugin decided
|
|
250
|
+
before the call:
|
|
228
251
|
|
|
229
252
|
```python
|
|
230
|
-
|
|
231
|
-
def _get_repository():
|
|
253
|
+
def _make_plugin():
|
|
232
254
|
backend = os.environ.get("PERSISTENCE_BACKEND", "postgres")
|
|
233
255
|
if backend == "postgres":
|
|
234
|
-
return
|
|
256
|
+
return PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
235
257
|
elif backend == "mongo":
|
|
236
|
-
return
|
|
258
|
+
return MongoPlugin(MongoSettings(), repository_class=ItemMongoRepository)
|
|
237
259
|
raise ValueError(f"Unknown PERSISTENCE_BACKEND: {backend!r}")
|
|
260
|
+
|
|
261
|
+
app = ApplicationBootstrap.compose(_make_plugin())
|
|
238
262
|
```
|
|
239
263
|
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
Both backends still register under the same `Capability.PERSISTENCE` — the
|
|
265
|
+
rest of your service (`app.get(Capability.PERSISTENCE)`) never knows which
|
|
266
|
+
one is active.
|
|
242
267
|
|
|
243
268
|
### Full wiring reference
|
|
244
269
|
|
|
245
|
-
For the complete guide —
|
|
246
|
-
wiring, and architecture
|
|
247
|
-
[
|
|
248
|
-
|
|
270
|
+
For the complete guide — the three-tier model (`compose()` → `configure()`
|
|
271
|
+
subclass → `.registry` escape hatch), lifespan wiring, and architecture
|
|
272
|
+
diagrams — see [How It Works § Choosing a Wiring Pattern](https://furious-meteors.github.io/openframe-core/developer-guide/how-it-works/#choosing-a-wiring-pattern)
|
|
273
|
+
in the `openframe-core` documentation.
|
|
249
274
|
|
|
250
275
|
---
|
|
251
276
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openframe-adapters
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: OpenFrame Microservice Suite - adapter meta-package. Install adapters by name or group.
|
|
5
5
|
Author-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
6
6
|
Maintainer-email: Furious Meteors Engineering <engineering@furiousmeteors.dev>
|
|
@@ -184,112 +184,134 @@ Every adapter wires into your service through a single file — `deps.py` or
|
|
|
184
184
|
that knows which adapter is active. Routes and services never import adapters
|
|
185
185
|
directly.
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
Wiring goes through `openframe-core`'s `ApplicationBootstrap`
|
|
188
|
+
(requires `openframe-core>=3.3`) — one recommended class, at two levels of
|
|
189
|
+
ceremony:
|
|
188
190
|
|
|
189
|
-
> **One adapter** —
|
|
190
|
-
> **
|
|
191
|
-
>
|
|
191
|
+
> **One or a few adapters, no per-adapter config needed** — `ApplicationBootstrap.compose(*plugins)`. No subclass.
|
|
192
|
+
> **An adapter needs its own `config=`/`init_timeout=`, or registration order depends on a runtime condition** — subclass with `configure()`.
|
|
193
|
+
> Either way, `ApplicationBootstrap` manages startup ordering, health aggregation, and graceful shutdown (including telemetry flush) across all adapters — you never hand-roll that part.
|
|
192
194
|
|
|
193
|
-
###
|
|
194
|
-
|
|
195
|
-
Install one adapter and wire it directly. Four lines. No registry needed.
|
|
195
|
+
### One adapter — `compose()`, no subclass
|
|
196
196
|
|
|
197
197
|
```python
|
|
198
198
|
# bootstrap/dependencies.py
|
|
199
|
-
from
|
|
200
|
-
from openframe.
|
|
199
|
+
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
200
|
+
from openframe.core.ports import Capability
|
|
201
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
201
202
|
from openframe.core.tracing import TracingProxy
|
|
202
203
|
from src.adapters.item_repository import ItemPostgresRepository
|
|
203
204
|
from src.application.services.item_service import ItemService
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
app = ApplicationBootstrap.compose(
|
|
207
|
+
PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
208
|
+
)
|
|
208
209
|
|
|
209
210
|
def get_item_service() -> ItemService:
|
|
210
|
-
|
|
211
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
212
|
+
return ItemService(repo)
|
|
211
213
|
```
|
|
212
214
|
|
|
213
215
|
`PostgresSettings()` reads `DATABASE_URL` from env at startup.
|
|
214
|
-
`
|
|
215
|
-
`TracingProxy` wraps it for automatic OTel spans on every call.
|
|
216
|
+
`TracingProxy` wraps the repository for automatic OTel spans on every call.
|
|
216
217
|
|
|
217
|
-
###
|
|
218
|
+
### Two or more adapters — still `compose()`, or subclass if you need per-adapter config
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
The registry handles startup ordering, health aggregation, and graceful
|
|
221
|
-
shutdown across all adapters.
|
|
220
|
+
`compose()` accepts any number of ports — pass them all in one call:
|
|
222
221
|
|
|
223
222
|
```python
|
|
224
223
|
# bootstrap/dependencies.py
|
|
225
224
|
from openframe.adapters.db.postgres import PostgresPlugin, PostgresSettings
|
|
226
225
|
from openframe.adapters.db.redis import RedisPlugin, RedisSettings
|
|
227
|
-
from openframe.core.
|
|
226
|
+
from openframe.core.ports import Capability
|
|
227
|
+
from openframe.core.runtime import ApplicationBootstrap
|
|
228
228
|
from openframe.core.tracing import TracingProxy
|
|
229
229
|
from src.application.services.item_service import ItemService
|
|
230
230
|
from src.application.services.session_service import SessionService
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
_registry = PluginRegistry()
|
|
237
|
-
_registry.register(PostgresPlugin(PostgresSettings())) # capability: Capability.PERSISTENCE
|
|
238
|
-
_registry.register(RedisPlugin(RedisSettings())) # capability: Capability.CACHE
|
|
239
|
-
await _registry.initialize_all() # fails fast if any backend unreachable
|
|
240
|
-
|
|
241
|
-
async def shutdown() -> None:
|
|
242
|
-
if _registry:
|
|
243
|
-
await _registry.shutdown_all() # never raises
|
|
232
|
+
app = ApplicationBootstrap.compose(
|
|
233
|
+
PostgresPlugin(PostgresSettings()), # capability: Capability.PERSISTENCE
|
|
234
|
+
RedisPlugin(RedisSettings()), # capability: Capability.CACHE
|
|
235
|
+
)
|
|
244
236
|
|
|
245
237
|
def get_item_service() -> ItemService:
|
|
246
|
-
repo = TracingProxy(
|
|
247
|
-
_registry.get("persistence").get_repository(),
|
|
248
|
-
prefix="repository.item",
|
|
249
|
-
)
|
|
238
|
+
repo = TracingProxy(app.get(Capability.PERSISTENCE).get_repository(), prefix="repository.item")
|
|
250
239
|
return ItemService(repo)
|
|
251
240
|
|
|
252
241
|
def get_session_service() -> SessionService:
|
|
253
|
-
cache = TracingProxy(
|
|
254
|
-
_registry.get("cache").get_repository(),
|
|
255
|
-
prefix="cache.session",
|
|
256
|
-
)
|
|
242
|
+
cache = TracingProxy(app.get(Capability.CACHE).get_repository(), prefix="cache.session")
|
|
257
243
|
return SessionService(cache)
|
|
258
244
|
```
|
|
259
245
|
|
|
260
|
-
|
|
246
|
+
Reach for a subclass instead once an adapter needs its own `config=` mapping,
|
|
247
|
+
a per-adapter `init_timeout=`, or registration order that depends on a
|
|
248
|
+
runtime condition:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
class AppBootstrap(ApplicationBootstrap):
|
|
252
|
+
def configure(self) -> None:
|
|
253
|
+
self.register(PostgresPlugin(PostgresSettings()), config={"pool_hint": "primary"})
|
|
254
|
+
self.register(RedisPlugin(RedisSettings()), init_timeout=5.0)
|
|
255
|
+
|
|
256
|
+
app = AppBootstrap()
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
For the deliberate multi-port-per-capability case (e.g. primary + replica
|
|
260
|
+
Postgres), use `app.get_all(Capability.PERSISTENCE)` or, for anything neither
|
|
261
|
+
covers, the underlying registry directly via `app.registry`.
|
|
262
|
+
|
|
263
|
+
Wire `start()` and `stop()` in your FastAPI lifespan:
|
|
261
264
|
|
|
262
265
|
```python
|
|
263
266
|
from contextlib import asynccontextmanager
|
|
264
267
|
from fastapi import FastAPI
|
|
265
268
|
from openframe.core.middleware import TelemetryMiddleware
|
|
266
269
|
from openframe.core.telemetry import setup_telemetry
|
|
267
|
-
from src.bootstrap import
|
|
270
|
+
from src.bootstrap.dependencies import app as bootstrap
|
|
268
271
|
|
|
269
272
|
@asynccontextmanager
|
|
270
273
|
async def lifespan(app: FastAPI):
|
|
271
274
|
setup_telemetry()
|
|
272
|
-
await
|
|
275
|
+
await bootstrap.start() # initializes every registered adapter — fails fast if any backend unreachable
|
|
273
276
|
yield
|
|
274
|
-
await
|
|
277
|
+
await bootstrap.stop() # shuts down every adapter, then flushes telemetry — never raises
|
|
275
278
|
|
|
276
279
|
app = FastAPI(lifespan=lifespan)
|
|
277
280
|
app.add_middleware(TelemetryMiddleware)
|
|
278
281
|
```
|
|
279
282
|
|
|
283
|
+
### Domain subclass registration
|
|
284
|
+
|
|
285
|
+
Every `*Plugin` supports registering a domain-specific subclass instead of
|
|
286
|
+
the plain base adapter class — `repository_class` (Postgres, Mongo, Redis),
|
|
287
|
+
`producer_class` and `consumer_class` (Kafka):
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
app = ApplicationBootstrap.compose(
|
|
291
|
+
PostgresPlugin(PostgresSettings(), table="items", repository_class=ItemRepository),
|
|
292
|
+
RedisPlugin(RedisSettings(), repository_class=SessionRepository),
|
|
293
|
+
KafkaPlugin(KafkaSettings(), producer_class=ArtifactEventProducer, consumer_class=OrderEventConsumer),
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
`get_repository()` / `get_producer()` / `make_consumer()` then return an
|
|
298
|
+
instance of the subclass you passed in, not the plain base class — so
|
|
299
|
+
overridden entity mapping (`_doc_to_entity`, `_entity_to_doc`, `_serialise`,
|
|
300
|
+
`_deserialise`, etc.) is preserved end-to-end through the registry.
|
|
301
|
+
|
|
280
302
|
### Plugin capabilities
|
|
281
303
|
|
|
282
304
|
Every `*Plugin` class declares a `capability` attribute — a typed
|
|
283
305
|
`openframe.core.ports.Capability` enum member (not a raw string as of
|
|
284
306
|
`openframe-core` v3.0; the module was `openframe.core.contracts` before
|
|
285
307
|
v3.1, which merged it into `openframe.core.ports`). This is the key used
|
|
286
|
-
by `
|
|
308
|
+
by `ApplicationBootstrap.get()` / `PluginRegistry.get()`:
|
|
287
309
|
|
|
288
310
|
```python
|
|
289
311
|
from openframe.core.ports import Capability
|
|
290
312
|
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
app.get(Capability.PERSISTENCE) # → PostgresPlugin / MongoPlugin
|
|
314
|
+
app.get(Capability.CACHE) # → RedisPlugin
|
|
293
315
|
```
|
|
294
316
|
|
|
295
317
|
The capability taxonomy is a closed enum shared across the entire OpenFrame
|
|
@@ -314,29 +336,32 @@ keep working, but new code should key on the enum member directly.
|
|
|
314
336
|
### The env-var swap exception
|
|
315
337
|
|
|
316
338
|
Switching between adapters via an environment variable (`PERSISTENCE_BACKEND=postgres`
|
|
317
|
-
vs `PERSISTENCE_BACKEND=mongo`)
|
|
318
|
-
|
|
339
|
+
vs `PERSISTENCE_BACKEND=mongo`) still only ever has **one** adapter active at
|
|
340
|
+
a time — `compose()` still applies, just with the chosen plugin decided
|
|
341
|
+
before the call:
|
|
319
342
|
|
|
320
343
|
```python
|
|
321
|
-
|
|
322
|
-
def _get_repository():
|
|
344
|
+
def _make_plugin():
|
|
323
345
|
backend = os.environ.get("PERSISTENCE_BACKEND", "postgres")
|
|
324
346
|
if backend == "postgres":
|
|
325
|
-
return
|
|
347
|
+
return PostgresPlugin(PostgresSettings(), repository_class=ItemPostgresRepository)
|
|
326
348
|
elif backend == "mongo":
|
|
327
|
-
return
|
|
349
|
+
return MongoPlugin(MongoSettings(), repository_class=ItemMongoRepository)
|
|
328
350
|
raise ValueError(f"Unknown PERSISTENCE_BACKEND: {backend!r}")
|
|
351
|
+
|
|
352
|
+
app = ApplicationBootstrap.compose(_make_plugin())
|
|
329
353
|
```
|
|
330
354
|
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
Both backends still register under the same `Capability.PERSISTENCE` — the
|
|
356
|
+
rest of your service (`app.get(Capability.PERSISTENCE)`) never knows which
|
|
357
|
+
one is active.
|
|
333
358
|
|
|
334
359
|
### Full wiring reference
|
|
335
360
|
|
|
336
|
-
For the complete guide —
|
|
337
|
-
wiring, and architecture
|
|
338
|
-
[
|
|
339
|
-
|
|
361
|
+
For the complete guide — the three-tier model (`compose()` → `configure()`
|
|
362
|
+
subclass → `.registry` escape hatch), lifespan wiring, and architecture
|
|
363
|
+
diagrams — see [How It Works § Choosing a Wiring Pattern](https://furious-meteors.github.io/openframe-core/developer-guide/how-it-works/#choosing-a-wiring-pattern)
|
|
364
|
+
in the `openframe-core` documentation.
|
|
340
365
|
|
|
341
366
|
---
|
|
342
367
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openframe-adapters"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "OpenFrame Microservice Suite - adapter meta-package. Install adapters by name or group."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/requires.txt
RENAMED
|
File without changes
|
{openframe_adapters-2.0.2 → openframe_adapters-2.0.3}/openframe_adapters.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|