pulse-framework 0.1.51__py3-none-any.whl → 0.1.53__py3-none-any.whl
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.
- pulse/__init__.py +542 -562
- pulse/_examples.py +29 -0
- pulse/app.py +0 -14
- pulse/cli/cmd.py +96 -80
- pulse/cli/dependencies.py +10 -41
- pulse/cli/folder_lock.py +3 -3
- pulse/cli/helpers.py +40 -67
- pulse/cli/logging.py +102 -0
- pulse/cli/packages.py +16 -0
- pulse/cli/processes.py +40 -23
- pulse/codegen/codegen.py +70 -35
- pulse/codegen/js.py +2 -4
- pulse/codegen/templates/route.py +94 -146
- pulse/component.py +115 -0
- pulse/components/for_.py +1 -1
- pulse/components/if_.py +1 -1
- pulse/components/react_router.py +16 -22
- pulse/{html → dom}/events.py +1 -1
- pulse/{html → dom}/props.py +6 -6
- pulse/{html → dom}/tags.py +11 -11
- pulse/dom/tags.pyi +480 -0
- pulse/form.py +7 -6
- pulse/hooks/init.py +1 -13
- pulse/js/__init__.py +37 -41
- pulse/js/__init__.pyi +22 -2
- pulse/js/_types.py +5 -3
- pulse/js/array.py +121 -38
- pulse/js/console.py +9 -9
- pulse/js/date.py +22 -19
- pulse/js/document.py +8 -4
- pulse/js/error.py +12 -14
- pulse/js/json.py +4 -3
- pulse/js/map.py +17 -7
- pulse/js/math.py +2 -2
- pulse/js/navigator.py +4 -4
- pulse/js/number.py +8 -8
- pulse/js/object.py +9 -13
- pulse/js/promise.py +25 -9
- pulse/js/regexp.py +6 -6
- pulse/js/set.py +20 -8
- pulse/js/string.py +7 -7
- pulse/js/weakmap.py +6 -6
- pulse/js/weakset.py +6 -6
- pulse/js/window.py +17 -14
- pulse/messages.py +1 -4
- pulse/react_component.py +3 -1001
- pulse/render_session.py +74 -66
- pulse/renderer.py +311 -238
- pulse/routing.py +1 -10
- pulse/transpiler/__init__.py +84 -114
- pulse/transpiler/builtins.py +661 -343
- pulse/transpiler/errors.py +78 -2
- pulse/transpiler/function.py +463 -133
- pulse/transpiler/id.py +18 -0
- pulse/transpiler/imports.py +230 -325
- pulse/transpiler/js_module.py +218 -209
- pulse/transpiler/modules/__init__.py +16 -13
- pulse/transpiler/modules/asyncio.py +45 -26
- pulse/transpiler/modules/json.py +12 -8
- pulse/transpiler/modules/math.py +161 -216
- pulse/transpiler/modules/pulse/__init__.py +5 -0
- pulse/transpiler/modules/pulse/tags.py +231 -0
- pulse/transpiler/modules/typing.py +33 -28
- pulse/transpiler/nodes.py +1607 -923
- pulse/transpiler/py_module.py +118 -95
- pulse/transpiler/react_component.py +51 -0
- pulse/transpiler/transpiler.py +593 -437
- pulse/transpiler/vdom.py +255 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/METADATA +1 -1
- pulse_framework-0.1.53.dist-info/RECORD +120 -0
- pulse/html/tags.pyi +0 -470
- pulse/transpiler/constants.py +0 -110
- pulse/transpiler/context.py +0 -26
- pulse/transpiler/ids.py +0 -16
- pulse/transpiler/modules/re.py +0 -466
- pulse/transpiler/modules/tags.py +0 -268
- pulse/transpiler/utils.py +0 -4
- pulse/vdom.py +0 -599
- pulse_framework-0.1.51.dist-info/RECORD +0 -119
- /pulse/{html → dom}/__init__.py +0 -0
- /pulse/{html → dom}/elements.py +0 -0
- /pulse/{html → dom}/svg.py +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.51.dist-info → pulse_framework-0.1.53.dist-info}/entry_points.txt +0 -0
pulse/render_session.py
CHANGED
|
@@ -11,7 +11,6 @@ from pulse.helpers import create_future_on_loop, create_task
|
|
|
11
11
|
from pulse.hooks.runtime import NotFoundInterrupt, RedirectInterrupt
|
|
12
12
|
from pulse.messages import (
|
|
13
13
|
ServerApiCallMessage,
|
|
14
|
-
ServerErrorMessage,
|
|
15
14
|
ServerErrorPhase,
|
|
16
15
|
ServerInitMessage,
|
|
17
16
|
ServerJsExecMessage,
|
|
@@ -31,10 +30,8 @@ from pulse.routing import (
|
|
|
31
30
|
ensure_absolute_path,
|
|
32
31
|
)
|
|
33
32
|
from pulse.state import State
|
|
34
|
-
from pulse.transpiler.
|
|
35
|
-
from pulse.transpiler.
|
|
36
|
-
from pulse.transpiler.nodes import JSExpr
|
|
37
|
-
from pulse.vdom import Element
|
|
33
|
+
from pulse.transpiler.id import next_id
|
|
34
|
+
from pulse.transpiler.nodes import Expr, Node, emit
|
|
38
35
|
|
|
39
36
|
if TYPE_CHECKING:
|
|
40
37
|
from pulse.channel import ChannelsManager
|
|
@@ -49,14 +46,14 @@ class JsExecError(Exception):
|
|
|
49
46
|
|
|
50
47
|
# Module-level convenience wrapper
|
|
51
48
|
@overload
|
|
52
|
-
def run_js(expr:
|
|
49
|
+
def run_js(expr: Expr | str, *, result: Literal[True]) -> asyncio.Future[Any]: ...
|
|
53
50
|
|
|
54
51
|
|
|
55
52
|
@overload
|
|
56
|
-
def run_js(expr:
|
|
53
|
+
def run_js(expr: Expr | str, *, result: Literal[False] = ...) -> None: ...
|
|
57
54
|
|
|
58
55
|
|
|
59
|
-
def run_js(expr:
|
|
56
|
+
def run_js(expr: Expr | str, *, result: bool = False) -> asyncio.Future[Any] | None:
|
|
60
57
|
"""Execute JavaScript on the client. Convenience wrapper for RenderSession.run_js()."""
|
|
61
58
|
ctx = PulseContext.get()
|
|
62
59
|
if ctx.render is None:
|
|
@@ -70,7 +67,7 @@ class RouteMount:
|
|
|
70
67
|
tree: RenderTree
|
|
71
68
|
effect: Effect | None
|
|
72
69
|
_pulse_ctx: PulseContext | None
|
|
73
|
-
element:
|
|
70
|
+
element: Node
|
|
74
71
|
rendered: bool
|
|
75
72
|
|
|
76
73
|
def __init__(
|
|
@@ -92,12 +89,13 @@ class RenderSession:
|
|
|
92
89
|
forms: "FormRegistry"
|
|
93
90
|
query_store: QueryStore
|
|
94
91
|
route_mounts: dict[str, RouteMount]
|
|
92
|
+
connected: bool
|
|
95
93
|
_server_address: str | None
|
|
96
94
|
_client_address: str | None
|
|
97
95
|
_send_message: Callable[[ServerMessage], Any] | None
|
|
98
96
|
_pending_api: dict[str, asyncio.Future[dict[str, Any]]]
|
|
97
|
+
_pending_js_results: dict[str, asyncio.Future[Any]]
|
|
99
98
|
_global_states: dict[str, State]
|
|
100
|
-
connected: bool
|
|
101
99
|
|
|
102
100
|
def __init__(
|
|
103
101
|
self,
|
|
@@ -118,7 +116,6 @@ class RenderSession:
|
|
|
118
116
|
# Best-effort client address, captured at prerender or socket connect time
|
|
119
117
|
self._client_address = client_address
|
|
120
118
|
self._send_message = None
|
|
121
|
-
self._pending_api = {}
|
|
122
119
|
# Registry of per-session global singletons (created via ps.global_state without id)
|
|
123
120
|
self._global_states = {}
|
|
124
121
|
self.query_store = QueryStore()
|
|
@@ -126,8 +123,9 @@ class RenderSession:
|
|
|
126
123
|
self.connected = False
|
|
127
124
|
self.channels = ChannelsManager(self)
|
|
128
125
|
self.forms = FormRegistry(self)
|
|
126
|
+
self._pending_api = {}
|
|
129
127
|
# Pending JS execution results (for awaiting run_js().result())
|
|
130
|
-
self._pending_js_results
|
|
128
|
+
self._pending_js_results = {}
|
|
131
129
|
|
|
132
130
|
@property
|
|
133
131
|
def server_address(self) -> str:
|
|
@@ -177,17 +175,18 @@ class RenderSession:
|
|
|
177
175
|
exc: BaseException,
|
|
178
176
|
details: dict[str, Any] | None = None,
|
|
179
177
|
):
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
178
|
+
self.send(
|
|
179
|
+
{
|
|
180
|
+
"type": "server_error",
|
|
181
|
+
"path": path,
|
|
182
|
+
"error": {
|
|
183
|
+
"message": str(exc),
|
|
184
|
+
"stack": traceback.format_exc(),
|
|
185
|
+
"phase": phase,
|
|
186
|
+
"details": details or {},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
)
|
|
191
190
|
logger.error(
|
|
192
191
|
"Error reported for path %r during %s: %s\n%s",
|
|
193
192
|
path,
|
|
@@ -211,6 +210,16 @@ class RenderSession:
|
|
|
211
210
|
if channel:
|
|
212
211
|
channel.closed = True
|
|
213
212
|
self.channels.dispose_channel(channel, reason="render.close")
|
|
213
|
+
# Cancel pending API calls
|
|
214
|
+
for fut in self._pending_api.values():
|
|
215
|
+
if not fut.done():
|
|
216
|
+
fut.cancel()
|
|
217
|
+
self._pending_api.clear()
|
|
218
|
+
# Cancel pending JS execution results
|
|
219
|
+
for fut in self._pending_js_results.values():
|
|
220
|
+
if not fut.done():
|
|
221
|
+
fut.cancel()
|
|
222
|
+
self._pending_js_results.clear()
|
|
214
223
|
# The effect will be garbage collected, and with it the dependencies
|
|
215
224
|
self._send_message = None
|
|
216
225
|
self.connected = False
|
|
@@ -240,12 +249,17 @@ class RenderSession:
|
|
|
240
249
|
headers: dict[str, str] | None = None,
|
|
241
250
|
body: Any | None = None,
|
|
242
251
|
credentials: str = "include",
|
|
252
|
+
timeout: float = 30.0,
|
|
243
253
|
) -> dict[str, Any]:
|
|
244
254
|
"""Request the client to perform a fetch and await the result.
|
|
245
255
|
|
|
246
256
|
Accepts either an absolute URL (http/https) or a relative path. When a
|
|
247
257
|
relative path is provided, it is resolved against this session's
|
|
248
258
|
server_address.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
timeout: Maximum seconds to wait for response (default 30s).
|
|
262
|
+
Raises asyncio.TimeoutError if exceeded.
|
|
249
263
|
"""
|
|
250
264
|
# Resolve to absolute URL if a relative path is passed
|
|
251
265
|
if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
|
|
@@ -274,7 +288,11 @@ class RenderSession:
|
|
|
274
288
|
credentials="include" if credentials == "include" else "omit",
|
|
275
289
|
)
|
|
276
290
|
)
|
|
277
|
-
|
|
291
|
+
try:
|
|
292
|
+
result = await asyncio.wait_for(fut, timeout=timeout)
|
|
293
|
+
except asyncio.TimeoutError:
|
|
294
|
+
self._pending_api.pop(corr_id, None)
|
|
295
|
+
raise
|
|
278
296
|
return result
|
|
279
297
|
|
|
280
298
|
def handle_api_result(self, data: dict[str, Any]):
|
|
@@ -296,21 +314,29 @@ class RenderSession:
|
|
|
296
314
|
# ---- JS Execution ----
|
|
297
315
|
@overload
|
|
298
316
|
def run_js(
|
|
299
|
-
self, expr:
|
|
317
|
+
self, expr: Expr | str, *, result: Literal[True], timeout: float = ...
|
|
300
318
|
) -> asyncio.Future[object]: ...
|
|
301
319
|
|
|
302
320
|
@overload
|
|
303
|
-
def run_js(
|
|
321
|
+
def run_js(
|
|
322
|
+
self,
|
|
323
|
+
expr: Expr | str,
|
|
324
|
+
*,
|
|
325
|
+
result: Literal[False] = ...,
|
|
326
|
+
timeout: float = ...,
|
|
327
|
+
) -> None: ...
|
|
304
328
|
|
|
305
329
|
def run_js(
|
|
306
|
-
self, expr:
|
|
330
|
+
self, expr: Expr | str, *, result: bool = False, timeout: float = 10.0
|
|
307
331
|
) -> asyncio.Future[object] | None:
|
|
308
332
|
"""Execute JavaScript on the client.
|
|
309
333
|
|
|
310
334
|
Args:
|
|
311
|
-
expr: A
|
|
335
|
+
expr: A Expr (e.g. from calling a @javascript function) or raw JS string.
|
|
312
336
|
result: If True, returns a Future that resolves with the JS return value.
|
|
313
337
|
If False (default), returns None (fire-and-forget).
|
|
338
|
+
timeout: Maximum seconds to wait for result (default 10s, only applies when
|
|
339
|
+
result=True). Future raises asyncio.TimeoutError if exceeded.
|
|
314
340
|
|
|
315
341
|
Returns:
|
|
316
342
|
None if result=False, otherwise a Future resolving to the JS result.
|
|
@@ -338,16 +364,16 @@ class RenderSession:
|
|
|
338
364
|
run_js("console.log('Hello from Python!')")
|
|
339
365
|
"""
|
|
340
366
|
ctx = PulseContext.get()
|
|
341
|
-
exec_id =
|
|
367
|
+
exec_id = next_id()
|
|
342
368
|
|
|
343
369
|
if isinstance(expr, str):
|
|
344
370
|
code = expr
|
|
345
371
|
else:
|
|
346
|
-
|
|
347
|
-
code = expr.emit()
|
|
372
|
+
code = emit(expr)
|
|
348
373
|
|
|
349
|
-
# Get path
|
|
350
|
-
path
|
|
374
|
+
# Get route pattern path (e.g., "/users/:id") not pathname (e.g., "/users/123")
|
|
375
|
+
# This must match the path used to key views on the client side
|
|
376
|
+
path = ctx.route.pulse_route.unique_path() if ctx.route else "/"
|
|
351
377
|
|
|
352
378
|
self.send(
|
|
353
379
|
ServerJsExecMessage(
|
|
@@ -362,6 +388,15 @@ class RenderSession:
|
|
|
362
388
|
loop = asyncio.get_running_loop()
|
|
363
389
|
future: asyncio.Future[object] = loop.create_future()
|
|
364
390
|
self._pending_js_results[exec_id] = future
|
|
391
|
+
|
|
392
|
+
# Schedule auto-timeout
|
|
393
|
+
def _on_timeout() -> None:
|
|
394
|
+
self._pending_js_results.pop(exec_id, None)
|
|
395
|
+
if not future.done():
|
|
396
|
+
future.set_exception(asyncio.TimeoutError())
|
|
397
|
+
|
|
398
|
+
loop.call_later(timeout, _on_timeout)
|
|
399
|
+
|
|
365
400
|
return future
|
|
366
401
|
|
|
367
402
|
return None
|
|
@@ -395,7 +430,7 @@ class RenderSession:
|
|
|
395
430
|
initial message instead of sending over a socket.
|
|
396
431
|
|
|
397
432
|
Returns a dict:
|
|
398
|
-
{ "type": "vdom_init", "vdom": VDOM
|
|
433
|
+
{ "type": "vdom_init", "vdom": VDOM } or
|
|
399
434
|
{ "type": "navigate_to", "path": str, "replace": bool }
|
|
400
435
|
"""
|
|
401
436
|
# If already mounted (e.g., repeated prerender), do nothing special.
|
|
@@ -409,15 +444,7 @@ class RenderSession:
|
|
|
409
444
|
if normalized_root is not None:
|
|
410
445
|
mount.element = normalized_root
|
|
411
446
|
mount.rendered = True
|
|
412
|
-
|
|
413
|
-
type="vdom_init",
|
|
414
|
-
path=path,
|
|
415
|
-
vdom=vdom,
|
|
416
|
-
callbacks=sorted(mount.tree.callbacks.keys()),
|
|
417
|
-
render_props=sorted(mount.tree.render_props),
|
|
418
|
-
jsexpr_paths=sorted(mount.tree.jsexpr_paths),
|
|
419
|
-
)
|
|
420
|
-
return msg
|
|
447
|
+
return ServerInitMessage(type="vdom_init", path=path, vdom=vdom)
|
|
421
448
|
|
|
422
449
|
captured: ServerInitMessage | ServerNavigateToMessage | None = None
|
|
423
450
|
|
|
@@ -428,12 +455,7 @@ class RenderSession:
|
|
|
428
455
|
return
|
|
429
456
|
if msg["type"] == "vdom_init" and msg["path"] == path:
|
|
430
457
|
captured = ServerInitMessage(
|
|
431
|
-
type="vdom_init",
|
|
432
|
-
path=path,
|
|
433
|
-
vdom=msg.get("vdom"),
|
|
434
|
-
callbacks=msg.get("callbacks", []),
|
|
435
|
-
render_props=msg.get("render_props", []),
|
|
436
|
-
jsexpr_paths=msg.get("jsexpr_paths", []),
|
|
458
|
+
type="vdom_init", path=path, vdom=msg.get("vdom")
|
|
437
459
|
)
|
|
438
460
|
elif msg["type"] == "navigate_to":
|
|
439
461
|
captured = ServerNavigateToMessage(
|
|
@@ -462,15 +484,7 @@ class RenderSession:
|
|
|
462
484
|
if normalized_root is not None:
|
|
463
485
|
mount.element = normalized_root
|
|
464
486
|
mount.rendered = True
|
|
465
|
-
|
|
466
|
-
type="vdom_init",
|
|
467
|
-
path=path,
|
|
468
|
-
vdom=vdom,
|
|
469
|
-
callbacks=sorted(mount.tree.callbacks.keys()),
|
|
470
|
-
render_props=sorted(mount.tree.render_props),
|
|
471
|
-
jsexpr_paths=sorted(mount.tree.jsexpr_paths),
|
|
472
|
-
)
|
|
473
|
-
return msg
|
|
487
|
+
return ServerInitMessage(type="vdom_init", path=path, vdom=vdom)
|
|
474
488
|
|
|
475
489
|
return captured
|
|
476
490
|
|
|
@@ -540,15 +554,9 @@ class RenderSession:
|
|
|
540
554
|
if normalized_root is not None:
|
|
541
555
|
mount.element = normalized_root
|
|
542
556
|
mount.rendered = True
|
|
543
|
-
|
|
544
|
-
type="vdom_init",
|
|
545
|
-
path=path,
|
|
546
|
-
vdom=vdom,
|
|
547
|
-
callbacks=sorted(mount.tree.callbacks.keys()),
|
|
548
|
-
render_props=sorted(mount.tree.render_props),
|
|
549
|
-
jsexpr_paths=sorted(mount.tree.jsexpr_paths),
|
|
557
|
+
self.send(
|
|
558
|
+
ServerInitMessage(type="vdom_init", path=path, vdom=vdom)
|
|
550
559
|
)
|
|
551
|
-
self.send(msg)
|
|
552
560
|
else:
|
|
553
561
|
ops = mount.tree.diff(mount.element)
|
|
554
562
|
normalized_root = getattr(mount.tree, "_normalized", None)
|