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.
Files changed (83) hide show
  1. {carterkit-0.1.0 → carterkit-0.2.0}/PKG-INFO +24 -7
  2. {carterkit-0.1.0 → carterkit-0.2.0}/README.md +23 -6
  3. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/__init__.py +4 -1
  4. carterkit-0.2.0/carterkit/__main__.py +3 -0
  5. carterkit-0.2.0/carterkit/bind.py +40 -0
  6. carterkit-0.2.0/carterkit/cli.py +128 -0
  7. carterkit-0.2.0/carterkit/controls.py +90 -0
  8. carterkit-0.2.0/carterkit/py.typed +0 -0
  9. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/PKG-INFO +24 -7
  10. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/SOURCES.txt +9 -0
  11. carterkit-0.2.0/carterkit.egg-info/entry_points.txt +2 -0
  12. {carterkit-0.1.0 → carterkit-0.2.0}/pyproject.toml +5 -2
  13. carterkit-0.2.0/tests/test_bind.py +40 -0
  14. carterkit-0.2.0/tests/test_cli.py +53 -0
  15. carterkit-0.2.0/tests/test_controls.py +54 -0
  16. {carterkit-0.1.0 → carterkit-0.2.0}/LICENSE +0 -0
  17. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/buffer.py +0 -0
  18. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/catalog.py +0 -0
  19. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/client.py +0 -0
  20. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/codegen.py +0 -0
  21. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/accordion.md +0 -0
  22. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/actions.md +0 -0
  23. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/animations.md +0 -0
  24. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/appearance.md +0 -0
  25. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/button.md +0 -0
  26. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/cardList.md +0 -0
  27. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/carousel.md +0 -0
  28. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/chat.md +0 -0
  29. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/color-picker.md +0 -0
  30. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/control-def.md +0 -0
  31. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/date-picker.md +0 -0
  32. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/divider.md +0 -0
  33. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/flip-card.md +0 -0
  34. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/gauge.md +0 -0
  35. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/graph.md +0 -0
  36. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/group-def.md +0 -0
  37. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/haptics.md +0 -0
  38. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/image.md +0 -0
  39. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/index.md +0 -0
  40. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/joystick.md +0 -0
  41. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/label.md +0 -0
  42. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/layout-config.md +0 -0
  43. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/list.md +0 -0
  44. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/log-console.md +0 -0
  45. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/long-press.md +0 -0
  46. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/map.md +0 -0
  47. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/picker.md +0 -0
  48. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/privacy.md +0 -0
  49. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/progress-ring.md +0 -0
  50. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/pulse.md +0 -0
  51. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/qr-code.md +0 -0
  52. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/segmented.md +0 -0
  53. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/slider.md +0 -0
  54. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/spacer.md +0 -0
  55. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/sparkline.md +0 -0
  56. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/status-light.md +0 -0
  57. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/stepper.md +0 -0
  58. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/sync.md +0 -0
  59. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/terms.md +0 -0
  60. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/text-input.md +0 -0
  61. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/theming.md +0 -0
  62. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/toggle.md +0 -0
  63. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/visibility.md +0 -0
  64. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/controldocs/web-view.md +0 -0
  65. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/e2ee.py +0 -0
  66. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/grid.py +0 -0
  67. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/infer.py +0 -0
  68. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/theming.py +0 -0
  69. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/tune.py +0 -0
  70. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit/validate.py +0 -0
  71. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/dependency_links.txt +0 -0
  72. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/requires.txt +0 -0
  73. {carterkit-0.1.0 → carterkit-0.2.0}/carterkit.egg-info/top_level.txt +0 -0
  74. {carterkit-0.1.0 → carterkit-0.2.0}/setup.cfg +0 -0
  75. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_buffer.py +0 -0
  76. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_catalog.py +0 -0
  77. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_codegen.py +0 -0
  78. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_e2ee.py +0 -0
  79. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_grid.py +0 -0
  80. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_infer.py +0 -0
  81. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_theming.py +0 -0
  82. {carterkit-0.1.0 → carterkit-0.2.0}/tests/test_tune.py +0 -0
  83. {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.1.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
- from carterkit import LayoutBuffer, validate_layout
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({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
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({"type": "button", "id": "refresh", "label": "Refresh"})
66
+ b.add_control(build.button(id="refresh", label="Refresh",
67
+ action=bind.action("refresh")))
61
68
 
62
- layout = b.layout
63
- findings = validate_layout(layout) # schema + grid lint against the bundled catalog
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
- from carterkit import LayoutBuffer, validate_layout
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({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
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({"type": "button", "id": "refresh", "label": "Refresh"})
43
+ b.add_control(build.button(id="refresh", label="Refresh",
44
+ action=bind.action("refresh")))
38
45
 
39
- layout = b.layout
40
- findings = validate_layout(layout) # schema + grid lint against the bundled catalog
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.1.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,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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.1.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
- from carterkit import LayoutBuffer, validate_layout
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({"type": "gauge", "id": "cpu", "label": "CPU", "min": 0, "max": 100},
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({"type": "button", "id": "refresh", "label": "Refresh"})
66
+ b.add_control(build.button(id="refresh", label="Refresh",
67
+ action=bind.action("refresh")))
61
68
 
62
- layout = b.layout
63
- findings = validate_layout(layout) # schema + grid lint against the bundled catalog
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
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ carterkit = carterkit.cli:main
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "carterkit"
7
- version = "0.1.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