Dependency Injection Reference¶
Complete reference for the cosalette dependency injection system: injectable types, per-archetype resolution frequency, and marker syntax.
Concept vs Reference
For the design rationale and resolution rules, see Dependency Injection (concept). For DI-related exceptions, see Error Taxonomy.
Injectable Types¶
The following types can be annotated on any handler parameter and will be
supplied by the framework at dispatch time. Resolution is by exact type
identity first, then issubclass fallback — never by parameter name.
Framework-provided types¶
These are available to every archetype without any extra registration.
| Annotation | Value injected | Notes |
|---|---|---|
DeviceContext |
Per-device context object | Full API: publish, settings, adapters, clock |
Settings or subclass |
App settings instance | Subclasses matched via issubclass; your AppSettings(Settings) works directly |
logging.Logger |
logging.getLogger("cosalette.<device>") |
Device-scoped logger |
ClockPort |
Abstracted clock | Mockable in tests; avoids datetime.now() |
asyncio.Event |
Shutdown event | Set when the app is stopping |
TriggerPayload |
Trigger context | Only meaningful for triggerable telemetry devices; .source is "scheduled", "mqtt" or "local" |
EntityNotifier |
In-process trigger notifier | Call it with an entity name to wake a triggerable="local" (or "both") telemetry entity, or a triggerable="local" device; see Local triggers |
DeviceTrigger |
In-process trigger handle | Injected into an @app.device registered with triggerable="local"; await trigger.wait(timeout=...) blocks until an EntityNotifier wakes this device or the heartbeat elapses; honours min_interval= when set |
Persistence¶
| Annotation | Value injected | Notes |
|---|---|---|
DeviceStore |
Per-device persistence store | Requires a store backend on App(); see Persistence concept |
Adapters¶
| Annotation | Value injected | Notes |
|---|---|---|
Any port type registered via app.adapter() |
The registered adapter instance | Matched by exact type or issubclass |
init= callback return type |
Value returned by the init= callback |
Injected into the device handler and all of its commands |
| Per-device config type | Per-device config object | From dict-name expansion; the concrete type is added to providers |
Lifespan-yielded types¶
| Annotation | Value injected | Notes |
|---|---|---|
Type yielded by lifespan= context manager |
The yielded value | Single value per app; concrete runtime type matched; not available in on_configure hooks |
Stream archetype only¶
| Annotation | Value injected | Notes |
|---|---|---|
Stream[T] |
Async stream iterator | Available only in @app.stream handlers for push-to-pull bridging |
Request-scoped (Annotated markers)¶
These require Annotated[T, marker] syntax and are only available in
archetypes that receive an MQTT message (commands, triggered telemetry).
Reactors (@app.react) have no inbound message context — Topic(), Payload(),
and Message raise TypeError in reactor handlers.
| Annotation | Value injected | Notes |
|---|---|---|
Annotated[T, Payload()] |
Parsed payload (T via Pydantic TypeAdapter) | JSON-decoded and validated; use Payload(raw=True) for raw string |
Annotated[str, Topic()] |
Full inbound MQTT topic string | Inner type must be str; raises TypeError otherwise |
Annotated[T, Depends(fn)] |
Return value of synchronous callable fn |
fn can declare its own injectable parameters |
Annotated[T \| None, Optional()] |
Provider T if registered; otherwise param default or None |
Fallback to None requires = None default or no default |
Message (bare type, no marker) |
Message(topic=..., payload=...) dataclass |
Both fields are raw strings; not available outside command context |
Per-Archetype Resolution Frequency¶
The framework builds a providers map once per device lifetime. What varies is
when resolve_request_kwargs is called — i.e. when handler kwargs are
built from that map.
| Archetype | Resolved | Source location |
|---|---|---|
@app.command |
Per message — once per incoming MQTT message | _runners/_command_runner.py, prepare_command_kwargs (called per dispatch) |
@app.react |
Per message — once per command that triggers the reactor | _wiring/_reactors.py, _dispatch_single_reactor_with_events |
@app.telemetry |
Once per device lifetime — before the publish loop | _runners/_telemetry_runner.py, _TelemetryRunner.run |
@app.device (periodic) |
Once per device lifetime — before the while True loop |
_runners/_periodic.py, resolved once then loop repeats |
@app.stream |
Once per device lifetime — before the async generator is iterated | _runners/_stream_runner.py, _build_handler_kwargs called once |
For telemetry, periodic, and stream handlers, the same resolved kwargs dict is
reused on every cycle. Injected objects — DeviceContext, Settings, adapter
instances — are stable references for the entire lifetime of the device.
For commands, kwargs are rebuilt on every MQTT message so that request-scoped
values (Topic(), Payload(), Message) reflect the current message. The
providers map itself (adapter instances, settings, etc.) is shared and not
rebuilt per message. Reactors rebuild kwargs per triggering command but have no
request-scoped context — Topic(), Payload(), and Message are not available.
Marker Syntax Quick Reference¶
from typing import Annotated
from cosalette import DeviceContext
from cosalette.di import Depends, Optional
from cosalette.mqtt import Message, Payload, Topic
# Framework type — exact or issubclass match
async def handler(ctx: DeviceContext) -> ...: ...
# Adapter port — registered via app.adapter(SensorPort, ...)
async def handler(sensor: SensorPort) -> ...: ...
# Parsed payload
async def handler(cmd: Annotated[SetCmd, Payload()]) -> ...: ...
# Raw payload string
async def handler(raw: Annotated[str, Payload(raw=True)]) -> ...: ...
# Topic string
async def handler(topic: Annotated[str, Topic()]) -> ...: ...
# Full message object (bare type, no marker)
async def handler(msg: Message) -> ...: ...
# Dependency callable
def get_device_id(topic: Annotated[str, Topic()]) -> str:
return topic.split("/", 3)[2]
async def handler(device_id: Annotated[str, Depends(get_device_id)]) -> ...: ...
# Optional provider (falls back to None if not registered)
async def handler(store: Annotated[DeviceStore | None, Optional()] = None) -> ...: ...