carterkit 0.1.0__tar.gz → 0.3.0__tar.gz
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.
- {carterkit-0.1.0 → carterkit-0.3.0}/PKG-INFO +39 -18
- {carterkit-0.1.0 → carterkit-0.3.0}/README.md +38 -17
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/__init__.py +5 -1
- carterkit-0.3.0/carterkit/__main__.py +3 -0
- carterkit-0.3.0/carterkit/bind.py +40 -0
- carterkit-0.3.0/carterkit/cli.py +128 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/client.py +8 -0
- carterkit-0.3.0/carterkit/controls.py +90 -0
- carterkit-0.3.0/carterkit/layout.py +87 -0
- carterkit-0.3.0/carterkit/py.typed +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/validate.py +20 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit.egg-info/PKG-INFO +39 -18
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit.egg-info/SOURCES.txt +14 -1
- carterkit-0.3.0/carterkit.egg-info/entry_points.txt +2 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/pyproject.toml +5 -2
- carterkit-0.3.0/tests/test_bind.py +40 -0
- carterkit-0.3.0/tests/test_cli.py +53 -0
- carterkit-0.3.0/tests/test_client.py +32 -0
- carterkit-0.3.0/tests/test_controls.py +54 -0
- carterkit-0.3.0/tests/test_layout.py +40 -0
- carterkit-0.3.0/tests/test_validate_bindings.py +40 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/LICENSE +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/buffer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/catalog.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/codegen.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/accordion.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/actions.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/animations.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/appearance.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/button.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/cardList.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/carousel.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/chat.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/color-picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/control-def.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/date-picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/divider.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/flip-card.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/gauge.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/graph.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/group-def.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/haptics.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/image.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/index.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/joystick.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/label.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/layout-config.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/list.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/log-console.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/long-press.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/map.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/privacy.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/progress-ring.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/pulse.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/qr-code.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/segmented.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/slider.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/spacer.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/sparkline.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/status-light.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/stepper.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/sync.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/terms.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/text-input.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/theming.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/toggle.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/visibility.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/controldocs/web-view.md +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/e2ee.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/grid.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/infer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/theming.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit/tune.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit.egg-info/dependency_links.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit.egg-info/requires.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/carterkit.egg-info/top_level.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/setup.cfg +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_buffer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_catalog.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_codegen.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_e2ee.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_grid.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_infer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_theming.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_tune.py +0 -0
- {carterkit-0.1.0 → carterkit-0.3.0}/tests/test_validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: carterkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Build and drive CAR-TER layouts from Python — the control docs are the library.
|
|
5
5
|
Author: Carter Beaudoin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,23 +51,45 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
from
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
b.add_control({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
|
|
59
|
-
default_span=[2, 2])
|
|
60
|
-
b.add_control({"type": "button", "id": "refresh", "label": "Refresh"})
|
|
54
|
+
The fluent `Layout` builder composes **typed builders** (`build.<control>`) and
|
|
55
|
+
**binding helpers** (`bind`) — all generated from / shaped by the bundled docs, so
|
|
56
|
+
unknown control types and bad enum values raise instead of silently shipping a broken
|
|
57
|
+
layout:
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
```python
|
|
60
|
+
from carterkit import Layout, build, bind
|
|
61
|
+
|
|
62
|
+
lay = (Layout("Dashboard", columns=4, rows=4)
|
|
63
|
+
.connect("ws://192.168.1.50:8765", channel="home")
|
|
64
|
+
.tab("Main", icon="gauge")
|
|
65
|
+
.add(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
66
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
67
|
+
default_span=[2, 2])
|
|
68
|
+
.add(build.button(id="refresh", label="Refresh", action=bind.action("refresh"))))
|
|
69
|
+
|
|
70
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
71
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
72
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
76
|
+
over a held draft; `lay.buffer` exposes it.
|
|
77
|
+
|
|
67
78
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
68
79
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
69
80
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
70
81
|
|
|
82
|
+
## CLI
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
carterkit catalog # list every control type
|
|
86
|
+
carterkit doc gauge # print a control's documentation
|
|
87
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
88
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
89
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
90
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
91
|
+
```
|
|
92
|
+
|
|
71
93
|
## Drive a device
|
|
72
94
|
|
|
73
95
|
```python
|
|
@@ -75,13 +97,12 @@ import asyncio
|
|
|
75
97
|
from carterkit import CarterClient
|
|
76
98
|
|
|
77
99
|
async def main():
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await c.close()
|
|
100
|
+
async with CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
|
|
101
|
+
channel="home", role="device", name="my-hub") as c:
|
|
102
|
+
c.on("toggle", lambda d: {"ok": True, **d})
|
|
103
|
+
await c.broadcast("reading", {"temp_c": 21.4})
|
|
104
|
+
await asyncio.sleep(60)
|
|
105
|
+
# leaving the `async with` disconnects automatically
|
|
85
106
|
|
|
86
107
|
asyncio.run(main())
|
|
87
108
|
```
|
|
@@ -28,23 +28,45 @@ carterkit.examples("button") # documented example snippets
|
|
|
28
28
|
|
|
29
29
|
## Build a layout
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
from
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
b.add_control({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
|
|
36
|
-
default_span=[2, 2])
|
|
37
|
-
b.add_control({"type": "button", "id": "refresh", "label": "Refresh"})
|
|
31
|
+
The fluent `Layout` builder composes **typed builders** (`build.<control>`) and
|
|
32
|
+
**binding helpers** (`bind`) — all generated from / shaped by the bundled docs, so
|
|
33
|
+
unknown control types and bad enum values raise instead of silently shipping a broken
|
|
34
|
+
layout:
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
```python
|
|
37
|
+
from carterkit import Layout, build, bind
|
|
38
|
+
|
|
39
|
+
lay = (Layout("Dashboard", columns=4, rows=4)
|
|
40
|
+
.connect("ws://192.168.1.50:8765", channel="home")
|
|
41
|
+
.tab("Main", icon="gauge")
|
|
42
|
+
.add(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
43
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
44
|
+
default_span=[2, 2])
|
|
45
|
+
.add(build.button(id="refresh", label="Refresh", action=bind.action("refresh"))))
|
|
46
|
+
|
|
47
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
48
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
49
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
42
50
|
```
|
|
43
51
|
|
|
52
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
53
|
+
over a held draft; `lay.buffer` exposes it.
|
|
54
|
+
|
|
44
55
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
45
56
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
46
57
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
47
58
|
|
|
59
|
+
## CLI
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
carterkit catalog # list every control type
|
|
63
|
+
carterkit doc gauge # print a control's documentation
|
|
64
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
65
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
66
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
67
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
68
|
+
```
|
|
69
|
+
|
|
48
70
|
## Drive a device
|
|
49
71
|
|
|
50
72
|
```python
|
|
@@ -52,13 +74,12 @@ import asyncio
|
|
|
52
74
|
from carterkit import CarterClient
|
|
53
75
|
|
|
54
76
|
async def main():
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await c.close()
|
|
77
|
+
async with CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
|
|
78
|
+
channel="home", role="device", name="my-hub") as c:
|
|
79
|
+
c.on("toggle", lambda d: {"ok": True, **d})
|
|
80
|
+
await c.broadcast("reading", {"temp_c": 21.4})
|
|
81
|
+
await asyncio.sleep(60)
|
|
82
|
+
# leaving the `async with` disconnects automatically
|
|
62
83
|
|
|
63
84
|
asyncio.run(main())
|
|
64
85
|
```
|
|
@@ -19,8 +19,11 @@ from . import catalog, grid, codegen, infer, theming, tune
|
|
|
19
19
|
from .buffer import LayoutBuffer, BufferError
|
|
20
20
|
from .validate import validate_layout as _validate_layout, format_findings
|
|
21
21
|
from .client import CarterClient, notify_http, CarterNotifyError
|
|
22
|
+
from . import bind
|
|
23
|
+
from .controls import build, control
|
|
24
|
+
from .layout import Layout
|
|
22
25
|
|
|
23
|
-
__version__ = "0.
|
|
26
|
+
__version__ = "0.2.0"
|
|
24
27
|
|
|
25
28
|
#: The layout/wire protocol version carterkit emits and understands. The JSON
|
|
26
29
|
#: contract — not this Python API — is the real compatibility boundary across the
|
|
@@ -65,5 +68,6 @@ __all__ = [
|
|
|
65
68
|
"LayoutBuffer", "BufferError",
|
|
66
69
|
"controls", "doc", "doc_markdown", "examples", "validate_layout",
|
|
67
70
|
"format_findings", "controldocs_dir",
|
|
71
|
+
"build", "control", "bind", "Layout",
|
|
68
72
|
"catalog", "grid", "codegen", "infer", "theming", "tune",
|
|
69
73
|
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Helpers that build the (verbose, easy-to-get-wrong) sync / action / connection
|
|
2
|
+
dicts controls use. Shapes mirror the bundled ControlDocs (sync.md, actions.md).
|
|
3
|
+
|
|
4
|
+
from carterkit import bind
|
|
5
|
+
bind.listen("cpu", filter={"msg_type": "telemetry"})
|
|
6
|
+
bind.action("set_power", payload={"state": "{{value}}"}, mode="request")
|
|
7
|
+
bind.connection("ws://192.168.1.50:8765", channel="home")
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def listen(value_path: str, *, event: str = "broadcast", filter: dict | None = None,
|
|
13
|
+
method: str = "meshsocket") -> dict:
|
|
14
|
+
"""A `sync` entry: subscribe to `event`, match `filter`, extract `value_path`
|
|
15
|
+
(dot-notation). Returns one sync dict — controls take a list of them."""
|
|
16
|
+
s: dict = {"method": method, "type": "listen", "event": event, "valuePath": value_path}
|
|
17
|
+
if filter is not None:
|
|
18
|
+
s["filter"] = filter
|
|
19
|
+
return s
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def action(event: str, *, payload: dict | None = None, mode: str = "broadcast",
|
|
23
|
+
method: str = "meshsocket") -> dict:
|
|
24
|
+
"""An `action` dict: fire `event` on tap/change. `mode` is "broadcast" (fire and
|
|
25
|
+
forget) or "request" (await reply). `payload` strings support `{{value}}`."""
|
|
26
|
+
if mode not in ("broadcast", "request"):
|
|
27
|
+
raise ValueError(f"mode must be 'broadcast' or 'request', got {mode!r}")
|
|
28
|
+
a: dict = {"method": method, "mode": mode, "event": event}
|
|
29
|
+
if payload is not None:
|
|
30
|
+
a["payload"] = payload
|
|
31
|
+
return a
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def connection(url: str, *, channel: str = "home", name: str = "CAR-TER",
|
|
35
|
+
role: str = "controller", token: str | None = None) -> dict:
|
|
36
|
+
"""A layout `connection` block (relay URL + identity)."""
|
|
37
|
+
conn: dict = {"url": url, "identity": {"name": name, "channel": channel, "role": role}}
|
|
38
|
+
if token is not None:
|
|
39
|
+
conn["token"] = token
|
|
40
|
+
return conn
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Command-line interface: ``carterkit <command>`` (also ``python -m carterkit``).
|
|
2
|
+
|
|
3
|
+
Commands: catalog · doc · examples · validate · gen · relay · version.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _cmd_catalog(args) -> int:
|
|
13
|
+
import carterkit
|
|
14
|
+
cat = carterkit.controls(types=[args.type] if args.type else None, include_theme=args.theme)
|
|
15
|
+
if args.json:
|
|
16
|
+
print(json.dumps(cat, indent=2))
|
|
17
|
+
else:
|
|
18
|
+
for t in sorted(cat):
|
|
19
|
+
spec = cat[t]
|
|
20
|
+
print(f"{t:18} {spec.get('label', ''):16} ({spec.get('category', '')})")
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _cmd_doc(args) -> int:
|
|
25
|
+
import carterkit
|
|
26
|
+
md = carterkit.doc_markdown(args.control)
|
|
27
|
+
if md is None:
|
|
28
|
+
print(f"no doc for {args.control!r}", file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
print(md)
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cmd_examples(args) -> int:
|
|
35
|
+
import carterkit
|
|
36
|
+
if args.name:
|
|
37
|
+
from carterkit import catalog, controldocs_dir
|
|
38
|
+
ex = catalog.find_example(controldocs_dir(), args.control, args.name)
|
|
39
|
+
if not ex:
|
|
40
|
+
print(f"no example {args.name!r} for {args.control!r}", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
print(ex["json"])
|
|
43
|
+
return 0
|
|
44
|
+
exs = carterkit.examples(args.control)
|
|
45
|
+
if not exs:
|
|
46
|
+
print(f"no examples for {args.control!r}", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
for ex in exs:
|
|
49
|
+
print("•", ex["name"])
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cmd_validate(args) -> int:
|
|
54
|
+
import carterkit
|
|
55
|
+
with open(args.file) as f:
|
|
56
|
+
layout = json.load(f)
|
|
57
|
+
findings = carterkit.validate_layout(layout)
|
|
58
|
+
print(carterkit.format_findings(findings))
|
|
59
|
+
return 1 if any(f["severity"] == "error" for f in findings) else 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_gen(args) -> int:
|
|
63
|
+
import carterkit
|
|
64
|
+
with open(args.file) as f:
|
|
65
|
+
layout = json.load(f)
|
|
66
|
+
print(carterkit.codegen.generate_service_stub(layout))
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cmd_relay(args) -> int:
|
|
71
|
+
import asyncio
|
|
72
|
+
from socket_server import MeshServer # bundled with meshsocket
|
|
73
|
+
print(f"MeshSocket relay on ws://{args.host}:{args.port}", file=sys.stderr)
|
|
74
|
+
asyncio.run(MeshServer(host=args.host, port=args.port).start())
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _cmd_version(args) -> int:
|
|
79
|
+
import carterkit
|
|
80
|
+
print(carterkit.__version__)
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
85
|
+
p = argparse.ArgumentParser(prog="carterkit", description="Build and drive CAR-TER layouts.")
|
|
86
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
87
|
+
|
|
88
|
+
c = sub.add_parser("catalog", help="list the control catalog")
|
|
89
|
+
c.add_argument("--type", help="restrict to one control type")
|
|
90
|
+
c.add_argument("--theme", action="store_true", help="include per-control theme fields")
|
|
91
|
+
c.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
|
92
|
+
c.set_defaults(fn=_cmd_catalog)
|
|
93
|
+
|
|
94
|
+
c = sub.add_parser("doc", help="print a control's documentation")
|
|
95
|
+
c.add_argument("control")
|
|
96
|
+
c.set_defaults(fn=_cmd_doc)
|
|
97
|
+
|
|
98
|
+
c = sub.add_parser("examples", help="list a control's examples, or print one with --name")
|
|
99
|
+
c.add_argument("control")
|
|
100
|
+
c.add_argument("--name", help="print the JSON of the named example")
|
|
101
|
+
c.set_defaults(fn=_cmd_examples)
|
|
102
|
+
|
|
103
|
+
c = sub.add_parser("validate", help="lint a layout JSON file (exit 1 on errors)")
|
|
104
|
+
c.add_argument("file")
|
|
105
|
+
c.set_defaults(fn=_cmd_validate)
|
|
106
|
+
|
|
107
|
+
c = sub.add_parser("gen", help="generate a MeshSocket service stub from a layout file")
|
|
108
|
+
c.add_argument("file")
|
|
109
|
+
c.set_defaults(fn=_cmd_gen)
|
|
110
|
+
|
|
111
|
+
c = sub.add_parser("relay", help="run the bundled MeshSocket relay")
|
|
112
|
+
c.add_argument("--host", default="0.0.0.0")
|
|
113
|
+
c.add_argument("--port", type=int, default=8765)
|
|
114
|
+
c.set_defaults(fn=_cmd_relay)
|
|
115
|
+
|
|
116
|
+
c = sub.add_parser("version", help="print the carterkit version")
|
|
117
|
+
c.set_defaults(fn=_cmd_version)
|
|
118
|
+
|
|
119
|
+
return p
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main(argv=None) -> int:
|
|
123
|
+
args = build_parser().parse_args(argv)
|
|
124
|
+
return args.fn(args)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Typed control constructors, generated at runtime from the bundled ControlDocs.
|
|
2
|
+
|
|
3
|
+
Because the catalog *is* the docs, every placeable control type is available as a
|
|
4
|
+
builder whose validation and `help()` come straight from its documentation:
|
|
5
|
+
|
|
6
|
+
from carterkit import build
|
|
7
|
+
build.gauge(id="cpu", min=0, max=100, sync=[bind.listen("cpu")])
|
|
8
|
+
build.color_picker(id="tint") # snake_case maps to the catalog type
|
|
9
|
+
help(build.gauge) # prints the gauge documentation
|
|
10
|
+
|
|
11
|
+
Unknown control types and bad enum values raise; unknown props are allowed
|
|
12
|
+
(forward-compatible — the device ignores props it doesn't understand).
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
|
|
18
|
+
from . import catalog
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def _catalog() -> dict:
|
|
23
|
+
from . import controldocs_dir
|
|
24
|
+
return catalog.build_catalog(controldocs_dir())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_type(name: str) -> str | None:
|
|
28
|
+
"""Map an attribute name to a catalog control type: exact, or snake_case→camelCase."""
|
|
29
|
+
cat = _catalog()
|
|
30
|
+
if name in cat:
|
|
31
|
+
return name
|
|
32
|
+
head, *rest = name.split("_")
|
|
33
|
+
camel = head + "".join(p.title() for p in rest)
|
|
34
|
+
return camel if camel in cat else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def control(type: str, *, id: str, position=None, span=None, **props) -> dict:
|
|
38
|
+
"""Build and validate one control dict. `id` is required; `position`/`span` are
|
|
39
|
+
optional. Raises on an unknown control type or an out-of-range enum value."""
|
|
40
|
+
cat = _catalog()
|
|
41
|
+
if type not in cat:
|
|
42
|
+
raise ValueError(f"unknown control type {type!r}. Known: {', '.join(sorted(cat))}")
|
|
43
|
+
spec = cat[type]
|
|
44
|
+
enums = {f["name"]: f["values"] for f in spec.get("fields", [])
|
|
45
|
+
if f.get("type") == "enum" and f.get("values")}
|
|
46
|
+
for key, val in props.items():
|
|
47
|
+
if key in enums and val not in enums[key]:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"{type}.{key}={val!r} is not a valid option; choose one of {enums[key]}")
|
|
50
|
+
out: dict = {"type": type, "id": id}
|
|
51
|
+
if position is not None:
|
|
52
|
+
out["position"] = list(position)
|
|
53
|
+
if span is not None:
|
|
54
|
+
out["span"] = list(span)
|
|
55
|
+
out.update(props)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _Controls:
|
|
60
|
+
"""Attribute access returns a builder bound to a catalog control type."""
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name: str):
|
|
63
|
+
ctype = _resolve_type(name)
|
|
64
|
+
if ctype is None:
|
|
65
|
+
raise AttributeError(
|
|
66
|
+
f"no control type {name!r}. Available: {', '.join(self.__dir__())}")
|
|
67
|
+
|
|
68
|
+
def make(*, id: str, position=None, span=None, **props) -> dict:
|
|
69
|
+
return control(ctype, id=id, position=position, span=span, **props)
|
|
70
|
+
|
|
71
|
+
doc = catalog.resolve_doc(_catalog_dir(), ctype)
|
|
72
|
+
make.__name__ = ctype
|
|
73
|
+
make.__qualname__ = f"controls.{ctype}"
|
|
74
|
+
make.__doc__ = (doc or {}).get("body") or f"Build a {ctype} control."
|
|
75
|
+
return make
|
|
76
|
+
|
|
77
|
+
def __dir__(self):
|
|
78
|
+
return sorted(_catalog())
|
|
79
|
+
|
|
80
|
+
def types(self) -> list[str]:
|
|
81
|
+
"""All placeable control types available as builders."""
|
|
82
|
+
return sorted(_catalog())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _catalog_dir():
|
|
86
|
+
from . import controldocs_dir
|
|
87
|
+
return controldocs_dir()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
build = _Controls()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""A fluent layout builder — the ergonomic front door over LayoutBuffer.
|
|
2
|
+
|
|
3
|
+
from carterkit import Layout, build, bind
|
|
4
|
+
|
|
5
|
+
layout = (Layout("Dashboard", columns=4, rows=4)
|
|
6
|
+
.connect("ws://192.168.1.50:8765", channel="home")
|
|
7
|
+
.tab("Main", icon="gauge")
|
|
8
|
+
.add(build.gauge(id="cpu", min=0, max=100,
|
|
9
|
+
sync=[bind.listen("cpu")]), default_span=[2, 2])
|
|
10
|
+
.add(build.button(id="refresh", action=bind.action("refresh")))
|
|
11
|
+
.layout)
|
|
12
|
+
|
|
13
|
+
Auto-placement, id de-duplication, and grid bookkeeping come from LayoutBuffer;
|
|
14
|
+
`.validate()` lints against the bundled catalog. Chainable: every mutator returns self.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
from . import bind as _bind
|
|
21
|
+
from . import validate as _validate
|
|
22
|
+
from .buffer import LayoutBuffer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Layout:
|
|
26
|
+
def __init__(self, name: str = "Layout", *, columns: int = 4, rows: int = 6,
|
|
27
|
+
accent: str = "#667eea"):
|
|
28
|
+
self._buf = LayoutBuffer.blank(name=name, columns=columns, rows=rows, accent=accent)
|
|
29
|
+
self._tab = 0
|
|
30
|
+
self._first_tab_used = False
|
|
31
|
+
|
|
32
|
+
def connect(self, url: str, **identity) -> "Layout":
|
|
33
|
+
"""Attach a `connection` block (see bind.connection for the identity kwargs)."""
|
|
34
|
+
self._buf.layout["connection"] = _bind.connection(url, **identity)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def tab(self, title: str, *, icon: str = "square.grid.2x2",
|
|
38
|
+
columns: int = 4, rows: int = 6) -> "Layout":
|
|
39
|
+
"""Start a tab and make it current. The first call configures the default tab;
|
|
40
|
+
later calls append new tabs."""
|
|
41
|
+
if not self._first_tab_used:
|
|
42
|
+
t = self._buf.tabs[0]
|
|
43
|
+
t["title"], t["icon"] = title, icon
|
|
44
|
+
t["grid"] = {"columns": columns, "rows": rows}
|
|
45
|
+
self._tab = 0
|
|
46
|
+
self._first_tab_used = True
|
|
47
|
+
else:
|
|
48
|
+
self._tab = self._buf.add_tab(title, icon=icon, columns=columns, rows=rows)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def add(self, control: dict, *, position=None, span=None, default_span=None) -> "Layout":
|
|
52
|
+
"""Add a control to the current tab (auto-placed unless `position` is given)."""
|
|
53
|
+
self._buf.add_control(control, tab_index=self._tab, position=position,
|
|
54
|
+
default_span=default_span or span)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def group(self, group: dict, *, position=None) -> "Layout":
|
|
58
|
+
"""Add a group container to the current tab."""
|
|
59
|
+
self._buf.add_group(group, tab_index=self._tab, position=position)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def layout(self) -> dict:
|
|
64
|
+
"""The composed layout dict (ready to push/save)."""
|
|
65
|
+
return self._buf.layout
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def buffer(self) -> LayoutBuffer:
|
|
69
|
+
"""The underlying LayoutBuffer, for advanced ops (update/move/remove)."""
|
|
70
|
+
return self._buf
|
|
71
|
+
|
|
72
|
+
def validate(self) -> list:
|
|
73
|
+
"""Lint against the bundled control catalog."""
|
|
74
|
+
import carterkit
|
|
75
|
+
return carterkit.validate_layout(self.layout)
|
|
76
|
+
|
|
77
|
+
def findings(self) -> str:
|
|
78
|
+
"""Human-readable lint report."""
|
|
79
|
+
return _validate.format_findings(self.validate())
|
|
80
|
+
|
|
81
|
+
def json(self, indent: int = 2) -> str:
|
|
82
|
+
return json.dumps(self.layout, indent=indent)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
n = sum(len(t.get("children", [])) for t in self._buf.tabs)
|
|
86
|
+
return (f"<Layout {self.layout.get('name')!r}: "
|
|
87
|
+
f"{len(self._buf.tabs)} tab(s), {n} control(s)>")
|
|
File without changes
|
|
@@ -116,6 +116,26 @@ def _validate_child(ch, catalog, where, findings, seen_ids):
|
|
|
116
116
|
if v not in fd["values"]:
|
|
117
117
|
findings.append(_f("error", "bad_enum", spot,
|
|
118
118
|
f"{ctype}.{k} = '{v}' is not one of {fd['values']}"))
|
|
119
|
+
_validate_bindings(ch, ctype, spot, findings)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _validate_bindings(ch, ctype, spot, findings):
|
|
123
|
+
"""Shape-check the data bindings: every `sync` entry needs a `valuePath`, and
|
|
124
|
+
`action`/`longPressAction` need an `event` (else they silently do nothing)."""
|
|
125
|
+
sync = ch.get("sync")
|
|
126
|
+
if sync is not None:
|
|
127
|
+
if not isinstance(sync, list):
|
|
128
|
+
findings.append(_f("warn", "bad_sync", spot, f"{ctype}.sync should be a list"))
|
|
129
|
+
else:
|
|
130
|
+
for i, s in enumerate(sync):
|
|
131
|
+
if not isinstance(s, dict) or not s.get("valuePath"):
|
|
132
|
+
findings.append(_f("warn", "bad_sync", spot,
|
|
133
|
+
f"{ctype}.sync[{i}] is missing a 'valuePath'"))
|
|
134
|
+
for akey in ("action", "longPressAction"):
|
|
135
|
+
a = ch.get(akey)
|
|
136
|
+
if a is not None and (not isinstance(a, dict) or not a.get("event")):
|
|
137
|
+
findings.append(_f("error", "bad_action", spot,
|
|
138
|
+
f"{ctype}.{akey} is missing an 'event'"))
|
|
119
139
|
|
|
120
140
|
|
|
121
141
|
def format_findings(findings: list[dict]) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: carterkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Build and drive CAR-TER layouts from Python — the control docs are the library.
|
|
5
5
|
Author: Carter Beaudoin
|
|
6
6
|
License-Expression: MIT
|
|
@@ -51,23 +51,45 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
from
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
b.add_control({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
|
|
59
|
-
default_span=[2, 2])
|
|
60
|
-
b.add_control({"type": "button", "id": "refresh", "label": "Refresh"})
|
|
54
|
+
The fluent `Layout` builder composes **typed builders** (`build.<control>`) and
|
|
55
|
+
**binding helpers** (`bind`) — all generated from / shaped by the bundled docs, so
|
|
56
|
+
unknown control types and bad enum values raise instead of silently shipping a broken
|
|
57
|
+
layout:
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
```python
|
|
60
|
+
from carterkit import Layout, build, bind
|
|
61
|
+
|
|
62
|
+
lay = (Layout("Dashboard", columns=4, rows=4)
|
|
63
|
+
.connect("ws://192.168.1.50:8765", channel="home")
|
|
64
|
+
.tab("Main", icon="gauge")
|
|
65
|
+
.add(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
66
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
67
|
+
default_span=[2, 2])
|
|
68
|
+
.add(build.button(id="refresh", label="Refresh", action=bind.action("refresh"))))
|
|
69
|
+
|
|
70
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
71
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
72
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
76
|
+
over a held draft; `lay.buffer` exposes it.
|
|
77
|
+
|
|
67
78
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
68
79
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
69
80
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
70
81
|
|
|
82
|
+
## CLI
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
carterkit catalog # list every control type
|
|
86
|
+
carterkit doc gauge # print a control's documentation
|
|
87
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
88
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
89
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
90
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
91
|
+
```
|
|
92
|
+
|
|
71
93
|
## Drive a device
|
|
72
94
|
|
|
73
95
|
```python
|
|
@@ -75,13 +97,12 @@ import asyncio
|
|
|
75
97
|
from carterkit import CarterClient
|
|
76
98
|
|
|
77
99
|
async def main():
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await c.close()
|
|
100
|
+
async with CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
|
|
101
|
+
channel="home", role="device", name="my-hub") as c:
|
|
102
|
+
c.on("toggle", lambda d: {"ok": True, **d})
|
|
103
|
+
await c.broadcast("reading", {"temp_c": 21.4})
|
|
104
|
+
await asyncio.sleep(60)
|
|
105
|
+
# leaving the `async with` disconnects automatically
|
|
85
106
|
|
|
86
107
|
asyncio.run(main())
|
|
87
108
|
```
|
|
@@ -2,19 +2,26 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
carterkit/__init__.py
|
|
5
|
+
carterkit/__main__.py
|
|
6
|
+
carterkit/bind.py
|
|
5
7
|
carterkit/buffer.py
|
|
6
8
|
carterkit/catalog.py
|
|
9
|
+
carterkit/cli.py
|
|
7
10
|
carterkit/client.py
|
|
8
11
|
carterkit/codegen.py
|
|
12
|
+
carterkit/controls.py
|
|
9
13
|
carterkit/e2ee.py
|
|
10
14
|
carterkit/grid.py
|
|
11
15
|
carterkit/infer.py
|
|
16
|
+
carterkit/layout.py
|
|
17
|
+
carterkit/py.typed
|
|
12
18
|
carterkit/theming.py
|
|
13
19
|
carterkit/tune.py
|
|
14
20
|
carterkit/validate.py
|
|
15
21
|
carterkit.egg-info/PKG-INFO
|
|
16
22
|
carterkit.egg-info/SOURCES.txt
|
|
17
23
|
carterkit.egg-info/dependency_links.txt
|
|
24
|
+
carterkit.egg-info/entry_points.txt
|
|
18
25
|
carterkit.egg-info/requires.txt
|
|
19
26
|
carterkit.egg-info/top_level.txt
|
|
20
27
|
carterkit/controldocs/accordion.md
|
|
@@ -61,12 +68,18 @@ carterkit/controldocs/theming.md
|
|
|
61
68
|
carterkit/controldocs/toggle.md
|
|
62
69
|
carterkit/controldocs/visibility.md
|
|
63
70
|
carterkit/controldocs/web-view.md
|
|
71
|
+
tests/test_bind.py
|
|
64
72
|
tests/test_buffer.py
|
|
65
73
|
tests/test_catalog.py
|
|
74
|
+
tests/test_cli.py
|
|
75
|
+
tests/test_client.py
|
|
66
76
|
tests/test_codegen.py
|
|
77
|
+
tests/test_controls.py
|
|
67
78
|
tests/test_e2ee.py
|
|
68
79
|
tests/test_grid.py
|
|
69
80
|
tests/test_infer.py
|
|
81
|
+
tests/test_layout.py
|
|
70
82
|
tests/test_theming.py
|
|
71
83
|
tests/test_tune.py
|
|
72
|
-
tests/test_validate.py
|
|
84
|
+
tests/test_validate.py
|
|
85
|
+
tests/test_validate_bindings.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "carterkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Build and drive CAR-TER layouts from Python — the control docs are the library."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -30,8 +30,11 @@ classifiers = [
|
|
|
30
30
|
Homepage = "https://github.com/Mariner10/carterkit"
|
|
31
31
|
Repository = "https://github.com/Mariner10/carterkit"
|
|
32
32
|
|
|
33
|
+
[project.scripts]
|
|
34
|
+
carterkit = "carterkit.cli:main"
|
|
35
|
+
|
|
33
36
|
[tool.setuptools]
|
|
34
37
|
packages = ["carterkit"]
|
|
35
38
|
|
|
36
39
|
[tool.setuptools.package-data]
|
|
37
|
-
carterkit = ["controldocs/*.md"]
|
|
40
|
+
carterkit = ["controldocs/*.md", "py.typed"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Tests for bind.py — sync/action/connection helper shapes."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from carterkit import bind
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_listen_basic():
|
|
9
|
+
assert bind.listen("cpu") == {
|
|
10
|
+
"method": "meshsocket", "type": "listen", "event": "broadcast", "valuePath": "cpu"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_listen_with_filter_and_event():
|
|
14
|
+
s = bind.listen("battery", event="metrics", filter={"msg_type": "telemetry"})
|
|
15
|
+
assert s["event"] == "metrics"
|
|
16
|
+
assert s["filter"] == {"msg_type": "telemetry"}
|
|
17
|
+
assert s["valuePath"] == "battery"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_action_broadcast_default():
|
|
21
|
+
assert bind.action("refresh") == {
|
|
22
|
+
"method": "meshsocket", "mode": "broadcast", "event": "refresh"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_action_request_with_payload():
|
|
26
|
+
a = bind.action("set_power", payload={"state": "{{value}}"}, mode="request")
|
|
27
|
+
assert a["mode"] == "request"
|
|
28
|
+
assert a["payload"] == {"state": "{{value}}"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_action_rejects_bad_mode():
|
|
32
|
+
with pytest.raises(ValueError):
|
|
33
|
+
bind.action("x", mode="nope")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_connection():
|
|
37
|
+
c = bind.connection("ws://h:8765", channel="lab", token="t")
|
|
38
|
+
assert c["url"] == "ws://h:8765"
|
|
39
|
+
assert c["identity"] == {"name": "CAR-TER", "channel": "lab", "role": "controller"}
|
|
40
|
+
assert c["token"] == "t"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Tests for the carterkit CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import carterkit
|
|
6
|
+
from carterkit import cli
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_catalog_json(capsys):
|
|
10
|
+
assert cli.main(["catalog", "--json"]) == 0
|
|
11
|
+
cat = json.loads(capsys.readouterr().out)
|
|
12
|
+
assert "gauge" in cat and "button" in cat
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_catalog_table(capsys):
|
|
16
|
+
assert cli.main(["catalog"]) == 0
|
|
17
|
+
assert "gauge" in capsys.readouterr().out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_doc(capsys):
|
|
21
|
+
assert cli.main(["doc", "gauge"]) == 0
|
|
22
|
+
assert "# Gauge" in capsys.readouterr().out
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_doc_unknown(capsys):
|
|
26
|
+
assert cli.main(["doc", "frobnicator"]) == 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_examples_list(capsys):
|
|
30
|
+
assert cli.main(["examples", "button"]) == 0
|
|
31
|
+
assert "•" in capsys.readouterr().out
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_version(capsys):
|
|
35
|
+
assert cli.main(["version"]) == 0
|
|
36
|
+
assert capsys.readouterr().out.strip() == carterkit.__version__
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_validate_clean(tmp_path):
|
|
40
|
+
b = carterkit.LayoutBuffer.blank(columns=4, rows=4)
|
|
41
|
+
b.add_control(carterkit.build.gauge(id="cpu", min=0, max=100), default_span=[2, 2])
|
|
42
|
+
f = tmp_path / "layout.json"
|
|
43
|
+
f.write_text(json.dumps(b.layout))
|
|
44
|
+
assert cli.main(["validate", str(f)]) == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_gen_emits_meshsocket_service(tmp_path, capsys):
|
|
48
|
+
b = carterkit.LayoutBuffer.blank(columns=4, rows=4)
|
|
49
|
+
b.add_control(carterkit.build.button(id="go"))
|
|
50
|
+
f = tmp_path / "layout.json"
|
|
51
|
+
f.write_text(json.dumps(b.layout))
|
|
52
|
+
assert cli.main(["gen", str(f)]) == 0
|
|
53
|
+
assert "from meshsocket import MeshSocket" in capsys.readouterr().out
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Test the CarterClient async context manager (no network — fake socket)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from carterkit import CarterClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _FakeSock:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.events = []
|
|
11
|
+
|
|
12
|
+
async def start(self):
|
|
13
|
+
self.events.append("start")
|
|
14
|
+
|
|
15
|
+
async def wait_until_ready(self):
|
|
16
|
+
self.events.append("ready")
|
|
17
|
+
|
|
18
|
+
async def stop(self):
|
|
19
|
+
self.events.append("stop")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_async_context_manager_connects_and_closes():
|
|
23
|
+
c = CarterClient(gateway_url="ws://x", token="t", channel="home")
|
|
24
|
+
fake = _FakeSock()
|
|
25
|
+
c._sock = fake # swap the real MeshSocket for a recorder
|
|
26
|
+
|
|
27
|
+
async def run():
|
|
28
|
+
async with c as ctx:
|
|
29
|
+
assert ctx is c
|
|
30
|
+
return fake.events
|
|
31
|
+
|
|
32
|
+
assert asyncio.run(run()) == ["start", "ready", "stop"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Tests for controls.py — typed builders generated from the bundled catalog."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import carterkit
|
|
6
|
+
from carterkit import build, control
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_build_gauge_returns_valid_control():
|
|
10
|
+
g = build.gauge(id="cpu", min=0, max=100, position=[0, 0], span=[2, 2])
|
|
11
|
+
assert g["type"] == "gauge" and g["id"] == "cpu"
|
|
12
|
+
assert g["position"] == [0, 0] and g["span"] == [2, 2]
|
|
13
|
+
assert g["min"] == 0 and g["max"] == 100
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_snake_case_alias_maps_to_camel_type():
|
|
17
|
+
assert build.color_picker(id="tint")["type"] == "colorPicker"
|
|
18
|
+
assert build.status_light(id="s")["type"] == "statusLight"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_unknown_control_raises():
|
|
22
|
+
with pytest.raises(AttributeError):
|
|
23
|
+
build.frobnicator # noqa: B018
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_bad_enum_value_raises():
|
|
27
|
+
with pytest.raises(ValueError):
|
|
28
|
+
build.gauge(id="g", gaugeStyle="triangular") # gaugeStyle is half|full
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_good_enum_value_ok():
|
|
32
|
+
assert build.gauge(id="g", gaugeStyle="full")["gaugeStyle"] == "full"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_control_factory_unknown_type():
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
control("nope", id="x")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_builder_doc_comes_from_controldocs():
|
|
41
|
+
assert build.gauge.__doc__.startswith("# Gauge")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_types_listing_nonempty():
|
|
45
|
+
types = build.types()
|
|
46
|
+
assert "gauge" in types and "button" in types and "colorPicker" in types
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_built_controls_validate_clean():
|
|
50
|
+
b = carterkit.LayoutBuffer.blank(columns=4, rows=4)
|
|
51
|
+
b.add_control(build.gauge(id="cpu", min=0, max=100), default_span=[2, 2])
|
|
52
|
+
b.add_control(build.button(id="go", label="Go"))
|
|
53
|
+
errs = [f for f in carterkit.validate_layout(b.layout) if f["severity"] == "error"]
|
|
54
|
+
assert errs == [], errs
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Tests for the fluent Layout builder."""
|
|
2
|
+
|
|
3
|
+
from carterkit import Layout, build, bind
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_fluent_compose():
|
|
7
|
+
lay = (Layout("Dash", columns=4, rows=4)
|
|
8
|
+
.connect("ws://h:8765", channel="home")
|
|
9
|
+
.tab("Main", icon="gauge")
|
|
10
|
+
.add(build.gauge(id="cpu", min=0, max=100, sync=[bind.listen("cpu")]),
|
|
11
|
+
default_span=[2, 2])
|
|
12
|
+
.add(build.button(id="go", action=bind.action("go"))))
|
|
13
|
+
layout = lay.layout
|
|
14
|
+
assert layout["name"] == "Dash"
|
|
15
|
+
assert layout["connection"]["url"] == "ws://h:8765"
|
|
16
|
+
tab0 = layout["tabs"][0]
|
|
17
|
+
assert tab0["title"] == "Main" and tab0["icon"] == "gauge"
|
|
18
|
+
assert {c["id"] for c in tab0["children"]} == {"cpu", "go"}
|
|
19
|
+
g = next(c for c in tab0["children"] if c["id"] == "cpu")
|
|
20
|
+
assert g["span"] == [2, 2] and g["position"] == [0, 0]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_first_tab_renames_then_appends():
|
|
24
|
+
lay = (Layout("X")
|
|
25
|
+
.tab("One").add(build.button(id="a"))
|
|
26
|
+
.tab("Two").add(build.button(id="b")))
|
|
27
|
+
assert [t["title"] for t in lay.layout["tabs"]] == ["One", "Two"]
|
|
28
|
+
assert lay.layout["tabs"][1]["children"][0]["id"] == "b"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_validate_and_findings_clean():
|
|
32
|
+
lay = Layout("X").add(
|
|
33
|
+
build.gauge(id="g", min=0, max=100, sync=[bind.listen("g")]), default_span=[2, 2])
|
|
34
|
+
assert [f for f in lay.validate() if f["severity"] == "error"] == []
|
|
35
|
+
assert "No issues" in lay.findings()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_repr_counts_controls():
|
|
39
|
+
lay = Layout("X").add(build.button(id="a")).add(build.button(id="b"))
|
|
40
|
+
assert "2 control" in repr(lay)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Tests for the richer sync/action binding validation."""
|
|
2
|
+
|
|
3
|
+
import carterkit
|
|
4
|
+
from carterkit import validate, build, bind
|
|
5
|
+
|
|
6
|
+
CAT = carterkit.controls(include_theme=True)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _layout(children):
|
|
10
|
+
return {"name": "T", "version": 1,
|
|
11
|
+
"tabs": [{"title": "Main", "icon": "house.fill",
|
|
12
|
+
"grid": {"columns": 4, "rows": 4}, "children": children}]}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _kinds(findings):
|
|
16
|
+
return {f["kind"] for f in findings}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_action_missing_event_is_error():
|
|
20
|
+
lay = _layout([{"type": "button", "id": "b", "position": [0, 0],
|
|
21
|
+
"action": {"method": "meshsocket", "mode": "broadcast"}}])
|
|
22
|
+
findings = validate.validate_layout(lay, CAT)
|
|
23
|
+
assert "bad_action" in _kinds(findings)
|
|
24
|
+
assert any(f["severity"] == "error" for f in findings if f["kind"] == "bad_action")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_sync_missing_valuepath_is_warning():
|
|
28
|
+
lay = _layout([{"type": "gauge", "id": "g", "position": [0, 0], "min": 0, "max": 100,
|
|
29
|
+
"sync": [{"method": "meshsocket", "type": "listen", "event": "broadcast"}]}])
|
|
30
|
+
bs = [f for f in validate.validate_layout(lay, CAT) if f["kind"] == "bad_sync"]
|
|
31
|
+
assert bs and bs[0]["severity"] == "warn"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_helper_built_bindings_are_clean():
|
|
35
|
+
lay = _layout([
|
|
36
|
+
build.button(id="b", position=[0, 0], action=bind.action("go")),
|
|
37
|
+
build.gauge(id="g", position=[0, 1], min=0, max=100, sync=[bind.listen("g")]),
|
|
38
|
+
])
|
|
39
|
+
kinds = _kinds(validate.validate_layout(lay, CAT))
|
|
40
|
+
assert "bad_action" not in kinds and "bad_sync" not in kinds
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|