pulse-framework 0.1.44__py3-none-any.whl → 0.1.47__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 +10 -24
- pulse/app.py +3 -25
- pulse/codegen/codegen.py +43 -88
- pulse/codegen/js.py +35 -5
- pulse/codegen/templates/route.py +341 -254
- pulse/form.py +1 -1
- pulse/helpers.py +40 -8
- pulse/hooks/core.py +2 -2
- pulse/hooks/effects.py +1 -1
- pulse/hooks/init.py +2 -1
- pulse/hooks/setup.py +1 -1
- pulse/hooks/stable.py +2 -2
- pulse/hooks/states.py +2 -2
- pulse/html/props.py +3 -2
- pulse/html/tags.py +135 -0
- pulse/html/tags.pyi +4 -0
- pulse/js/__init__.py +110 -0
- pulse/js/__init__.pyi +95 -0
- pulse/js/_types.py +297 -0
- pulse/js/array.py +253 -0
- pulse/js/console.py +47 -0
- pulse/js/date.py +113 -0
- pulse/js/document.py +138 -0
- pulse/js/error.py +139 -0
- pulse/js/json.py +62 -0
- pulse/js/map.py +84 -0
- pulse/js/math.py +66 -0
- pulse/js/navigator.py +76 -0
- pulse/js/number.py +54 -0
- pulse/js/object.py +173 -0
- pulse/js/promise.py +150 -0
- pulse/js/regexp.py +54 -0
- pulse/js/set.py +109 -0
- pulse/js/string.py +35 -0
- pulse/js/weakmap.py +50 -0
- pulse/js/weakset.py +45 -0
- pulse/js/window.py +199 -0
- pulse/messages.py +22 -3
- pulse/queries/client.py +7 -7
- pulse/queries/effect.py +16 -0
- pulse/queries/infinite_query.py +138 -29
- pulse/queries/mutation.py +1 -15
- pulse/queries/protocol.py +136 -0
- pulse/queries/query.py +610 -174
- pulse/queries/store.py +11 -14
- pulse/react_component.py +167 -14
- pulse/reactive.py +19 -1
- pulse/reactive_extensions.py +5 -5
- pulse/render_session.py +185 -59
- pulse/renderer.py +80 -158
- pulse/routing.py +1 -18
- pulse/transpiler/__init__.py +131 -0
- pulse/transpiler/builtins.py +731 -0
- pulse/transpiler/constants.py +110 -0
- pulse/transpiler/context.py +26 -0
- pulse/transpiler/errors.py +2 -0
- pulse/transpiler/function.py +250 -0
- pulse/transpiler/ids.py +16 -0
- pulse/transpiler/imports.py +409 -0
- pulse/transpiler/js_module.py +274 -0
- pulse/transpiler/modules/__init__.py +30 -0
- pulse/transpiler/modules/asyncio.py +38 -0
- pulse/transpiler/modules/json.py +20 -0
- pulse/transpiler/modules/math.py +320 -0
- pulse/transpiler/modules/re.py +466 -0
- pulse/transpiler/modules/tags.py +268 -0
- pulse/transpiler/modules/typing.py +59 -0
- pulse/transpiler/nodes.py +1216 -0
- pulse/transpiler/py_module.py +119 -0
- pulse/transpiler/transpiler.py +938 -0
- pulse/transpiler/utils.py +4 -0
- pulse/types/event_handler.py +3 -2
- pulse/vdom.py +212 -13
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/METADATA +1 -1
- pulse_framework-0.1.47.dist-info/RECORD +119 -0
- pulse/codegen/imports.py +0 -204
- pulse/css.py +0 -155
- pulse_framework-0.1.44.dist-info/RECORD +0 -79
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.44.dist-info → pulse_framework-0.1.47.dist-info}/entry_points.txt +0 -0
pulse/js/window.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Browser window global Any.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from pulse.js import window
|
|
5
|
+
window.alert("Hello!") # -> window.alert("Hello!")
|
|
6
|
+
window.innerWidth # -> window.innerWidth
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable as _Callable
|
|
10
|
+
from typing import Any as _Any
|
|
11
|
+
|
|
12
|
+
from pulse.js._types import Element as _Element
|
|
13
|
+
from pulse.js._types import Selection as _Selection
|
|
14
|
+
from pulse.transpiler.js_module import register_js_module as _register_js_module
|
|
15
|
+
|
|
16
|
+
# Dimensions
|
|
17
|
+
innerWidth: int
|
|
18
|
+
innerHeight: int
|
|
19
|
+
outerWidth: int
|
|
20
|
+
outerHeight: int
|
|
21
|
+
|
|
22
|
+
# Scroll position
|
|
23
|
+
scrollX: float
|
|
24
|
+
scrollY: float
|
|
25
|
+
pageXOffset: float # Alias for scrollX
|
|
26
|
+
pageYOffset: float # Alias for scrollY
|
|
27
|
+
|
|
28
|
+
# Screen information
|
|
29
|
+
devicePixelRatio: float
|
|
30
|
+
|
|
31
|
+
# Location and history (typed as Any since they're complex interfaces)
|
|
32
|
+
location: _Any
|
|
33
|
+
history: _Any
|
|
34
|
+
navigator: _Any
|
|
35
|
+
document: _Any
|
|
36
|
+
|
|
37
|
+
# Storage
|
|
38
|
+
localStorage: _Any
|
|
39
|
+
sessionStorage: _Any
|
|
40
|
+
|
|
41
|
+
# Performance
|
|
42
|
+
performance: _Any
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Dialog methods
|
|
46
|
+
def alert(message: str = "") -> None:
|
|
47
|
+
"""Display an alert dialog with the given message."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def confirm(message: str = "") -> bool:
|
|
52
|
+
"""Display a confirmation dialog. Returns True if user clicks OK."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def prompt(message: str = "", default: str = "") -> str | None:
|
|
57
|
+
"""Display a prompt dialog. Returns input or None if cancelled."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Scroll methods
|
|
62
|
+
def scrollTo(x: float | dict[str, float], y: float | None = None) -> None:
|
|
63
|
+
"""Scroll to the given position."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def scrollBy(x: float | dict[str, float], y: float | None = None) -> None:
|
|
68
|
+
"""Scroll by the given amount."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def scroll(x: float | dict[str, float], y: float | None = None) -> None:
|
|
73
|
+
"""Alias for scrollTo."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Selection
|
|
78
|
+
def getSelection() -> _Selection | None:
|
|
79
|
+
"""Return the current text selection."""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def getComputedStyle(element: _Element, pseudoElt: str | None = None) -> _Any:
|
|
84
|
+
"""Return the computed style of an element."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Focus
|
|
89
|
+
def focus() -> None:
|
|
90
|
+
"""Give focus to the window."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def blur() -> None:
|
|
95
|
+
"""Remove focus from the window."""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Open/close
|
|
100
|
+
def open(
|
|
101
|
+
url: str = "",
|
|
102
|
+
target: str = "_blank",
|
|
103
|
+
features: str = "",
|
|
104
|
+
) -> _Any | None:
|
|
105
|
+
"""Open a new window. Returns the new window Any or None."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def close() -> None:
|
|
110
|
+
"""Close the window (only works for windows opened by script)."""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Timers (these return timer IDs)
|
|
115
|
+
def setTimeout(handler: _Callable[..., None], timeout: int = 0, *args: _Any) -> int:
|
|
116
|
+
"""Schedule a function to run after a delay. Returns timer ID."""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def clearTimeout(timeoutId: int) -> None:
|
|
121
|
+
"""Cancel a timeout scheduled with setTimeout."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def setInterval(handler: _Callable[..., None], timeout: int = 0, *args: _Any) -> int:
|
|
126
|
+
"""Schedule a function to run repeatedly. Returns timer ID."""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def clearInterval(intervalId: int) -> None:
|
|
131
|
+
"""Cancel an interval scheduled with setInterval."""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Animation
|
|
136
|
+
def requestAnimationFrame(callback: _Callable[[float], None]) -> int:
|
|
137
|
+
"""Request a callback before the next repaint. Returns request ID."""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def cancelAnimationFrame(requestId: int) -> None:
|
|
142
|
+
"""Cancel an animation frame request."""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Event listeners
|
|
147
|
+
def addEventListener(
|
|
148
|
+
type: str,
|
|
149
|
+
listener: _Callable[..., None],
|
|
150
|
+
options: bool | dict[str, bool] | None = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Add an event listener to the window."""
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def removeEventListener(
|
|
157
|
+
type: str,
|
|
158
|
+
listener: _Callable[..., None],
|
|
159
|
+
options: bool | dict[str, bool] | None = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Remove an event listener from the window."""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def dispatchEvent(event: _Any) -> bool:
|
|
166
|
+
"""Dispatch an event to the window."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# Encoding
|
|
171
|
+
def atob(encoded: str) -> str:
|
|
172
|
+
"""Decode a base64 encoded string."""
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def btoa(data: str) -> str:
|
|
177
|
+
"""Encode a string as base64."""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Misc
|
|
182
|
+
def matchMedia(query: str) -> _Any:
|
|
183
|
+
"""Return a MediaQueryList for the given media query."""
|
|
184
|
+
...
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def print_() -> None:
|
|
188
|
+
"""Open the print dialog."""
|
|
189
|
+
...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def postMessage(
|
|
193
|
+
message: _Any, targetOrigin: str, transfer: list[_Any] | None = None
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Post a message to another window."""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_register_js_module(name="window")
|
pulse/messages.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from typing import Any, Literal, NotRequired, TypedDict
|
|
2
2
|
|
|
3
|
-
from pulse.renderer import VDOMOperation
|
|
4
3
|
from pulse.routing import RouteInfo
|
|
5
|
-
from pulse.vdom import VDOM
|
|
4
|
+
from pulse.vdom import VDOM, VDOMOperation
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
# ====================
|
|
@@ -14,7 +13,7 @@ class ServerInitMessage(TypedDict):
|
|
|
14
13
|
vdom: VDOM
|
|
15
14
|
callbacks: list[str]
|
|
16
15
|
render_props: list[str]
|
|
17
|
-
|
|
16
|
+
jsexpr_paths: list[str] # paths containing JS expressions
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class ServerUpdateMessage(TypedDict):
|
|
@@ -82,6 +81,15 @@ class ServerChannelResponseMessage(TypedDict):
|
|
|
82
81
|
error: NotRequired[Any]
|
|
83
82
|
|
|
84
83
|
|
|
84
|
+
class ServerJsExecMessage(TypedDict):
|
|
85
|
+
"""Execute JavaScript code on the client."""
|
|
86
|
+
|
|
87
|
+
type: Literal["js_exec"]
|
|
88
|
+
path: str
|
|
89
|
+
id: str
|
|
90
|
+
code: str
|
|
91
|
+
|
|
92
|
+
|
|
85
93
|
# ====================
|
|
86
94
|
# Client messages
|
|
87
95
|
# ====================
|
|
@@ -136,6 +144,15 @@ class ClientChannelResponseMessage(TypedDict):
|
|
|
136
144
|
error: NotRequired[Any]
|
|
137
145
|
|
|
138
146
|
|
|
147
|
+
class ClientJsResultMessage(TypedDict):
|
|
148
|
+
"""Result of client-side JS execution."""
|
|
149
|
+
|
|
150
|
+
type: Literal["js_result"]
|
|
151
|
+
id: str
|
|
152
|
+
result: Any
|
|
153
|
+
error: str | None
|
|
154
|
+
|
|
155
|
+
|
|
139
156
|
ServerChannelMessage = ServerChannelRequestMessage | ServerChannelResponseMessage
|
|
140
157
|
ServerMessage = (
|
|
141
158
|
ServerInitMessage
|
|
@@ -144,6 +161,7 @@ ServerMessage = (
|
|
|
144
161
|
| ServerApiCallMessage
|
|
145
162
|
| ServerNavigateToMessage
|
|
146
163
|
| ServerChannelMessage
|
|
164
|
+
| ServerJsExecMessage
|
|
147
165
|
)
|
|
148
166
|
|
|
149
167
|
|
|
@@ -153,6 +171,7 @@ ClientPulseMessage = (
|
|
|
153
171
|
| ClientNavigateMessage
|
|
154
172
|
| ClientUnmountMessage
|
|
155
173
|
| ClientApiResultMessage
|
|
174
|
+
| ClientJsResultMessage
|
|
156
175
|
)
|
|
157
176
|
ClientChannelMessage = ClientChannelRequestMessage | ClientChannelResponseMessage
|
|
158
177
|
ClientMessage = ClientPulseMessage | ClientChannelMessage
|
pulse/queries/client.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any, TypeVar, overload
|
|
|
5
5
|
from pulse.context import PulseContext
|
|
6
6
|
from pulse.queries.common import ActionResult, QueryKey
|
|
7
7
|
from pulse.queries.infinite_query import InfiniteQuery, Page
|
|
8
|
-
from pulse.queries.query import
|
|
8
|
+
from pulse.queries.query import KeyedQuery
|
|
9
9
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
@@ -62,11 +62,11 @@ class QueryClient:
|
|
|
62
62
|
# Query accessors
|
|
63
63
|
# ─────────────────────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
-
def get(self, key: QueryKey)
|
|
65
|
+
def get(self, key: QueryKey):
|
|
66
66
|
"""Get an existing regular query by key, or None if not found."""
|
|
67
67
|
return self._get_store().get(key)
|
|
68
68
|
|
|
69
|
-
def get_infinite(self, key: QueryKey)
|
|
69
|
+
def get_infinite(self, key: QueryKey):
|
|
70
70
|
"""Get an existing infinite query by key, or None if not found."""
|
|
71
71
|
return self._get_store().get_infinite(key)
|
|
72
72
|
|
|
@@ -75,7 +75,7 @@ class QueryClient:
|
|
|
75
75
|
filter: QueryFilter | None = None,
|
|
76
76
|
*,
|
|
77
77
|
include_infinite: bool = True,
|
|
78
|
-
) -> list[
|
|
78
|
+
) -> list[KeyedQuery[Any] | InfiniteQuery[Any, Any]]:
|
|
79
79
|
"""
|
|
80
80
|
Get all queries matching the filter.
|
|
81
81
|
|
|
@@ -89,7 +89,7 @@ class QueryClient:
|
|
|
89
89
|
"""
|
|
90
90
|
store = self._get_store()
|
|
91
91
|
predicate = _normalize_filter(filter)
|
|
92
|
-
results: list[
|
|
92
|
+
results: list[KeyedQuery[Any] | InfiniteQuery[Any, Any]] = []
|
|
93
93
|
|
|
94
94
|
for key, entry in store.items():
|
|
95
95
|
if predicate is not None and not predicate(key):
|
|
@@ -100,11 +100,11 @@ class QueryClient:
|
|
|
100
100
|
|
|
101
101
|
return results
|
|
102
102
|
|
|
103
|
-
def get_queries(self, filter: QueryFilter | None = None) -> list[
|
|
103
|
+
def get_queries(self, filter: QueryFilter | None = None) -> list[KeyedQuery[Any]]:
|
|
104
104
|
"""Get all regular queries matching the filter."""
|
|
105
105
|
store = self._get_store()
|
|
106
106
|
predicate = _normalize_filter(filter)
|
|
107
|
-
results: list[
|
|
107
|
+
results: list[KeyedQuery[Any]] = []
|
|
108
108
|
|
|
109
109
|
for key, entry in store.items():
|
|
110
110
|
if isinstance(entry, InfiniteQuery):
|
pulse/queries/effect.py
CHANGED
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
from collections.abc import Awaitable, Callable
|
|
3
3
|
from typing import (
|
|
4
4
|
Any,
|
|
5
|
+
Literal,
|
|
5
6
|
Protocol,
|
|
6
7
|
override,
|
|
7
8
|
)
|
|
@@ -11,15 +12,21 @@ from pulse.reactive import AsyncEffect, Computed, Signal
|
|
|
11
12
|
|
|
12
13
|
class Fetcher(Protocol):
|
|
13
14
|
is_fetching: Signal[bool]
|
|
15
|
+
data: Signal[Any]
|
|
16
|
+
status: Signal[Literal["loading", "success", "error"]]
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class AsyncQueryEffect(AsyncEffect):
|
|
17
20
|
"""
|
|
18
21
|
Specialized AsyncEffect for queries that synchronously sets loading state
|
|
19
22
|
when rescheduled/run.
|
|
23
|
+
|
|
24
|
+
For unkeyed queries (deps=None), also resets data/status when re-running
|
|
25
|
+
due to dependency changes, to behave like keyed queries on key change.
|
|
20
26
|
"""
|
|
21
27
|
|
|
22
28
|
fetcher: Fetcher
|
|
29
|
+
_is_unkeyed: bool
|
|
23
30
|
|
|
24
31
|
def __init__(
|
|
25
32
|
self,
|
|
@@ -30,10 +37,19 @@ class AsyncQueryEffect(AsyncEffect):
|
|
|
30
37
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
31
38
|
):
|
|
32
39
|
self.fetcher = fetcher
|
|
40
|
+
# Unkeyed queries have deps=None (auto-track), keyed have deps=[] (no auto-track)
|
|
41
|
+
self._is_unkeyed = deps is None
|
|
33
42
|
super().__init__(fn, name=name, lazy=lazy, deps=deps)
|
|
34
43
|
|
|
35
44
|
@override
|
|
36
45
|
def run(self) -> asyncio.Task[Any]:
|
|
37
46
|
# Immediately set loading state before running the effect
|
|
38
47
|
self.fetcher.is_fetching.write(True)
|
|
48
|
+
|
|
49
|
+
# For unkeyed queries on re-run (dependency changed), reset data/status
|
|
50
|
+
# to behave like keyed queries when key changes (new Query with data=None)
|
|
51
|
+
if self._is_unkeyed and self.runs > 0:
|
|
52
|
+
self.fetcher.data.write(None)
|
|
53
|
+
self.fetcher.status.write("loading")
|
|
54
|
+
|
|
39
55
|
return super().run()
|