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.
Files changed (87) hide show
  1. {carterkit-0.2.0 → carterkit-0.3.0}/PKG-INFO +24 -20
  2. {carterkit-0.2.0 → carterkit-0.3.0}/README.md +23 -19
  3. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/__init__.py +2 -1
  4. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/client.py +8 -0
  5. carterkit-0.3.0/carterkit/layout.py +87 -0
  6. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/validate.py +20 -0
  7. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/PKG-INFO +24 -20
  8. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/SOURCES.txt +5 -1
  9. {carterkit-0.2.0 → carterkit-0.3.0}/pyproject.toml +1 -1
  10. carterkit-0.3.0/tests/test_client.py +32 -0
  11. carterkit-0.3.0/tests/test_layout.py +40 -0
  12. carterkit-0.3.0/tests/test_validate_bindings.py +40 -0
  13. {carterkit-0.2.0 → carterkit-0.3.0}/LICENSE +0 -0
  14. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/__main__.py +0 -0
  15. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/bind.py +0 -0
  16. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/buffer.py +0 -0
  17. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/catalog.py +0 -0
  18. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/cli.py +0 -0
  19. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/codegen.py +0 -0
  20. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/accordion.md +0 -0
  21. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/actions.md +0 -0
  22. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/animations.md +0 -0
  23. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/appearance.md +0 -0
  24. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/button.md +0 -0
  25. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/cardList.md +0 -0
  26. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/carousel.md +0 -0
  27. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/chat.md +0 -0
  28. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/color-picker.md +0 -0
  29. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/control-def.md +0 -0
  30. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/date-picker.md +0 -0
  31. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/divider.md +0 -0
  32. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/flip-card.md +0 -0
  33. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/gauge.md +0 -0
  34. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/graph.md +0 -0
  35. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/group-def.md +0 -0
  36. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/haptics.md +0 -0
  37. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/image.md +0 -0
  38. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/index.md +0 -0
  39. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/joystick.md +0 -0
  40. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/label.md +0 -0
  41. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/layout-config.md +0 -0
  42. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/list.md +0 -0
  43. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/log-console.md +0 -0
  44. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/long-press.md +0 -0
  45. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/map.md +0 -0
  46. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/picker.md +0 -0
  47. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/privacy.md +0 -0
  48. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/progress-ring.md +0 -0
  49. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/pulse.md +0 -0
  50. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/qr-code.md +0 -0
  51. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/segmented.md +0 -0
  52. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/slider.md +0 -0
  53. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/spacer.md +0 -0
  54. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/sparkline.md +0 -0
  55. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/status-light.md +0 -0
  56. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/stepper.md +0 -0
  57. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/sync.md +0 -0
  58. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/terms.md +0 -0
  59. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/text-input.md +0 -0
  60. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/theming.md +0 -0
  61. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/toggle.md +0 -0
  62. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/visibility.md +0 -0
  63. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controldocs/web-view.md +0 -0
  64. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/controls.py +0 -0
  65. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/e2ee.py +0 -0
  66. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/grid.py +0 -0
  67. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/infer.py +0 -0
  68. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/py.typed +0 -0
  69. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/theming.py +0 -0
  70. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit/tune.py +0 -0
  71. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/dependency_links.txt +0 -0
  72. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/entry_points.txt +0 -0
  73. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/requires.txt +0 -0
  74. {carterkit-0.2.0 → carterkit-0.3.0}/carterkit.egg-info/top_level.txt +0 -0
  75. {carterkit-0.2.0 → carterkit-0.3.0}/setup.cfg +0 -0
  76. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_bind.py +0 -0
  77. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_buffer.py +0 -0
  78. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_catalog.py +0 -0
  79. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_cli.py +0 -0
  80. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_codegen.py +0 -0
  81. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_controls.py +0 -0
  82. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_e2ee.py +0 -0
  83. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_grid.py +0 -0
  84. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_infer.py +0 -0
  85. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_theming.py +0 -0
  86. {carterkit-0.2.0 → carterkit-0.3.0}/tests/test_tune.py +0 -0
  87. {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.2.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
- 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:
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 carterkit
60
- from carterkit import LayoutBuffer, build, bind, validate_layout
60
+ from carterkit import Layout, build, bind
61
61
 
62
- b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
63
- b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
64
- sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
65
- default_span=[2, 2])
66
- b.add_control(build.button(id="refresh", label="Refresh",
67
- action=bind.action("refresh")))
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(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
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
- c = CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
96
- channel="home", role="device", name="my-hub")
97
- c.on("toggle", lambda d: {"ok": True, **d})
98
- await c.connect()
99
- await c.broadcast("reading", {"temp_c": 21.4})
100
- await asyncio.sleep(60)
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
- 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:
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 carterkit
37
- from carterkit import LayoutBuffer, build, bind, validate_layout
37
+ from carterkit import Layout, build, bind
38
38
 
39
- b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
40
- b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
41
- sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
42
- default_span=[2, 2])
43
- b.add_control(build.button(id="refresh", label="Refresh",
44
- action=bind.action("refresh")))
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(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
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
- c = CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
73
- channel="home", role="device", name="my-hub")
74
- c.on("toggle", lambda d: {"ok": True, **d})
75
- await c.connect()
76
- await c.broadcast("reading", {"temp_c": 21.4})
77
- await asyncio.sleep(60)
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
  ]
@@ -136,3 +136,11 @@ class CarterClient:
136
136
 
137
137
  async def close(self):
138
138
  await self._sock.stop()
139
+
140
+ async def __aenter__(self):
141
+ await self.connect()
142
+ return self
143
+
144
+ async def __aexit__(self, *exc):
145
+ await self.close()
146
+ return False
@@ -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.2.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
- 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:
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 carterkit
60
- from carterkit import LayoutBuffer, build, bind, validate_layout
60
+ from carterkit import Layout, build, bind
61
61
 
62
- b = LayoutBuffer.blank(name="Dashboard", columns=4, rows=4)
63
- b.add_control(build.gauge(id="cpu", label="CPU", min=0, max=100,
64
- sync=[bind.listen("cpu", filter={"msg_type": "metrics"})]),
65
- default_span=[2, 2])
66
- b.add_control(build.button(id="refresh", label="Refresh",
67
- action=bind.action("refresh")))
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(carterkit.format_findings(validate_layout(b.layout))) # schema + grid lint
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
- c = CarterClient(gateway_url="ws://localhost:18080", token="<mesh token>",
96
- channel="home", role="device", name="my-hub")
97
- c.on("toggle", lambda d: {"ok": True, **d})
98
- await c.connect()
99
- await c.broadcast("reading", {"temp_c": 21.4})
100
- await asyncio.sleep(60)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "carterkit"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Build and drive CAR-TER layouts from Python — the control docs are the library."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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