pulse-framework 0.1.41__py3-none-any.whl → 0.1.43__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 +17 -3
- pulse/context.py +3 -2
- pulse/decorators.py +8 -172
- pulse/helpers.py +39 -23
- pulse/hooks/core.py +4 -6
- pulse/hooks/init.py +460 -0
- pulse/queries/client.py +462 -0
- pulse/queries/common.py +28 -0
- pulse/queries/effect.py +39 -0
- pulse/queries/infinite_query.py +1157 -0
- pulse/queries/mutation.py +47 -0
- pulse/queries/query.py +560 -53
- pulse/queries/store.py +81 -18
- pulse/react_component.py +2 -1
- pulse/reactive.py +102 -23
- pulse/reactive_extensions.py +19 -7
- pulse/state.py +5 -0
- pulse/vdom.py +3 -1
- {pulse_framework-0.1.41.dist-info → pulse_framework-0.1.43.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.41.dist-info → pulse_framework-0.1.43.dist-info}/RECORD +22 -19
- pulse/queries/query_observer.py +0 -365
- {pulse_framework-0.1.41.dist-info → pulse_framework-0.1.43.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.41.dist-info → pulse_framework-0.1.43.dist-info}/entry_points.txt +0 -0
pulse/__init__.py
CHANGED
|
@@ -72,8 +72,6 @@ from pulse.css import (
|
|
|
72
72
|
# Decorators
|
|
73
73
|
from pulse.decorators import computed as computed
|
|
74
74
|
from pulse.decorators import effect as effect
|
|
75
|
-
from pulse.decorators import mutation as mutation
|
|
76
|
-
from pulse.decorators import query as query
|
|
77
75
|
|
|
78
76
|
# Environment
|
|
79
77
|
from pulse.env import PulseEnv as PulseEnv
|
|
@@ -161,6 +159,11 @@ from pulse.hooks.core import (
|
|
|
161
159
|
# Hooks - Effects
|
|
162
160
|
from pulse.hooks.effects import EffectsHookState as EffectsHookState
|
|
163
161
|
from pulse.hooks.effects import effects as effects
|
|
162
|
+
|
|
163
|
+
# Hooks - Init
|
|
164
|
+
from pulse.hooks.init import (
|
|
165
|
+
init as init,
|
|
166
|
+
)
|
|
164
167
|
from pulse.hooks.runtime import (
|
|
165
168
|
GLOBAL_STATES as GLOBAL_STATES,
|
|
166
169
|
)
|
|
@@ -1344,7 +1347,18 @@ from pulse.middleware import (
|
|
|
1344
1347
|
|
|
1345
1348
|
# Plugin
|
|
1346
1349
|
from pulse.plugin import Plugin as Plugin
|
|
1347
|
-
from pulse.queries.
|
|
1350
|
+
from pulse.queries.client import QueryClient as QueryClient
|
|
1351
|
+
from pulse.queries.client import QueryFilter as QueryFilter
|
|
1352
|
+
from pulse.queries.client import queries as queries
|
|
1353
|
+
from pulse.queries.common import ActionError as ActionError
|
|
1354
|
+
from pulse.queries.common import ActionResult as ActionResult
|
|
1355
|
+
from pulse.queries.common import ActionSuccess as ActionSuccess
|
|
1356
|
+
from pulse.queries.common import QueryKey as QueryKey
|
|
1357
|
+
from pulse.queries.common import QueryStatus as QueryStatus
|
|
1358
|
+
from pulse.queries.infinite_query import infinite_query as infinite_query
|
|
1359
|
+
from pulse.queries.mutation import mutation as mutation
|
|
1360
|
+
from pulse.queries.query import QueryResult as QueryResult
|
|
1361
|
+
from pulse.queries.query import query as query
|
|
1348
1362
|
|
|
1349
1363
|
# React component registry
|
|
1350
1364
|
from pulse.react_component import (
|
pulse/context.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from contextvars import ContextVar, Token
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from types import TracebackType
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
6
|
|
|
7
7
|
from pulse.routing import RouteContext
|
|
8
8
|
|
|
@@ -58,10 +58,11 @@ class PulseContext:
|
|
|
58
58
|
exc_type: type[BaseException] | None = None,
|
|
59
59
|
exc_val: BaseException | None = None,
|
|
60
60
|
exc_tb: TracebackType | None = None,
|
|
61
|
-
):
|
|
61
|
+
) -> Literal[False]:
|
|
62
62
|
if self._token is not None:
|
|
63
63
|
PULSE_CONTEXT.reset(self._token)
|
|
64
64
|
self._token = None
|
|
65
|
+
return False
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
PULSE_CONTEXT: ContextVar["PulseContext | None"] = ContextVar(
|
pulse/decorators.py
CHANGED
|
@@ -2,16 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, ParamSpec, Protocol, TypeVar, overload
|
|
6
6
|
|
|
7
|
-
from pulse.helpers import MISSING
|
|
8
|
-
from pulse.queries.common import OnErrorFn, OnSuccessFn
|
|
9
|
-
from pulse.queries.mutation import MutationProperty
|
|
10
|
-
from pulse.queries.query import RETRY_DELAY_DEFAULT
|
|
11
|
-
from pulse.queries.query_observer import (
|
|
12
|
-
QueryProperty,
|
|
13
|
-
QueryPropertyWithInitial,
|
|
14
|
-
)
|
|
15
7
|
from pulse.reactive import (
|
|
16
8
|
AsyncEffect,
|
|
17
9
|
AsyncEffectFn,
|
|
@@ -89,6 +81,7 @@ def effect(
|
|
|
89
81
|
lazy: bool = False,
|
|
90
82
|
on_error: Callable[[Exception], None] | None = None,
|
|
91
83
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
84
|
+
interval: float | None = None,
|
|
92
85
|
) -> Effect: ...
|
|
93
86
|
|
|
94
87
|
|
|
@@ -101,6 +94,7 @@ def effect(
|
|
|
101
94
|
lazy: bool = False,
|
|
102
95
|
on_error: Callable[[Exception], None] | None = None,
|
|
103
96
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
97
|
+
interval: float | None = None,
|
|
104
98
|
) -> AsyncEffect: ...
|
|
105
99
|
# In practice this overload returns a StateEffect, but it gets converted into an
|
|
106
100
|
# Effect at state instantiation.
|
|
@@ -117,6 +111,7 @@ def effect(
|
|
|
117
111
|
lazy: bool = False,
|
|
118
112
|
on_error: Callable[[Exception], None] | None = None,
|
|
119
113
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
114
|
+
interval: float | None = None,
|
|
120
115
|
) -> EffectBuilder: ...
|
|
121
116
|
|
|
122
117
|
|
|
@@ -128,6 +123,7 @@ def effect(
|
|
|
128
123
|
lazy: bool = False,
|
|
129
124
|
on_error: Callable[[Exception], None] | None = None,
|
|
130
125
|
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
126
|
+
interval: float | None = None,
|
|
131
127
|
):
|
|
132
128
|
# The type checker is not happy if I don't specify the `/` here.
|
|
133
129
|
def decorator(func: Callable[..., Any], /):
|
|
@@ -146,6 +142,7 @@ def effect(
|
|
|
146
142
|
lazy=lazy,
|
|
147
143
|
on_error=on_error,
|
|
148
144
|
deps=deps,
|
|
145
|
+
interval=interval,
|
|
149
146
|
)
|
|
150
147
|
|
|
151
148
|
if len(params) > 0:
|
|
@@ -161,6 +158,7 @@ def effect(
|
|
|
161
158
|
lazy=lazy,
|
|
162
159
|
on_error=on_error,
|
|
163
160
|
deps=deps,
|
|
161
|
+
interval=interval,
|
|
164
162
|
)
|
|
165
163
|
return Effect(
|
|
166
164
|
func, # type: ignore[arg-type]
|
|
@@ -169,169 +167,7 @@ def effect(
|
|
|
169
167
|
lazy=lazy,
|
|
170
168
|
on_error=on_error,
|
|
171
169
|
deps=deps,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if fn:
|
|
175
|
-
return decorator(fn)
|
|
176
|
-
return decorator
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
# -----------------
|
|
180
|
-
# Query decorator
|
|
181
|
-
# -----------------
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# With initial (narrowed return type) - more specific overloads first
|
|
185
|
-
@overload
|
|
186
|
-
def query(
|
|
187
|
-
fn: Callable[[TState], Awaitable[T]],
|
|
188
|
-
*,
|
|
189
|
-
key: Callable[[TState], tuple[Any, ...]] | None = None,
|
|
190
|
-
stale_time: float = 0.0,
|
|
191
|
-
gc_time: float | None = 300.0,
|
|
192
|
-
keep_previous_data: bool = False,
|
|
193
|
-
retries: int = 3,
|
|
194
|
-
retry_delay: float | None = None,
|
|
195
|
-
initial: T | Callable[[TState], T],
|
|
196
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
197
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
198
|
-
) -> QueryPropertyWithInitial[T, TState]: ...
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@overload
|
|
202
|
-
def query(
|
|
203
|
-
fn: None = None,
|
|
204
|
-
*,
|
|
205
|
-
key: Callable[[TState], tuple[Any, ...]] | None = None,
|
|
206
|
-
stale_time: float = 0.0,
|
|
207
|
-
gc_time: float | None = 300.0,
|
|
208
|
-
keep_previous_data: bool = False,
|
|
209
|
-
retries: int = 3,
|
|
210
|
-
retry_delay: float | None = None,
|
|
211
|
-
initial: T | Callable[[TState], T],
|
|
212
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
213
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
214
|
-
) -> Callable[
|
|
215
|
-
[Callable[[TState], Awaitable[T]]], QueryPropertyWithInitial[T, TState]
|
|
216
|
-
]: ...
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
@overload
|
|
220
|
-
def query(
|
|
221
|
-
fn: Callable[[TState], Awaitable[T]],
|
|
222
|
-
*,
|
|
223
|
-
key: Callable[[TState], tuple[Any, ...]] | None = None,
|
|
224
|
-
stale_time: float = 0.0,
|
|
225
|
-
gc_time: float | None = 300.0,
|
|
226
|
-
keep_previous_data: bool = False,
|
|
227
|
-
retries: int = 3,
|
|
228
|
-
retry_delay: float | None = None,
|
|
229
|
-
initial: T | Callable[[TState], T] | None = ...,
|
|
230
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
231
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
232
|
-
) -> QueryProperty[T, TState]: ...
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
@overload
|
|
236
|
-
def query(
|
|
237
|
-
fn: None = None,
|
|
238
|
-
*,
|
|
239
|
-
key: Callable[[TState], tuple[Any, ...]] | None = None,
|
|
240
|
-
stale_time: float = 0.0,
|
|
241
|
-
gc_time: float | None = 300.0,
|
|
242
|
-
keep_previous_data: bool = False,
|
|
243
|
-
retries: int = 3,
|
|
244
|
-
retry_delay: float | None = None,
|
|
245
|
-
initial: T | Callable[[TState], T] | None = ...,
|
|
246
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
247
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
248
|
-
) -> Callable[[Callable[[TState], Awaitable[T]]], QueryProperty[T, TState]]: ...
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def query(
|
|
252
|
-
fn: Callable[[TState], Awaitable[T]] | None = None,
|
|
253
|
-
*,
|
|
254
|
-
key: Callable[[TState], tuple[Any, ...]] | None = None,
|
|
255
|
-
stale_time: float = 0.0,
|
|
256
|
-
gc_time: float | None = 300.0,
|
|
257
|
-
keep_previous_data: bool = False,
|
|
258
|
-
retries: int = 3,
|
|
259
|
-
retry_delay: float | None = None,
|
|
260
|
-
initial: T | Callable[[TState], T] | None = MISSING,
|
|
261
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
262
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
263
|
-
):
|
|
264
|
-
def decorator(
|
|
265
|
-
func: Callable[[TState], Awaitable[T]], /
|
|
266
|
-
) -> QueryProperty[T, TState] | QueryPropertyWithInitial[T, TState]:
|
|
267
|
-
sig = inspect.signature(func)
|
|
268
|
-
params = list(sig.parameters.values())
|
|
269
|
-
# Only state-method form supported for now (single 'self')
|
|
270
|
-
if not (len(params) == 1 and params[0].name == "self"):
|
|
271
|
-
raise TypeError("@query currently only supports state methods (self)")
|
|
272
|
-
|
|
273
|
-
prop_cls = QueryPropertyWithInitial if initial is not None else QueryProperty
|
|
274
|
-
|
|
275
|
-
return prop_cls(
|
|
276
|
-
func.__name__,
|
|
277
|
-
func,
|
|
278
|
-
key=key,
|
|
279
|
-
stale_time=stale_time,
|
|
280
|
-
gc_time=gc_time if gc_time is not None else 300.0,
|
|
281
|
-
keep_previous_data=keep_previous_data,
|
|
282
|
-
retries=retries,
|
|
283
|
-
retry_delay=RETRY_DELAY_DEFAULT if retry_delay is None else retry_delay,
|
|
284
|
-
initial=initial,
|
|
285
|
-
on_success=on_success,
|
|
286
|
-
on_error=on_error,
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
if fn:
|
|
290
|
-
return decorator(fn)
|
|
291
|
-
return decorator
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
# -----------------
|
|
295
|
-
# Mutation decorator
|
|
296
|
-
# -----------------
|
|
297
|
-
@overload
|
|
298
|
-
def mutation(
|
|
299
|
-
fn: Callable[Concatenate[TState, P], Awaitable[T]],
|
|
300
|
-
*,
|
|
301
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
302
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
303
|
-
) -> MutationProperty[T, TState, P]: ...
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
@overload
|
|
307
|
-
def mutation(
|
|
308
|
-
fn: None = None,
|
|
309
|
-
*,
|
|
310
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
311
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
312
|
-
) -> Callable[
|
|
313
|
-
[Callable[Concatenate[TState, P], Awaitable[T]]], MutationProperty[T, TState, P]
|
|
314
|
-
]: ...
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def mutation(
|
|
318
|
-
fn: Callable[Concatenate[TState, P], Awaitable[T]] | None = None,
|
|
319
|
-
*,
|
|
320
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
321
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
322
|
-
):
|
|
323
|
-
def decorator(func: Callable[Concatenate[TState, P], Awaitable[T]], /):
|
|
324
|
-
sig = inspect.signature(func)
|
|
325
|
-
params = list(sig.parameters.values())
|
|
326
|
-
|
|
327
|
-
if len(params) == 0 or params[0].name != "self":
|
|
328
|
-
raise TypeError("@mutation method must have 'self' as first argument")
|
|
329
|
-
|
|
330
|
-
return MutationProperty(
|
|
331
|
-
func.__name__,
|
|
332
|
-
func,
|
|
333
|
-
on_success=on_success,
|
|
334
|
-
on_error=on_error,
|
|
170
|
+
interval=interval,
|
|
335
171
|
)
|
|
336
172
|
|
|
337
173
|
if fn:
|
pulse/helpers.py
CHANGED
|
@@ -212,31 +212,39 @@ def later(
|
|
|
212
212
|
"""
|
|
213
213
|
Schedule `fn(*args, **kwargs)` to run after `delay` seconds.
|
|
214
214
|
Works with sync or async functions. Returns a TimerHandle; call .cancel() to cancel.
|
|
215
|
+
|
|
216
|
+
The callback runs with no reactive scope to avoid accidentally capturing
|
|
217
|
+
reactive dependencies from the calling context. Other context vars (like
|
|
218
|
+
PulseContext) are preserved normally.
|
|
215
219
|
"""
|
|
220
|
+
|
|
221
|
+
from pulse.reactive import Untrack
|
|
222
|
+
|
|
216
223
|
loop = asyncio.get_running_loop()
|
|
217
224
|
|
|
218
225
|
def _run():
|
|
219
226
|
try:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
227
|
+
with Untrack():
|
|
228
|
+
res = fn(*args, **kwargs)
|
|
229
|
+
if asyncio.iscoroutine(res):
|
|
230
|
+
task = loop.create_task(res)
|
|
231
|
+
|
|
232
|
+
def _log_task_exception(t: asyncio.Task[Any]):
|
|
233
|
+
try:
|
|
234
|
+
t.result()
|
|
235
|
+
except asyncio.CancelledError:
|
|
236
|
+
# Normal cancellation path
|
|
237
|
+
pass
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
loop.call_exception_handler(
|
|
240
|
+
{
|
|
241
|
+
"message": "Unhandled exception in later() task",
|
|
242
|
+
"exception": exc,
|
|
243
|
+
"context": {"callback": fn},
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
task.add_done_callback(_log_task_exception)
|
|
240
248
|
except Exception as exc:
|
|
241
249
|
# Surface exceptions via the loop's exception handler and continue
|
|
242
250
|
loop.call_exception_handler(
|
|
@@ -273,9 +281,16 @@ def repeat(interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwa
|
|
|
273
281
|
For async functions, waits for completion before starting the next delay.
|
|
274
282
|
Returns a handle with .cancel() to stop future runs.
|
|
275
283
|
|
|
284
|
+
The callback runs with no reactive scope to avoid accidentally capturing
|
|
285
|
+
reactive dependencies from the calling context. Other context vars (like
|
|
286
|
+
PulseContext) are preserved normally.
|
|
287
|
+
|
|
276
288
|
Optional kwargs:
|
|
277
289
|
- immediate: bool = False # run once immediately before the first interval
|
|
278
290
|
"""
|
|
291
|
+
|
|
292
|
+
from pulse.reactive import Untrack
|
|
293
|
+
|
|
279
294
|
loop = asyncio.get_running_loop()
|
|
280
295
|
handle = RepeatHandle()
|
|
281
296
|
|
|
@@ -288,9 +303,10 @@ def repeat(interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwa
|
|
|
288
303
|
if handle.cancelled:
|
|
289
304
|
break
|
|
290
305
|
try:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
306
|
+
with Untrack():
|
|
307
|
+
result = fn(*args, **kwargs)
|
|
308
|
+
if asyncio.iscoroutine(result):
|
|
309
|
+
await result
|
|
294
310
|
except asyncio.CancelledError:
|
|
295
311
|
# Propagate to outer handler to finish cleanly
|
|
296
312
|
raise
|
pulse/hooks/core.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
from collections.abc import Callable, Mapping
|
|
4
4
|
from contextvars import ContextVar, Token
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any, Generic, TypeVar, override
|
|
6
|
+
from typing import Any, Generic, Literal, TypeVar, override
|
|
7
7
|
|
|
8
8
|
from pulse.helpers import Disposable, call_flexible
|
|
9
9
|
|
|
@@ -40,10 +40,7 @@ class HookMetadata:
|
|
|
40
40
|
class HookState(Disposable):
|
|
41
41
|
"""Base class returned by hook factories."""
|
|
42
42
|
|
|
43
|
-
render_cycle: int
|
|
44
|
-
|
|
45
|
-
def __init__(self) -> None:
|
|
46
|
-
self.render_cycle = 0
|
|
43
|
+
render_cycle: int = 0
|
|
47
44
|
|
|
48
45
|
def on_render_start(self, render_cycle: int) -> None:
|
|
49
46
|
self.render_cycle = render_cycle
|
|
@@ -177,13 +174,14 @@ class HookContext:
|
|
|
177
174
|
exc_type: type[BaseException] | None,
|
|
178
175
|
exc_val: BaseException | None,
|
|
179
176
|
exc_tb: Any,
|
|
180
|
-
):
|
|
177
|
+
) -> Literal[False]:
|
|
181
178
|
if self._token is not None:
|
|
182
179
|
HOOK_CONTEXT.reset(self._token)
|
|
183
180
|
self._token = None
|
|
184
181
|
|
|
185
182
|
for namespace in self.namespaces.values():
|
|
186
183
|
namespace.on_render_end(self.render_cycle)
|
|
184
|
+
return False
|
|
187
185
|
|
|
188
186
|
def namespace_for(self, hook: Hook[T]) -> HookNamespace[T]:
|
|
189
187
|
namespace = self.namespaces.get(hook.name)
|