Build and drive CAR-TER layouts from Python — the control docs are the library.
Project description
carterkit
Build and drive CAR-TER layouts from Python.
The control docs are the library. Every control's schema, fields, and examples are parsed at runtime from the ControlDocs markdown bundled inside the package — the exact same docs the CAR-TER app renders — so the catalog never drifts from the definitions.
pip install carterkit
Explore the controls (zero config)
import carterkit
carterkit.controls() # {type: schema} for every placeable control
carterkit.doc("gauge") # full parsed doc: fields, themeFields, examples
print(carterkit.doc_markdown("gauge")) # the rendered documentation prose
carterkit.examples("button") # documented example snippets
Build a layout
Controls are methods on the layout, ids are positional, tabs and groups are context managers, and bindings fold into kwargs. Each control method returns a handle you can use as a binding target or patch later. Unknown control types and bad enum values raise instead of silently shipping a broken layout:
from carterkit import Layout
with Layout("Dashboard", cols=4, rows=4) as ui:
ui.connect("ws://192.168.1.50:8765", channel="home")
with ui.tab("Main", icon="gauge"):
cpu = ui.gauge("cpu", label="CPU", min=0, max=100, span=(2, 2),
listen="cpu", when={"msg_type": "metrics"})
ui.status_light("warn", visible=cpu > 90) # handle → visibility condition
ui.button("refresh", label="Refresh", send="refresh")
print(ui.findings()) # schema + grid + binding lint against the bundled catalog
ui.save("dashboard.json") # the composed layout, ready to push/load
Binding sugar: listen=/when=/event= build a sync, and send=/payload= build
an action — send="refresh" compiles to the one shape the relay actually forwards
(broadcast_request tagged msg_type: "refresh"; Hub.on demuxes it back for you).
Pass sync=[...]/action={...} (via carterkit.bind) for anything fancier. A handle
comparison (cpu > 90) becomes a real visibility condition; ==/!= stay normal
Python, so use .eq()/.neq(). help(carterkit.build.gauge) prints any control's
documentation, straight from the bundled docs.
Naming: multi-word controls are
snake_caseasLayoutmethods (ui.status_light(...),ui.log_console(...),ui.progress_ring(...)) butcamelCaseas the JSONtypeand ascarterkit.build.*functions ("statusLight",build.logConsole). Single-word controls (gauge,button) look the same either way. Grid size (cols/rows) set onLayout(...)is the default for every tab; override it per tab withui.tab("Name", rows=…).
Prefer a declarative style? A class veneer compiles to the same layout — ids come from attribute names, tabs/groups are nested classes (great for fixed dashboards; the flat builder reads better for generated ones):
from carterkit.declare import Screen, Tab, Connect, Gauge, Button, StatusLight
class Dashboard(Screen, cols=4, rows=4):
relay = Connect("ws://192.168.1.50:8765", channel="home")
class Main(Tab, icon="gauge"):
cpu = Gauge(label="CPU", min=0, max=100, span=(2, 2), listen="cpu")
warn = StatusLight(visible=cpu > 90)
refresh = Button(label="Refresh", send="refresh")
Dashboard.save("dashboard.json")
Dynamic groups
Generate controls in for/if loops (auto-placed in the group's own grid), or mark a
group dynamic="event" and replace its children live at runtime. Build that replacement
payload with Fragment, then lint it against the broadcasts your server actually emits —
catching events that never arrive, missing children arrays, and off-grid/invalid
injected controls before they ship:
import carterkit
from carterkit import Fragment
ui.group("Now Playing", span=(3, 4), cols=4, rows=3, dynamic="player_state")
frag = Fragment(cols=4, rows=3)
frag.label("title", text="Song", span=(1, 4))
frag.button("play", label="Play", send="play")
# your server broadcasts frag.payload("player_state") == {"msg_type": ..., "children": [...]}
print(carterkit.format_findings(
carterkit.lint_dynamic_traffic(ui.layout, [frag.payload("player_state")])))
Prefer surgical edits? LayoutBuffer gives add_control / update_control / move_control
over a held draft; lay.buffer exposes it.
infer.build_layout(payload) generates a wired layout from a sample telemetry dict;
codegen.generate_service_stub(layout) emits a runnable Hub-based server skeleton;
theming.theme_for(...) and tune.tune_gauge(...) round out the authoring tools.
CLI
carterkit catalog # list every control type
carterkit doc gauge # print a control's documentation
carterkit examples button # list a control's examples (--name to print one)
carterkit validate layout.json # lint a layout (exit 1 on errors)
carterkit gen layout.json # generate a runnable Hub server stub
carterkit relay --port 8765 # run the bundled MeshSocket relay
Drive the layout you just built
The layout already declares every control's wire contract, so the same object that
authored the UI also drives it — ctrl.push(value) derives the broadcast from the
control's sync binding, and @ctrl.on derives the demux from its action:
import asyncio
from carterkit import Layout
with Layout("Thermostat") as ui:
with ui.tab("Main"):
temp = ui.gauge("temp", label="Temp", min=0, max=40,
listen="temp", when={"msg_type": "climate"})
target = ui.slider("target", min=10, max=30, send="set_target")
async def main():
async with ui.serve() as hub: # zero config: embedded LocalRelay
print("pair the app with:", hub.qr_json())
await hub.wait_for_device()
await hub.push_layout() # routed apply-layout; echoes what rendered
@target.on
async def _(data):
heater.set(data["value"])
while True:
await temp.push(read_temp())
await asyncio.sleep(2)
asyncio.run(main())
The same surface works cross-process off the saved JSON — the layout file is the
contract: Hub("dashboard.json").push("temp", 21.5). Dynamic groups fill with
hub.fill(group, fragment); the hub answers late joiners with the last pushed values
(control-state authority) by default.
One connection story
Connection.parse(...) accepts every connection artifact in the ecosystem, and
ui.connect(...) / ui.serve(...) / Hub(...) all take it:
| You have | Pass | Works |
|---|---|---|
| nothing (LAN dev) | ui.serve() |
embedded LocalRelay, QR pairing |
| a self-hosted relay | "ws://192.168.1.50:8765" (+ token=) |
symmetric: same config for app & hub |
| Connect+ | the Add Device JSON from the app (Members → Add Device) | token self-refresh + room E2EE automatic |
The asymmetry to know: self-hosted is symmetric (one URL + shared key both sides); on Connect+ the app joins with its own account while the hub holds the per-device credential — which is the hub's identity, so it is never embedded into a layout.
CarterClient remains the lower-level client (on/broadcast/request).
End-to-end encryption (ChaCha20-Poly1305 + per-session salt) is transparent when an
e2ee_key is present. Send a push to every device on a Connect+ account with
CarterClient.notify(...) or the stdlib-only carterkit.notify_http(...).
Built on
meshsocket — the WebSocket mesh transport.
The ControlDocs are vendored from the CAR-TER app repo; refresh them with
scripts/sync-controldocs.sh.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file carterkit-0.6.0.tar.gz.
File metadata
- Download URL: carterkit-0.6.0.tar.gz
- Upload date:
- Size: 156.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5792c1124b1a5ea6f4d31227d08b77e162ee8ccbf80eaa621037de0b2f7f5a9c
|
|
| MD5 |
f37b0d28de53ec40857f681bbebed301
|
|
| BLAKE2b-256 |
109e49c9726b914d063018a510f9096b61fbeba502e1a4215c81e5c39a71adf8
|
Provenance
The following attestation bundles were made for carterkit-0.6.0.tar.gz:
Publisher:
publish.yml on Mariner10/carterkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
carterkit-0.6.0.tar.gz -
Subject digest:
5792c1124b1a5ea6f4d31227d08b77e162ee8ccbf80eaa621037de0b2f7f5a9c - Sigstore transparency entry: 2170786705
- Sigstore integration time:
-
Permalink:
Mariner10/carterkit@b6f313ec3b72098063a5ea428316c3d286c49395 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/Mariner10
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b6f313ec3b72098063a5ea428316c3d286c49395 -
Trigger Event:
push
-
Statement type:
File details
Details for the file carterkit-0.6.0-py3-none-any.whl.
File metadata
- Download URL: carterkit-0.6.0-py3-none-any.whl
- Upload date:
- Size: 178.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1325ca6a612441531552fdde3c44ddba599c60dffeb0a69929f5160eb2211353
|
|
| MD5 |
ec7ef9736aca89e827bc5e8986cb6676
|
|
| BLAKE2b-256 |
dca7e7c44a3ed07039e9deb9cb40f049fb682a00cd13affff005651039b3d055
|
Provenance
The following attestation bundles were made for carterkit-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on Mariner10/carterkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
carterkit-0.6.0-py3-none-any.whl -
Subject digest:
1325ca6a612441531552fdde3c44ddba599c60dffeb0a69929f5160eb2211353 - Sigstore transparency entry: 2170786756
- Sigstore integration time:
-
Permalink:
Mariner10/carterkit@b6f313ec3b72098063a5ea428316c3d286c49395 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/Mariner10
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b6f313ec3b72098063a5ea428316c3d286c49395 -
Trigger Event:
push
-
Statement type: