pulse-framework 0.1.72__tar.gz → 0.1.74__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.
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/PKG-INFO +2 -2
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/pyproject.toml +2 -2
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/__init__.py +16 -4
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/processes.py +2 -0
- pulse_framework-0.1.74/src/pulse/debounce.py +79 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/decorators.py +4 -3
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/effects.py +5 -5
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/runtime.py +25 -8
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/setup.py +6 -10
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/stable.py +5 -9
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/state.py +4 -8
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/common.py +1 -1
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/infinite_query.py +2 -1
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/mutation.py +2 -1
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/query.py +2 -1
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/render_session.py +2 -2
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/renderer.py +30 -2
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/routing.py +19 -5
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/serializer.py +38 -19
- pulse_framework-0.1.74/src/pulse/state/__init__.py +1 -0
- pulse_framework-0.1.74/src/pulse/state/property.py +218 -0
- pulse_framework-0.1.74/src/pulse/state/query_param.py +538 -0
- {pulse_framework-0.1.72/src/pulse → pulse_framework-0.1.74/src/pulse/state}/state.py +66 -220
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/__init__.py +5 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/function.py +56 -32
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/nodes.py +43 -4
- pulse_framework-0.1.74/src/pulse/transpiler/parse.py +70 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/transpiler.py +413 -81
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/vdom.py +1 -1
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/README.md +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/app.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/code_analysis.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/component.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/forms.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/obj.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/pulse.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/react.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/client.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/queries/store.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/requirements.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/scheduling.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/test_helpers.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/assets.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/dynamic_import.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/emit_context.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/imports.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/js_module.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/transpiler/py_module.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.72 → pulse_framework-0.1.74}/src/pulse/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pulse-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.74
|
|
4
4
|
Summary: Pulse - Full-stack framework for building real-time React applications in Python
|
|
5
5
|
Requires-Dist: fastapi>=0.128.0
|
|
6
6
|
Requires-Dist: uvicorn>=0.24.0
|
|
@@ -15,7 +15,7 @@ Requires-Dist: urllib3>=2.6.3
|
|
|
15
15
|
Requires-Dist: watchfiles>=1.1.0
|
|
16
16
|
Requires-Dist: httpx>=0.28.1
|
|
17
17
|
Requires-Dist: aiohttp>=3.12.0
|
|
18
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
21
|
# Pulse Python
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pulse-framework"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.74"
|
|
4
4
|
description = "Pulse - Full-stack framework for building real-time React applications in Python"
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"fastapi>=0.128.0",
|
|
9
9
|
"uvicorn>=0.24.0",
|
|
@@ -60,6 +60,14 @@ from pulse.context import PulseContext as PulseContext
|
|
|
60
60
|
from pulse.cookies import Cookie as Cookie
|
|
61
61
|
from pulse.cookies import SetCookie as SetCookie
|
|
62
62
|
|
|
63
|
+
# Debounce
|
|
64
|
+
from pulse.debounce import (
|
|
65
|
+
Debounced as Debounced,
|
|
66
|
+
)
|
|
67
|
+
from pulse.debounce import (
|
|
68
|
+
debounced as debounced,
|
|
69
|
+
)
|
|
70
|
+
|
|
63
71
|
# Decorators
|
|
64
72
|
from pulse.decorators import computed as computed
|
|
65
73
|
from pulse.decorators import effect as effect
|
|
@@ -1200,7 +1208,7 @@ from pulse.hooks.core import (
|
|
|
1200
1208
|
)
|
|
1201
1209
|
|
|
1202
1210
|
# Hooks - Effects (import to register inline_effect_hook before registry locks)
|
|
1203
|
-
from pulse.hooks.effects import
|
|
1211
|
+
from pulse.hooks.effects import EffectState as EffectState
|
|
1204
1212
|
|
|
1205
1213
|
# Hooks - Init
|
|
1206
1214
|
from pulse.hooks.init import (
|
|
@@ -1235,6 +1243,9 @@ from pulse.hooks.runtime import (
|
|
|
1235
1243
|
from pulse.hooks.runtime import (
|
|
1236
1244
|
not_found as not_found,
|
|
1237
1245
|
)
|
|
1246
|
+
from pulse.hooks.runtime import (
|
|
1247
|
+
pulse_route as pulse_route,
|
|
1248
|
+
)
|
|
1238
1249
|
from pulse.hooks.runtime import (
|
|
1239
1250
|
redirect as redirect,
|
|
1240
1251
|
)
|
|
@@ -1259,7 +1270,7 @@ from pulse.hooks.runtime import (
|
|
|
1259
1270
|
|
|
1260
1271
|
# Hooks - Setup
|
|
1261
1272
|
from pulse.hooks.setup import (
|
|
1262
|
-
|
|
1273
|
+
SetupState as SetupState,
|
|
1263
1274
|
)
|
|
1264
1275
|
from pulse.hooks.setup import (
|
|
1265
1276
|
setup as setup,
|
|
@@ -1271,7 +1282,7 @@ from pulse.hooks.stable import (
|
|
|
1271
1282
|
StableEntry as StableEntry,
|
|
1272
1283
|
)
|
|
1273
1284
|
from pulse.hooks.stable import (
|
|
1274
|
-
|
|
1285
|
+
StableState as StableState,
|
|
1275
1286
|
)
|
|
1276
1287
|
|
|
1277
1288
|
# Hooks - Stable
|
|
@@ -1433,7 +1444,8 @@ from pulse.serializer import deserialize as deserialize
|
|
|
1433
1444
|
from pulse.serializer import serialize as serialize
|
|
1434
1445
|
|
|
1435
1446
|
# State and routing
|
|
1436
|
-
from pulse.state import
|
|
1447
|
+
from pulse.state.query_param import QueryParam as QueryParam
|
|
1448
|
+
from pulse.state.state import State as State
|
|
1437
1449
|
|
|
1438
1450
|
# Transpiler v2
|
|
1439
1451
|
from pulse.transpiler.function import JsFunction as JsFunction
|
|
@@ -272,6 +272,8 @@ def _write_tagged_line(name: str, message: str, tag_mode: TagMode) -> None:
|
|
|
272
272
|
"Network: use --host to expose" in clean_message
|
|
273
273
|
or "press h + enter to show help" in clean_message
|
|
274
274
|
or "➜ Local:" in clean_message
|
|
275
|
+
or "/__manifest" in clean_message
|
|
276
|
+
or "?import" in clean_message
|
|
275
277
|
):
|
|
276
278
|
return
|
|
277
279
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Generic, ParamSpec, TypeVar
|
|
8
|
+
|
|
9
|
+
from pulse.context import PULSE_CONTEXT
|
|
10
|
+
from pulse.scheduling import TimerHandleLike, later
|
|
11
|
+
|
|
12
|
+
P = ParamSpec("P")
|
|
13
|
+
R = TypeVar("R")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class Debounced(Generic[P, R]):
|
|
18
|
+
fn: Callable[P, R]
|
|
19
|
+
delay_ms: float
|
|
20
|
+
_handle: TimerHandleLike | asyncio.Handle | None = field(
|
|
21
|
+
default=None, init=False, repr=False, compare=False
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any:
|
|
25
|
+
if self._handle is not None:
|
|
26
|
+
self._handle.cancel()
|
|
27
|
+
|
|
28
|
+
delay = self.delay_ms / 1000.0
|
|
29
|
+
|
|
30
|
+
def _run() -> None:
|
|
31
|
+
object.__setattr__(self, "_handle", None)
|
|
32
|
+
result = self.fn(*args, **kwargs)
|
|
33
|
+
if asyncio.iscoroutine(result):
|
|
34
|
+
loop = asyncio.get_running_loop()
|
|
35
|
+
task = loop.create_task(result)
|
|
36
|
+
|
|
37
|
+
def _log_task_exception(t: asyncio.Task[Any]) -> None:
|
|
38
|
+
try:
|
|
39
|
+
t.result()
|
|
40
|
+
except asyncio.CancelledError:
|
|
41
|
+
pass
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
loop.call_exception_handler(
|
|
44
|
+
{
|
|
45
|
+
"message": "Unhandled exception in debounced() task",
|
|
46
|
+
"exception": exc,
|
|
47
|
+
"context": {"callback": self.fn},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
task.add_done_callback(_log_task_exception)
|
|
52
|
+
|
|
53
|
+
if PULSE_CONTEXT.get() is not None:
|
|
54
|
+
handle = later(delay, _run)
|
|
55
|
+
else:
|
|
56
|
+
try:
|
|
57
|
+
loop = asyncio.get_running_loop()
|
|
58
|
+
except RuntimeError:
|
|
59
|
+
try:
|
|
60
|
+
loop = asyncio.get_event_loop()
|
|
61
|
+
except RuntimeError as exc:
|
|
62
|
+
raise RuntimeError("debounced() requires an event loop") from exc
|
|
63
|
+
handle = loop.call_later(delay, _run)
|
|
64
|
+
|
|
65
|
+
object.__setattr__(self, "_handle", handle)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def debounced(fn: Callable[P, R], delay_ms: int | float) -> Debounced[P, R]:
|
|
69
|
+
"""Return a debounced callback marker (delay in milliseconds)."""
|
|
70
|
+
if not callable(fn):
|
|
71
|
+
raise TypeError("debounced() requires a callable")
|
|
72
|
+
if isinstance(delay_ms, bool) or not isinstance(delay_ms, (int, float)):
|
|
73
|
+
raise TypeError("debounced() delay must be a number (ms)")
|
|
74
|
+
if not math.isfinite(delay_ms) or delay_ms < 0:
|
|
75
|
+
raise ValueError("debounced() delay must be finite and >= 0")
|
|
76
|
+
return Debounced(fn=fn, delay_ms=float(delay_ms))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["Debounced", "debounced"]
|
|
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
|
|
|
5
5
|
from typing import Any, ParamSpec, Protocol, TypeVar, cast, overload
|
|
6
6
|
|
|
7
7
|
from pulse.hooks.core import HOOK_CONTEXT
|
|
8
|
-
from pulse.hooks.effects import
|
|
8
|
+
from pulse.hooks.effects import effect_state
|
|
9
9
|
from pulse.hooks.state import collect_component_identity
|
|
10
10
|
from pulse.reactive import (
|
|
11
11
|
AsyncEffect,
|
|
@@ -16,7 +16,8 @@ from pulse.reactive import (
|
|
|
16
16
|
EffectFn,
|
|
17
17
|
Signal,
|
|
18
18
|
)
|
|
19
|
-
from pulse.state import ComputedProperty,
|
|
19
|
+
from pulse.state.property import ComputedProperty, StateEffect
|
|
20
|
+
from pulse.state.state import State
|
|
20
21
|
|
|
21
22
|
T = TypeVar("T")
|
|
22
23
|
TState = TypeVar("TState", bound=State)
|
|
@@ -336,7 +337,7 @@ def effect(
|
|
|
336
337
|
else:
|
|
337
338
|
identity = key
|
|
338
339
|
|
|
339
|
-
state =
|
|
340
|
+
state = effect_state()
|
|
340
341
|
return state.get_or_create(cast(Any, identity), key, create_effect)
|
|
341
342
|
|
|
342
343
|
if fn is not None:
|
|
@@ -5,7 +5,7 @@ from pulse.hooks.core import HookMetadata, HookState, hooks
|
|
|
5
5
|
from pulse.reactive import REACTIVE_CONTEXT, AsyncEffect, Effect
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class EffectState(HookState):
|
|
9
9
|
"""Stores inline effects keyed by function identity or explicit key."""
|
|
10
10
|
|
|
11
11
|
__slots__ = ("effects", "_seen_this_render") # pyright: ignore[reportUnannotatedClassAttribute]
|
|
@@ -86,9 +86,9 @@ class InlineEffectHookState(HookState):
|
|
|
86
86
|
self._seen_this_render.clear()
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
effect_state = hooks.create(
|
|
90
90
|
"pulse:core.inline_effects",
|
|
91
|
-
|
|
91
|
+
factory=EffectState,
|
|
92
92
|
metadata=HookMetadata(
|
|
93
93
|
owner="pulse.core",
|
|
94
94
|
description="Storage for inline @ps.effect decorators in components",
|
|
@@ -97,6 +97,6 @@ inline_effect_hook = hooks.create(
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
__all__ = [
|
|
100
|
-
"
|
|
101
|
-
"
|
|
100
|
+
"EffectState",
|
|
101
|
+
"effect_state",
|
|
102
102
|
]
|
|
@@ -13,8 +13,8 @@ from typing import (
|
|
|
13
13
|
from pulse.context import PulseContext
|
|
14
14
|
from pulse.hooks.core import HOOK_CONTEXT
|
|
15
15
|
from pulse.reactive_extensions import ReactiveDict
|
|
16
|
-
from pulse.routing import
|
|
17
|
-
from pulse.state import State
|
|
16
|
+
from pulse.routing import Layout, Route, RouteInfo
|
|
17
|
+
from pulse.state.state import State
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class RedirectInterrupt(Exception):
|
|
@@ -47,11 +47,11 @@ class NotFoundInterrupt(Exception):
|
|
|
47
47
|
pass
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def route() ->
|
|
51
|
-
"""Get the current route
|
|
50
|
+
def route() -> RouteInfo:
|
|
51
|
+
"""Get the current route info.
|
|
52
52
|
|
|
53
53
|
Returns:
|
|
54
|
-
|
|
54
|
+
RouteInfo: Mapping with access to route parameters, path, and query.
|
|
55
55
|
|
|
56
56
|
Raises:
|
|
57
57
|
RuntimeError: If called outside of a component render context.
|
|
@@ -61,8 +61,8 @@ def route() -> RouteContext:
|
|
|
61
61
|
```python
|
|
62
62
|
def user_page():
|
|
63
63
|
r = ps.route()
|
|
64
|
-
user_id = r.
|
|
65
|
-
page = r.
|
|
64
|
+
user_id = r["pathParams"].get("user_id") # From /users/:user_id
|
|
65
|
+
page = r["queryParams"].get("page", "1") # From ?page=2
|
|
66
66
|
return m.Text(f"User {user_id}, Page {page}")
|
|
67
67
|
```
|
|
68
68
|
"""
|
|
@@ -71,7 +71,24 @@ def route() -> RouteContext:
|
|
|
71
71
|
raise RuntimeError(
|
|
72
72
|
"`pulse.route` can only be called within a component during rendering."
|
|
73
73
|
)
|
|
74
|
-
return ctx.route
|
|
74
|
+
return ctx.route.info
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pulse_route() -> Route | Layout:
|
|
78
|
+
"""Get the current route definition.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Route | Layout: The active route or layout definition.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
RuntimeError: If called outside of a component render context.
|
|
85
|
+
"""
|
|
86
|
+
ctx = PulseContext.get()
|
|
87
|
+
if not ctx or not ctx.route:
|
|
88
|
+
raise RuntimeError(
|
|
89
|
+
"`pulse.pulse_route` can only be called within a component during rendering."
|
|
90
|
+
)
|
|
91
|
+
return ctx.route.pulse_route
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
def session() -> ReactiveDict[str, Any]:
|
|
@@ -10,7 +10,7 @@ P = ParamSpec("P")
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class
|
|
13
|
+
class SetupState(HookState):
|
|
14
14
|
"""Internal hook state for the setup hook.
|
|
15
15
|
|
|
16
16
|
Manages the initialization, argument tracking, and lifecycle of
|
|
@@ -140,13 +140,9 @@ class SetupHookState(HookState):
|
|
|
140
140
|
return key
|
|
141
141
|
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
return SetupHookState()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
_setup_hook = hooks.create(
|
|
143
|
+
setup_state = hooks.create(
|
|
148
144
|
"pulse:core.setup",
|
|
149
|
-
|
|
145
|
+
factory=SetupState,
|
|
150
146
|
metadata=HookMetadata(
|
|
151
147
|
owner="pulse.core",
|
|
152
148
|
description="Internal storage for pulse.setup hook",
|
|
@@ -195,7 +191,7 @@ def setup(init_func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
|
195
191
|
- Use ``ps.setup()`` directly when AST rewriting is problematic
|
|
196
192
|
- Arguments must be consistent across renders (same count and names)
|
|
197
193
|
"""
|
|
198
|
-
state =
|
|
194
|
+
state = setup_state()
|
|
199
195
|
state.ensure_not_called()
|
|
200
196
|
|
|
201
197
|
key = state.consume_pending_key()
|
|
@@ -245,10 +241,10 @@ def setup_key(key: str) -> None:
|
|
|
245
241
|
"""
|
|
246
242
|
if not isinstance(key, str):
|
|
247
243
|
raise TypeError("setup_key() requires a string key")
|
|
248
|
-
state =
|
|
244
|
+
state = setup_state()
|
|
249
245
|
if state.called_this_render:
|
|
250
246
|
raise RuntimeError("setup_key() must be called before setup() in a render")
|
|
251
247
|
state.set_pending_key(key)
|
|
252
248
|
|
|
253
249
|
|
|
254
|
-
__all__ = ["setup", "setup_key", "
|
|
250
|
+
__all__ = ["setup", "setup_key", "SetupState"]
|
|
@@ -35,7 +35,7 @@ class StableEntry:
|
|
|
35
35
|
self.wrapper = wrapper
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class
|
|
38
|
+
class StableState(HookState):
|
|
39
39
|
"""Internal hook state that stores stable entries by key.
|
|
40
40
|
|
|
41
41
|
Maintains a dictionary of StableEntry objects, allowing stable
|
|
@@ -50,13 +50,9 @@ class StableRegistry(HookState):
|
|
|
50
50
|
self.entries: dict[str, StableEntry] = {}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
return StableRegistry()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
_stable_hook = hooks.create(
|
|
53
|
+
stable_state = hooks.create(
|
|
58
54
|
"pulse:core.stable",
|
|
59
|
-
|
|
55
|
+
factory=StableState,
|
|
60
56
|
metadata=HookMetadata(
|
|
61
57
|
owner="pulse.core",
|
|
62
58
|
description="Internal registry for pulse.stable values",
|
|
@@ -119,7 +115,7 @@ def stable(key: str, value: Any = MISSING) -> Any:
|
|
|
119
115
|
if not key:
|
|
120
116
|
raise ValueError("stable() requires a non-empty string key")
|
|
121
117
|
|
|
122
|
-
registry =
|
|
118
|
+
registry = stable_state()
|
|
123
119
|
entry = registry.entries.get(key)
|
|
124
120
|
|
|
125
121
|
if value is not MISSING:
|
|
@@ -135,4 +131,4 @@ def stable(key: str, value: Any = MISSING) -> Any:
|
|
|
135
131
|
return entry.wrapper
|
|
136
132
|
|
|
137
133
|
|
|
138
|
-
__all__ = ["stable", "
|
|
134
|
+
__all__ = ["stable", "StableState", "StableEntry"]
|
|
@@ -5,7 +5,7 @@ from typing import Any, TypeVar, override
|
|
|
5
5
|
|
|
6
6
|
from pulse.component import is_component_code
|
|
7
7
|
from pulse.hooks.core import HookMetadata, HookState, hooks
|
|
8
|
-
from pulse.state import State
|
|
8
|
+
from pulse.state.state import State
|
|
9
9
|
|
|
10
10
|
S = TypeVar("S", bound=State)
|
|
11
11
|
|
|
@@ -99,10 +99,6 @@ def _instantiate_state(arg: State | Callable[[], State]) -> State:
|
|
|
99
99
|
return instance
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def _state_factory():
|
|
103
|
-
return StateHookState()
|
|
104
|
-
|
|
105
|
-
|
|
106
102
|
def _frame_offset(frame: FrameType) -> int:
|
|
107
103
|
offset = frame.f_lasti
|
|
108
104
|
if offset < 0:
|
|
@@ -123,9 +119,9 @@ def collect_component_identity(
|
|
|
123
119
|
return tuple(identity[:1])
|
|
124
120
|
|
|
125
121
|
|
|
126
|
-
|
|
122
|
+
state_hook = hooks.create(
|
|
127
123
|
"pulse:core.state",
|
|
128
|
-
|
|
124
|
+
factory=StateHookState,
|
|
129
125
|
metadata=HookMetadata(
|
|
130
126
|
owner="pulse.core",
|
|
131
127
|
description="Internal storage for pulse.state hook",
|
|
@@ -185,7 +181,7 @@ def state(
|
|
|
185
181
|
else:
|
|
186
182
|
identity = resolved_key
|
|
187
183
|
|
|
188
|
-
hook_state =
|
|
184
|
+
hook_state = state_hook()
|
|
189
185
|
return hook_state.get_or_create_state(identity, resolved_key, resolved_arg) # pyright: ignore[reportReturnType]
|
|
190
186
|
|
|
191
187
|
|
|
@@ -39,7 +39,8 @@ from pulse.queries.query import RETRY_DELAY_DEFAULT, QueryConfig
|
|
|
39
39
|
from pulse.reactive import Computed, Effect, Signal, Untrack
|
|
40
40
|
from pulse.reactive_extensions import ReactiveList, unwrap
|
|
41
41
|
from pulse.scheduling import TimerHandleLike, create_task, later
|
|
42
|
-
from pulse.state import InitializableProperty
|
|
42
|
+
from pulse.state.property import InitializableProperty
|
|
43
|
+
from pulse.state.state import State
|
|
43
44
|
|
|
44
45
|
T = TypeVar("T")
|
|
45
46
|
TParam = TypeVar("TParam")
|
|
@@ -13,7 +13,8 @@ from typing import (
|
|
|
13
13
|
from pulse.helpers import call_flexible, maybe_await
|
|
14
14
|
from pulse.queries.common import OnErrorFn, OnSuccessFn, bind_state
|
|
15
15
|
from pulse.reactive import Signal
|
|
16
|
-
from pulse.state import InitializableProperty
|
|
16
|
+
from pulse.state.property import InitializableProperty
|
|
17
|
+
from pulse.state.state import State
|
|
17
18
|
|
|
18
19
|
T = TypeVar("T")
|
|
19
20
|
TState = TypeVar("TState", bound=State)
|
|
@@ -37,7 +37,8 @@ from pulse.queries.common import (
|
|
|
37
37
|
from pulse.queries.effect import AsyncQueryEffect
|
|
38
38
|
from pulse.reactive import Computed, Effect, Signal, Untrack
|
|
39
39
|
from pulse.scheduling import TimerHandleLike, create_task, is_pytest, later
|
|
40
|
-
from pulse.state import InitializableProperty
|
|
40
|
+
from pulse.state.property import InitializableProperty
|
|
41
|
+
from pulse.state.state import State
|
|
41
42
|
|
|
42
43
|
if TYPE_CHECKING:
|
|
43
44
|
from pulse.queries.protocol import QueryResult
|
|
@@ -34,7 +34,7 @@ from pulse.scheduling import (
|
|
|
34
34
|
TimerRegistry,
|
|
35
35
|
create_future,
|
|
36
36
|
)
|
|
37
|
-
from pulse.state import State
|
|
37
|
+
from pulse.state.state import State
|
|
38
38
|
from pulse.transpiler.id import next_id
|
|
39
39
|
from pulse.transpiler.nodes import Expr
|
|
40
40
|
|
|
@@ -111,7 +111,7 @@ class RouteMount:
|
|
|
111
111
|
) -> None:
|
|
112
112
|
self.render = render
|
|
113
113
|
self.path = ensure_absolute_path(path)
|
|
114
|
-
self.route = RouteContext(route_info, route)
|
|
114
|
+
self.route = RouteContext(route_info, route, render)
|
|
115
115
|
self.effect = None
|
|
116
116
|
self._pulse_ctx = None
|
|
117
117
|
self.tree = RenderTree(route.render())
|
|
@@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from types import NoneType
|
|
7
7
|
from typing import Any, NamedTuple, TypeAlias, cast
|
|
8
8
|
|
|
9
|
+
from pulse.debounce import Debounced
|
|
9
10
|
from pulse.helpers import values_equal
|
|
10
11
|
from pulse.hooks.core import HookContext
|
|
11
12
|
from pulse.transpiler import Import
|
|
@@ -33,7 +34,7 @@ from pulse.transpiler.vdom import (
|
|
|
33
34
|
VDOMPropValue,
|
|
34
35
|
)
|
|
35
36
|
|
|
36
|
-
PropValue: TypeAlias = Node | Callable[..., Any]
|
|
37
|
+
PropValue: TypeAlias = Node | Callable[..., Any] | Debounced[Any, Any]
|
|
37
38
|
|
|
38
39
|
FRAGMENT_TAG = ""
|
|
39
40
|
MOUNT_PREFIX = "$$"
|
|
@@ -404,6 +405,21 @@ class Renderer:
|
|
|
404
405
|
updated[key] = value.render()
|
|
405
406
|
continue
|
|
406
407
|
|
|
408
|
+
if isinstance(value, Debounced):
|
|
409
|
+
eval_keys.add(key)
|
|
410
|
+
if isinstance(old_value, (Element, PulseNode)):
|
|
411
|
+
unmount_element(old_value)
|
|
412
|
+
if normalized is None:
|
|
413
|
+
normalized = current.copy()
|
|
414
|
+
normalized[key] = value
|
|
415
|
+
register_callback(self.callbacks, prop_path, value.fn)
|
|
416
|
+
prev_delay = (
|
|
417
|
+
old_value.delay_ms if isinstance(old_value, Debounced) else None
|
|
418
|
+
)
|
|
419
|
+
if prev_delay != value.delay_ms:
|
|
420
|
+
updated[key] = format_callback_placeholder(value.delay_ms)
|
|
421
|
+
continue
|
|
422
|
+
|
|
407
423
|
if callable(value):
|
|
408
424
|
eval_keys.add(key)
|
|
409
425
|
if isinstance(old_value, (Element, PulseNode)):
|
|
@@ -412,7 +428,7 @@ class Renderer:
|
|
|
412
428
|
normalized = current.copy()
|
|
413
429
|
normalized[key] = value
|
|
414
430
|
register_callback(self.callbacks, prop_path, value)
|
|
415
|
-
if not callable(old_value):
|
|
431
|
+
if not callable(old_value) or isinstance(old_value, Debounced):
|
|
416
432
|
updated[key] = CALLBACK_PLACEHOLDER
|
|
417
433
|
continue
|
|
418
434
|
|
|
@@ -483,6 +499,8 @@ def prop_requires_eval(value: PropValue) -> bool:
|
|
|
483
499
|
return True
|
|
484
500
|
if isinstance(value, Expr):
|
|
485
501
|
return True
|
|
502
|
+
if isinstance(value, Debounced):
|
|
503
|
+
return True
|
|
486
504
|
return callable(value)
|
|
487
505
|
|
|
488
506
|
|
|
@@ -530,6 +548,16 @@ def normalize_children(children: Children | None) -> list[Node]:
|
|
|
530
548
|
return out
|
|
531
549
|
|
|
532
550
|
|
|
551
|
+
def format_callback_placeholder(delay_ms: float | None) -> str:
|
|
552
|
+
if delay_ms is None:
|
|
553
|
+
return CALLBACK_PLACEHOLDER
|
|
554
|
+
if delay_ms.is_integer():
|
|
555
|
+
suffix = str(int(delay_ms))
|
|
556
|
+
else:
|
|
557
|
+
suffix = format(delay_ms, "g")
|
|
558
|
+
return f"{CALLBACK_PLACEHOLDER}:{suffix}"
|
|
559
|
+
|
|
560
|
+
|
|
533
561
|
def register_callback(
|
|
534
562
|
callbacks: Callbacks,
|
|
535
563
|
path: str,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from collections.abc import Sequence
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
-
from typing import TypedDict, cast, override
|
|
4
|
+
from typing import TYPE_CHECKING, TypedDict, cast, override
|
|
5
5
|
|
|
6
6
|
from pulse.component import Component
|
|
7
7
|
from pulse.env import env
|
|
8
8
|
from pulse.reactive_extensions import ReactiveDict
|
|
9
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pulse.render_session import RenderSession
|
|
12
|
+
from pulse.state.query_param import QueryParamSync
|
|
13
|
+
|
|
10
14
|
# angle brackets cannot appear in a regular URL path, this ensures no name conflicts
|
|
11
15
|
LAYOUT_INDICATOR = "<layout>"
|
|
12
16
|
|
|
@@ -516,7 +520,8 @@ class RouteContext:
|
|
|
516
520
|
"""Runtime context for the current route.
|
|
517
521
|
|
|
518
522
|
Provides reactive access to the current route's URL components and
|
|
519
|
-
parameters.
|
|
523
|
+
parameters. Available via `ps.route()` (route info) and `ps.pulse_route()`
|
|
524
|
+
(route definition) in components.
|
|
520
525
|
|
|
521
526
|
Attributes:
|
|
522
527
|
info: Current route info (reactive, auto-updates on navigation).
|
|
@@ -534,18 +539,27 @@ class RouteContext:
|
|
|
534
539
|
```python
|
|
535
540
|
@ps.component
|
|
536
541
|
def UserProfile():
|
|
537
|
-
|
|
538
|
-
user_id =
|
|
542
|
+
info = ps.route()
|
|
543
|
+
user_id = info["pathParams"].get("id")
|
|
539
544
|
return ps.div(f"User: {user_id}")
|
|
540
545
|
```
|
|
541
546
|
"""
|
|
542
547
|
|
|
543
548
|
info: RouteInfo
|
|
544
549
|
pulse_route: Route | Layout
|
|
550
|
+
query_param_sync: "QueryParamSync"
|
|
545
551
|
|
|
546
|
-
def __init__(
|
|
552
|
+
def __init__(
|
|
553
|
+
self,
|
|
554
|
+
info: RouteInfo,
|
|
555
|
+
pulse_route: Route | Layout,
|
|
556
|
+
render: "RenderSession",
|
|
557
|
+
):
|
|
547
558
|
self.info = cast(RouteInfo, cast(object, ReactiveDict(info)))
|
|
548
559
|
self.pulse_route = pulse_route
|
|
560
|
+
from pulse.state.query_param import QueryParamSync
|
|
561
|
+
|
|
562
|
+
self.query_param_sync = QueryParamSync(render, self)
|
|
549
563
|
|
|
550
564
|
def update(self, info: RouteInfo) -> None:
|
|
551
565
|
"""Update the route info with new values.
|