carterkit 0.1.0__tar.gz → 0.2.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.2.0}/PKG-INFO +24 -7
- {carterkit-0.1.0 → carterkit-0.2.0}/README.md +23 -6
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/__init__.py +4 -1
- carterkit-0.2.0/carterkit/__main__.py +3 -0
- carterkit-0.2.0/carterkit/bind.py +40 -0
- carterkit-0.2.0/carterkit/cli.py +128 -0
- carterkit-0.2.0/carterkit/controls.py +90 -0
- carterkit-0.2.0/carterkit/py.typed +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/PKG-INFO +24 -7
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/SOURCES.txt +9 -0
- carterkit-0.2.0/carterkit.egg-info/entry_points.txt +2 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/pyproject.toml +5 -2
- carterkit-0.2.0/tests/test_bind.py +40 -0
- carterkit-0.2.0/tests/test_cli.py +53 -0
- carterkit-0.2.0/tests/test_controls.py +54 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/LICENSE +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/buffer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/catalog.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/client.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/codegen.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/accordion.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/actions.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/animations.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/appearance.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/button.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/cardList.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/carousel.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/chat.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/color-picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/control-def.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/date-picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/divider.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/flip-card.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/gauge.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/graph.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/group-def.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/haptics.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/image.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/index.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/joystick.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/label.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/layout-config.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/list.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/log-console.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/long-press.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/map.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/picker.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/privacy.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/progress-ring.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/pulse.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/qr-code.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/segmented.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/slider.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/spacer.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/sparkline.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/status-light.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/stepper.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/sync.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/terms.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/text-input.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/theming.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/toggle.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/visibility.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/web-view.md +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/e2ee.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/grid.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/infer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/theming.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/tune.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/validate.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/dependency_links.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/requires.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/top_level.txt +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/setup.cfg +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_buffer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_catalog.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_codegen.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_e2ee.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_grid.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_infer.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_theming.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_tune.py +0 -0
- {carterkit-0.1.0 → carterkit-0.2.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.2.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,40 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
+
Use **typed builders** (`build.<control>`) and **binding helpers** (`bind`) — both
|
|
55
|
+
generated from / shaped by the bundled docs, so unknown control types and bad enum
|
|
56
|
+
values raise instead of silently shipping a broken layout:
|
|
57
|
+
|
|
54
58
|
```python
|
|
55
|
-
|
|
59
|
+
import carterkit
|
|
60
|
+
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
56
61
|
|
|
57
62
|
b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
|
|
58
|
-
b.add_control(
|
|
63
|
+
b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
64
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
59
65
|
default_span=[2, 2])
|
|
60
|
-
b.add_control(
|
|
66
|
+
b.add_control(build.button(id="refresh", label="Refresh",
|
|
67
|
+
action=bind.action("refresh")))
|
|
61
68
|
|
|
62
|
-
layout
|
|
63
|
-
|
|
64
|
-
print(carterkit.format_findings(findings))
|
|
69
|
+
print(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
|
|
70
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
68
74
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
69
75
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
70
76
|
|
|
77
|
+
## CLI
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
carterkit catalog # list every control type
|
|
81
|
+
carterkit doc gauge # print a control's documentation
|
|
82
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
83
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
84
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
85
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
86
|
+
```
|
|
87
|
+
|
|
71
88
|
## Drive a device
|
|
72
89
|
|
|
73
90
|
```python
|
|
@@ -28,23 +28,40 @@ carterkit.examples("button") # documented example snippets
|
|
|
28
28
|
|
|
29
29
|
## Build a layout
|
|
30
30
|
|
|
31
|
+
Use **typed builders** (`build.<control>`) and **binding helpers** (`bind`) — both
|
|
32
|
+
generated from / shaped by the bundled docs, so unknown control types and bad enum
|
|
33
|
+
values raise instead of silently shipping a broken layout:
|
|
34
|
+
|
|
31
35
|
```python
|
|
32
|
-
|
|
36
|
+
import carterkit
|
|
37
|
+
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
33
38
|
|
|
34
39
|
b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
|
|
35
|
-
b.add_control(
|
|
40
|
+
b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
41
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
36
42
|
default_span=[2, 2])
|
|
37
|
-
b.add_control(
|
|
43
|
+
b.add_control(build.button(id="refresh", label="Refresh",
|
|
44
|
+
action=bind.action("refresh")))
|
|
38
45
|
|
|
39
|
-
layout
|
|
40
|
-
|
|
41
|
-
print(carterkit.format_findings(findings))
|
|
46
|
+
print(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
|
|
47
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
42
48
|
```
|
|
43
49
|
|
|
44
50
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
45
51
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
46
52
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
47
53
|
|
|
54
|
+
## CLI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
carterkit catalog # list every control type
|
|
58
|
+
carterkit doc gauge # print a control's documentation
|
|
59
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
60
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
61
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
62
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
63
|
+
```
|
|
64
|
+
|
|
48
65
|
## Drive a device
|
|
49
66
|
|
|
50
67
|
```python
|
|
@@ -19,8 +19,10 @@ 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
|
|
22
24
|
|
|
23
|
-
__version__ = "0.
|
|
25
|
+
__version__ = "0.2.0"
|
|
24
26
|
|
|
25
27
|
#: The layout/wire protocol version carterkit emits and understands. The JSON
|
|
26
28
|
#: contract — not this Python API — is the real compatibility boundary across the
|
|
@@ -65,5 +67,6 @@ __all__ = [
|
|
|
65
67
|
"LayoutBuffer", "BufferError",
|
|
66
68
|
"controls", "doc", "doc_markdown", "examples", "validate_layout",
|
|
67
69
|
"format_findings", "controldocs_dir",
|
|
70
|
+
"build", "control", "bind",
|
|
68
71
|
"catalog", "grid", "codegen", "infer", "theming", "tune",
|
|
69
72
|
]
|
|
@@ -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()
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: carterkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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,40 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
+
Use **typed builders** (`build.<control>`) and **binding helpers** (`bind`) — both
|
|
55
|
+
generated from / shaped by the bundled docs, so unknown control types and bad enum
|
|
56
|
+
values raise instead of silently shipping a broken layout:
|
|
57
|
+
|
|
54
58
|
```python
|
|
55
|
-
|
|
59
|
+
import carterkit
|
|
60
|
+
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
56
61
|
|
|
57
62
|
b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
|
|
58
|
-
b.add_control(
|
|
63
|
+
b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
|
|
64
|
+
sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
|
|
59
65
|
default_span=[2, 2])
|
|
60
|
-
b.add_control(
|
|
66
|
+
b.add_control(build.button(id="refresh", label="Refresh",
|
|
67
|
+
action=bind.action("refresh")))
|
|
61
68
|
|
|
62
|
-
layout
|
|
63
|
-
|
|
64
|
-
print(carterkit.format_findings(findings))
|
|
69
|
+
print(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
|
|
70
|
+
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
68
74
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
69
75
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
70
76
|
|
|
77
|
+
## CLI
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
carterkit catalog # list every control type
|
|
81
|
+
carterkit doc gauge # print a control's documentation
|
|
82
|
+
carterkit examples button # list a control's examples (--name to print one)
|
|
83
|
+
carterkit validate layout.json # lint a layout (exit 1 on errors)
|
|
84
|
+
carterkit gen layout.json # generate a MeshSocket service stub
|
|
85
|
+
carterkit relay --port 8765 # run the bundled MeshSocket relay
|
|
86
|
+
```
|
|
87
|
+
|
|
71
88
|
## Drive a device
|
|
72
89
|
|
|
73
90
|
```python
|
|
@@ -2,19 +2,25 @@ 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/py.typed
|
|
12
17
|
carterkit/theming.py
|
|
13
18
|
carterkit/tune.py
|
|
14
19
|
carterkit/validate.py
|
|
15
20
|
carterkit.egg-info/PKG-INFO
|
|
16
21
|
carterkit.egg-info/SOURCES.txt
|
|
17
22
|
carterkit.egg-info/dependency_links.txt
|
|
23
|
+
carterkit.egg-info/entry_points.txt
|
|
18
24
|
carterkit.egg-info/requires.txt
|
|
19
25
|
carterkit.egg-info/top_level.txt
|
|
20
26
|
carterkit/controldocs/accordion.md
|
|
@@ -61,9 +67,12 @@ carterkit/controldocs/theming.md
|
|
|
61
67
|
carterkit/controldocs/toggle.md
|
|
62
68
|
carterkit/controldocs/visibility.md
|
|
63
69
|
carterkit/controldocs/web-view.md
|
|
70
|
+
tests/test_bind.py
|
|
64
71
|
tests/test_buffer.py
|
|
65
72
|
tests/test_catalog.py
|
|
73
|
+
tests/test_cli.py
|
|
66
74
|
tests/test_codegen.py
|
|
75
|
+
tests/test_controls.py
|
|
67
76
|
tests/test_e2ee.py
|
|
68
77
|
tests/test_grid.py
|
|
69
78
|
tests/test_infer.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.2.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,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
|
|
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
|
|
File without changes
|
|
File without changes
|