Archetypes¶
The classes App compiles to. Subclass one directly when the
decorators stop fitting — several interacting state machines, a
custom event walk, or a rule that needs the whole detection
batch at once. Same process, same manifest, same alerts.
opennvr_app_sdk.Detector
¶
Detector(
config: Any,
dispatcher: AlertDispatcher,
*,
clock: Callable[[], datetime] | None = None,
)
Bases: ContractMixin, NatsSubscriberMixin
Base class for NATS-subscribing detection apps.
Subclasses set a class-level manifest (:class:AppManifest),
optionally override :meth:setup to allocate state, and implement
:meth:on_detections. cfg is the app-parsed config object; the
NATS loop reads cfg.nats_url, cfg.nats_token (optional) and
cfg.subject_pattern from it.
clock is a callable returning a UTC datetime; per-event
timestamps from the NATS payload are preferred, but timestamp
parsing needs a "now" fallback for missing / malformed values.
Tests pass a controlled clock for determinism.
Source code in opennvr_app_sdk/detector.py
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
setup
¶
setup() -> None
Optional hook — allocate per-app state (keyed_state et
al.). Runs once at construction, after cfg is set.
Source code in opennvr_app_sdk/detector.py
125 126 127 | |
on_detections
¶
on_detections(
camera_id: str,
detections: list[dict[str, Any]],
event: dict[str, Any],
) -> Iterable[Alert] | None
The rule. Called once per decoded inference event that has a
camera_id and a result.detections list. Return or yield
the :class:Alert objects to fire (or None / empty).
Source code in opennvr_app_sdk/detector.py
129 130 131 132 133 134 135 136 137 138 | |
keyed_state
¶
keyed_state(ttl: float, **kwargs: Any) -> KeyedState
Convenience for setup() — see :func:~.state.keyed_state.
Source code in opennvr_app_sdk/detector.py
140 141 142 | |
handle_event
¶
handle_event(event: Any) -> list[Alert]
Process one decoded InferenceCompletedEvent dict:
extract camera_id + result.detections (defensively —
malformed shapes return []), delegate to
:meth:on_detections, and dispatch every returned alert.
Returns the list of alerts fired.
Source code in opennvr_app_sdk/detector.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
parse_event_ts
¶
parse_event_ts(raw: Any) -> float
Extract a POSIX timestamp from the NATS event bus
completed_at ISO string. Falls back to the clock for
missing / malformed values so a misbehaving publisher doesn't
break app state machines.
Source code in opennvr_app_sdk/detector.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | |
run
async
¶
run(*, once: bool = False) -> None
Connect to NATS, subscribe, drive the handler on every
received event. Returns when stop() is called or when
once=True and one message has been processed.
Also owns the app-contract lifecycle (spec §03): starts the
/health / /manifest / /state server when
cfg.contract_port is set and self-registers with the
OpenNVR app registry when cfg.opennvr_url is set — both
best-effort no-ops otherwise.
The connect / subscribe / drain machinery itself lives on
:class:~.nats_loop.NatsSubscriberMixin, shared with the
AlertSubscriber archetype.
Source code in opennvr_app_sdk/detector.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | |
opennvr_app_sdk.FrameApp
¶
FrameApp(
config: Any,
dispatcher: AlertDispatcher,
*,
frame_source: FrameSource,
cameras: Iterable[str] | None = None,
poll_interval_seconds: float | None = None,
)
Bases: ContractMixin
Base class for frame-polling apps.
Subclasses set manifest, optionally override :meth:setup, and
implement :meth:on_frame. The base owns the interval loop and
alert dispatch; per-camera fetch and rule failures are isolated so
one bad camera never stalls the rest.
cameras / poll_interval_seconds default from the config
object (cfg.cameras may be a list of ids or a dict keyed by
id; cfg.poll_interval_seconds defaults to 5.0).
Source code in opennvr_app_sdk/frame_app.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | |
setup
¶
setup() -> None
Optional hook — allocate per-app state.
Source code in opennvr_app_sdk/frame_app.py
196 197 | |
on_frame
¶
on_frame(
camera_id: str, frame_bytes: bytes
) -> Iterable[Alert] | None
The rule. Called once per fetched frame. Return or yield
Alerts to fire (or None / empty).
Source code in opennvr_app_sdk/frame_app.py
199 200 201 202 203 204 | |
on_cameras_update
¶
on_cameras_update(camera_ids: frozenset[int]) -> None
Follow the cameras picked for this app: the poll loop fetches
frames for exactly those, as cam<id> handles.
Only runs when the app is connected (the live config poll is what delivers picks). A standalone run keeps the camera list it was constructed with. Nothing picked → an empty list → the loop ticks and does nothing, which is the point.
Source code in opennvr_app_sdk/frame_app.py
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
handle_tick
¶
handle_tick() -> list[Alert]
One poll cycle: fetch a frame per camera, run the rule, dispatch whatever it produced. Fetch / rule failures are logged per camera and never propagate.
Source code in opennvr_app_sdk/frame_app.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | |
run
async
¶
run(*, once: bool = False) -> None
Poll every poll_interval_seconds until stop() (or
one tick with once=True). The inter-tick sleep is
interruptible so shutdown is immediate.
Also owns the app-contract lifecycle (spec §03): starts the
/health / /manifest / /state server when
cfg.contract_port is set and self-registers with the
OpenNVR app registry when cfg.opennvr_url is set — both
best-effort no-ops otherwise.
Source code in opennvr_app_sdk/frame_app.py
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
opennvr_app_sdk.AlertSubscriber
¶
AlertSubscriber(config: Any)
Bases: ContractMixin, NatsSubscriberMixin
Base class for NATS alert-consuming apps.
Subclasses optionally set a class-level manifest
(:class:AppManifest), optionally override :meth:setup to
allocate state, and implement :meth:on_alert. cfg is the
app-parsed config object; the NATS loop reads cfg.nats_url,
cfg.nats_token (optional) and cfg.subject_pattern from it
(apps default the pattern to "opennvr.alerts.>").
Source code in opennvr_app_sdk/alert_subscriber.py
68 69 70 71 72 73 74 75 76 | |
setup
¶
setup() -> None
Optional hook — allocate per-app state (counters, HTTP
clients, …). Runs once at construction, after cfg is set.
Source code in opennvr_app_sdk/alert_subscriber.py
80 81 82 | |
on_alert
¶
on_alert(alert: dict[str, Any], subject: str) -> None
The sink. Called once per JSON-decoded alert envelope with the raw dict and the NATS subject it arrived on. Forward it, store it, page someone — whatever the bridge is for.
Source code in opennvr_app_sdk/alert_subscriber.py
84 85 86 87 88 | |
run
async
¶
run(*, once: bool = False) -> None
Connect to NATS, subscribe, drive the sink on every received
alert. Returns when stop() is called or when once=True
and one message has been processed.
Also owns the app-contract lifecycle (spec §03), same as the
other archetypes: starts the /health / /manifest /
/state server when cfg.contract_port is set and
self-registers when cfg.opennvr_url is set — both
best-effort no-ops otherwise. The connect / subscribe / drain
machinery lives on :class:~.nats_loop.NatsSubscriberMixin,
shared with Detector.
Source code in opennvr_app_sdk/alert_subscriber.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
opennvr_app_sdk.DomainEventSubscriber
¶
DomainEventSubscriber(
config: Any, dispatcher: AlertDispatcher | None = None
)
Bases: ContractMixin, NatsSubscriberMixin
Base class for apps that react to contracted domain events.
Set subscriptions (schemas or subjects), implement
:meth:on_event. Reads cfg.nats_url / cfg.nats_token; a
cfg.subject_pattern overrides subscriptions when present.
The contract server, self-registration and the config poll run
exactly as for the other archetypes.
Source code in opennvr_app_sdk/domain_subscriber.py
111 112 113 114 115 116 117 118 119 120 121 122 | |
dispatcher
property
¶
dispatcher: AlertDispatcher
stdout + the webhook_url / nats_alerts_* channels from
cfg (:class:~.config.BaseAppConfig), built on first use.
Pass dispatcher= to the constructor to substitute one.
setup
¶
setup() -> None
Optional: allocate state after cfg is set.
Source code in opennvr_app_sdk/domain_subscriber.py
124 125 | |
fire
¶
fire(alert: Alert) -> dict[str, bool]
Dispatch one alert as this app and count it for /health.
Source code in opennvr_app_sdk/domain_subscriber.py
143 144 145 146 147 | |
opennvr_app_sdk.app
¶
app(
detector_cls: type[Detector],
*,
load_config: Callable[[str], Any] | None = None,
) -> AppRunner
Wrap a Detector subclass in a CLI runner::
if name == "main": raise SystemExit(app(Loitering, load_config=load_config).run())
Source code in opennvr_app_sdk/detector.py
352 353 354 355 356 357 358 359 360 361 362 | |
opennvr_app_sdk.alert_app
¶
alert_app(
subscriber_cls: type[AlertSubscriber],
*,
load_config: Callable[[str], Any] | None = None,
) -> AlertSubscriberRunner
Wrap an AlertSubscriber subclass in a CLI runner::
if name == "main": raise SystemExit( alert_app(MyBridge, load_config=load_config).run() )
Source code in opennvr_app_sdk/alert_subscriber.py
214 215 216 217 218 219 220 221 222 223 224 225 226 | |
opennvr_app_sdk.domain_event_app
¶
domain_event_app(
app_cls, *, load_config=None
) -> AlertSubscriberRunner
CLI runner factory, the same shape as :func:alert_app::
if name == "main": raise SystemExit(domain_event_app(Gate, load_config=load_config).run())
Source code in opennvr_app_sdk/domain_subscriber.py
196 197 198 199 200 201 202 | |
opennvr_app_sdk.AppRunner
¶
AppRunner(
detector_cls: type[Detector],
*,
load_config: Callable[[str], Any] | None = None,
)
The app(MyDetector) return value — owns argparse, logging
setup, dispatcher construction, and the signal-driven lifecycle.
Behavior ported from the loitering-detection example's main().
Source code in opennvr_app_sdk/detector.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 | |
opennvr_app_sdk.AlertSubscriberRunner
¶
AlertSubscriberRunner(
subscriber_cls: type[AlertSubscriber],
*,
load_config: Callable[[str], Any] | None = None,
)
The alert_app(MySubscriber) return value — owns argparse,
logging setup, and the signal-driven lifecycle. Mirrors the
Detector's :class:~.detector.AppRunner minus the dispatcher: an
AlertSubscriber consumes alerts rather than emitting them.
Source code in opennvr_app_sdk/alert_subscriber.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
opennvr_app_sdk.BaseAppConfig
dataclass
¶
BaseAppConfig(
nats_url: str = "",
nats_token: str | None = None,
subject_pattern: str | None = None,
webhook_url: str | None = None,
nats_alerts_url: str | None = None,
nats_alerts_token: str | None = None,
nats_alerts_subject_prefix: str = DEFAULT_ALERT_SUBJECT_PREFIX,
contract_port: int | None = None,
contract_bind_host: str | None = None,
contract_host: str | None = None,
opennvr_url: str | None = None,
opennvr_token: str | None = None,
config_poll_seconds: float | None = None,
consume_tier0: bool = False,
)
What the SDK reads from cfg — the runners, the NATS loop, the
alert dispatcher, the contract server and the registry client all
look these up by name. Subclass and add your own fields::
@dataclass
class AppConfig(BaseAppConfig):
watch_labels: list[str] = field(default_factory=lambda: ["person"])
dwell_s: float = 30.0
cfg = load_app_config("config.yml", AppConfig)
opennvr_app_sdk.load_app_config
¶
load_app_config(
path: str | Path, cls: type = BaseAppConfig
)
Load path into cls — :class:BaseAppConfig or a dataclass
subclass of it. Base keys get the standard validation; every extra
field of cls is taken from the file by name when present, from
its default otherwise, and a field with no default is required.
Put app-specific checks in __post_init__; raise ValueError
with a message an operator can act on.
Source code in opennvr_app_sdk/config.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | |