hypereffect-ui 0.1.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.
- hypereffect_ui-0.1.0/.gitignore +11 -0
- hypereffect_ui-0.1.0/PKG-INFO +117 -0
- hypereffect_ui-0.1.0/README.md +104 -0
- hypereffect_ui-0.1.0/examples/__init__.py +0 -0
- hypereffect_ui-0.1.0/examples/todos.py +168 -0
- hypereffect_ui-0.1.0/pyproject.toml +36 -0
- hypereffect_ui-0.1.0/src/hypereffect/__init__.py +45 -0
- hypereffect_ui-0.1.0/src/hypereffect/_cel.py +702 -0
- hypereffect_ui-0.1.0/src/hypereffect/client.py +616 -0
- hypereffect_ui-0.1.0/src/hypereffect/components.py +149 -0
- hypereffect_ui-0.1.0/src/hypereffect/message.py +365 -0
- hypereffect_ui-0.1.0/src/hypereffect/server.py +123 -0
- hypereffect_ui-0.1.0/src/hypereffect/signals.py +91 -0
- hypereffect_ui-0.1.0/src/hypereffect/ui.py +77 -0
- hypereffect_ui-0.1.0/tests/test_fixtures.py +118 -0
- hypereffect_ui-0.1.0/tests/test_message.py +135 -0
- hypereffect_ui-0.1.0/tests/test_todos.py +43 -0
- hypereffect_ui-0.1.0/uv.lock +617 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hypereffect-ui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Types and helpers for writing hypereffect servers: messages, line framing, signals and components, an App/Server session driver, and a renderer client with a loopback for tests.
|
|
5
|
+
Project-URL: Homepage, https://hypereffect.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/hypereffect/hypereffect
|
|
7
|
+
Project-URL: Issues, https://github.com/hypereffect/hypereffect/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: cel,gui,hypermedia,server-driven-ui,ui
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: cel-python<0.6,>=0.5
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# hypereffect (Python)
|
|
15
|
+
|
|
16
|
+
Types and helpers for writing [hypereffect](https://hypereffect.dev) servers in
|
|
17
|
+
Python, ported from the [Rust crate](https://github.com/hypereffect/hypereffect/tree/master/rust):
|
|
18
|
+
elements, handlers and messages with their line framing, signals, the
|
|
19
|
+
components and their nesting rules, an `App`/`Server` session driver, and a
|
|
20
|
+
renderer client with a loopback for testing apps without a renderer.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pip install hypereffect-ui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The distribution is `hypereffect-ui`; the package is imported as
|
|
27
|
+
`hypereffect`. It needs Python 3.11 or later.
|
|
28
|
+
|
|
29
|
+
## Servers
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from hypereffect import Action, App, Handler, Outbox, cel, ui
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Counter(App):
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self.count = 0
|
|
38
|
+
|
|
39
|
+
def hello(self, out: Outbox, params: dict[str, str]) -> None:
|
|
40
|
+
out.signal({"count": 0})
|
|
41
|
+
out.element(ui.column("main"))
|
|
42
|
+
out.element(ui.text("value").parent("main").text(cel("'Count: ' + string(s.count)")))
|
|
43
|
+
out.element(ui.button("inc").parent("main").label("+").on_press(Handler(action="add")))
|
|
44
|
+
|
|
45
|
+
def action(self, out: Outbox, action: Action) -> None:
|
|
46
|
+
if action.name == "add":
|
|
47
|
+
self.count += 1
|
|
48
|
+
out.signal({"count": self.count})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `ui.text("id")` and the other constructors return an `Element` with setters
|
|
52
|
+
for every attribute (`label`, `variant`, `disabled`, …, or `set` for any
|
|
53
|
+
other); each takes a literal or `cel("...")`. `bind` sets a signal path, and
|
|
54
|
+
`on_press`, `on_change`, `on_submit` and `on_dismiss` take a `Handler`,
|
|
55
|
+
built with keywords: `Handler(action="save", payload=7, indicator="_busy")`,
|
|
56
|
+
`Handler(patch={"_open": True})` or `Handler(open="https://…")`.
|
|
57
|
+
- `parse_server_message`, `parse_renderer_message` and each message's
|
|
58
|
+
`to_line` convert to and from lines; `LineBuffer` splits a stream of bytes
|
|
59
|
+
or text into lines.
|
|
60
|
+
- `hypereffect.signals` has merge patch, local-signal stripping and signal
|
|
61
|
+
paths.
|
|
62
|
+
- `Server` drives an `App` from what a renderer sends, creating a new app on
|
|
63
|
+
every hello. An app that overrides `tick_interval` gets `tick` calls.
|
|
64
|
+
|
|
65
|
+
The library does not open connections. Any transport that carries lines
|
|
66
|
+
works: pass what arrives to `server.receive`, then send
|
|
67
|
+
`server.output.take()`. Over stdio:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
server = Server(Counter)
|
|
73
|
+
for chunk in iter(sys.stdin.buffer.readline, b""):
|
|
74
|
+
server.receive(chunk)
|
|
75
|
+
sys.stdout.write(server.output.take())
|
|
76
|
+
sys.stdout.flush()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
`hypereffect.client` is the renderer side without drawing, on top of
|
|
82
|
+
[cel-python](https://github.com/cloud-custodian/cel-python).
|
|
83
|
+
`Loopback` connects it to a `Server` in one process:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from hypereffect.client import Loopback
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_counter() -> None:
|
|
90
|
+
l = Loopback(Counter) # sends hello
|
|
91
|
+
assert l.trigger("inc", "onPress") # True if an action was sent
|
|
92
|
+
assert l.client.attr("value", "text") == "Count: 1"
|
|
93
|
+
assert l.client.take_diagnostics() == [] # rejected elements or failed expressions
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`l.write(path, value)` writes a bound signal as a control does,
|
|
97
|
+
`l.tick(now_ms)` runs the timer, `l.client.value(path)` reads a signal and
|
|
98
|
+
`l.client.take_opens()` returns the URLs buttons asked to open.
|
|
99
|
+
`examples/todos.py` is the task list demo; `tests/test_todos.py` tests it.
|
|
100
|
+
|
|
101
|
+
cel-python lacks parts of the CEL environment the spec requires, so
|
|
102
|
+
`hypereffect._cel` adds them: comparison of ints and doubles by value, the
|
|
103
|
+
strings, math and lists extensions (following cel-go), the `sortBy` macro,
|
|
104
|
+
and the `math.`, `strings.` and `lists.` namespaces. It relies on
|
|
105
|
+
cel-python's evaluator internals, so the dependency is pinned to 0.5.x.
|
|
106
|
+
|
|
107
|
+
There is no WebAssembly export like the Rust `wasm_app!`.
|
|
108
|
+
|
|
109
|
+
## Tests
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
uv run pytest
|
|
113
|
+
uv run mypy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The tests include the shared renderer fixtures in the repository's
|
|
117
|
+
[`fixtures/`](https://github.com/hypereffect/hypereffect/tree/master/fixtures).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# hypereffect (Python)
|
|
2
|
+
|
|
3
|
+
Types and helpers for writing [hypereffect](https://hypereffect.dev) servers in
|
|
4
|
+
Python, ported from the [Rust crate](https://github.com/hypereffect/hypereffect/tree/master/rust):
|
|
5
|
+
elements, handlers and messages with their line framing, signals, the
|
|
6
|
+
components and their nesting rules, an `App`/`Server` session driver, and a
|
|
7
|
+
renderer client with a loopback for testing apps without a renderer.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install hypereffect-ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The distribution is `hypereffect-ui`; the package is imported as
|
|
14
|
+
`hypereffect`. It needs Python 3.11 or later.
|
|
15
|
+
|
|
16
|
+
## Servers
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from hypereffect import Action, App, Handler, Outbox, cel, ui
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Counter(App):
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.count = 0
|
|
25
|
+
|
|
26
|
+
def hello(self, out: Outbox, params: dict[str, str]) -> None:
|
|
27
|
+
out.signal({"count": 0})
|
|
28
|
+
out.element(ui.column("main"))
|
|
29
|
+
out.element(ui.text("value").parent("main").text(cel("'Count: ' + string(s.count)")))
|
|
30
|
+
out.element(ui.button("inc").parent("main").label("+").on_press(Handler(action="add")))
|
|
31
|
+
|
|
32
|
+
def action(self, out: Outbox, action: Action) -> None:
|
|
33
|
+
if action.name == "add":
|
|
34
|
+
self.count += 1
|
|
35
|
+
out.signal({"count": self.count})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `ui.text("id")` and the other constructors return an `Element` with setters
|
|
39
|
+
for every attribute (`label`, `variant`, `disabled`, …, or `set` for any
|
|
40
|
+
other); each takes a literal or `cel("...")`. `bind` sets a signal path, and
|
|
41
|
+
`on_press`, `on_change`, `on_submit` and `on_dismiss` take a `Handler`,
|
|
42
|
+
built with keywords: `Handler(action="save", payload=7, indicator="_busy")`,
|
|
43
|
+
`Handler(patch={"_open": True})` or `Handler(open="https://…")`.
|
|
44
|
+
- `parse_server_message`, `parse_renderer_message` and each message's
|
|
45
|
+
`to_line` convert to and from lines; `LineBuffer` splits a stream of bytes
|
|
46
|
+
or text into lines.
|
|
47
|
+
- `hypereffect.signals` has merge patch, local-signal stripping and signal
|
|
48
|
+
paths.
|
|
49
|
+
- `Server` drives an `App` from what a renderer sends, creating a new app on
|
|
50
|
+
every hello. An app that overrides `tick_interval` gets `tick` calls.
|
|
51
|
+
|
|
52
|
+
The library does not open connections. Any transport that carries lines
|
|
53
|
+
works: pass what arrives to `server.receive`, then send
|
|
54
|
+
`server.output.take()`. Over stdio:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import sys
|
|
58
|
+
|
|
59
|
+
server = Server(Counter)
|
|
60
|
+
for chunk in iter(sys.stdin.buffer.readline, b""):
|
|
61
|
+
server.receive(chunk)
|
|
62
|
+
sys.stdout.write(server.output.take())
|
|
63
|
+
sys.stdout.flush()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Testing
|
|
67
|
+
|
|
68
|
+
`hypereffect.client` is the renderer side without drawing, on top of
|
|
69
|
+
[cel-python](https://github.com/cloud-custodian/cel-python).
|
|
70
|
+
`Loopback` connects it to a `Server` in one process:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from hypereffect.client import Loopback
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_counter() -> None:
|
|
77
|
+
l = Loopback(Counter) # sends hello
|
|
78
|
+
assert l.trigger("inc", "onPress") # True if an action was sent
|
|
79
|
+
assert l.client.attr("value", "text") == "Count: 1"
|
|
80
|
+
assert l.client.take_diagnostics() == [] # rejected elements or failed expressions
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`l.write(path, value)` writes a bound signal as a control does,
|
|
84
|
+
`l.tick(now_ms)` runs the timer, `l.client.value(path)` reads a signal and
|
|
85
|
+
`l.client.take_opens()` returns the URLs buttons asked to open.
|
|
86
|
+
`examples/todos.py` is the task list demo; `tests/test_todos.py` tests it.
|
|
87
|
+
|
|
88
|
+
cel-python lacks parts of the CEL environment the spec requires, so
|
|
89
|
+
`hypereffect._cel` adds them: comparison of ints and doubles by value, the
|
|
90
|
+
strings, math and lists extensions (following cel-go), the `sortBy` macro,
|
|
91
|
+
and the `math.`, `strings.` and `lists.` namespaces. It relies on
|
|
92
|
+
cel-python's evaluator internals, so the dependency is pinned to 0.5.x.
|
|
93
|
+
|
|
94
|
+
There is no WebAssembly export like the Rust `wasm_app!`.
|
|
95
|
+
|
|
96
|
+
## Tests
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
uv run pytest
|
|
100
|
+
uv run mypy
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The tests include the shared renderer fixtures in the repository's
|
|
104
|
+
[`fixtures/`](https://github.com/hypereffect/hypereffect/tree/master/fixtures).
|
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""A task list: adding, checking off and deleting tasks, a local filter, and
|
|
2
|
+
a confirmation dialog. The Python port of the demo on hypereffect.dev."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from hypereffect import Action, App, Element, Handler, Json, Outbox, cel, ui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Task:
|
|
13
|
+
id: str
|
|
14
|
+
title: str
|
|
15
|
+
done: bool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Todos(App):
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self.next = 1
|
|
21
|
+
self.tasks = [
|
|
22
|
+
Task(self.new_id(), title, done)
|
|
23
|
+
for title, done in [("Water the plants", True), ("Call the bank", False), ("Read the spec", False)]
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def new_id(self) -> str:
|
|
27
|
+
id = f"t{self.next}"
|
|
28
|
+
self.next += 1
|
|
29
|
+
return id
|
|
30
|
+
|
|
31
|
+
def row(self, task: Task) -> list[Element]:
|
|
32
|
+
id = task.id
|
|
33
|
+
done = f"s.done.{id}"
|
|
34
|
+
return [
|
|
35
|
+
ui.row(id)
|
|
36
|
+
.parent("items")
|
|
37
|
+
.justify("between")
|
|
38
|
+
.visible(cel(f"s._filter == 'all' || (s._filter == 'done') == {done}")),
|
|
39
|
+
ui.checkbox(f"{id}-check")
|
|
40
|
+
.parent(id)
|
|
41
|
+
.label(task.title)
|
|
42
|
+
.bind(f"done.{id}")
|
|
43
|
+
.on_change(Handler(action="toggle", payload=id)),
|
|
44
|
+
ui.button(f"{id}-del")
|
|
45
|
+
.parent(id)
|
|
46
|
+
.label("Delete")
|
|
47
|
+
.variant("plain")
|
|
48
|
+
.on_press(Handler(action="delete", payload=id)),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def hello(self, out: Outbox, params: dict[str, str]) -> None:
|
|
52
|
+
out.signal({
|
|
53
|
+
"new": "",
|
|
54
|
+
"done": {t.id: t.done for t in self.tasks},
|
|
55
|
+
"_filter": "all",
|
|
56
|
+
"_adding": False,
|
|
57
|
+
"_confirm": False,
|
|
58
|
+
"_clearing": False,
|
|
59
|
+
})
|
|
60
|
+
out.element(ui.card("todos").title("Tasks"))
|
|
61
|
+
out.element(ui.row("add-row").parent("todos"))
|
|
62
|
+
out.element(
|
|
63
|
+
ui.input("new")
|
|
64
|
+
.parent("add-row")
|
|
65
|
+
.placeholder("New task")
|
|
66
|
+
.bind("new")
|
|
67
|
+
.error(cel("size(s.new) > 80 ? 'Keep it under 80 characters.' : ''"))
|
|
68
|
+
.on_submit(Handler(action="add", indicator="_adding"))
|
|
69
|
+
)
|
|
70
|
+
out.element(
|
|
71
|
+
ui.button("add")
|
|
72
|
+
.parent("add-row")
|
|
73
|
+
.variant("primary")
|
|
74
|
+
.label(cel("s._adding ? 'Adding…' : 'Add'"))
|
|
75
|
+
.disabled(cel("s._adding || s.new.trim() == '' || size(s.new) > 80"))
|
|
76
|
+
.on_press(Handler(action="add", indicator="_adding"))
|
|
77
|
+
)
|
|
78
|
+
out.element(
|
|
79
|
+
ui.select("filter")
|
|
80
|
+
.parent("todos")
|
|
81
|
+
.variant("segmented")
|
|
82
|
+
.bind("_filter")
|
|
83
|
+
.options([
|
|
84
|
+
{"value": "all", "label": "All"},
|
|
85
|
+
{"value": "open", "label": "Open"},
|
|
86
|
+
{"value": "done", "label": "Done"},
|
|
87
|
+
])
|
|
88
|
+
)
|
|
89
|
+
out.element(ui.list("items").parent("todos").empty("Nothing here."))
|
|
90
|
+
for t in self.tasks:
|
|
91
|
+
for e in self.row(t):
|
|
92
|
+
out.element(e)
|
|
93
|
+
out.element(ui.row("footer").parent("todos").justify("between"))
|
|
94
|
+
out.element(ui.text("left").parent("footer").variant("caption").text(cel(
|
|
95
|
+
"string(s.done.filter(k, !s.done[k]).size()) + ' of ' + string(size(s.done)) + ' left'"
|
|
96
|
+
)))
|
|
97
|
+
out.element(
|
|
98
|
+
ui.button("clear")
|
|
99
|
+
.parent("footer")
|
|
100
|
+
.variant("failure")
|
|
101
|
+
.label("Clear completed…")
|
|
102
|
+
.disabled(cel("!s.done.exists(k, s.done[k]) || s._clearing"))
|
|
103
|
+
.on_press(Handler(patch={"_confirm": True}))
|
|
104
|
+
)
|
|
105
|
+
out.element(
|
|
106
|
+
ui.dialog("confirm")
|
|
107
|
+
.title("Clear completed tasks?")
|
|
108
|
+
.open(cel("s._confirm"))
|
|
109
|
+
.on_dismiss(Handler(patch={"_confirm": False}))
|
|
110
|
+
)
|
|
111
|
+
out.element(ui.text("confirm-text").parent("confirm").text(cel(
|
|
112
|
+
"string(s.done.filter(k, s.done[k]).size()) + ' tasks will be deleted.'"
|
|
113
|
+
)))
|
|
114
|
+
out.element(ui.row("confirm-row").parent("confirm").justify("end"))
|
|
115
|
+
out.element(
|
|
116
|
+
ui.button("confirm-no")
|
|
117
|
+
.parent("confirm-row")
|
|
118
|
+
.label("Cancel")
|
|
119
|
+
.on_press(Handler(patch={"_confirm": False}))
|
|
120
|
+
)
|
|
121
|
+
out.element(
|
|
122
|
+
ui.button("confirm-yes")
|
|
123
|
+
.parent("confirm-row")
|
|
124
|
+
.variant("failure")
|
|
125
|
+
.label("Delete")
|
|
126
|
+
.on_press(Handler(action="clear", patch={"_confirm": False}, indicator="_clearing"))
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def find(self, id: Json) -> Task | None:
|
|
130
|
+
return next((t for t in self.tasks if t.id == id), None)
|
|
131
|
+
|
|
132
|
+
def action(self, out: Outbox, action: Action) -> None:
|
|
133
|
+
if action.name == "add":
|
|
134
|
+
raw = action.signal("new")
|
|
135
|
+
title = raw.strip() if isinstance(raw, str) else ""
|
|
136
|
+
if not title or len(title) > 80:
|
|
137
|
+
out.signal({"_adding": False})
|
|
138
|
+
return
|
|
139
|
+
task = Task(self.new_id(), title, False)
|
|
140
|
+
# New tasks go at the top.
|
|
141
|
+
first = self.tasks[0].id if self.tasks else None
|
|
142
|
+
# The signal comes first so that the row's expressions can read
|
|
143
|
+
# done.<id> as soon as it arrives.
|
|
144
|
+
out.signal({"new": "", "done": {task.id: False}, "_adding": False})
|
|
145
|
+
for i, e in enumerate(self.row(task)):
|
|
146
|
+
out.element(e.before(first) if i == 0 and first else e)
|
|
147
|
+
self.tasks.insert(0, task)
|
|
148
|
+
elif action.name == "toggle":
|
|
149
|
+
task = self.find(action.payload)
|
|
150
|
+
if task is None:
|
|
151
|
+
return
|
|
152
|
+
done = action.signal(f"done.{task.id}")
|
|
153
|
+
if isinstance(done, bool):
|
|
154
|
+
task.done = done
|
|
155
|
+
out.signal({"done": {task.id: task.done}})
|
|
156
|
+
elif action.name == "delete":
|
|
157
|
+
task = self.find(action.payload)
|
|
158
|
+
if task is None:
|
|
159
|
+
return
|
|
160
|
+
self.tasks.remove(task)
|
|
161
|
+
out.remove(task.id)
|
|
162
|
+
out.signal({"done": {task.id: None}})
|
|
163
|
+
elif action.name == "clear":
|
|
164
|
+
for t in self.tasks:
|
|
165
|
+
if t.done:
|
|
166
|
+
out.remove(t.id)
|
|
167
|
+
out.signal({"done": {t.id: None for t in self.tasks if t.done}, "_clearing": False})
|
|
168
|
+
self.tasks = [t for t in self.tasks if not t.done]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hypereffect-ui"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Types and helpers for writing hypereffect servers: messages, line framing, signals and components, an App/Server session driver, and a renderer client with a loopback for tests."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = ["cel-python>=0.5,<0.6"]
|
|
9
|
+
keywords = ["hypermedia", "gui", "ui", "server-driven-ui", "cel"]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://hypereffect.dev"
|
|
13
|
+
Repository = "https://github.com/hypereffect/hypereffect"
|
|
14
|
+
Issues = "https://github.com/hypereffect/hypereffect/issues"
|
|
15
|
+
|
|
16
|
+
[dependency-groups]
|
|
17
|
+
dev = ["pytest>=8", "mypy>=1.10"]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
pythonpath = ["."]
|
|
26
|
+
|
|
27
|
+
[tool.mypy]
|
|
28
|
+
strict = true
|
|
29
|
+
files = ["src"]
|
|
30
|
+
|
|
31
|
+
[[tool.mypy.overrides]]
|
|
32
|
+
module = ["celpy", "celpy.*", "lark", "lark.*"]
|
|
33
|
+
ignore_missing_imports = true
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/hypereffect"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Types and helpers for `hypereffect <https://hypereffect.dev>`_, a small
|
|
2
|
+
hypermedia protocol for server-driven UI.
|
|
3
|
+
|
|
4
|
+
- Elements, handlers, server and renderer messages, and the line framing.
|
|
5
|
+
Building an interface looks like
|
|
6
|
+
``ui.text("title").parent("main").variant("h1").text("Inbox")``.
|
|
7
|
+
- :mod:`hypereffect.signals`: merge patch, local signals and signal paths.
|
|
8
|
+
- The components, their attributes and nesting rules.
|
|
9
|
+
- An :class:`App` base class for per-session logic, and a :class:`Server`
|
|
10
|
+
that drives one from any transport.
|
|
11
|
+
- :mod:`hypereffect.client`: the renderer side without drawing, and a
|
|
12
|
+
``Loopback`` that connects it to a Server for tests.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from . import signals, ui
|
|
16
|
+
from .components import Attr, AttrType, Component, accepts
|
|
17
|
+
from .message import (
|
|
18
|
+
Action,
|
|
19
|
+
Element,
|
|
20
|
+
Handler,
|
|
21
|
+
Hello,
|
|
22
|
+
Json,
|
|
23
|
+
LineBuffer,
|
|
24
|
+
Metadata,
|
|
25
|
+
ParseError,
|
|
26
|
+
ParseErrorKind,
|
|
27
|
+
RendererMessage,
|
|
28
|
+
Remove,
|
|
29
|
+
ServerMessage,
|
|
30
|
+
Signal,
|
|
31
|
+
cel,
|
|
32
|
+
encode_line,
|
|
33
|
+
is_cel,
|
|
34
|
+
parse_line,
|
|
35
|
+
parse_renderer_message,
|
|
36
|
+
parse_server_message,
|
|
37
|
+
)
|
|
38
|
+
from .server import App, Outbox, Server
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Action", "App", "Attr", "AttrType", "Component", "Element", "Handler", "Hello", "Json",
|
|
42
|
+
"LineBuffer", "Metadata", "Outbox", "ParseError", "ParseErrorKind", "RendererMessage",
|
|
43
|
+
"Remove", "Server", "ServerMessage", "Signal", "accepts", "cel", "encode_line", "is_cel",
|
|
44
|
+
"parse_line", "parse_renderer_message", "parse_server_message", "signals", "ui",
|
|
45
|
+
]
|