aboutsummaryrefslogtreecommitdiffstats
path: root/deckit/module.py
blob: b30fbf5ed66d76cd06f770f0c0632e7a61ee0ebc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"""The module contract and the registry that dispatches to modules.

A module is a data source plus an allow-listed set of actions. The client sends
a module name and an action id; it never sends a command. The registry is the
single place that maps ids to code, so adding a module never touches the server.
"""

from dataclasses import dataclass, field
from typing import Callable


class ModuleError(Exception):
    """A bad module/action id, or a module that cannot serve the request."""


@dataclass
class Module:
    name: str
    title: str
    state: Callable[[], dict]
    actions: dict[str, Callable[[dict], dict | None]] = field(default_factory=dict)
    start: Callable[[], None] | None = None
    stop: Callable[[], None] | None = None


class Registry:
    def __init__(self, modules):
        self._modules = {m.name: m for m in modules}

    def names(self) -> list[dict]:
        return [{"name": m.name, "title": m.title} for m in self._modules.values()]

    def _get(self, name: str) -> Module:
        if name not in self._modules:
            raise ModuleError(f"unknown module: {name}")
        return self._modules[name]

    def state(self, name: str) -> dict:
        return self._get(name).state()

    def run_action(self, name: str, action: str, body: dict) -> dict | None:
        module = self._get(name)
        if action not in module.actions:
            raise ModuleError(f"unknown action: {name}/{action}")
        return module.actions[action](body)