piphi-runtime-kit-python 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,875 @@
1
+ Metadata-Version: 2.1
2
+ Name: piphi-runtime-kit-python
3
+ Version: 0.3.0
4
+ Summary: PiPhi Network runtime integration helpers
5
+ Keywords: piphi,runtime,integration,iot
6
+ Author-Email: KelvinSan <support@piphi.network>
7
+ License: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Framework :: FastAPI
15
+ Project-URL: Homepage, https://github.com/PiPhi-io/piphi-runtime-kit-python#readme
16
+ Project-URL: Repository, https://github.com/PiPhi-io/piphi-runtime-kit-python
17
+ Project-URL: Issues, https://github.com/PiPhi-io/piphi-runtime-kit-python/issues
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx<1.0,>=0.27
20
+ Requires-Dist: pydantic<3.0,>=2.8
21
+ Provides-Extra: fastapi
22
+ Requires-Dist: fastapi<1.0,>=0.115; extra == "fastapi"
23
+ Provides-Extra: mqtt
24
+ Requires-Dist: aiomqtt<3.0,>=2.4; extra == "mqtt"
25
+ Description-Content-Type: text/markdown
26
+
27
+ # piphi-runtime-kit-python
28
+
29
+ Small Python helpers for building PiPhi runtime integrations.
30
+
31
+ This package is intentionally thin. It removes repetitive PiPhi runtime
32
+ plumbing without hiding the HTTP contract behind a large framework. The goal
33
+ is to help developers move faster while still understanding what their runtime
34
+ is doing.
35
+
36
+ Version `0.3.0` is the current documented baseline.
37
+
38
+ > New to PiPhi? Start with [First 15 Minutes](#first-15-minutes), then read [The Golden Path](#the-golden-path), then compare your code to the example app.
39
+
40
+ > Safety note: every ID, token, hostname, and UUID shown in this README is placeholder example data. Do not copy real internal tokens, production hosts, or live container IDs into code samples, docs, or tests.
41
+
42
+ ## Quick Navigation
43
+
44
+ - [Who this is for](#who-this-is-for)
45
+ - [Install](#install)
46
+ - [First 15 Minutes](#first-15-minutes)
47
+ - [Route Contract At A Glance](#route-contract-at-a-glance)
48
+ - [UI Config Endpoints](#ui-config-endpoints)
49
+ - [One Complete Mini Example](#one-complete-mini-example)
50
+ - [The Golden Path](#the-golden-path)
51
+ - [The IDs You Need To Understand](#the-ids-you-need-to-understand)
52
+ - [Plain-Language Concepts](#plain-language-concepts)
53
+ - [When To Use Which Helper](#when-to-use-which-helper)
54
+ - [Troubleshooting](#troubleshooting)
55
+ - [What A Real Integration Still Needs](#what-a-real-integration-still-needs)
56
+
57
+ ## Reading Paths
58
+
59
+ - New developer
60
+ Read `First 15 Minutes`, `One Complete Mini Example`, `The Golden Path`, and `Plain-Language Concepts`.
61
+ - Experienced developer
62
+ Read `Route Contract At A Glance`, `When To Use Which Helper`, and `What A Real Integration Still Needs`.
63
+ - Debugging a runtime
64
+ Jump to `Troubleshooting`, `Common Mistakes`, and `Clear Error Handling`.
65
+
66
+ ## Who this is for
67
+
68
+ This SDK is for developers building PiPhi integrations in Python.
69
+
70
+ It is a good fit if you are using:
71
+
72
+ - FastAPI
73
+ - Starlette
74
+ - another async Python HTTP framework
75
+ - plain request/response handlers where you still want PiPhi helpers
76
+
77
+ If you are brand new to PiPhi, start with the golden path in this README and
78
+ then compare your code to the example app.
79
+
80
+ ## What the SDK handles
81
+
82
+ The SDK is meant to own the shared PiPhi runtime plumbing:
83
+
84
+ - runtime auth context
85
+ - request/header auth helpers
86
+ - optional FastAPI request helpers
87
+ - process state
88
+ - background task tracking
89
+ - telemetry delivery to PiPhi Core
90
+ - event delivery to PiPhi Core
91
+ - config sync helpers
92
+ - typed config validation helpers
93
+ - discovery normalization and response helpers
94
+ - lifecycle bootstrap helpers
95
+ - runtime health and diagnostics helpers
96
+ - in-memory runtime registry for active entries, state, and recent events
97
+ - clearer PiPhi-specific delivery errors
98
+
99
+ ## What stays in your integration
100
+
101
+ Your integration still owns vendor-specific behavior:
102
+
103
+ - how to discover devices
104
+ - how to talk to the vendor API or device
105
+ - how often to poll
106
+ - how to transform vendor data into PiPhi entities or telemetry
107
+ - which runtime events matter for that integration
108
+
109
+ This boundary is important. The SDK should make runtime plumbing easier, not
110
+ hide vendor logic behind an overly magical abstraction.
111
+
112
+ ## Install
113
+
114
+ The package currently supports Python `>=3.11`.
115
+
116
+ Install from a local checkout:
117
+
118
+ ```bash
119
+ pdm add /path/to/piphi-runtime-kit-python
120
+ ```
121
+
122
+ If you need the optional FastAPI helpers:
123
+
124
+ ```bash
125
+ pdm add "/path/to/piphi-runtime-kit-python[fastapi]"
126
+ ```
127
+
128
+ ## First 15 Minutes
129
+
130
+ If you want the shortest path to a working runtime, do this:
131
+
132
+ 1. install the SDK
133
+ 2. create a starter with `create_runtime_starter(...)`
134
+ 3. define one typed config model
135
+ 4. add `GET /health`
136
+ 5. add `POST /config`
137
+ 6. add one telemetry example route
138
+ 7. compare your result to the example app
139
+
140
+ Minimal success checklist:
141
+
142
+ - the runtime starts
143
+ - `/health` returns `200`
144
+ - `/config` stores one device in the registry
145
+ - a route can queue telemetry without crashing
146
+
147
+ If those four things work, you already have a real PiPhi runtime foundation.
148
+
149
+ ## Route Contract At A Glance
150
+
151
+ These are the most common runtime routes and what they are for.
152
+
153
+ | Route | Method | Usually Required | Purpose | Helpful SDK Pieces |
154
+ | --- | --- | --- | --- | --- |
155
+ | `/health` | `GET` | Yes | Basic runtime health | `starter.health_response(...)` |
156
+ | `/diagnostics` | `GET` | Yes | Support/debug details | `starter.diagnostics_response(...)` |
157
+ | `/discover` | `POST` | Usually | Find devices or accounts | `normalize_discovery_inputs(...)`, `build_discovery_response(...)` |
158
+ | `/config` | `POST` | Yes | Apply one config | `validate_typed_config(...)`, `build_config_apply_response(...)` |
159
+ | `/config/sync` | `POST` | Usually | Reconcile the runtime to a full snapshot | `validate_typed_configs(...)`, `config_sync.apply_snapshot(...)` |
160
+ | `/deconfigure` | `POST` | Usually | Remove one config | `RuntimeConfigRemoveResponse` |
161
+ | `/events` | `GET` | Common | Show recent local runtime events | `build_event_list_response(...)` |
162
+ | `/state` | `GET` | Common | Show current runtime state | `registry.entries`, `registry.state_snapshots` |
163
+ | `/entities` | `GET` | Integration-specific | Show normalized entity list | integration-owned |
164
+ | `/ui` or `/ui-config` | `GET` | Optional | Return config UI metadata | integration-owned |
165
+
166
+ ## UI Config Endpoints
167
+
168
+ Many integrations expose `/ui` or `/ui-config` so the PiPhi frontend knows how
169
+ to render a configuration form.
170
+
171
+ The important thing to know is:
172
+
173
+ - this is still just plain JSON
174
+ - the runtime SDK does not require a special wrapper for it
175
+ - your integration can return schema data directly
176
+
177
+ The usual pattern is to return:
178
+
179
+ - a JSON Schema object under `schema`
180
+ - a UI customization object under `uiSchema`
181
+
182
+ Simple example:
183
+
184
+ ```python
185
+ @app.get("/ui-config")
186
+ async def ui_config() -> dict[str, Any]:
187
+ return {
188
+ "schema": {
189
+ "title": "Demo Device Setup",
190
+ "type": "object",
191
+ "required": ["host"],
192
+ "properties": {
193
+ "host": {
194
+ "type": "string",
195
+ "title": "Host",
196
+ },
197
+ "alias": {
198
+ "type": "string",
199
+ "title": "Alias",
200
+ },
201
+ },
202
+ },
203
+ "uiSchema": {
204
+ "host": {
205
+ "placeholder": "192.168.1.50",
206
+ },
207
+ "alias": {
208
+ "placeholder": "Office Sensor",
209
+ },
210
+ },
211
+ }
212
+ ```
213
+
214
+ If your frontend uses `svelte-jsonschema-form`, the official docs are here:
215
+
216
+ - https://x0k.dev/svelte-jsonschema-form/
217
+
218
+ That library is a good fit when your frontend is already rendering JSON Schema
219
+ forms and you want integrations to stay simple by returning plain schema data.
220
+
221
+ For now, the recommended SDK approach is:
222
+
223
+ - document `/ui-config`
224
+ - return plain JSON schema/uiSchema objects
225
+ - keep any frontend-specific rendering helpers outside the runtime SDK
226
+
227
+ ## Optional MQTT Source Topics
228
+
229
+ Some integrations work better with a shared source stream than direct runtime-to-runtime HTTP calls.
230
+
231
+ Examples:
232
+
233
+ - `rtl_433` collectors
234
+ - packet capture helpers
235
+ - protocol bridges that many integrations may want to listen to
236
+
237
+ For those cases, the SDK now includes an optional MQTT helper with a small source-oriented topic contract.
238
+
239
+ Recommended topic layout:
240
+
241
+ - `piphi/sources/<source>/packets`
242
+ - `piphi/sources/<source>/models/<model>/packets`
243
+ - `piphi/sources/<source>/status`
244
+ - `piphi/sources/<source>/errors`
245
+
246
+ For `rtl_433`, the default shared packet topic is:
247
+
248
+ ```text
249
+ piphi/sources/rtl433/packets
250
+ ```
251
+
252
+ The payload should carry detailed device hints in the JSON body so subscribers do not need complex topic parsing just to identify a packet.
253
+
254
+ ## One Complete Mini Example
255
+
256
+ The snippets in this README are useful, but sometimes it helps to see one small
257
+ working shape in one place.
258
+
259
+ ```python
260
+ from fastapi import FastAPI, Request
261
+
262
+ from piphi_runtime_kit_python import (
263
+ RuntimeConfig,
264
+ build_config_apply_response,
265
+ create_runtime_starter,
266
+ schedule_telemetry_delivery,
267
+ validate_typed_config,
268
+ )
269
+ from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
270
+
271
+
272
+ class DemoConfig(RuntimeConfig):
273
+ host: str
274
+
275
+
276
+ starter = create_runtime_starter(
277
+ integration_id="demo-runtime",
278
+ integration_name="Demo Runtime",
279
+ version="0.1.0",
280
+ )
281
+ app = FastAPI()
282
+
283
+
284
+ @app.get("/health")
285
+ async def health():
286
+ return starter.health_response()
287
+
288
+
289
+ @app.post("/config")
290
+ async def config(payload: DemoConfig, request: Request):
291
+ sync_runtime_auth_from_fastapi_payload(starter.runtime, request, payload)
292
+ typed_payload = validate_typed_config(payload, DemoConfig)
293
+ starter.registry.set(
294
+ typed_payload.id,
295
+ {
296
+ "config_id": typed_payload.config_id or typed_payload.id,
297
+ "device_id": typed_payload.device_id or typed_payload.id,
298
+ "host": typed_payload.host,
299
+ },
300
+ )
301
+ return build_config_apply_response(config_id=typed_payload.config_id or typed_payload.id)
302
+
303
+
304
+ @app.post("/telemetry/example")
305
+ async def telemetry_example():
306
+ entry = starter.registry.primary_entry()
307
+ if entry is None:
308
+ return {"status": "skipped", "reason": "no configured device"}
309
+
310
+ schedule_telemetry_delivery(
311
+ process_state=starter.runtime.process_state,
312
+ telemetry_client=starter.telemetry_client,
313
+ auth_context=starter.runtime.auth,
314
+ device_id=str(entry["device_id"]),
315
+ metrics={"temperature_c": 21.4},
316
+ units={"temperature_c": "C"},
317
+ )
318
+ return {"status": "queued"}
319
+ ```
320
+
321
+ That example is not production-ready, but it is enough to show the most
322
+ important runtime ideas working together.
323
+
324
+ ## The Golden Path
325
+
326
+ If you only read one section, read this one. This is the intended beginner path.
327
+
328
+ ### 1. Create a starter
329
+
330
+ Start with `create_runtime_starter(...)`. It gives you one obvious object that
331
+ already contains the most common pieces:
332
+
333
+ - shared runtime auth and process state
334
+ - an in-memory registry
335
+ - a telemetry client
336
+ - an event client
337
+ - a config sync coordinator
338
+
339
+ ```python
340
+ from piphi_runtime_kit_python import create_runtime_starter
341
+
342
+ starter = create_runtime_starter(
343
+ integration_id="demo-runtime",
344
+ integration_name="Demo Runtime",
345
+ version="0.1.0",
346
+ )
347
+
348
+ runtime = starter.runtime
349
+ registry = starter.registry
350
+ telemetry = starter.telemetry_client
351
+ events = starter.event_client
352
+ config_sync = starter.config_sync
353
+ ```
354
+
355
+ For most new integrations, this is the right place to begin.
356
+
357
+ ### 2. Define a typed config model
358
+
359
+ Each integration should subclass `RuntimeConfig` with its own fields.
360
+
361
+ ```python
362
+ from piphi_runtime_kit_python import RuntimeConfig
363
+
364
+
365
+ class DemoDeviceConfig(RuntimeConfig):
366
+ host: str
367
+ alias: str | None = None
368
+ poll_interval_seconds: int = 30
369
+ ```
370
+
371
+ Use `validate_typed_config(...)` when accepting config payloads:
372
+
373
+ ```python
374
+ from piphi_runtime_kit_python import validate_typed_config
375
+
376
+ typed_payload = validate_typed_config(payload, DemoDeviceConfig)
377
+ ```
378
+
379
+ ### 3. Sync auth from each request
380
+
381
+ PiPhi runtimes receive auth and scope through headers. Your integration should
382
+ sync that into the runtime context for every relevant request.
383
+
384
+ Framework-agnostic usage:
385
+
386
+ ```python
387
+ runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)
388
+ ```
389
+
390
+ FastAPI helper usage:
391
+
392
+ ```python
393
+ from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
394
+
395
+ parsed = sync_runtime_auth_from_fastapi_payload(runtime, request, payload)
396
+ ```
397
+
398
+ ### 4. Store active runtime entries in the registry
399
+
400
+ Use the runtime registry for the in-memory working set of active devices.
401
+
402
+ ```python
403
+ registry.set(
404
+ typed_payload.id,
405
+ {
406
+ "device_id": typed_payload.id,
407
+ "config_id": typed_payload.config_id or typed_payload.id,
408
+ "integration_id": typed_payload.integration_id,
409
+ "host": typed_payload.host,
410
+ },
411
+ )
412
+ ```
413
+
414
+ This is not the source of truth for configs. PiPhi Core is. The registry is
415
+ just the runtime's active in-memory working set.
416
+
417
+ ### 5. Send telemetry and events
418
+
419
+ You can call the clients directly:
420
+
421
+ ```python
422
+ await starter.telemetry_client.send_metrics(
423
+ auth_context=starter.runtime.auth,
424
+ device_id="plug-1",
425
+ metrics={"is_on": True, "current_power_w": 13.2},
426
+ )
427
+ ```
428
+
429
+ Or queue delivery in the background:
430
+
431
+ ```python
432
+ from piphi_runtime_kit_python import (
433
+ schedule_event_delivery,
434
+ schedule_telemetry_delivery,
435
+ )
436
+
437
+ schedule_telemetry_delivery(
438
+ process_state=runtime.process_state,
439
+ telemetry_client=telemetry,
440
+ auth_context=runtime.auth,
441
+ device_id="plug-1",
442
+ metrics={"is_on": True},
443
+ container_id=runtime.auth.container_id,
444
+ )
445
+
446
+ schedule_event_delivery(
447
+ process_state=runtime.process_state,
448
+ event_client=events,
449
+ auth_context=runtime.auth,
450
+ event_type="device.turned_on",
451
+ device={
452
+ "device_id": "plug-1",
453
+ "config_id": "core-config-uuid",
454
+ "integration_id": "demo-runtime",
455
+ },
456
+ source="demo_runtime",
457
+ )
458
+ ```
459
+
460
+ ### 6. Expose the common runtime routes
461
+
462
+ Most runtimes should provide at least:
463
+
464
+ - `/health`
465
+ - `/diagnostics`
466
+ - `/discover`
467
+ - `/config`
468
+ - `/configs/sync` or `/config/sync`
469
+ - `/deconfigure`
470
+ - `/events`
471
+ - `/state`
472
+ - `/entities`
473
+
474
+ Some integrations also expose `/ui` or `/ui-config`.
475
+
476
+ ### 7. Use the example app as your checklist
477
+
478
+ The example app shows the intended flow end to end:
479
+
480
+ - [`examples/minimal_fastapi_runtime/app.py`](./examples/minimal_fastapi_runtime/app.py)
481
+ - [`examples/minimal_fastapi_runtime/README.md`](./examples/minimal_fastapi_runtime/README.md)
482
+
483
+ ## The IDs You Need To Understand
484
+
485
+ These are the identifiers that show up most often:
486
+
487
+ - `id`
488
+ This is the runtime's local config id in the payload your integration receives.
489
+ - `config_id`
490
+ This is the real PiPhi Core config UUID.
491
+ - `device_id`
492
+ This is the physical or logical device identifier used by the runtime.
493
+ - `container_id`
494
+ This is the runtime/container scope used for Core auth.
495
+ - `integration_id`
496
+ This is the installed integration id in Core.
497
+
498
+ The most common beginner mistake is confusing `id` with `config_id`.
499
+
500
+ A safe mental model is:
501
+
502
+ - `id` is local to the runtime payload shape
503
+ - `config_id` is the real Core identity for config-backed event flows
504
+
505
+ If you are sending events back to Core, `config_id`, `container_id`, and
506
+ `integration_id` need to be correct.
507
+
508
+ ## Plain-Language Concepts
509
+
510
+ If you are new to the platform, these terms can feel more complicated than they
511
+ really are. Here is the simple version.
512
+
513
+ - `snapshot`
514
+ A snapshot is just PiPhi saying, "Here is the full list of configs you should
515
+ have right now." You compare that list to what your runtime currently has.
516
+ Then you add the missing ones and remove the stale ones.
517
+ Example:
518
+ `await config_sync.apply_snapshot(snapshot=payload, active_config_ids=registry.ids(), apply_config=apply_config, remove_config=remove_config, get_active_config_ids=registry.ids)`
519
+ - `config sync`
520
+ Config sync is the process of making your runtime match the latest snapshot.
521
+ Think of it like refreshing a shopping list and making sure your cart matches it.
522
+ Example:
523
+ `typed_configs = validate_typed_configs(snapshot.configs, DemoDeviceConfig)`
524
+ - `registry`
525
+ The registry is the runtime's in-memory notebook. It keeps track of the
526
+ devices and state that are active right now.
527
+ Example:
528
+ `registry.set(config.id, {"device_id": config.device_id or config.id, "host": config.host})`
529
+ - `telemetry`
530
+ Telemetry is the stream of measurements, like temperature, humidity, power,
531
+ or signal strength.
532
+ Example:
533
+ `schedule_telemetry_delivery(process_state=runtime.process_state, telemetry_client=telemetry, auth_context=runtime.auth, device_id="plug-1", metrics={"temperature_c": 21.4}, units={"temperature_c": "C"})`
534
+ - `event`
535
+ An event is a meaningful thing that happened, like "device configured" or
536
+ "device turned on."
537
+ Example:
538
+ `registry.append_event(build_local_event_record(event_type="device.configured", device=entry, payload={"host": entry["host"]}, source="demo-runtime", severity="info"))`
539
+ - `container_id`
540
+ This is the identity of the running runtime process from Core's point of view.
541
+ It helps Core know which runtime is talking to it.
542
+ Example:
543
+ `runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)`
544
+ - `config_id`
545
+ This is the real Core-side id for a config. If a route or event needs the
546
+ official Core identity, this is the one that matters.
547
+ Example:
548
+ `entry = {"config_id": payload.config_id or payload.id, "device_id": payload.device_id or payload.id}`
549
+ - `device_id`
550
+ This is the actual device or logical thing you are monitoring or controlling.
551
+ One config often points at one device, but they are not always the same idea.
552
+ Example:
553
+ `await telemetry.send_metrics(auth_context=runtime.auth, device_id="plug-1", metrics={"is_on": True})`
554
+ - `starter`
555
+ The starter is the beginner-friendly bundle that gives you the common SDK
556
+ pieces in one place so you do not have to wire them up one by one.
557
+ Example:
558
+ `starter = create_runtime_starter(integration_id="demo-runtime", integration_name="Demo Runtime", version="0.1.0")`
559
+
560
+ ## When To Use Which Helper
561
+
562
+ Some helpers look similar at first. This is the fast way to choose.
563
+
564
+ | Use this | When you want | Notes |
565
+ | --- | --- | --- |
566
+ | `create_runtime_starter(...)` | one obvious SDK entry point | Best starting point for new integrations |
567
+ | `validate_typed_config(...)` | one incoming config validated into your model | Use in `/config` |
568
+ | `validate_typed_configs(...)` | many incoming configs validated at once | Use in `/config/sync` |
569
+ | `runtime.auth.sync_from_headers(...)` | sync auth in any framework | Lowest-level option |
570
+ | `sync_runtime_auth_from_fastapi_payload(...)` | sync auth in FastAPI with less boilerplate | Best FastAPI path |
571
+ | `telemetry_client.send_metrics(...)` | send telemetry right now | Use when you want direct control |
572
+ | `schedule_telemetry_delivery(...)` | queue telemetry in the background | Best for route handlers and poll loops |
573
+ | `event_client.send_event(...)` | send a Core event right now | Use when you want direct control |
574
+ | `schedule_event_delivery(...)` | queue Core event delivery in the background | Best for async runtime workflows |
575
+ | `build_local_event_record(...)` | record a runtime-local event | This is not the same as Core delivery |
576
+ | `config_sync.apply_snapshot(...)` | reconcile the runtime to a full snapshot | Best for `/config/sync` |
577
+ | `starter.health_response(...)` | return standard `/health` | Simple and recommended |
578
+ | `starter.diagnostics_response(...)` | return standard `/diagnostics` | Simple and recommended |
579
+
580
+ ## Typical Runtime Flow
581
+
582
+ Most integrations follow this shape:
583
+
584
+ 1. PiPhi calls your runtime.
585
+ 2. Your route syncs auth from headers.
586
+ 3. You validate the config payload into a typed model.
587
+ 4. You connect to the vendor API or local device.
588
+ 5. You store the active runtime entry in the registry.
589
+ 6. You begin polling or listening for updates.
590
+ 7. You send telemetry to Core.
591
+ 8. You emit meaningful events back to Core.
592
+ 9. You expose health and diagnostics so the runtime can be supported.
593
+
594
+ The SDK is designed to make steps `2`, `3`, `5`, `7`, `8`, and `9` easier.
595
+
596
+ ## Core Usage Patterns
597
+
598
+ ### Request auth helpers
599
+
600
+ The kit includes framework-agnostic helpers for extracting PiPhi runtime auth
601
+ from request-like header mappings.
602
+
603
+ ```python
604
+ from piphi_runtime_kit_python import extract_runtime_auth_headers
605
+
606
+ parsed = extract_runtime_auth_headers(request.headers)
607
+ runtime.auth.sync_from_headers(request.headers, payload_container_id="runtime-123")
608
+ ```
609
+
610
+ For FastAPI integrations, the optional adapter layer trims route boilerplate:
611
+
612
+ ```python
613
+ from piphi_runtime_kit_python import format_runtime_auth_sync_log
614
+ from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
615
+
616
+ parsed = sync_runtime_auth_from_fastapi_payload(runtime, request, payload)
617
+ logger.info(
618
+ format_runtime_auth_sync_log(
619
+ parsed,
620
+ payload_container_id=payload.container_id,
621
+ )
622
+ )
623
+ ```
624
+
625
+ ### Discovery helpers
626
+
627
+ Use the discovery helpers to normalize input and return a consistent response:
628
+
629
+ ```python
630
+ from piphi_runtime_kit_python import (
631
+ build_discovery_response,
632
+ format_discovery_attempt_log,
633
+ normalize_discovery_inputs,
634
+ )
635
+
636
+ inputs = normalize_discovery_inputs({"username": " user@example.com ", "password": " "})
637
+ logger.info(format_discovery_attempt_log(inputs=inputs))
638
+ response = build_discovery_response(devices)
639
+ ```
640
+
641
+ ### Event helpers
642
+
643
+ Use the event helpers when you want consistent runtime event logging and Core
644
+ event payload generation:
645
+
646
+ ```python
647
+ from piphi_runtime_kit_python import (
648
+ build_core_event_payload,
649
+ build_event_ingest_response,
650
+ format_event_log,
651
+ )
652
+
653
+ logger.info(format_event_log(payload))
654
+ event_response = build_event_ingest_response(event)
655
+
656
+ core_event = build_core_event_payload(
657
+ event_type="device.turned_on",
658
+ integration_id="demo-runtime",
659
+ config_id="core-config-uuid",
660
+ container_id="runtime-123",
661
+ device_id="plug-1",
662
+ payload={"host": "10.0.0.227"},
663
+ )
664
+ ```
665
+
666
+ ### Health and diagnostics helpers
667
+
668
+ The SDK can build consistent support endpoints:
669
+
670
+ ```python
671
+ from piphi_runtime_kit_python import (
672
+ build_runtime_diagnostics_response,
673
+ build_runtime_health_response,
674
+ )
675
+
676
+ health = build_runtime_health_response(
677
+ runtime,
678
+ integration={"id": "demo-runtime", "version": "0.1.0"},
679
+ )
680
+
681
+ diagnostics = build_runtime_diagnostics_response(
682
+ runtime,
683
+ integration={"id": "demo-runtime", "version": "0.1.0"},
684
+ diagnostics={"configured_device_ids": ["plug-1"]},
685
+ )
686
+ ```
687
+
688
+ These helpers include:
689
+
690
+ - pending task counts
691
+ - current config generation
692
+ - whether runtime auth is present
693
+ - whether a shared Core client is bound
694
+
695
+ ## Clear Error Handling
696
+
697
+ The SDK now classifies common delivery failures into PiPhi-specific errors.
698
+
699
+ Important examples:
700
+
701
+ - `CoreUnavailableError`
702
+ PiPhi Core could not be reached at all.
703
+ - `CoreTimeoutError`
704
+ PiPhi Core did not respond before the client timeout.
705
+ - `CoreRouteNotFoundError`
706
+ The expected Core route is not mounted or the URL is wrong.
707
+ - `CoreAuthError`
708
+ PiPhi Core rejected runtime auth.
709
+ - `CoreServerError`
710
+ PiPhi Core returned a server-side failure.
711
+
712
+ This is meant to make runtime logs easier to understand than raw `httpx`
713
+ exceptions alone.
714
+
715
+ ## Common Mistakes
716
+
717
+ - Mistake: using `id` where `config_id` should be used.
718
+ Wrong:
719
+ `{"config_id": payload.id}`
720
+ Right:
721
+ `{"config_id": payload.config_id or payload.id}`
722
+ Symptom:
723
+ Core event delivery may fail or point at the wrong config identity.
724
+ - Mistake: forgetting to sync auth from request headers before sending telemetry.
725
+ Wrong:
726
+ `await telemetry.send_metrics(auth_context=runtime.auth, device_id="plug-1", metrics={"is_on": True})`
727
+ Right:
728
+ `runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)`
729
+ Symptom:
730
+ Core may reject or ignore the request because the runtime context has no valid scope.
731
+ - Mistake: sending events without `integration_id`, `config_id`, or `container_id`.
732
+ Wrong:
733
+ building event payloads with only `device_id`
734
+ Right:
735
+ include the full device/config/runtime scope whenever the event goes back to Core
736
+ Symptom:
737
+ Core event delivery may fail or become ambiguous.
738
+ - Mistake: treating the registry as the source of truth instead of Core.
739
+ Wrong:
740
+ storing config state only in the registry and assuming that is enough
741
+ Right:
742
+ treat Core as the source of truth and the registry as runtime working memory
743
+ Symptom:
744
+ config sync and rehydrate flows drift from what Core expects.
745
+ - Mistake: putting polling cadence and vendor logic into the SDK layer instead of the integration.
746
+ Wrong:
747
+ expecting the SDK to decide vendor polling behavior
748
+ Right:
749
+ keep vendor behavior in the integration and use the SDK for runtime plumbing
750
+ Symptom:
751
+ the integration becomes harder to reason about and the SDK becomes too magical.
752
+
753
+ ## Troubleshooting
754
+
755
+ ### Symptom: telemetry times out
756
+
757
+ Check:
758
+
759
+ - PiPhi Core is reachable
760
+ - the request timeout is long enough for your environment
761
+ - Core is not busy or restarting
762
+ - your route is using `schedule_telemetry_delivery(...)` if background delivery is acceptable
763
+
764
+ ### Symptom: event delivery returns `404`
765
+
766
+ Check:
767
+
768
+ - the correct Core route exists
769
+ - the runtime is pointing at the right Core base URL
770
+ - `config_id` is the real Core config UUID
771
+ - `container_id` and `integration_id` are present
772
+
773
+ ### Symptom: Core is unavailable
774
+
775
+ Check:
776
+
777
+ - PiPhi Core is actually running
778
+ - the runtime can reach the host and port
779
+ - the SDK error is `CoreUnavailableError` and not a different failure class
780
+
781
+ ### Symptom: config sync removes devices unexpectedly
782
+
783
+ Check:
784
+
785
+ - the snapshot really contains the configs you expect
786
+ - your registry is storing the right active ids
787
+ - your `get_active_config_ids` callback matches what you actually applied
788
+ - you are not mixing local `id` and real `config_id`
789
+
790
+ ### Symptom: telemetry or events are rejected by Core
791
+
792
+ Check:
793
+
794
+ - auth was synced before delivery
795
+ - the runtime has a valid `container_id`
796
+ - the device/config scope is complete
797
+ - the typed delivery error explains whether this is auth, routing, timeout, or server failure
798
+
799
+ ## Example App
800
+
801
+ The example app is intentionally small, but it is meant to be a real reference:
802
+
803
+ - [`examples/minimal_fastapi_runtime/app.py`](./examples/minimal_fastapi_runtime/app.py)
804
+ - [`examples/minimal_fastapi_runtime/README.md`](./examples/minimal_fastapi_runtime/README.md)
805
+
806
+ ## Summary
807
+
808
+ If you are unsure where to begin:
809
+
810
+ 1. create a starter
811
+ 2. define a typed config model
812
+ 3. sync auth in every route
813
+ 4. store active entries in the registry
814
+ 5. send telemetry and events through the SDK
815
+ 6. compare your runtime to the example app
816
+
817
+ ## What belongs in the SDK
818
+
819
+ The SDK should own PiPhi-specific plumbing:
820
+
821
+ - runtime auth parsing and outbound Core headers
822
+ - config sync orchestration and typed config validation
823
+ - telemetry and Core event publishing
824
+ - health and diagnostics helpers
825
+ - background task tracking
826
+ - thin framework adapters
827
+
828
+ ## What stays in the integration
829
+
830
+ The integration should own vendor logic:
831
+
832
+ - device library calls and protocol handling
833
+ - discovery strategy specific to the vendor
834
+ - entity modeling and command behavior
835
+ - device-specific event semantics
836
+ - integration-specific UI schema and config fields
837
+
838
+ ## What A Real Integration Still Needs
839
+
840
+ Even with the SDK, a real integration still needs application code.
841
+
842
+ You still need to write:
843
+
844
+ - a vendor client or local device client
845
+ - discovery logic that makes sense for that vendor
846
+ - entity mapping for the PiPhi frontend and automation model
847
+ - polling or subscription logic
848
+ - command handling if the device supports actions
849
+ - integration-specific config fields and UI schema
850
+
851
+ The SDK is the runtime foundation, not the whole house.
852
+
853
+ ## Versioning and compatibility
854
+
855
+ See [VERSIONING.md](./VERSIONING.md) for:
856
+
857
+ - semver policy
858
+ - `0.x` stability expectations
859
+ - Core compatibility guidance
860
+ - release checklist notes
861
+
862
+ ## Planned direction
863
+
864
+ The kit is intentionally small. It should cover PiPhi runtime plumbing, not
865
+ device-specific integration logic.
866
+
867
+ Good candidates for future additions:
868
+
869
+ - framework adapters layered on top of the core runtime helpers
870
+ - more route-level helpers once the core abstractions stabilize
871
+
872
+ ## Included example
873
+
874
+ See [examples/minimal_fastapi_runtime](./examples/minimal_fastapi_runtime) for a
875
+ small reference runtime that shows how the kit fits together in a real app.