carterkit 0.2.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.2.0 → carterkit-0.3.0}/PKG-INFO +24 -20
- {carterkit-0.2.0 → carterkit-0.3.0}/README.md +23 -19
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/__init__.py +2 -1
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/client.py +8 -0
- carterkit-0.3.0/carterkit/layout.py +87 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/validate.py +20 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/PKG-INFO +24 -20
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/SOURCES.txt +5 -1
- {carterkit-0.2.0 → carterkit-0.3.0}/pyproject.toml +1 -1
- carterkit-0.3.0/tests/test_client.py +32 -0
- carterkit-0.3.0/tests/test_layout.py +40 -0
- carterkit-0.3.0/tests/test_validate_bindings.py +40 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/LICENSE +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/__main__.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/bind.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/buffer.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/catalog.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/cli.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/codegen.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/accordion.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/actions.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/animations.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/appearance.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/button.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/cardList.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/carousel.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/chat.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/color-picker.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/control-def.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/date-picker.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/divider.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/flip-card.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/gauge.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/graph.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/group-def.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/haptics.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/image.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/index.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/joystick.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/label.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/layout-config.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/list.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/log-console.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/long-press.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/map.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/picker.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/privacy.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/progress-ring.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/pulse.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/qr-code.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/segmented.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/slider.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/spacer.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/sparkline.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/status-light.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/stepper.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/sync.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/terms.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/text-input.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/theming.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/toggle.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/visibility.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/web-view.md +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controls.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/e2ee.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/grid.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/infer.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/py.typed +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/theming.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/tune.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/dependency_links.txt +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/entry_points.txt +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/requires.txt +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/top_level.txt +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/setup.cfg +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_bind.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_buffer.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_catalog.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_cli.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_codegen.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_controls.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_e2ee.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_grid.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_infer.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_theming.py +0 -0
- {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_tune.py +0 -0
- {carterkit-0.2.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,25 +51,30 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
generated from / shaped by the bundled docs, so
|
|
56
|
-
values raise instead of silently shipping a broken
|
|
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:
|
|
57
58
|
|
|
58
59
|
```python
|
|
59
|
-
import
|
|
60
|
-
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
60
|
+
from carterkit import Layout, build, bind
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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"))))
|
|
68
69
|
|
|
69
|
-
print(
|
|
70
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
70
71
|
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
72
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
76
|
+
over a held draft; `lay.buffer` exposes it.
|
|
77
|
+
|
|
73
78
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
74
79
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
75
80
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
@@ -92,13 +97,12 @@ import asyncio
|
|
|
92
97
|
from carterkit import CarterClient
|
|
93
98
|
|
|
94
99
|
async def main():
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
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
|
|
102
106
|
|
|
103
107
|
asyncio.run(main())
|
|
104
108
|
```
|
|
@@ -28,25 +28,30 @@ carterkit.examples("button") # documented example snippets
|
|
|
28
28
|
|
|
29
29
|
## Build a layout
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
generated from / shaped by the bundled docs, so
|
|
33
|
-
values raise instead of silently shipping a broken
|
|
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:
|
|
34
35
|
|
|
35
36
|
```python
|
|
36
|
-
import
|
|
37
|
-
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
37
|
+
from carterkit import Layout, build, bind
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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"))))
|
|
45
46
|
|
|
46
|
-
print(
|
|
47
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
47
48
|
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
49
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
48
50
|
```
|
|
49
51
|
|
|
52
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
53
|
+
over a held draft; `lay.buffer` exposes it.
|
|
54
|
+
|
|
50
55
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
51
56
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
52
57
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
@@ -69,13 +74,12 @@ import asyncio
|
|
|
69
74
|
from carterkit import CarterClient
|
|
70
75
|
|
|
71
76
|
async def main():
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
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
|
|
79
83
|
|
|
80
84
|
asyncio.run(main())
|
|
81
85
|
```
|
|
@@ -21,6 +21,7 @@ from .validate import validate_layout as _validate_layout, format_findings
|
|
|
21
21
|
from .client import CarterClient, notify_http, CarterNotifyError
|
|
22
22
|
from . import bind
|
|
23
23
|
from .controls import build, control
|
|
24
|
+
from .layout import Layout
|
|
24
25
|
|
|
25
26
|
__version__ = "0.2.0"
|
|
26
27
|
|
|
@@ -67,6 +68,6 @@ __all__ = [
|
|
|
67
68
|
"LayoutBuffer", "BufferError",
|
|
68
69
|
"controls", "doc", "doc_markdown", "examples", "validate_layout",
|
|
69
70
|
"format_findings", "controldocs_dir",
|
|
70
|
-
"build", "control", "bind",
|
|
71
|
+
"build", "control", "bind", "Layout",
|
|
71
72
|
"catalog", "grid", "codegen", "infer", "theming", "tune",
|
|
72
73
|
]
|
|
@@ -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)>")
|
|
@@ -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,25 +51,30 @@ carterkit.examples("button") # documented example snippets
|
|
|
51
51
|
|
|
52
52
|
## Build a layout
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
generated from / shaped by the bundled docs, so
|
|
56
|
-
values raise instead of silently shipping a broken
|
|
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:
|
|
57
58
|
|
|
58
59
|
```python
|
|
59
|
-
import
|
|
60
|
-
from carterkit import LayoutBuffer, build, bind, validate_layout
|
|
60
|
+
from carterkit import Layout, build, bind
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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"))))
|
|
68
69
|
|
|
69
|
-
print(
|
|
70
|
+
print(lay.findings()) # schema + grid + binding lint against the bundled catalog
|
|
70
71
|
help(build.gauge) # ← prints the gauge documentation, straight from the docs
|
|
72
|
+
layout = lay.layout # the composed dict, ready to push/save
|
|
71
73
|
```
|
|
72
74
|
|
|
75
|
+
Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
|
|
76
|
+
over a held draft; `lay.buffer` exposes it.
|
|
77
|
+
|
|
73
78
|
`infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
|
|
74
79
|
`codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
|
|
75
80
|
`theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
|
|
@@ -92,13 +97,12 @@ import asyncio
|
|
|
92
97
|
from carterkit import CarterClient
|
|
93
98
|
|
|
94
99
|
async def main():
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
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
|
|
102
106
|
|
|
103
107
|
asyncio.run(main())
|
|
104
108
|
```
|
|
@@ -13,6 +13,7 @@ carterkit/controls.py
|
|
|
13
13
|
carterkit/e2ee.py
|
|
14
14
|
carterkit/grid.py
|
|
15
15
|
carterkit/infer.py
|
|
16
|
+
carterkit/layout.py
|
|
16
17
|
carterkit/py.typed
|
|
17
18
|
carterkit/theming.py
|
|
18
19
|
carterkit/tune.py
|
|
@@ -71,11 +72,14 @@ tests/test_bind.py
|
|
|
71
72
|
tests/test_buffer.py
|
|
72
73
|
tests/test_catalog.py
|
|
73
74
|
tests/test_cli.py
|
|
75
|
+
tests/test_client.py
|
|
74
76
|
tests/test_codegen.py
|
|
75
77
|
tests/test_controls.py
|
|
76
78
|
tests/test_e2ee.py
|
|
77
79
|
tests/test_grid.py
|
|
78
80
|
tests/test_infer.py
|
|
81
|
+
tests/test_layout.py
|
|
79
82
|
tests/test_theming.py
|
|
80
83
|
tests/test_tune.py
|
|
81
|
-
tests/test_validate.py
|
|
84
|
+
tests/test_validate.py
|
|
85
|
+
tests/test_validate_bindings.py
|
|
@@ -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,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
|
|
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
|