buchelib 0.0.1__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.
- buchelib-0.0.1/PKG-INFO +18 -0
- buchelib-0.0.1/README.md +6 -0
- buchelib-0.0.1/pyproject.toml +28 -0
- buchelib-0.0.1/src/buchelib/__init__.py +44 -0
- buchelib-0.0.1/src/buchelib/bridge.py +294 -0
- buchelib-0.0.1/src/buchelib/htmlgen.py +41 -0
- buchelib-0.0.1/src/buchelib/interpreter.py +36 -0
- buchelib-0.0.1/src/buchelib/js.py +75 -0
- buchelib-0.0.1/src/buchelib/lib.js +46 -0
- buchelib-0.0.1/src/buchelib/py.typed +0 -0
- buchelib-0.0.1/src/buchelib/selection.py +49 -0
- buchelib-0.0.1/src/buchelib/srx.py +72 -0
buchelib-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: buchelib
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: Olivier Breuleux
|
|
6
|
+
Author-email: Olivier Breuleux <breuleux@gmail.com>
|
|
7
|
+
Requires-Dist: hypetext>=0.0.4
|
|
8
|
+
Requires-Dist: ovld>=0.5.17
|
|
9
|
+
Requires-Dist: serieux>=0.3.13
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# buchelib
|
|
15
|
+
|
|
16
|
+
**WIP**
|
|
17
|
+
|
|
18
|
+
A library to easily generate interface for the `buche` shell.
|
buchelib-0.0.1/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "buchelib"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Olivier Breuleux", email = "breuleux@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"hypetext>=0.0.4",
|
|
12
|
+
"ovld>=0.5.17",
|
|
13
|
+
"serieux>=0.3.13",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.9.3,<0.10.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.ruff]
|
|
21
|
+
line-length = 99
|
|
22
|
+
|
|
23
|
+
[tool.ruff.lint]
|
|
24
|
+
extend-select = ["I"]
|
|
25
|
+
ignore = ["F811", "F722"]
|
|
26
|
+
|
|
27
|
+
[tool.ruff.lint.isort]
|
|
28
|
+
combine-as-imports = true
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from serieux import Serieux
|
|
5
|
+
|
|
6
|
+
from .bridge import Bridge, Cell
|
|
7
|
+
from .htmlgen import BucheInterpreter
|
|
8
|
+
from .srx import BucheSerieux
|
|
9
|
+
|
|
10
|
+
here = Path(__file__).parent
|
|
11
|
+
|
|
12
|
+
__bridge = None
|
|
13
|
+
__main_cell = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def bridge():
|
|
17
|
+
global __bridge
|
|
18
|
+
if not __bridge:
|
|
19
|
+
__bridge = Bridge()
|
|
20
|
+
return __bridge
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main_cell():
|
|
24
|
+
global __main_cell
|
|
25
|
+
if not __main_cell:
|
|
26
|
+
__main_cell = Cell(
|
|
27
|
+
bridge=bridge(),
|
|
28
|
+
interpreter_class=BucheInterpreter,
|
|
29
|
+
srx=(Serieux + BucheSerieux)(),
|
|
30
|
+
id="main",
|
|
31
|
+
)
|
|
32
|
+
__main_cell.body().exec(here / "lib.js")
|
|
33
|
+
return __main_cell
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_available():
|
|
37
|
+
fd_str = os.environ.get("BUCHE_CONTROL_FD")
|
|
38
|
+
if not fd_str:
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
os.fstat(int(fd_str))
|
|
42
|
+
return True
|
|
43
|
+
except ValueError, OSError:
|
|
44
|
+
return False
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import select
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from glob import glob
|
|
8
|
+
from itertools import count
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import FunctionType
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from ovld import ovld, recurse
|
|
15
|
+
from serieux import JSON, Context, Serieux, serieux
|
|
16
|
+
from serieux.features.tagset import TagDict
|
|
17
|
+
|
|
18
|
+
here = Path(__file__).parent
|
|
19
|
+
|
|
20
|
+
_current_id = count()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@ovld
|
|
24
|
+
def _expand_paths(p: str):
|
|
25
|
+
if "*" in p:
|
|
26
|
+
for path in glob(p):
|
|
27
|
+
yield from recurse(Path(path))
|
|
28
|
+
else:
|
|
29
|
+
yield from recurse(Path(p))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@ovld
|
|
33
|
+
def _expand_paths(p: Path):
|
|
34
|
+
if p.is_dir():
|
|
35
|
+
raise Exception("Availing a directory is not allowed. Avail individual files.")
|
|
36
|
+
yield p
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@ovld
|
|
40
|
+
def _expand_paths(p: Iterable):
|
|
41
|
+
for x in p:
|
|
42
|
+
yield from recurse(x)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CellMessage:
|
|
47
|
+
message: JSON
|
|
48
|
+
cell: Cell
|
|
49
|
+
|
|
50
|
+
def parse(self):
|
|
51
|
+
return self.cell.deserialize(self.message)
|
|
52
|
+
|
|
53
|
+
async def dispatch(self):
|
|
54
|
+
await self.parse().call()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class PromptMessage:
|
|
59
|
+
message: JSON
|
|
60
|
+
prompt: Prompt
|
|
61
|
+
|
|
62
|
+
async def dispatch(self):
|
|
63
|
+
return await self.prompt.handler(self.prompt, self.message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Bridge:
|
|
67
|
+
def __init__(self, fd=None):
|
|
68
|
+
if fd is None:
|
|
69
|
+
fd = int(os.environ.get("BUCHE_CONTROL_FD", 5))
|
|
70
|
+
self.ctlin = os.fdopen(fd, "r", buffering=1)
|
|
71
|
+
self._outfd = os.dup(fd)
|
|
72
|
+
self.catalogue = {}
|
|
73
|
+
self.cells = {}
|
|
74
|
+
self.prompts = {}
|
|
75
|
+
|
|
76
|
+
def send(self, payload):
|
|
77
|
+
data = (json.dumps(payload) + "\n").encode()
|
|
78
|
+
written = 0
|
|
79
|
+
while written < len(data):
|
|
80
|
+
select.select([], [self._outfd], [])
|
|
81
|
+
try:
|
|
82
|
+
written += os.write(self._outfd, data[written:])
|
|
83
|
+
except BlockingIOError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def _pack_file(self, nonce, rel, p):
|
|
87
|
+
match p.suffix:
|
|
88
|
+
case ".css":
|
|
89
|
+
packed = {"mimetype": "text/css", "content": p.read_text()}
|
|
90
|
+
case ".js":
|
|
91
|
+
packed = {"mimetype": "text/javascript", "content": p.read_text()}
|
|
92
|
+
case other:
|
|
93
|
+
raise Exception(f"Unsupported format: {other}")
|
|
94
|
+
self.catalogue[str(p)] = f"buche://nonce/{nonce}/{rel}"
|
|
95
|
+
return packed
|
|
96
|
+
|
|
97
|
+
def avail(self, *files):
|
|
98
|
+
concrete = list(_expand_paths(files))
|
|
99
|
+
paths = [Path(f).resolve() for f in concrete]
|
|
100
|
+
base = os.path.commonpath([str(p.parent) for p in paths])
|
|
101
|
+
nonce = str(uuid4())
|
|
102
|
+
self.send(
|
|
103
|
+
{
|
|
104
|
+
"type": "library",
|
|
105
|
+
"nonce": nonce,
|
|
106
|
+
"files": {
|
|
107
|
+
str(rel := p.relative_to(base)): self._pack_file(nonce, rel, p) for p in paths
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def url(self, p):
|
|
113
|
+
sp = str(Path(p).resolve())
|
|
114
|
+
if sp not in self.catalogue:
|
|
115
|
+
self.avail(p)
|
|
116
|
+
return self.catalogue[sp]
|
|
117
|
+
|
|
118
|
+
@cached_property
|
|
119
|
+
def srx(self):
|
|
120
|
+
from .srx import BucheSerieux
|
|
121
|
+
|
|
122
|
+
return (Serieux + BucheSerieux)()
|
|
123
|
+
|
|
124
|
+
def __setup_cell(self, cell):
|
|
125
|
+
self.cells[cell.id] = cell
|
|
126
|
+
cell.body().exec(here / "lib.js")
|
|
127
|
+
return cell
|
|
128
|
+
|
|
129
|
+
@cached_property
|
|
130
|
+
def main_cell(self):
|
|
131
|
+
from .htmlgen import BucheInterpreter
|
|
132
|
+
|
|
133
|
+
cell = Cell(
|
|
134
|
+
bridge=self,
|
|
135
|
+
interpreter_class=BucheInterpreter,
|
|
136
|
+
srx=self.srx,
|
|
137
|
+
id="main",
|
|
138
|
+
)
|
|
139
|
+
self.__setup_cell(cell)
|
|
140
|
+
return cell
|
|
141
|
+
|
|
142
|
+
def cell(self, echo=None):
|
|
143
|
+
from .htmlgen import BucheInterpreter
|
|
144
|
+
|
|
145
|
+
_id = uuid4().hex
|
|
146
|
+
|
|
147
|
+
cell = Cell(
|
|
148
|
+
bridge=self,
|
|
149
|
+
interpreter_class=BucheInterpreter,
|
|
150
|
+
srx=self.srx,
|
|
151
|
+
id=_id,
|
|
152
|
+
)
|
|
153
|
+
self.send(
|
|
154
|
+
{
|
|
155
|
+
"type": "cell_create",
|
|
156
|
+
"address": {"cell_id": _id},
|
|
157
|
+
"to": {"target": "terminal", "cell": _id},
|
|
158
|
+
"mode": "data",
|
|
159
|
+
"echo_html": echo,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
self.__setup_cell(cell)
|
|
163
|
+
return cell
|
|
164
|
+
|
|
165
|
+
def prompt(self, label, handler=None, language=None):
|
|
166
|
+
prompt = Prompt(bridge=self, label=label, handler=handler)
|
|
167
|
+
self.send(
|
|
168
|
+
{
|
|
169
|
+
"type": "prompt_create",
|
|
170
|
+
"to": {"target": "terminal", "prompt": "python"},
|
|
171
|
+
"address": {"prompt_id": label},
|
|
172
|
+
"prompt": "<span style='color:#4ec9b0;'>sktk ]]]</span>",
|
|
173
|
+
"tab_html": label,
|
|
174
|
+
"name": label,
|
|
175
|
+
"tag": "python",
|
|
176
|
+
"language": language,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
self.prompts[label] = prompt
|
|
180
|
+
return prompt
|
|
181
|
+
|
|
182
|
+
async def __aiter__(self):
|
|
183
|
+
loop = asyncio.get_event_loop()
|
|
184
|
+
reader = asyncio.StreamReader()
|
|
185
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
186
|
+
await loop.connect_read_pipe(lambda: protocol, self.ctlin)
|
|
187
|
+
async for line in reader:
|
|
188
|
+
data = json.loads(line)
|
|
189
|
+
to = data.pop("to", {})
|
|
190
|
+
if prid := to.get("prompt_id", None):
|
|
191
|
+
yield PromptMessage(message=data, prompt=self.prompts.get(prid, None))
|
|
192
|
+
else:
|
|
193
|
+
cell_id = to.get("cell_id", "main")
|
|
194
|
+
yield CellMessage(message=data, cell=self.cells.get(cell_id, None))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
_tagdict_defaults = {}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(kw_only=True)
|
|
201
|
+
class Cell:
|
|
202
|
+
bridge: Bridge
|
|
203
|
+
id: str = "main"
|
|
204
|
+
interpreter_class: type
|
|
205
|
+
tagset: TagDict = field(default_factory=lambda: TagDict(_tagdict_defaults))
|
|
206
|
+
srx: Serieux = field(default_factory=lambda: serieux)
|
|
207
|
+
function_registry: dict = field(default_factory=dict)
|
|
208
|
+
handler: Any = None
|
|
209
|
+
|
|
210
|
+
def __post_init__(self):
|
|
211
|
+
self.address = {"cell_id": self.id} if self.id != "main" else {}
|
|
212
|
+
self.interpreter = self.interpreter_class(self)
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def register_type_default(cls, fn):
|
|
216
|
+
_tagdict_defaults[f"{fn.__module__}:{fn.__qualname__}"] = fn
|
|
217
|
+
|
|
218
|
+
def register_type(self, *args, **kwargs):
|
|
219
|
+
self.tagset.register(*args, **kwargs)
|
|
220
|
+
|
|
221
|
+
def register_function(self, fn):
|
|
222
|
+
if fn not in self.function_registry:
|
|
223
|
+
self.function_registry[fn] = f"F{next(_current_id)}"
|
|
224
|
+
return self.function_registry[fn]
|
|
225
|
+
|
|
226
|
+
async def inputs(self):
|
|
227
|
+
loop = asyncio.get_event_loop()
|
|
228
|
+
reader = asyncio.StreamReader()
|
|
229
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
230
|
+
await loop.connect_read_pipe(lambda: protocol, self.bridge.ctlin)
|
|
231
|
+
async for line in reader:
|
|
232
|
+
data = json.loads(line)
|
|
233
|
+
data.pop("to", None)
|
|
234
|
+
yield self.deserialize(data)
|
|
235
|
+
|
|
236
|
+
def avail(self, *files):
|
|
237
|
+
return self.bridge.avail(*files)
|
|
238
|
+
|
|
239
|
+
def command(self, **args):
|
|
240
|
+
return self.bridge.send(
|
|
241
|
+
{
|
|
242
|
+
"type": "cell_send",
|
|
243
|
+
"message": args,
|
|
244
|
+
"address": self.address,
|
|
245
|
+
"to": {"target": "terminal", "cell": self.id},
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def configure(self, **options):
|
|
250
|
+
self.bridge.send(
|
|
251
|
+
{
|
|
252
|
+
"type": "cell_configure",
|
|
253
|
+
"address": self.address,
|
|
254
|
+
"to": {"target": "terminal", "cell": self.id},
|
|
255
|
+
**options,
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def close(self, return_code=0):
|
|
260
|
+
self.bridge.send(
|
|
261
|
+
{
|
|
262
|
+
"type": "cell_close",
|
|
263
|
+
"address": self.address,
|
|
264
|
+
"to": {"target": "terminal", "cell": self.id},
|
|
265
|
+
"return_code": return_code,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@ovld
|
|
270
|
+
def serialize(self, obj: FunctionType):
|
|
271
|
+
return t"embed({self.register_function(obj)})"
|
|
272
|
+
|
|
273
|
+
@ovld
|
|
274
|
+
def serialize(self, obj: object):
|
|
275
|
+
return self.srx.serialize(Any @ self.tagset, obj, CellContext(self))
|
|
276
|
+
|
|
277
|
+
def deserialize(self, data):
|
|
278
|
+
return self.srx.deserialize(Any @ self.tagset, data, CellContext(self))
|
|
279
|
+
|
|
280
|
+
def body(self):
|
|
281
|
+
from .selection import Selection
|
|
282
|
+
|
|
283
|
+
return Selection(cell=self)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class CellContext(Context):
|
|
287
|
+
cell: Cell
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(kw_only=True)
|
|
291
|
+
class Prompt:
|
|
292
|
+
bridge: Bridge
|
|
293
|
+
label: str
|
|
294
|
+
handler: Any
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from html import escape
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from string.templatelib import Interpolation
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from hypetext import Interpreter
|
|
7
|
+
from ovld import ovld
|
|
8
|
+
from serieux import JSON
|
|
9
|
+
|
|
10
|
+
from .bridge import Cell, CellContext
|
|
11
|
+
from .js import js
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BucheInterpreter(Interpreter):
|
|
15
|
+
cell: Cell
|
|
16
|
+
|
|
17
|
+
def _js_serialize(self, value):
|
|
18
|
+
t = type(value)
|
|
19
|
+
if t in (list, dict):
|
|
20
|
+
t = JSON
|
|
21
|
+
return js(self.cell.srx.serialize(t, value, CellContext(self.cell)))
|
|
22
|
+
|
|
23
|
+
def gen(self, value: Path, fmt: str, tag: str, attr: object):
|
|
24
|
+
yield ("lit", str(self.cell.bridge.url(value)))
|
|
25
|
+
|
|
26
|
+
@ovld(priority=1)
|
|
27
|
+
def gen(self, value: object, fmt: Literal["js"], tag: str, attr: str):
|
|
28
|
+
yield ("lit", escape(self._js_serialize(value)))
|
|
29
|
+
|
|
30
|
+
@ovld(priority=1)
|
|
31
|
+
def gen(self, value: object, fmt: Literal["js"], tag: str, attr: None):
|
|
32
|
+
yield ("lit", self._js_serialize(value))
|
|
33
|
+
|
|
34
|
+
@ovld(priority=1)
|
|
35
|
+
def gen(self, value: Interpolation, fmt: str, tag: Literal["script"], attr: Any):
|
|
36
|
+
if isinstance(value.value, Path):
|
|
37
|
+
yield ("lit", repr(self.cell.bridge.url(value.value)))
|
|
38
|
+
elif value.format_spec == "raw":
|
|
39
|
+
yield ("lit", str(value.value))
|
|
40
|
+
else:
|
|
41
|
+
yield ("lit", self._js_serialize(value.value))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Interpreter:
|
|
2
|
+
async def eval(self, cell, command):
|
|
3
|
+
raise NotImplementedError()
|
|
4
|
+
|
|
5
|
+
async def on_error(self, cell, error):
|
|
6
|
+
raise error
|
|
7
|
+
|
|
8
|
+
async def handle_parse(self, prompt, message):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
async def handle_prompt_submit(self, prompt, message):
|
|
12
|
+
cell = prompt.bridge.cell(echo=message.get("echo_html", None))
|
|
13
|
+
try:
|
|
14
|
+
result = await self.eval(cell, message["text"])
|
|
15
|
+
if result is not None:
|
|
16
|
+
cell.body().print(result)
|
|
17
|
+
cell.close()
|
|
18
|
+
except Exception as exc:
|
|
19
|
+
await self.on_error(cell, exc)
|
|
20
|
+
cell.close(1)
|
|
21
|
+
|
|
22
|
+
async def handle_prompt_close(self, prompt, message):
|
|
23
|
+
raise SystemExit(0)
|
|
24
|
+
|
|
25
|
+
async def __call__(self, prompt, message):
|
|
26
|
+
msg_type = message.get("type")
|
|
27
|
+
handler_name = f"handle_{msg_type}"
|
|
28
|
+
handler = getattr(self, handler_name, None)
|
|
29
|
+
if handler is not None and callable(handler):
|
|
30
|
+
return await handler(prompt, message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PythonInterpreter:
|
|
34
|
+
async def eval(self, cell, command):
|
|
35
|
+
result = eval(command)
|
|
36
|
+
return result if result is None else str(result)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from string.templatelib import Template
|
|
4
|
+
|
|
5
|
+
from ovld import ovld, recurse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SerializationState:
|
|
10
|
+
indent: int = 0
|
|
11
|
+
prefix: str = ""
|
|
12
|
+
|
|
13
|
+
def deeper(self):
|
|
14
|
+
ind = " " * self.indent
|
|
15
|
+
return SerializationState(self.indent, self.prefix + ind)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@ovld
|
|
19
|
+
def js(x: object, *, indent: int = 0):
|
|
20
|
+
return recurse(x, SerializationState(indent))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@ovld
|
|
24
|
+
def js(x: None, state: SerializationState):
|
|
25
|
+
return "null"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@ovld
|
|
29
|
+
def js(x: bool, state: SerializationState):
|
|
30
|
+
return "true" if x else "false"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@ovld
|
|
34
|
+
def js(x: int, state: SerializationState):
|
|
35
|
+
return str(x)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@ovld
|
|
39
|
+
def js(x: float, state: SerializationState):
|
|
40
|
+
return str(x)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@ovld
|
|
44
|
+
def js(x: str, state: SerializationState):
|
|
45
|
+
return json.dumps(x)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@ovld
|
|
49
|
+
def js(x: list, state: SerializationState):
|
|
50
|
+
if not state.indent:
|
|
51
|
+
return "[" + ", ".join(recurse(item, state) for item in x) + "]"
|
|
52
|
+
inner = state.deeper()
|
|
53
|
+
items = (",\n" + inner.prefix).join(recurse(item, inner) for item in x)
|
|
54
|
+
return f"[\n{inner.prefix}{items}\n{state.prefix}]"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@ovld
|
|
58
|
+
def js(x: dict, state: SerializationState):
|
|
59
|
+
if not state.indent:
|
|
60
|
+
return "{" + ", ".join(f"{json.dumps(k)}: {recurse(v, state)}" for k, v in x.items()) + "}"
|
|
61
|
+
inner = state.deeper()
|
|
62
|
+
items = (",\n" + inner.prefix).join(
|
|
63
|
+
f"{json.dumps(k)}: {recurse(v, inner)}" for k, v in x.items()
|
|
64
|
+
)
|
|
65
|
+
return f"{{\n{inner.prefix}{items}\n{state.prefix}}}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@ovld
|
|
69
|
+
def js(x: Template, state: SerializationState):
|
|
70
|
+
parts = []
|
|
71
|
+
for s, i in zip(x.strings, x.interpolations):
|
|
72
|
+
parts.append(s)
|
|
73
|
+
parts.append(recurse(i.value, state))
|
|
74
|
+
parts.append(x.strings[-1])
|
|
75
|
+
return "".join(parts)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
async function indicate(f, args, eventTarget, selector, indicatorClass = "buche-indicator") {
|
|
2
|
+
if (!selector) {
|
|
3
|
+
selector = eventTarget.getAttribute("indicator-selector") || null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let targets;
|
|
7
|
+
if (!selector) {
|
|
8
|
+
targets = [eventTarget];
|
|
9
|
+
} else if (selector.startsWith("closest ")) {
|
|
10
|
+
const closestSelector = selector.slice(8);
|
|
11
|
+
const found = eventTarget.closest(closestSelector);
|
|
12
|
+
targets = found ? [found] : [];
|
|
13
|
+
} else {
|
|
14
|
+
targets = Array.from(document.querySelectorAll(selector));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const el of targets) {
|
|
18
|
+
el.classList.add(indicatorClass);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return await f(...args);
|
|
23
|
+
} finally {
|
|
24
|
+
for (const el of targets) {
|
|
25
|
+
el.classList.remove(indicatorClass);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function embed(function_id) {
|
|
31
|
+
return async (...args) => {
|
|
32
|
+
const serial = JSON.parse(JSON.stringify(args));
|
|
33
|
+
return await window.buche.request({
|
|
34
|
+
$class: "buchelib.srx:Call",
|
|
35
|
+
function: function_id,
|
|
36
|
+
args: serial
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Event.prototype.indicate = function(fn, ...args) {
|
|
42
|
+
return indicate(fn, args, this.currentTarget || this.target);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
window.indicate = indicate;
|
|
46
|
+
window.embed = embed;
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from string.templatelib import Template
|
|
4
|
+
|
|
5
|
+
from hypetext import html
|
|
6
|
+
from ovld import ovld, recurse
|
|
7
|
+
|
|
8
|
+
from .bridge import Cell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(kw_only=True)
|
|
12
|
+
class Selection:
|
|
13
|
+
cell: Cell
|
|
14
|
+
selector: str = None
|
|
15
|
+
|
|
16
|
+
def __getitem__(self, selector):
|
|
17
|
+
sel = f"{self.selector} {selector}" if self.selector else selector
|
|
18
|
+
return type(self)(cell=self.cell, selector=sel)
|
|
19
|
+
|
|
20
|
+
@ovld
|
|
21
|
+
def exec(self, code: Path):
|
|
22
|
+
recurse(code.read_text())
|
|
23
|
+
|
|
24
|
+
@ovld
|
|
25
|
+
def exec(self, code: Template):
|
|
26
|
+
scode = "".join(self.cell.interpreter.string_parts(code, "", "script"))
|
|
27
|
+
recurse(scode)
|
|
28
|
+
|
|
29
|
+
@ovld
|
|
30
|
+
def exec(self, code: str):
|
|
31
|
+
self.cell.command(type="exec", code=code, selector=self.selector)
|
|
32
|
+
|
|
33
|
+
def print(self, tpl):
|
|
34
|
+
node = html(tpl)
|
|
35
|
+
self.cell.command(
|
|
36
|
+
type="html",
|
|
37
|
+
mode="append",
|
|
38
|
+
selector=self.selector,
|
|
39
|
+
content="".join(self.cell.interpreter.string_parts(node)),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def set(self, tpl):
|
|
43
|
+
node = html(tpl)
|
|
44
|
+
self.cell.command(
|
|
45
|
+
type="html",
|
|
46
|
+
mode="set",
|
|
47
|
+
selector=self.selector,
|
|
48
|
+
content="".join(self.cell.interpreter.string_parts(node)),
|
|
49
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import FunctionType
|
|
4
|
+
|
|
5
|
+
from ovld import Medley
|
|
6
|
+
from serieux import JSON
|
|
7
|
+
|
|
8
|
+
from .bridge import Cell, CellContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ResponseId:
|
|
13
|
+
id: str
|
|
14
|
+
cell: Cell
|
|
15
|
+
|
|
16
|
+
def resolve(self, value):
|
|
17
|
+
self.cell.command(type="resolve", value=value, response_id=self.id)
|
|
18
|
+
|
|
19
|
+
def reject(self, error):
|
|
20
|
+
self.cell.command(type="resolve", error=str(error), response_id=self.id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@Cell.register_type_default
|
|
24
|
+
@dataclass
|
|
25
|
+
class Call:
|
|
26
|
+
function: FunctionType
|
|
27
|
+
args: list[JSON]
|
|
28
|
+
response_id: ResponseId
|
|
29
|
+
|
|
30
|
+
def __post_init__(self):
|
|
31
|
+
self.cell = self.response_id.cell
|
|
32
|
+
|
|
33
|
+
async def call(self):
|
|
34
|
+
try:
|
|
35
|
+
params = list(inspect.signature(self.function).parameters.values())
|
|
36
|
+
for p in params:
|
|
37
|
+
if p.annotation is inspect.Parameter.empty:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"Parameter '{p.name}' of {self.function.__name__} has no type annotation"
|
|
40
|
+
)
|
|
41
|
+
deserialized = [
|
|
42
|
+
self.cell.srx.deserialize(p.annotation, arg) for p, arg in zip(params, self.args)
|
|
43
|
+
]
|
|
44
|
+
result = await self.function(*deserialized)
|
|
45
|
+
self.response_id.resolve(result)
|
|
46
|
+
return result
|
|
47
|
+
except Exception as exc:
|
|
48
|
+
self.response_id.reject(exc)
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BucheSerieux(Medley):
|
|
53
|
+
######################
|
|
54
|
+
# Custom serializers #
|
|
55
|
+
######################
|
|
56
|
+
|
|
57
|
+
def serialize(self, t: type[FunctionType], obj: FunctionType, ctx: CellContext):
|
|
58
|
+
return t"embed({ctx.cell.register_function(obj)})"
|
|
59
|
+
|
|
60
|
+
########################
|
|
61
|
+
# Custom deserializers #
|
|
62
|
+
########################
|
|
63
|
+
|
|
64
|
+
def deserialize(self, t: type[FunctionType], obj: str, ctx: CellContext):
|
|
65
|
+
for fn, tag in ctx.cell.function_registry.items():
|
|
66
|
+
if tag == obj:
|
|
67
|
+
return fn
|
|
68
|
+
else:
|
|
69
|
+
raise Exception(f"No function with tag: {obj}")
|
|
70
|
+
|
|
71
|
+
def deserialize(self, t: type[ResponseId], obj: str, ctx: CellContext):
|
|
72
|
+
return ResponseId(obj, ctx.cell)
|