Registry Introspection¶
Cosalette's introspection system lets you inspect all registered devices, telemetry, commands, and adapters at any point after registration — before the app even starts running.
Why Introspect?¶
A cosalette app assembles its registrations declaratively via decorators:
@app.telemetry("temp", interval=30, publish=Every(seconds=60) | OnChange())
async def temp() -> dict[str, object]:
return {"celsius": read_sensor()}
But once an app has dozens of registrations across multiple files, it becomes hard to answer simple questions:
- What devices are registered?
- What interval does
tempuse? Is it deferred? - Which telemetry uses a
SaveOnPublishpolicy? - What adapters are wired up?
build_registry_snapshot() answers all of these programmatically.
The Snapshot¶
build_registry_snapshot(app) returns a plain Python dict that is
fully JSON-serializable — no custom encoders needed:
import cosalette
from cosalette import build_registry_snapshot
app = cosalette.App(name="mybridge", version="1.0.0")
# ... register devices, telemetry, commands, adapters ...
snapshot = build_registry_snapshot(app)
The returned dict has this structure:
{
"app": {
"name": "mybridge",
"version": "1.0.0",
"description": "IoT-to-MQTT bridge",
},
"devices": [ ... ],
"telemetry": [ ... ],
"commands": [ ... ],
"adapters": [ ... ],
}
Telemetry Entries¶
Each telemetry entry captures the full configuration:
{
"name": "temp",
"type": "telemetry",
"func": "sensors.temp",
"interval": 30.0, # (1)!
"strategy": "AnyStrategy(Every(seconds=60.0), OnChange())", # (2)!
"persist": "SaveOnPublish()", # (3)!
"group": "sensors", # (4)!
"is_root": False,
"has_init": False,
"dependencies": [["store", "DeviceStore"]], # (5)!
}
- Concrete float, or
"<deferred>"if the interval is a settings-derived callable - Strategy
repr()— composites are shown recursively - Persist policy
repr(), ornullif not set - Coalescing group name, or
null - Injected parameters as
[param_name, type_name]pairs
Deferred Intervals¶
Intervals can be a concrete float or a callable that resolves from settings at runtime (see ADR-020):
# Concrete — shows as 30.0
@app.telemetry("temp", interval=30.0)
# Deferred — shows as "<deferred>"
@app.telemetry("temp", interval=lambda s: s.sensor_interval)
Before the app runs, deferred intervals cannot be resolved because settings
haven't been validated yet. The snapshot shows "<deferred>" as a clear
indicator.
Device and Command Entries¶
# Device entry
{"name": "motor", "type": "device", "func": "devices.motor",
"is_root": False, "has_init": True,
"dependencies": [["ctx", "DeviceContext"]]}
# Command entry
{"name": "valve", "type": "command", "func": "handlers.valve",
"mqtt_params": ["payload", "topic"], "is_root": False,
"has_init": False, "dependencies": []}
Adapter Entries¶
Adapter impl and dry_run fields show:
- Class name for type-based registration
- Import string for lazy registration (e.g.,
"mypackage.adapters:MyAdapter") - Qualified name for callable factories
Use Cases¶
| Use case | How |
|---|---|
| CLI diagnostics | --show-devices flag renders the snapshot as a table (see CLI Reference) |
| Machine-readable output | --show-devices-json dumps as JSON (see CLI Reference) |
| Agent consumption | AI agents parse the JSON to understand app structure |
| Test assertions | Verify registration correctness in integration tests |
Formatting¶
Two convenience functions turn a snapshot into display-ready output:
from cosalette import build_registry_snapshot, format_registry_table, format_registry_json
snapshot = build_registry_snapshot(app)
# Human-readable table (used by --show-devices)
print(format_registry_table(snapshot))
# Indented JSON via orjson (used by --show-devices-json)
print(format_registry_json(snapshot))
format_registry_table groups registrations by type (devices, telemetry,
commands, adapters), omitting empty sections. Booleans are rendered as
✓/— and missing values as —.
format_registry_json delegates to orjson with two-space indentation,
consistent with ADR-021.
Design Notes¶
The introspection module reads the App's internal registries directly.
It produces a read-only snapshot — no mutations, no side effects. The
output uses repr() on strategies and persist policies, which means
adding a custom strategy only requires implementing __repr__ for it
to appear correctly in snapshots.
Open/Closed Principle
New strategy or policy classes automatically work with introspection
as long as they implement __repr__. No changes to the introspection
module are needed — the system is open for extension, closed for
modification.