feat: add extension points for DI provider and event handler overrides#90
Conversation
create_app() now accepts `providers` and `extra_handlers` kwargs, allowing external packages to swap infrastructure adapters (e.g. container runners, storage backends) and register additional event handlers without duplicating any app wiring. - create_container() accepts *extra_providers and extra_handlers - EventProvider accepts extra_handlers, merging them with core handlers for subscription routing, WorkerPool registration, and DI resolution - Handler DI bindings moved from class-body locals() trick to dynamic self.provide() in __init__ to support runtime extension
|
Greptile SummaryThis PR adds extension points to Key changes:
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| server/osa/infrastructure/event/di.py | Replaces the class-body locals() trick for handler DI bindings with dynamic self.provide() calls in __init__, and exposes extra_handlers to allow runtime handler extension. The core logic is sound; no guard exists against accidentally passing a core handler in extra_handlers, which would cause double registration. |
| server/osa/application/di.py | Cleanly adds *extra_providers (varargs) and extra_handlers parameters to create_container(). Extra providers are appended after built-in ones for last-wins override semantics. No issues found. |
| server/osa/application/api/rest/app.py | Adds keyword-only providers and extra_handlers to create_app() and threads them through to create_container(). Module-level app = create_app() is unchanged and works correctly with no arguments. |
| server/osa/config.py | Adds derive_frontend_url model validator that auto-derives frontend.url from base_url when the field holds the default string value. This sentinel approach is ambiguous — an explicit env-var set to the default string is silently overridden — and changes the local-dev default (removes the :3000 port), potentially breaking CORS and redirect configuration. |
Sequence Diagram
sequenceDiagram
participant Host as External Host
participant CA as create_app()
participant CC as create_container()
participant EP as EventProvider.__init__()
participant DK as Dishka Container
Host->>CA: create_app(providers=[K8sProvider()],<br/>extra_handlers=[MeterUsage])
CA->>CC: create_container(*providers,<br/>extra_handlers=extra_handlers)
CC->>EP: EventProvider(extra_handlers=[MeterUsage])
EP->>EP: _all_handlers = [*_CORE_HANDLERS, MeterUsage]
loop for each handler in _all_handlers
EP->>EP: self.provide(handler_type, scope=UOW)
end
EP-->>CC: EventProvider instance
CC->>DK: make_async_container(..., *extra_providers)
DK-->>CC: AsyncContainer
CC-->>CA: AsyncContainer
CA-->>Host: FastAPI app
Last reviewed commit: "feat: add extension ..."
| self._all_handlers = HandlerTypes([*_CORE_HANDLERS, *(extra_handlers or [])]) | ||
|
|
||
| # Register DI bindings for every handler (core + extra). | ||
| # Each handler becomes a UOW-scoped dependency that Dishka can | ||
| # instantiate with its declared fields injected. | ||
| for handler_type in self._all_handlers: | ||
| self.provide(handler_type, scope=Scope.UOW) |
There was a problem hiding this comment.
No guard against duplicate handler registration
_CORE_HANDLERS and extra_handlers are concatenated without deduplication. If a caller passes a handler that already exists in _CORE_HANDLERS, self.provide() will be called twice for the same concrete type. Depending on the Dishka version this will either silently ignore the second registration or raise a DuplicateFactoryError at container start-up.
Adding a deduplication step (or at least a clear error) keeps the failure surface small:
seen: set[type] = set()
for handler_type in self._all_handlers:
if handler_type in seen:
raise ValueError(
f"Duplicate event handler registration: {handler_type.__name__!r}. "
"Remove it from extra_handlers — it is already registered as a core handler."
)
seen.add(handler_type)
self.provide(handler_type, scope=Scope.UOW)- Remove module-level app = create_app() to eliminate import side effects - Switch uvicorn to --factory mode in Dockerfile and Justfile - Add test_app_factory: tests create_app and create_container with provider overrides, extra event handlers, and both combined - Add test_event_provider: tests EventProvider default behaviour, extra handler merging, subscription registry, and DI resolution
Raise ValueError if the same handler type appears in both core and extra handlers, or is passed twice in extra_handlers. Prevents silent misbehaviour or DuplicateFactoryError depending on Dishka version.
Summary
create_app()now acceptsprovidersandextra_handlerskeyword arguments, allowing infrastructure to be swapped at startupcreate_container()accepts*extra_providers(appended after defaults, last-wins override) andextra_handlers(merged intoEventProvider)EventProviderconstructor acceptsextra_handlers, merging them with core handlers for subscription routing, WorkerPool registration, and DI resolutionlocals()trick to dynamicself.provide()in__init__to support runtime extensionTest plan