pulse-framework 0.1.42__tar.gz → 0.1.43__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.42 → pulse_framework-0.1.43}/PKG-INFO +1 -1
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/pyproject.toml +1 -1
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/__init__.py +12 -3
- pulse_framework-0.1.43/src/pulse/decorators.py +175 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/helpers.py +39 -23
- pulse_framework-0.1.43/src/pulse/queries/client.py +462 -0
- pulse_framework-0.1.43/src/pulse/queries/common.py +52 -0
- pulse_framework-0.1.43/src/pulse/queries/effect.py +39 -0
- pulse_framework-0.1.43/src/pulse/queries/infinite_query.py +1157 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/queries/mutation.py +47 -0
- pulse_framework-0.1.43/src/pulse/queries/query.py +777 -0
- pulse_framework-0.1.43/src/pulse/queries/store.py +123 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/reactive.py +95 -20
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/reactive_extensions.py +19 -7
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/state.py +5 -0
- pulse_framework-0.1.42/src/pulse/decorators.py +0 -339
- pulse_framework-0.1.42/src/pulse/queries/common.py +0 -24
- pulse_framework-0.1.42/src/pulse/queries/query.py +0 -270
- pulse_framework-0.1.42/src/pulse/queries/query_observer.py +0 -365
- pulse_framework-0.1.42/src/pulse/queries/store.py +0 -60
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/README.md +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/app.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/imports.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/css.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/hooks/states.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/elements.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/events.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/props.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/svg.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/tags.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/html/tags.pyi +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/render_session.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/vdom.py +0 -0
- {pulse_framework-0.1.42 → pulse_framework-0.1.43}/src/pulse/version.py +0 -0
|
@@ -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
|
|
@@ -1349,7 +1347,18 @@ from pulse.middleware import (
|
|
|
1349
1347
|
|
|
1350
1348
|
# Plugin
|
|
1351
1349
|
from pulse.plugin import Plugin as Plugin
|
|
1352
|
-
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
|
|
1353
1362
|
|
|
1354
1363
|
# React component registry
|
|
1355
1364
|
from pulse.react_component import (
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Separate file from reactive.py due to needing to import from state too
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any, ParamSpec, Protocol, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
from pulse.reactive import (
|
|
8
|
+
AsyncEffect,
|
|
9
|
+
AsyncEffectFn,
|
|
10
|
+
Computed,
|
|
11
|
+
Effect,
|
|
12
|
+
EffectCleanup,
|
|
13
|
+
EffectFn,
|
|
14
|
+
Signal,
|
|
15
|
+
)
|
|
16
|
+
from pulse.state import ComputedProperty, State, StateEffect
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
TState = TypeVar("TState", bound=State)
|
|
20
|
+
P = ParamSpec("P")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# -> @ps.computed The chalenge is:
|
|
24
|
+
# - We want to turn regular functions with no arguments into a Computed object
|
|
25
|
+
# - We want to turn state methods into a ComputedProperty (which wraps a
|
|
26
|
+
# Computed, but gives it access to the State object).
|
|
27
|
+
@overload
|
|
28
|
+
def computed(fn: Callable[[], T], *, name: str | None = None) -> Computed[T]: ...
|
|
29
|
+
@overload
|
|
30
|
+
def computed(
|
|
31
|
+
fn: Callable[[TState], T], *, name: str | None = None
|
|
32
|
+
) -> ComputedProperty[T]: ...
|
|
33
|
+
@overload
|
|
34
|
+
def computed(
|
|
35
|
+
fn: None = None, *, name: str | None = None
|
|
36
|
+
) -> Callable[[Callable[[], T]], Computed[T]]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def computed(fn: Callable[..., Any] | None = None, *, name: str | None = None):
|
|
40
|
+
# The type checker is not happy if I don't specify the `/` here.
|
|
41
|
+
def decorator(fn: Callable[..., Any], /):
|
|
42
|
+
sig = inspect.signature(fn)
|
|
43
|
+
params = list(sig.parameters.values())
|
|
44
|
+
# Check if it's a method with exactly one argument called 'self'
|
|
45
|
+
if len(params) == 1 and params[0].name == "self":
|
|
46
|
+
return ComputedProperty(fn.__name__, fn)
|
|
47
|
+
# If it has any arguments at all, it's not allowed (except for 'self')
|
|
48
|
+
if len(params) > 0:
|
|
49
|
+
raise TypeError(
|
|
50
|
+
f"@computed: Function '{fn.__name__}' must take no arguments or a single 'self' argument"
|
|
51
|
+
)
|
|
52
|
+
return Computed(fn, name=name or fn.__name__)
|
|
53
|
+
|
|
54
|
+
if fn is not None:
|
|
55
|
+
return decorator(fn)
|
|
56
|
+
else:
|
|
57
|
+
return decorator
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
StateEffectFn = Callable[[TState], EffectCleanup | None]
|
|
61
|
+
AsyncStateEffectFn = Callable[[TState], Awaitable[EffectCleanup | None]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EffectBuilder(Protocol):
|
|
65
|
+
@overload
|
|
66
|
+
def __call__(self, fn: EffectFn | StateEffectFn[Any]) -> Effect: ...
|
|
67
|
+
@overload
|
|
68
|
+
def __call__(self, fn: AsyncEffectFn | AsyncStateEffectFn[Any]) -> AsyncEffect: ...
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
fn: EffectFn | StateEffectFn[Any] | AsyncEffectFn | AsyncStateEffectFn[Any],
|
|
72
|
+
) -> Effect | AsyncEffect: ...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@overload
|
|
76
|
+
def effect(
|
|
77
|
+
fn: EffectFn,
|
|
78
|
+
*,
|
|
79
|
+
name: str | None = None,
|
|
80
|
+
immediate: bool = False,
|
|
81
|
+
lazy: bool = False,
|
|
82
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
83
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
84
|
+
interval: float | None = None,
|
|
85
|
+
) -> Effect: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@overload
|
|
89
|
+
def effect(
|
|
90
|
+
fn: AsyncEffectFn,
|
|
91
|
+
*,
|
|
92
|
+
name: str | None = None,
|
|
93
|
+
immediate: bool = False,
|
|
94
|
+
lazy: bool = False,
|
|
95
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
96
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
97
|
+
interval: float | None = None,
|
|
98
|
+
) -> AsyncEffect: ...
|
|
99
|
+
# In practice this overload returns a StateEffect, but it gets converted into an
|
|
100
|
+
# Effect at state instantiation.
|
|
101
|
+
@overload
|
|
102
|
+
def effect(fn: StateEffectFn[Any]) -> Effect: ...
|
|
103
|
+
@overload
|
|
104
|
+
def effect(fn: AsyncStateEffectFn[Any]) -> AsyncEffect: ...
|
|
105
|
+
@overload
|
|
106
|
+
def effect(
|
|
107
|
+
fn: None = None,
|
|
108
|
+
*,
|
|
109
|
+
name: str | None = None,
|
|
110
|
+
immediate: bool = False,
|
|
111
|
+
lazy: bool = False,
|
|
112
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
113
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
114
|
+
interval: float | None = None,
|
|
115
|
+
) -> EffectBuilder: ...
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def effect(
|
|
119
|
+
fn: Callable[..., Any] | None = None,
|
|
120
|
+
*,
|
|
121
|
+
name: str | None = None,
|
|
122
|
+
immediate: bool = False,
|
|
123
|
+
lazy: bool = False,
|
|
124
|
+
on_error: Callable[[Exception], None] | None = None,
|
|
125
|
+
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
126
|
+
interval: float | None = None,
|
|
127
|
+
):
|
|
128
|
+
# The type checker is not happy if I don't specify the `/` here.
|
|
129
|
+
def decorator(func: Callable[..., Any], /):
|
|
130
|
+
sig = inspect.signature(func)
|
|
131
|
+
params = list(sig.parameters.values())
|
|
132
|
+
|
|
133
|
+
# Disallow intermediate + async
|
|
134
|
+
if immediate and inspect.iscoroutinefunction(func):
|
|
135
|
+
raise ValueError("Async effects cannot have immediate=True")
|
|
136
|
+
|
|
137
|
+
if len(params) == 1 and params[0].name == "self":
|
|
138
|
+
return StateEffect(
|
|
139
|
+
func,
|
|
140
|
+
name=name,
|
|
141
|
+
immediate=immediate,
|
|
142
|
+
lazy=lazy,
|
|
143
|
+
on_error=on_error,
|
|
144
|
+
deps=deps,
|
|
145
|
+
interval=interval,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if len(params) > 0:
|
|
149
|
+
raise TypeError(
|
|
150
|
+
f"@effect: Function '{func.__name__}' must take no arguments or a single 'self' argument"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# This is a standalone effect function. Choose subclass based on async-ness
|
|
154
|
+
if inspect.iscoroutinefunction(func):
|
|
155
|
+
return AsyncEffect(
|
|
156
|
+
func, # type: ignore[arg-type]
|
|
157
|
+
name=name or func.__name__,
|
|
158
|
+
lazy=lazy,
|
|
159
|
+
on_error=on_error,
|
|
160
|
+
deps=deps,
|
|
161
|
+
interval=interval,
|
|
162
|
+
)
|
|
163
|
+
return Effect(
|
|
164
|
+
func, # type: ignore[arg-type]
|
|
165
|
+
name=name or func.__name__,
|
|
166
|
+
immediate=immediate,
|
|
167
|
+
lazy=lazy,
|
|
168
|
+
on_error=on_error,
|
|
169
|
+
deps=deps,
|
|
170
|
+
interval=interval,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if fn:
|
|
174
|
+
return decorator(fn)
|
|
175
|
+
return decorator
|
|
@@ -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
|