flexdeck 0.1.1__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.
- {flexdeck-0.1.1 → flexdeck-0.2.0}/PKG-INFO +1 -1
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck.egg-info/PKG-INFO +1 -1
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/__init__.py +42 -10
- {flexdeck-0.1.1 → flexdeck-0.2.0}/pyproject.toml +1 -1
- {flexdeck-0.1.1 → flexdeck-0.2.0}/tests/test_sdk.py +18 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/LICENSE +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/README.md +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck.egg-info/SOURCES.txt +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck.egg-info/dependency_links.txt +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck.egg-info/entry_points.txt +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck.egg-info/top_level.txt +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/__main__.py +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/conformance.py +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/pack.py +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/template.py +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/flexdeck_plugin/testing.py +0 -0
- {flexdeck-0.1.1 → flexdeck-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flexdeck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Write FlexDeck plugins in Python: declare actions, values, events and feeds; test, pack and verify them against FlexDeck itself.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: flexdeck,plugin,sdk,deck,tablet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flexdeck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Write FlexDeck plugins in Python: declare actions, values, events and feeds; test, pack and verify them against FlexDeck itself.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: flexdeck,plugin,sdk,deck,tablet
|
|
@@ -53,6 +53,8 @@ PERMISSIONS = ("network", "notifications", "processes", "filesystem")
|
|
|
53
53
|
MAX_SERIES_POINTS = 600
|
|
54
54
|
MAX_IMAGE_BYTES = 256 * 1024
|
|
55
55
|
IMAGE_MIMES = ("image/png", "image/jpeg", "image/webp")
|
|
56
|
+
CAPABILITIES = ("series", "presets", "feeds", "toggles")
|
|
57
|
+
_PLACEHOLDER = re.compile(r"\{\{\s*([^{}\s]+)\s*\}\}")
|
|
56
58
|
|
|
57
59
|
Value = Union[str, float, int, bool]
|
|
58
60
|
|
|
@@ -109,6 +111,15 @@ class _Action:
|
|
|
109
111
|
run: Callable[[Dict[str, str], "Context"], Any]
|
|
110
112
|
arguments: Dict[str, Argument]
|
|
111
113
|
check: Optional[Callable[[Dict[str, str], "Context"], Optional[str]]] = None
|
|
114
|
+
active: Optional[Dict[str, str]] = None
|
|
115
|
+
|
|
116
|
+
def active_dto(self, offered: Iterable[str]) -> Optional[Dict[str, str]]:
|
|
117
|
+
"""The light as the host reads it: the plain kind (``equals``, no
|
|
118
|
+
placeholders) for any host, the rest only for one offering ``toggles``."""
|
|
119
|
+
if self.active is None:
|
|
120
|
+
return None
|
|
121
|
+
plain = "equals" in self.active and not _PLACEHOLDER.search(self.active["variable"])
|
|
122
|
+
return dict(self.active) if plain or "toggles" in offered else None
|
|
112
123
|
|
|
113
124
|
|
|
114
125
|
@dataclass
|
|
@@ -311,17 +322,31 @@ class Plugin:
|
|
|
311
322
|
"""Something that happens, which an automation can be started by."""
|
|
312
323
|
self._events[name] = {"description": description, "fields": list(fields)}
|
|
313
324
|
|
|
314
|
-
def action(self, name: str, description: str, arguments: Optional[Mapping[str, Argument]] = None, *, check=None):
|
|
325
|
+
def action(self, name: str, description: str, arguments: Optional[Mapping[str, Argument]] = None, *, check=None, active: Optional[Mapping[str, str]] = None):
|
|
315
326
|
"""Decorate a function ``run(args, ctx)`` as an action. It may return
|
|
316
|
-
``{"state": "<look>"}`` for the key to show.
|
|
327
|
+
``{"state": "<look>"}`` for the key to show.
|
|
328
|
+
|
|
329
|
+
``active`` says when a key running it is lit: ``{"variable": "x.muted"}``
|
|
330
|
+
while that is true, ``{"variable": "x.mode", "is": "ptt"}`` while it holds
|
|
331
|
+
that value, ``{"variable": "x.scene", "equals": "scene"}`` while it holds
|
|
332
|
+
the ``scene`` argument's value. The variable may hold ``{{argument}}``
|
|
333
|
+
placeholders."""
|
|
317
334
|
arguments = dict(arguments or {})
|
|
318
335
|
for arg_name, argument in arguments.items():
|
|
319
336
|
_require(argument.kind in KINDS, f"{name}.{arg_name}: '{argument.kind}' is not a kind")
|
|
320
337
|
if argument.kind == "choice":
|
|
321
338
|
_require(argument.example in argument.choices, f"{name}.{arg_name}: the example must be one of its choices")
|
|
339
|
+
if active is not None:
|
|
340
|
+
active = dict(active)
|
|
341
|
+
variable = active.get("variable", "")
|
|
342
|
+
_require(set(active) <= {"variable", "equals", "is"}, f"{name}.active: takes variable, equals and is")
|
|
343
|
+
_require(not ("equals" in active and "is" in active), f"{name}.active: takes equals or is, not both")
|
|
344
|
+
_require(variable.startswith(f"{self.id}."), f"{name}.active: variable '{variable}' must start with '{self.id}.'")
|
|
345
|
+
for named in _PLACEHOLDER.findall(variable) + ([active["equals"]] if "equals" in active else []):
|
|
346
|
+
_require(named in arguments, f"{name}.active: '{named}' is not one of its arguments")
|
|
322
347
|
|
|
323
348
|
def register(run):
|
|
324
|
-
self._actions[name] = _Action(description=description, run=run, arguments=arguments, check=check)
|
|
349
|
+
self._actions[name] = _Action(description=description, run=run, arguments=arguments, check=check, active=active)
|
|
325
350
|
return run
|
|
326
351
|
|
|
327
352
|
return register
|
|
@@ -360,8 +385,16 @@ class Plugin:
|
|
|
360
385
|
|
|
361
386
|
# ---- What the host is told -------------------------------------------------
|
|
362
387
|
|
|
363
|
-
def manifest(self) -> Dict[str, Any]:
|
|
364
|
-
"""What ``initialize`` answers with
|
|
388
|
+
def manifest(self, offered: Iterable[str] = CAPABILITIES) -> Dict[str, Any]:
|
|
389
|
+
"""What ``initialize`` answers with, to a host offering ``offered``."""
|
|
390
|
+
offered = tuple(offered)
|
|
391
|
+
actions = []
|
|
392
|
+
for n, a in self._actions.items():
|
|
393
|
+
dto: Dict[str, Any] = {"action": n, "description": a.description, "arguments": [arg.dto(k) for k, arg in a.arguments.items()]}
|
|
394
|
+
light = a.active_dto(offered)
|
|
395
|
+
if light is not None:
|
|
396
|
+
dto["active"] = light
|
|
397
|
+
actions.append(dto)
|
|
365
398
|
manifest: Dict[str, Any] = {
|
|
366
399
|
"abi": ABI_VERSION,
|
|
367
400
|
"id": self.id,
|
|
@@ -369,10 +402,7 @@ class Plugin:
|
|
|
369
402
|
"version": self.version,
|
|
370
403
|
"description": self.description,
|
|
371
404
|
"author": self.author,
|
|
372
|
-
"actions":
|
|
373
|
-
{"action": n, "description": a.description, "arguments": [arg.dto(k) for k, arg in a.arguments.items()]}
|
|
374
|
-
for n, a in self._actions.items()
|
|
375
|
-
],
|
|
405
|
+
"actions": actions,
|
|
376
406
|
"settings": [
|
|
377
407
|
{"key": k, "description": s["description"], "kind": s["kind"], "choices": s["choices"], "defaultValue": s["default"], "secret": s["secret"]}
|
|
378
408
|
for k, s in self._settings_spec.items()
|
|
@@ -395,6 +425,8 @@ class Plugin:
|
|
|
395
425
|
}
|
|
396
426
|
for n, f in self._feeds.items()
|
|
397
427
|
]
|
|
428
|
+
if "toggles" in offered and any(a.active is not None and a.active_dto(()) is None for a in self._actions.values()):
|
|
429
|
+
uses.append("toggles")
|
|
398
430
|
manifest["uses"] = uses
|
|
399
431
|
return manifest
|
|
400
432
|
|
|
@@ -451,7 +483,7 @@ class Plugin:
|
|
|
451
483
|
if asked not in SUPPORTED_ABIS:
|
|
452
484
|
raise PluginError(f"this plugin speaks ABI {' or '.join(map(str, SUPPORTED_ABIS))}, the agent speaks {asked}")
|
|
453
485
|
self.configure(body.get("settings") or {}, body.get("dataDir"))
|
|
454
|
-
return {"manifest": self.manifest(), "variables": self.variables()}
|
|
486
|
+
return {"manifest": self.manifest(body.get("hostCapabilities") or ()), "variables": self.variables()}
|
|
455
487
|
if method == "variables":
|
|
456
488
|
return self.variables()
|
|
457
489
|
if method == "check":
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flexdeck"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Write FlexDeck plugins in Python: declare actions, values, events and feeds; test, pack and verify them against FlexDeck itself."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -57,6 +57,24 @@ class DefinitionTest(unittest.TestCase):
|
|
|
57
57
|
)
|
|
58
58
|
self.assertEqual(manifest["uses"], ["series"])
|
|
59
59
|
|
|
60
|
+
def test_a_toggle_light_reaches_only_a_host_that_offers_toggles(self):
|
|
61
|
+
plugin = Plugin(id="lights", name="Lights", version="1.0.0")
|
|
62
|
+
plugin.variable("lights.room.desk.on", "bool")
|
|
63
|
+
plugin.action("toggle", "Toggle", {"room": arg("text", "desk")}, active={"variable": "lights.room.{{room}}.on"})(lambda args, ctx: None)
|
|
64
|
+
plugin.action("pick", "Pick", {"scene": arg("text", "a")}, active={"variable": "lights.room.desk.on", "equals": "scene"})(lambda args, ctx: None)
|
|
65
|
+
|
|
66
|
+
newer = plugin.handle("initialize", {"abi": 4, "hostCapabilities": ["series", "toggles"]})["manifest"]
|
|
67
|
+
self.assertEqual([a.get("active") for a in newer["actions"]], [{"variable": "lights.room.{{room}}.on"}, {"variable": "lights.room.desk.on", "equals": "scene"}])
|
|
68
|
+
self.assertIn("toggles", newer["uses"])
|
|
69
|
+
older = plugin.handle("initialize", {"abi": 4, "hostCapabilities": ["series"]})["manifest"]
|
|
70
|
+
self.assertEqual([a.get("active") for a in older["actions"]], [None, {"variable": "lights.room.desk.on", "equals": "scene"}])
|
|
71
|
+
self.assertNotIn("toggles", older["uses"])
|
|
72
|
+
|
|
73
|
+
with self.assertRaisesRegex(PluginError, "'place' is not one of its arguments"):
|
|
74
|
+
plugin.action("bad", "Bad", {}, active={"variable": "lights.{{place}}.on"})
|
|
75
|
+
with self.assertRaisesRegex(PluginError, "must start with 'lights.'"):
|
|
76
|
+
plugin.action("bad", "Bad", {}, active={"variable": "obs.on"})
|
|
77
|
+
|
|
60
78
|
|
|
61
79
|
class ProtocolTest(unittest.TestCase):
|
|
62
80
|
def test_actions_values_events_and_failures_as_the_host_sees_them(self):
|
|
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
|