App surfaces¶
What the app exposes back. Declare a state view or an action and the App Catalog renders it with no frontend of yours; declare a licence gate and core will not enable the app until your code says the key is good.
opennvr_app_sdk.StateView
dataclass
¶
StateView(
name: str,
label: str,
kind: str = "metric",
path: str = "",
columns: list[str] = list(),
description: str = "",
min: float | None = None,
max: float | None = None,
unit: str = "",
warn: float | None = None,
danger: float | None = None,
limit: int | None = None,
)
One declarative view over the app's GET /state payload.
The catalog renders these with ZERO app-specific UI code — the same
bet as params → config form. An app that exposes richer live
state (occupancy per zone, plates deduped, tracks active) declares
how to show it instead of shipping a frontend:
kind="metric"
A single scalar at path rendered as a stat chip
(e.g. path="denylist_size" → "Denylist · 4").
kind="table"
A list at path; columns names the keys to show when the
rows are dicts. A list of scalars renders as one column.
kind="gauge"
A numeric path rendered as a horizontal bar between min
and max, coloured amber past warn and red past
danger (e.g. zone occupancy). A dict-of-numbers renders one
gauge per key (per camera / per zone).
kind="log"
A recent-events feed: path is a list of strings or dicts
{message, time, level}; newest limit shown first.
kind="gallery"
A thumbnail wall: path is a list of dicts
{image|url, label, time} — for plate crops, package or
doorbell snapshots. image may be a data: URI.
path is a dot-path into the /state dict ("zones",
"counters.in"). A missing path renders as an em-dash, never an
error — /state is live data and may not have filled in yet.
opennvr_app_sdk.Action
dataclass
¶
Action(
name: str,
label: str,
params: list[Param] = list(),
description: str = "",
confirm: bool = False,
)
One operator-invokable action on the app's contract surface.
Declared like params, rendered like params: the catalog builds a
generic form from params and POSTs it to
/actions/{name} on the app — proxied through the server's
POST /api/v1/apps/{id}/actions/{name}, which is user-JWT
only. The governance boundary is deliberate: actions are operator
verbs (search footage, enroll a face); the OpenNVR Agent's service
key can read state but can NEVER invoke an action.
confirm=True makes the catalog ask before invoking (for actions
with side effects). The app implements the verb by overriding
:meth:ContractMixin.on_action.
opennvr_app_sdk.ContractServer
¶
ContractServer(
*,
health: Callable[[], dict[str, Any]],
manifest: Callable[[], dict[str, Any]],
state: Callable[[], dict[str, Any]],
openapi: "Callable[[], dict[str, Any]] | None" = None,
asyncapi: "Callable[[], dict[str, Any]] | None" = None,
action: "Callable[[str, dict[str, Any]], Any] | None" = None,
action_token: "str | None" = None,
ui: "Callable[[], str] | None" = None,
user_secret: "Callable[[], str | None] | None" = None,
app_id: "str | None" = None,
license_verifier: "Callable[[str], Any] | None" = None,
host: str = "0.0.0.0",
port: int = 0,
)
Serve the §03 contract endpoints on a background daemon thread.
stdlib-only by design (http.server) — the contract surface must
not drag a web framework into every 60-line detector. Three GETs a
few times a second is comfortably inside ThreadingHTTPServer
territory.
Source code in opennvr_app_sdk/contract.py
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | |
opennvr_app_sdk.Entitlement
dataclass
¶
Entitlement(
valid: bool,
plan: str = "",
expires_at: str | None = None,
message: str = "",
limits: dict[str, Any] = dict(),
)
A licence verdict from :meth:ContractMixin.verify_license.
opennvr_app_sdk.PRICING_MODELS
module-attribute
¶
PRICING_MODELS = frozenset(
{"free", "paid", "subscription", "contact"}
)
opennvr_app_sdk.ENTITLEMENT_MODES
module-attribute
¶
ENTITLEMENT_MODES = frozenset({'none', 'license_key'})
opennvr_app_sdk.UserContext
dataclass
¶
UserContext(
user_id: int,
username: str,
is_superuser: bool = False,
cameras: frozenset[int] | None = None,
manage: frozenset[int] | None = None,
purpose: str = "",
raw: dict = dict(),
)
can_see
¶
can_see(camera) -> bool
camera as an int id or a camN handle.
Source code in opennvr_app_sdk/usercontext.py
52 53 54 | |
visible
¶
visible(camera_ids) -> list
Filter a list of ids/handles down to what this user may see.
Source code in opennvr_app_sdk/usercontext.py
59 60 61 | |
opennvr_app_sdk.current_user
¶
current_user() -> UserContext | None
The operator behind the request being served, or None.
Source code in opennvr_app_sdk/usercontext.py
80 81 82 | |
opennvr_app_sdk.verify_call_token
¶
verify_call_token(
token: str | None,
secret: str | None,
*,
audience: str | None = None,
purpose: str | None = None,
now: float | None = None,
) -> dict | None
Verify an X-OpenNVR-Call value — core proving a request to
this app's write surfaces (/actions/*, /entitlement/verify)
is its own, signed with the sha256 of the app's key so no site-wide
credential ever reaches the app. purpose must match when given.
Returns the claims, or None.
Source code in opennvr_app_sdk/usercontext.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
opennvr_app_sdk.contract_openapi
¶
contract_openapi(
manifest: AppManifest, *, port: int | None = None
) -> dict[str, Any]
The OpenAPI 3.1 document for this app's contract server.
Only the paths the app actually serves appear: /ui when the
manifest sets has_ui with ui_mode="internal", one
/actions/{name} path per declared action, and
/entitlement/verify only when entitlement="license_key".
port fills the server URL's default when known (the contract
server passes its bound port).
Source code in opennvr_app_sdk/openapi.py
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |
opennvr_app_sdk.contract_asyncapi
¶
contract_asyncapi(
manifest: AppManifest, *, publishes: Sequence[str] = ()
) -> dict[str, Any]
The AsyncAPI 3.0 document for this app's NATS surface.
Three groups of channels, all derived from the manifest: what the
app subscribes to (subscribes), what it publishes
(opennvr.alerts.app.<id>.<camera_id>, one per declared alert
type), and the contracted domain events its requires_scopes
grant — scopes are how an app asks for PII-bearing events, so they
belong in the spec rather than in prose.
publishes names contracted domain events the app emits
(app.publishes(...) on the facade), so a consumer can generate a
client for them.
Source code in opennvr_app_sdk/openapi.py
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 | |
opennvr_app_sdk.proxy_address
¶
proxy_address(
scheme: str = "https",
) -> tuple[str, int] | None
(host, port) of the proxy, or None.
Source code in opennvr_app_sdk/egress.py
41 42 43 44 45 46 47 48 49 | |
opennvr_app_sdk.connect_via_proxy
¶
connect_via_proxy(
host: str, port: int
) -> tuple[str, int] | None
Where a plain-TCP client should tunnel to reach host:port:
the proxy address, or None to connect directly (no proxy set,
or the host is on NO_PROXY).
Source code in opennvr_app_sdk/egress.py
73 74 75 76 77 78 79 | |