carterkit 0.3.1__tar.gz → 0.4.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.
Files changed (95) hide show
  1. {carterkit-0.3.1 → carterkit-0.4.0}/PKG-INFO +63 -18
  2. carterkit-0.4.0/README.md +141 -0
  3. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/__init__.py +14 -5
  4. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/index.md +4 -4
  5. carterkit-0.4.0/carterkit/declare.py +280 -0
  6. carterkit-0.4.0/carterkit/dynamic.py +99 -0
  7. carterkit-0.4.0/carterkit/layout.py +444 -0
  8. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/PKG-INFO +63 -18
  9. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/SOURCES.txt +5 -0
  10. {carterkit-0.3.1 → carterkit-0.4.0}/pyproject.toml +1 -1
  11. carterkit-0.4.0/tests/test_client.py +153 -0
  12. carterkit-0.4.0/tests/test_declare.py +109 -0
  13. carterkit-0.4.0/tests/test_dynamic.py +67 -0
  14. carterkit-0.4.0/tests/test_ui.py +92 -0
  15. carterkit-0.3.1/README.md +0 -96
  16. carterkit-0.3.1/carterkit/layout.py +0 -87
  17. carterkit-0.3.1/tests/test_client.py +0 -32
  18. {carterkit-0.3.1 → carterkit-0.4.0}/LICENSE +0 -0
  19. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/__main__.py +0 -0
  20. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/bind.py +0 -0
  21. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/buffer.py +0 -0
  22. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/catalog.py +0 -0
  23. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/cli.py +0 -0
  24. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/client.py +0 -0
  25. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/codegen.py +0 -0
  26. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/accordion.md +0 -0
  27. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/actions.md +0 -0
  28. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/animations.md +0 -0
  29. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/appearance.md +0 -0
  30. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/button.md +0 -0
  31. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/cardList.md +0 -0
  32. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/carousel.md +0 -0
  33. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/chat.md +0 -0
  34. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/color-picker.md +0 -0
  35. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/control-def.md +0 -0
  36. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/date-picker.md +0 -0
  37. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/divider.md +0 -0
  38. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/flip-card.md +0 -0
  39. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/gauge.md +0 -0
  40. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/graph.md +0 -0
  41. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/group-def.md +0 -0
  42. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/haptics.md +0 -0
  43. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/image.md +0 -0
  44. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/joystick.md +0 -0
  45. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/label.md +0 -0
  46. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/layout-config.md +0 -0
  47. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/list.md +0 -0
  48. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/log-console.md +0 -0
  49. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/long-press.md +0 -0
  50. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/map.md +0 -0
  51. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/picker.md +0 -0
  52. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/privacy.md +0 -0
  53. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/progress-ring.md +0 -0
  54. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/pulse.md +0 -0
  55. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/qr-code.md +0 -0
  56. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/segmented.md +0 -0
  57. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/slider.md +0 -0
  58. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/spacer.md +0 -0
  59. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/sparkline.md +0 -0
  60. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/status-light.md +0 -0
  61. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/stepper.md +0 -0
  62. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/sync.md +0 -0
  63. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/terms.md +0 -0
  64. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/text-input.md +0 -0
  65. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/theming.md +0 -0
  66. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/toggle.md +0 -0
  67. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/visibility.md +0 -0
  68. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controldocs/web-view.md +0 -0
  69. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/controls.py +0 -0
  70. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/e2ee.py +0 -0
  71. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/grid.py +0 -0
  72. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/infer.py +0 -0
  73. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/py.typed +0 -0
  74. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/theming.py +0 -0
  75. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/tune.py +0 -0
  76. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit/validate.py +0 -0
  77. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/dependency_links.txt +0 -0
  78. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/entry_points.txt +0 -0
  79. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/requires.txt +0 -0
  80. {carterkit-0.3.1 → carterkit-0.4.0}/carterkit.egg-info/top_level.txt +0 -0
  81. {carterkit-0.3.1 → carterkit-0.4.0}/setup.cfg +0 -0
  82. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_bind.py +0 -0
  83. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_buffer.py +0 -0
  84. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_catalog.py +0 -0
  85. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_cli.py +0 -0
  86. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_codegen.py +0 -0
  87. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_controls.py +0 -0
  88. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_e2ee.py +0 -0
  89. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_grid.py +0 -0
  90. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_infer.py +0 -0
  91. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_layout.py +0 -0
  92. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_theming.py +0 -0
  93. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_tune.py +0 -0
  94. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_validate.py +0 -0
  95. {carterkit-0.3.1 → carterkit-0.4.0}/tests/test_validate_bindings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: carterkit
3
- Version: 0.3.1
3
+ Version: 0.4.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,70 @@ carterkit.examples("button") # documented example snippets
51
51
 
52
52
  ## Build a layout
53
53
 
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:
54
+ Controls are **methods on the layout**, ids are positional, tabs and groups are context
55
+ managers, and bindings fold into kwargs. Each control method returns a **handle** you can
56
+ use as a binding target or patch later. Unknown control types and bad enum values raise
57
+ instead of silently shipping a broken layout:
58
58
 
59
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
60
+ from carterkit import Layout
61
+
62
+ with Layout("Dashboard", cols=4, rows=4) as ui:
63
+ ui.connect("ws://192.168.1.50:8765", channel="home")
64
+ with ui.tab("Main", icon="gauge"):
65
+ cpu = ui.gauge("cpu", label="CPU", min=0, max=100, span=(2, 2),
66
+ listen="cpu", when={"msg_type": "metrics"})
67
+ ui.status_light("warn", visible=cpu > 90) # handle → visibility condition
68
+ ui.button("refresh", label="Refresh", send="refresh", request=True)
69
+
70
+ print(ui.findings()) # schema + grid + binding lint against the bundled catalog
71
+ ui.save("dashboard.json") # the composed layout, ready to push/load
72
+ ```
73
+
74
+ Binding sugar: `listen=`/`when=`/`event=` build a `sync`, and `send=`/`request=`/`payload=`
75
+ build an `action`; pass `sync=[...]`/`action={...}` (via `carterkit.bind`) for anything
76
+ fancier. A handle comparison (`cpu > 90`) becomes a real visibility condition; `==`/`!=`
77
+ stay normal Python, so use `.eq()`/`.neq()`. `help(carterkit.build.gauge)` prints any
78
+ control's documentation, straight from the bundled docs.
79
+
80
+ **Prefer a declarative style?** A class veneer compiles to the *same* layout — ids come
81
+ from attribute names, tabs/groups are nested classes (great for fixed dashboards; the flat
82
+ builder reads better for generated ones):
83
+
84
+ ```python
85
+ from carterkit.declare import Screen, Tab, Connect, Gauge, Button, StatusLight
86
+
87
+ class Dashboard(Screen, cols=4, rows=4):
88
+ relay = Connect("ws://192.168.1.50:8765", channel="home")
89
+ class Main(Tab, icon="gauge"):
90
+ cpu = Gauge(label="CPU", min=0, max=100, span=(2, 2), listen="cpu")
91
+ warn = StatusLight(visible=cpu > 90)
92
+ refresh = Button(label="Refresh", send="refresh", request=True)
93
+
94
+ Dashboard.save("dashboard.json")
95
+ ```
96
+
97
+ ### Dynamic groups
98
+
99
+ Generate controls in `for`/`if` loops (auto-placed in the group's own grid), or mark a
100
+ group `dynamic="event"` and replace its children live at runtime. Build that replacement
101
+ payload with `Fragment`, then lint it against the broadcasts your server actually emits —
102
+ catching events that never arrive, missing `children` arrays, and off-grid/invalid
103
+ injected controls before they ship:
104
+
105
+ ```python
106
+ import carterkit
107
+ from carterkit import Fragment
108
+
109
+ ui.group("Now Playing", span=(3, 4), cols=4, rows=3, dynamic="player_state")
110
+
111
+ frag = Fragment(cols=4, rows=3)
112
+ frag.label("title", text="Song", span=(1, 4))
113
+ frag.button("play", label="Play", send="play")
114
+ # your server broadcasts frag.payload("player_state") == {"msg_type": ..., "children": [...]}
115
+
116
+ print(carterkit.format_findings(
117
+ carterkit.lint_dynamic_traffic(ui.layout, [frag.payload("player_state")])))
73
118
  ```
74
119
 
75
120
  Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
@@ -0,0 +1,141 @@
1
+ # carterkit
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/carterkit.svg)](https://pypi.org/project/carterkit/)
4
+ [![Downloads](https://static.pepy.tech/badge/carterkit)](https://pepy.tech/project/carterkit)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/carterkit.svg)](https://pypi.org/project/carterkit/)
6
+
7
+ Build and drive [CAR-TER](https://carterbeaudoin.net/CAR-TER) layouts from Python.
8
+
9
+ **The control docs are the library.** Every control's schema, fields, and examples
10
+ are parsed at runtime from the ControlDocs markdown bundled inside the package — the
11
+ exact same docs the CAR-TER app renders — so the catalog never drifts from the
12
+ definitions.
13
+
14
+ ```bash
15
+ pip install carterkit
16
+ ```
17
+
18
+ ## Explore the controls (zero config)
19
+
20
+ ```python
21
+ import carterkit
22
+
23
+ carterkit.controls() # {type: schema} for every placeable control
24
+ carterkit.doc("gauge") # full parsed doc: fields, themeFields, examples
25
+ print(carterkit.doc_markdown("gauge")) # the rendered documentation prose
26
+ carterkit.examples("button") # documented example snippets
27
+ ```
28
+
29
+ ## Build a layout
30
+
31
+ Controls are **methods on the layout**, ids are positional, tabs and groups are context
32
+ managers, and bindings fold into kwargs. Each control method returns a **handle** you can
33
+ use as a binding target or patch later. Unknown control types and bad enum values raise
34
+ instead of silently shipping a broken layout:
35
+
36
+ ```python
37
+ from carterkit import Layout
38
+
39
+ with Layout("Dashboard", cols=4, rows=4) as ui:
40
+ ui.connect("ws://192.168.1.50:8765", channel="home")
41
+ with ui.tab("Main", icon="gauge"):
42
+ cpu = ui.gauge("cpu", label="CPU", min=0, max=100, span=(2, 2),
43
+ listen="cpu", when={"msg_type": "metrics"})
44
+ ui.status_light("warn", visible=cpu > 90) # handle → visibility condition
45
+ ui.button("refresh", label="Refresh", send="refresh", request=True)
46
+
47
+ print(ui.findings()) # schema + grid + binding lint against the bundled catalog
48
+ ui.save("dashboard.json") # the composed layout, ready to push/load
49
+ ```
50
+
51
+ Binding sugar: `listen=`/`when=`/`event=` build a `sync`, and `send=`/`request=`/`payload=`
52
+ build an `action`; pass `sync=[...]`/`action={...}` (via `carterkit.bind`) for anything
53
+ fancier. A handle comparison (`cpu > 90`) becomes a real visibility condition; `==`/`!=`
54
+ stay normal Python, so use `.eq()`/`.neq()`. `help(carterkit.build.gauge)` prints any
55
+ control's documentation, straight from the bundled docs.
56
+
57
+ **Prefer a declarative style?** A class veneer compiles to the *same* layout — ids come
58
+ from attribute names, tabs/groups are nested classes (great for fixed dashboards; the flat
59
+ builder reads better for generated ones):
60
+
61
+ ```python
62
+ from carterkit.declare import Screen, Tab, Connect, Gauge, Button, StatusLight
63
+
64
+ class Dashboard(Screen, cols=4, rows=4):
65
+ relay = Connect("ws://192.168.1.50:8765", channel="home")
66
+ class Main(Tab, icon="gauge"):
67
+ cpu = Gauge(label="CPU", min=0, max=100, span=(2, 2), listen="cpu")
68
+ warn = StatusLight(visible=cpu > 90)
69
+ refresh = Button(label="Refresh", send="refresh", request=True)
70
+
71
+ Dashboard.save("dashboard.json")
72
+ ```
73
+
74
+ ### Dynamic groups
75
+
76
+ Generate controls in `for`/`if` loops (auto-placed in the group's own grid), or mark a
77
+ group `dynamic="event"` and replace its children live at runtime. Build that replacement
78
+ payload with `Fragment`, then lint it against the broadcasts your server actually emits —
79
+ catching events that never arrive, missing `children` arrays, and off-grid/invalid
80
+ injected controls before they ship:
81
+
82
+ ```python
83
+ import carterkit
84
+ from carterkit import Fragment
85
+
86
+ ui.group("Now Playing", span=(3, 4), cols=4, rows=3, dynamic="player_state")
87
+
88
+ frag = Fragment(cols=4, rows=3)
89
+ frag.label("title", text="Song", span=(1, 4))
90
+ frag.button("play", label="Play", send="play")
91
+ # your server broadcasts frag.payload("player_state") == {"msg_type": ..., "children": [...]}
92
+
93
+ print(carterkit.format_findings(
94
+ carterkit.lint_dynamic_traffic(ui.layout, [frag.payload("player_state")])))
95
+ ```
96
+
97
+ Prefer surgical edits? `LayoutBuffer` gives `add_control` / `update_control` / `move_control`
98
+ over a held draft; `lay.buffer` exposes it.
99
+
100
+ `infer.build_layout(payload)` generates a wired layout from a sample telemetry dict;
101
+ `codegen.generate_service_stub(layout)` emits a runnable MeshSocket server skeleton;
102
+ `theming.theme_for(...)` and `tune.tune_gauge(...)` round out the authoring tools.
103
+
104
+ ## CLI
105
+
106
+ ```bash
107
+ carterkit catalog # list every control type
108
+ carterkit doc gauge # print a control's documentation
109
+ carterkit examples button # list a control's examples (--name to print one)
110
+ carterkit validate layout.json # lint a layout (exit 1 on errors)
111
+ carterkit gen layout.json # generate a MeshSocket service stub
112
+ carterkit relay --port 8765 # run the bundled MeshSocket relay
113
+ ```
114
+
115
+ ## Drive a device
116
+
117
+ ```python
118
+ import asyncio
119
+ from carterkit import CarterClient
120
+
121
+ async def main():
122
+ async with CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
123
+ channel="home", role="device", name="my-hub") as c:
124
+ c.on("toggle", lambda d: {"ok": True, **d})
125
+ await c.broadcast("reading", {"temp_c": 21.4})
126
+ await asyncio.sleep(60)
127
+ # leaving the `async with` disconnects automatically
128
+
129
+ asyncio.run(main())
130
+ ```
131
+
132
+ End-to-end encryption (ChaCha20-Poly1305 + per-session salt) is transparent when you
133
+ pass an `e2ee_key`. Send a push to every device on a Connect+ account with
134
+ `CarterClient.notify(...)` or the stdlib-only `carterkit.notify_http(...)`.
135
+
136
+ ## Built on
137
+
138
+ [`meshsocket`](https://pypi.org/project/meshsocket/) — the WebSocket mesh transport.
139
+
140
+ The ControlDocs are vendored from the CAR-TER app repo; refresh them with
141
+ `scripts/sync-controldocs.sh`.
@@ -7,21 +7,24 @@ renders. So the docs never drift from the definitions; they are one and the same
7
7
 
8
8
  Quick map:
9
9
  - ``controls()`` / ``doc()`` / ``examples()`` — the docs-as-catalog surface
10
+ - ``Layout`` / ``Fragment`` — the flat builder: controls as methods, live handles,
11
+ context-manager tabs/groups (``declare.Screen`` is the declarative-class veneer)
10
12
  - ``LayoutBuffer`` — incrementally build a layout (auto-placement, dedupe)
11
13
  - ``validate_layout()`` — schema + grid lint against the bundled catalog
14
+ - ``lint_dynamic_traffic()`` — check ``dynamic=`` groups against observed broadcasts
12
15
  - ``infer`` / ``codegen`` / ``theming`` / ``tune`` — generate layouts, servers, themes
13
16
  - ``CarterClient`` / ``notify_http`` — connect over MeshSocket, push, send alerts
14
17
  """
15
18
  from importlib.resources import files
16
19
  from pathlib import Path
17
20
 
18
- from . import catalog, grid, codegen, infer, theming, tune
21
+ from . import catalog, grid, codegen, infer, theming, tune, dynamic
19
22
  from .buffer import LayoutBuffer, BufferError
20
23
  from .validate import validate_layout as _validate_layout, format_findings
21
24
  from .client import CarterClient, notify_http, CarterNotifyError
22
25
  from . import bind
23
26
  from .controls import build, control
24
- from .layout import Layout
27
+ from .layout import Layout, Fragment, Control, Condition
25
28
 
26
29
  try:
27
30
  from importlib.metadata import PackageNotFoundError, version as _pkg_version
@@ -66,12 +69,18 @@ def validate_layout(layout: dict, catalog_: dict = None) -> list:
66
69
  return _validate_layout(layout, catalog_ if catalog_ is not None else controls(include_theme=True))
67
70
 
68
71
 
72
+ def lint_dynamic_traffic(layout: dict, observed, catalog_: dict = None) -> list:
73
+ """Lint a layout's `dynamic=` groups against observed broadcast payloads. Returns
74
+ `validate`-style findings (see `dynamic.lint_dynamic_traffic`)."""
75
+ return dynamic.lint_dynamic_traffic(layout, observed, catalog=catalog_)
76
+
77
+
69
78
  __all__ = [
70
79
  "__version__", "PROTOCOL_VERSION",
71
80
  "CarterClient", "notify_http", "CarterNotifyError",
72
81
  "LayoutBuffer", "BufferError",
73
82
  "controls", "doc", "doc_markdown", "examples", "validate_layout",
74
- "format_findings", "controldocs_dir",
75
- "build", "control", "bind", "Layout",
76
- "catalog", "grid", "codegen", "infer", "theming", "tune",
83
+ "lint_dynamic_traffic", "format_findings", "controldocs_dir",
84
+ "build", "control", "bind", "Layout", "Fragment", "Control", "Condition",
85
+ "catalog", "grid", "codegen", "infer", "theming", "tune", "dynamic",
77
86
  ]
@@ -133,9 +133,9 @@ Tap any node in the graph to explore the full documentation for each control typ
133
133
 
134
134
  1. **Explore the docs** — tap nodes in the graph to learn about each control
135
135
  2. **Write a layout** — create a JSON file following the [[layout-config]] schema
136
- 3. **Run a server** — start a MeshSocket server to handle events
136
+ 3. **Run a server** — drive your layout from Python with `pip install carterkit`, or
137
+ speak the MeshSocket protocol directly from any language
137
138
  4. **Connect** — load your layout and watch it come alive
138
139
 
139
- The full MeshSocket server implementation and protocol documentation is available on GitHub:
140
-
141
- [github.com/carterbeaudoin/MeshSocket](https://github.com/carterbeaudoin/MeshSocket)
140
+ Full developer docs building servers, the wire protocol, and the `carterkit`
141
+ library — live at **carterbeaudoin.net/CAR-TER**.
@@ -0,0 +1,280 @@
1
+ """Declarative layouts — a class veneer over the flat builder.
2
+
3
+ For static, hand-written dashboards this reads like a schema: a control's id is its
4
+ attribute name, tabs and groups are nested classes, and cross-control visibility falls
5
+ out of comparing handles. It compiles to exactly the same `Layout`/`LayoutBuffer` the
6
+ flat builder uses (so `.layout` is an ordinary layout dict) — pick whichever front door
7
+ fits: generated/dynamic UIs read better flat, fixed ones read better here.
8
+
9
+ from carterkit.declare import Screen, Tab, Group, Connect, Ref, Gauge, Button, Slider, StatusLight
10
+
11
+ class Bench(Screen, cols=4, rows=4):
12
+ relay = Connect("ws://192.168.1.50:8765", channel="lab")
13
+
14
+ class Main(Tab, icon="gauge"):
15
+ cpu = Gauge(label="CPU", min=0, max=100, span=(2, 2),
16
+ listen="cpu", when={"msg_type": "metrics"})
17
+ warn = StatusLight(visible=cpu > 90) # handle -> visibility cond
18
+ refresh = Button(label="Refresh", send="refresh", request=True)
19
+
20
+ class Motors(Group, span=(2, 2), cols=2, rows=2):
21
+ m0 = Slider(min=0, max=255)
22
+ m1 = Slider(min=0, max=255)
23
+
24
+ layout = Bench.layout # the dict
25
+ Bench.save("bench.json")
26
+
27
+ Control classes (`Gauge`, `Button`, `StatusLight`, `ColorPicker`, …) are generated from
28
+ the bundled catalog: a PascalCase name maps to its control type, so `from
29
+ carterkit.declare import *` (or attribute access) exposes one per placeable control.
30
+ Kwargs are the same sugar the flat builder takes (`listen=/when=/send=/request=/span=/
31
+ visible=`). For a reference to a control by literal id (forward refs, other tabs) use
32
+ `Ref("id") > 90`. `==`/`!=` keep normal Python semantics — use `.eq()`/`.neq()`.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ from functools import lru_cache
37
+
38
+ from . import controls as _controls
39
+ from .layout import Layout
40
+
41
+
42
+ class DeclareError(Exception):
43
+ """Raised when a declarative Screen is shaped in a way that can't compile."""
44
+
45
+
46
+ # ─── references & conditions ─────────────────────────────────────────────────
47
+ class _Cmp:
48
+ """A pending visibility condition: a handle/Ref compared to a value, resolved to
49
+ `{when, operator, value}` once attribute-name ids are known."""
50
+
51
+ def __init__(self, target, op: str, value):
52
+ self.target, self.op, self.value = target, op, value
53
+
54
+ def resolve(self, idmap: dict) -> dict:
55
+ if isinstance(self.target, Ref):
56
+ when = self.target.id
57
+ else:
58
+ when = idmap.get(id(self.target))
59
+ if when is None:
60
+ raise DeclareError(
61
+ "a visibility condition references a control that isn't a named "
62
+ "attribute in this Screen — use Ref('id') to point at it by id")
63
+ return {"when": when, "operator": self.op, "value": self.value}
64
+
65
+
66
+ class _Comparable:
67
+ def __gt__(self, o): return _Cmp(self, "gt", o)
68
+ def __ge__(self, o): return _Cmp(self, "gte", o)
69
+ def __lt__(self, o): return _Cmp(self, "lt", o)
70
+ def __le__(self, o): return _Cmp(self, "lte", o)
71
+ def eq(self, o): return _Cmp(self, "eq", o)
72
+ def neq(self, o): return _Cmp(self, "neq", o)
73
+
74
+
75
+ class Ref(_Comparable):
76
+ """Reference another control by literal id, for visibility conditions:
77
+ ``hidden = Label(visible=Ref("power").eq(True))``."""
78
+
79
+ def __init__(self, id: str):
80
+ self.id = id
81
+
82
+
83
+ class Connect:
84
+ """A connection declaration: ``relay = Connect(url, channel=..., role=..., token=...)``."""
85
+
86
+ def __init__(self, url: str, **identity):
87
+ self.url, self.identity = url, identity
88
+
89
+
90
+ # ─── control specs ───────────────────────────────────────────────────────────
91
+ class _Spec(_Comparable):
92
+ """A deferred control: its catalog `_ctype` plus the kwargs to build it. The id is
93
+ filled in from the attribute name at compile time."""
94
+
95
+ _ctype: str = ""
96
+
97
+ def __init__(self, **kwargs):
98
+ self._kwargs = kwargs
99
+
100
+
101
+ @lru_cache(maxsize=None)
102
+ def _spec_class(ctype: str, clsname: str) -> type:
103
+ return type(clsname, (_Spec,), {"_ctype": ctype,
104
+ "__doc__": f"Declarative {ctype} control."})
105
+
106
+
107
+ @lru_cache(maxsize=1)
108
+ def _catalog() -> dict:
109
+ from . import controldocs_dir
110
+ from . import catalog as _catmod
111
+ return _catmod.build_catalog(controldocs_dir())
112
+
113
+
114
+ def _type_for(clsname: str):
115
+ """PascalCase class name -> catalog control type, or None if not a control."""
116
+ if not clsname[:1].isupper():
117
+ return None
118
+ ctype = clsname[0].lower() + clsname[1:]
119
+ return ctype if ctype in _catalog() else None
120
+
121
+
122
+ # ─── compile helpers ─────────────────────────────────────────────────────────
123
+ def _members(klass):
124
+ """Ordered (name, value) of a class body, skipping dunders and internals."""
125
+ for name, value in vars(klass).items():
126
+ if name.startswith("_"):
127
+ continue
128
+ yield name, value
129
+
130
+
131
+ def _is_sub(value, base) -> bool:
132
+ return isinstance(value, type) and issubclass(value, base)
133
+
134
+
135
+ def _collect_ids(klass, idmap: dict) -> None:
136
+ """Map every spec object (by identity) to its attribute-name id, recursively."""
137
+ for name, value in _members(klass):
138
+ if isinstance(value, _Spec):
139
+ idmap[id(value)] = name
140
+ elif _is_sub(value, (Tab, Group)):
141
+ _collect_ids(value, idmap)
142
+
143
+
144
+ def _resolve_kwargs(kwargs: dict, idmap: dict) -> dict:
145
+ return {k: (v.resolve(idmap) if isinstance(v, _Cmp) else v) for k, v in kwargs.items()}
146
+
147
+
148
+ def _populate(scope, klass, idmap: dict) -> None:
149
+ """Materialise a class body into a layout scope (a tab or group handle)."""
150
+ for name, value in _members(klass):
151
+ if isinstance(value, _Spec):
152
+ make = getattr(scope, value._ctype)
153
+ make(name, **_resolve_kwargs(value._kwargs, idmap))
154
+ elif _is_sub(value, Group):
155
+ gm = value._group_meta
156
+ gh = scope.group(gm["label"], id=name, span=gm["span"], cols=gm["cols"],
157
+ rows=gm["rows"], dynamic=gm["dynamic"], pulse=gm["pulse"],
158
+ hide_background=gm["hide_background"],
159
+ visible=(gm["visible"].resolve(idmap)
160
+ if isinstance(gm["visible"], _Cmp) else gm["visible"]))
161
+ with gh:
162
+ _populate(gh, value, idmap)
163
+
164
+
165
+ # ─── declarative bases ───────────────────────────────────────────────────────
166
+ class Tab:
167
+ """Base for a nested tab class: ``class Main(Tab, icon="gauge", cols=4, rows=6): ...``"""
168
+
169
+ @classmethod
170
+ def __init_subclass__(cls, *, title: str = None, icon: str = "square.grid.2x2",
171
+ cols: int = None, rows: int = 6, columns: int = None, **rest):
172
+ super().__init_subclass__(**rest)
173
+ cls._tab_meta = {"title": title or cls.__name__, "icon": icon,
174
+ "cols": cols if cols is not None else (columns or 4), "rows": rows}
175
+
176
+
177
+ class Group:
178
+ """Base for a nested group class. Class kwargs mirror `Layout.group`:
179
+ ``class Motors(Group, span=(2,2), cols=2, rows=2, dynamic="motor_state"): ...``"""
180
+
181
+ @classmethod
182
+ def __init_subclass__(cls, *, label: str = None, span=None, cols: int = 4, rows: int = 4,
183
+ dynamic: str = None, visible=None, pulse=None,
184
+ hide_background: bool = None, **rest):
185
+ super().__init_subclass__(**rest)
186
+ cls._group_meta = {"label": label if label is not None else cls.__name__,
187
+ "span": span, "cols": cols, "rows": rows, "dynamic": dynamic,
188
+ "visible": visible, "pulse": pulse, "hide_background": hide_background}
189
+
190
+
191
+ class _ScreenMeta(type):
192
+ @property
193
+ def layout(cls) -> dict:
194
+ return cls.build().layout
195
+
196
+
197
+ class Screen(metaclass=_ScreenMeta):
198
+ """Base for a declarative layout. Subclass kwargs: ``name=``, ``cols=``, ``rows=``,
199
+ ``accent=``. Access the result via ``.layout`` / ``.build()`` / ``.json()`` /
200
+ ``.save(path)`` / ``.validate()`` / ``.findings()``."""
201
+
202
+ @classmethod
203
+ def __init_subclass__(cls, *, name: str = None, cols: int = None, rows: int = 6,
204
+ columns: int = None, accent: str = "#667eea", **rest):
205
+ super().__init_subclass__(**rest)
206
+ cls._screen_meta = {"name": name or cls.__name__,
207
+ "cols": cols if cols is not None else (columns or 4),
208
+ "rows": rows, "accent": accent}
209
+ cls._compiled = None
210
+
211
+ @classmethod
212
+ def _compile(cls) -> Layout:
213
+ meta = cls._screen_meta
214
+ lay = Layout(meta["name"], cols=meta["cols"], rows=meta["rows"], accent=meta["accent"])
215
+
216
+ idmap: dict = {}
217
+ _collect_ids(cls, idmap)
218
+
219
+ for _n, value in _members(cls):
220
+ if isinstance(value, Connect):
221
+ lay.connect(value.url, **value.identity)
222
+
223
+ tabs = [(n, v) for n, v in _members(cls) if _is_sub(v, Tab)]
224
+ loose = [(n, v) for n, v in _members(cls)
225
+ if isinstance(v, _Spec) or _is_sub(v, Group)]
226
+ if tabs:
227
+ if any(isinstance(v, _Spec) for _n, v in loose):
228
+ raise DeclareError(
229
+ "controls are defined directly on the Screen alongside Tab classes — "
230
+ "move them inside a Tab (controls must live in a tab)")
231
+ for _n, tabcls in tabs:
232
+ tm = tabcls._tab_meta
233
+ handle = lay.tab(tm["title"], icon=tm["icon"], cols=tm["cols"], rows=tm["rows"])
234
+ with handle:
235
+ _populate(handle, tabcls, idmap)
236
+ else:
237
+ _populate(lay, cls, idmap)
238
+ return lay
239
+
240
+ @classmethod
241
+ def build(cls) -> Layout:
242
+ """Compile (once) to a `Layout`. Cached on the class."""
243
+ if cls.__dict__.get("_compiled") is None:
244
+ cls._compiled = cls._compile()
245
+ return cls._compiled
246
+
247
+ @classmethod
248
+ def json(cls, indent: int = 2) -> str:
249
+ return cls.build().json(indent)
250
+
251
+ @classmethod
252
+ def save(cls, path: str, indent: int = 2) -> str:
253
+ return cls.build().save(path, indent)
254
+
255
+ @classmethod
256
+ def validate(cls) -> list:
257
+ return cls.build().validate()
258
+
259
+ @classmethod
260
+ def findings(cls) -> str:
261
+ return cls.build().findings()
262
+
263
+
264
+ # ─── module-level control-class access (PEP 562) ─────────────────────────────
265
+ _STATIC = {"Screen", "Tab", "Group", "Connect", "Ref", "DeclareError"}
266
+
267
+
268
+ def __getattr__(name: str):
269
+ ctype = _type_for(name)
270
+ if ctype is not None:
271
+ return _spec_class(ctype, name)
272
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
273
+
274
+
275
+ def __dir__():
276
+ pascal = [t[0].upper() + t[1:] for t in _catalog()]
277
+ return sorted(_STATIC | set(pascal))
278
+
279
+
280
+ __all__ = sorted(_STATIC | {t[0].upper() + t[1:] for t in _catalog()})