pulse-framework 0.1.42__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 +12 -3
- pulse/decorators.py +8 -172
- pulse/helpers.py +39 -23
- 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/reactive.py +95 -20
- pulse/reactive_extensions.py +19 -7
- pulse/state.py +5 -0
- {pulse_framework-0.1.42.dist-info → pulse_framework-0.1.43.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.42.dist-info → pulse_framework-0.1.43.dist-info}/RECORD +17 -15
- pulse/queries/query_observer.py +0 -365
- {pulse_framework-0.1.42.dist-info → pulse_framework-0.1.43.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.42.dist-info → pulse_framework-0.1.43.dist-info}/entry_points.txt +0 -0
pulse/queries/query_observer.py
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import inspect
|
|
2
|
-
import time
|
|
3
|
-
from collections.abc import Awaitable, Callable
|
|
4
|
-
from typing import (
|
|
5
|
-
Any,
|
|
6
|
-
Generic,
|
|
7
|
-
TypeVar,
|
|
8
|
-
cast,
|
|
9
|
-
override,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
from pulse.context import PulseContext
|
|
13
|
-
from pulse.helpers import MISSING, Disposable
|
|
14
|
-
from pulse.queries.common import OnErrorFn, OnSuccessFn, bind_state
|
|
15
|
-
from pulse.queries.query import (
|
|
16
|
-
RETRY_DELAY_DEFAULT,
|
|
17
|
-
Query,
|
|
18
|
-
QueryFetchStatus,
|
|
19
|
-
QueryKey,
|
|
20
|
-
QueryStatus,
|
|
21
|
-
)
|
|
22
|
-
from pulse.reactive import Computed, Effect, Untrack
|
|
23
|
-
from pulse.state import InitializableProperty, State
|
|
24
|
-
|
|
25
|
-
T = TypeVar("T")
|
|
26
|
-
TState = TypeVar("TState", bound=State)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class QueryResult(Generic[T], Disposable):
|
|
30
|
-
"""
|
|
31
|
-
Thin wrapper around Query that adds callbacks, staleness tracking,
|
|
32
|
-
and observation lifecycle.
|
|
33
|
-
|
|
34
|
-
For keyed queries, uses a Computed to resolve the correct query based on the key.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
_query: Computed[Query[T]]
|
|
38
|
-
_stale_time: float
|
|
39
|
-
_gc_time: float
|
|
40
|
-
_keep_previous_data: bool
|
|
41
|
-
_on_success: Callable[[T], Awaitable[None] | None] | None
|
|
42
|
-
_on_error: Callable[[Exception], Awaitable[None] | None] | None
|
|
43
|
-
_callback_effect: Effect
|
|
44
|
-
_observe_effect: Effect
|
|
45
|
-
_data_computed: Computed[T | None]
|
|
46
|
-
_disposed_data: T | None
|
|
47
|
-
|
|
48
|
-
def __init__(
|
|
49
|
-
self,
|
|
50
|
-
query: Computed[Query[T]],
|
|
51
|
-
stale_time: float = 0.0,
|
|
52
|
-
gc_time: float = 300.0,
|
|
53
|
-
keep_previous_data: bool = False,
|
|
54
|
-
on_success: Callable[[T], Awaitable[None] | None] | None = None,
|
|
55
|
-
on_error: Callable[[Exception], Awaitable[None] | None] | None = None,
|
|
56
|
-
):
|
|
57
|
-
self._query = query
|
|
58
|
-
self._stale_time = stale_time
|
|
59
|
-
self._gc_time = gc_time
|
|
60
|
-
self._keep_previous_data = keep_previous_data
|
|
61
|
-
self._on_success = on_success
|
|
62
|
-
self._on_error = on_error
|
|
63
|
-
self._disposed_data = None
|
|
64
|
-
|
|
65
|
-
def observe_effect():
|
|
66
|
-
query = self._query()
|
|
67
|
-
with Untrack():
|
|
68
|
-
# This may create an effect, which should live independently of our observe effect
|
|
69
|
-
query.observe(self)
|
|
70
|
-
|
|
71
|
-
# If stale or loading, schedule refetch
|
|
72
|
-
if self.is_stale():
|
|
73
|
-
query.invalidate()
|
|
74
|
-
|
|
75
|
-
# Return cleanup function that captures the observer
|
|
76
|
-
return lambda: query.unobserve(self)
|
|
77
|
-
|
|
78
|
-
self._observe_effect = Effect(
|
|
79
|
-
observe_effect,
|
|
80
|
-
name=f"query_observe({self._query().key})",
|
|
81
|
-
immediate=True,
|
|
82
|
-
)
|
|
83
|
-
self._data_computed = Computed(
|
|
84
|
-
self._data_computed_fn, name=f"query_data({self._query().key})"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def status(self) -> QueryStatus:
|
|
89
|
-
return self._query().status()
|
|
90
|
-
|
|
91
|
-
@property
|
|
92
|
-
def fetch_status(self) -> QueryFetchStatus:
|
|
93
|
-
return self._query().fetch_status()
|
|
94
|
-
|
|
95
|
-
# Forward property reads to the query's signals (with automatic reactive tracking)
|
|
96
|
-
@property
|
|
97
|
-
def is_loading(self) -> bool:
|
|
98
|
-
return self.status == "loading"
|
|
99
|
-
|
|
100
|
-
@property
|
|
101
|
-
def is_success(self) -> bool:
|
|
102
|
-
return self.status == "success"
|
|
103
|
-
|
|
104
|
-
@property
|
|
105
|
-
def is_error(self) -> bool:
|
|
106
|
-
return self.status == "error"
|
|
107
|
-
|
|
108
|
-
@property
|
|
109
|
-
def is_fetching(self) -> bool:
|
|
110
|
-
return self.fetch_status == "fetching"
|
|
111
|
-
|
|
112
|
-
@property
|
|
113
|
-
def error(self) -> Exception | None:
|
|
114
|
-
return self._query().error.read()
|
|
115
|
-
|
|
116
|
-
def _data_computed_fn(self, prev: T | None) -> T | None:
|
|
117
|
-
query = self._query()
|
|
118
|
-
if self._keep_previous_data and query.status() != "success":
|
|
119
|
-
return prev
|
|
120
|
-
return query.data()
|
|
121
|
-
|
|
122
|
-
@property
|
|
123
|
-
def data(self) -> T | None:
|
|
124
|
-
return self._data_computed()
|
|
125
|
-
|
|
126
|
-
@property
|
|
127
|
-
def has_loaded(self) -> bool:
|
|
128
|
-
return self.status in ("success", "error")
|
|
129
|
-
|
|
130
|
-
def is_stale(self) -> bool:
|
|
131
|
-
"""Check if the query data is stale based on stale_time."""
|
|
132
|
-
query = self._query()
|
|
133
|
-
return (time.time() - query.last_updated.read()) > self._stale_time
|
|
134
|
-
|
|
135
|
-
async def refetch(self, cancel_refetch: bool = True) -> T:
|
|
136
|
-
"""Refetch the query data."""
|
|
137
|
-
return await self._query().refetch(cancel_refetch=cancel_refetch)
|
|
138
|
-
|
|
139
|
-
async def wait(self) -> T:
|
|
140
|
-
return await self._query().wait()
|
|
141
|
-
|
|
142
|
-
def invalidate(self):
|
|
143
|
-
"""Mark the query as stale and refetch if there are observers."""
|
|
144
|
-
query = self._query()
|
|
145
|
-
query.invalidate()
|
|
146
|
-
|
|
147
|
-
def set_data(self, data: T):
|
|
148
|
-
"""Optimistically set data without changing loading/error state."""
|
|
149
|
-
query = self._query()
|
|
150
|
-
query.data.write(data)
|
|
151
|
-
|
|
152
|
-
@override
|
|
153
|
-
def dispose(self):
|
|
154
|
-
"""Clean up the result and its observe effect."""
|
|
155
|
-
self._observe_effect.dispose()
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
class QueryProperty(Generic[T, TState], InitializableProperty):
|
|
159
|
-
"""
|
|
160
|
-
Descriptor for state-bound queries.
|
|
161
|
-
|
|
162
|
-
Usage:
|
|
163
|
-
class S(ps.State):
|
|
164
|
-
@ps.query()
|
|
165
|
-
async def user(self) -> User: ...
|
|
166
|
-
|
|
167
|
-
@user.key
|
|
168
|
-
def _user_key(self):
|
|
169
|
-
return ("user", self.user_id)
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
name: str
|
|
173
|
-
_fetch_fn: "Callable[[TState], Awaitable[T]]"
|
|
174
|
-
_keep_alive: bool
|
|
175
|
-
_keep_previous_data: bool
|
|
176
|
-
_stale_time: float
|
|
177
|
-
_gc_time: float
|
|
178
|
-
_retries: int
|
|
179
|
-
_retry_delay: float
|
|
180
|
-
_initial_data: T | Callable[[TState], T] | None
|
|
181
|
-
_key_fn: Callable[[TState], QueryKey] | None
|
|
182
|
-
# Not using OnSuccessFn and OnErrorFn since unions of callables are not well
|
|
183
|
-
# supported in the type system. We just need to be careful to use
|
|
184
|
-
# call_flexible to invoke these functions.
|
|
185
|
-
_on_success_fn: Callable[[TState, T], Any] | None
|
|
186
|
-
_on_error_fn: Callable[[TState, Exception], Any] | None
|
|
187
|
-
_priv_result: str
|
|
188
|
-
|
|
189
|
-
def __init__(
|
|
190
|
-
self,
|
|
191
|
-
name: str,
|
|
192
|
-
fetch_fn: "Callable[[TState], Awaitable[T]]",
|
|
193
|
-
keep_previous_data: bool = False,
|
|
194
|
-
stale_time: float = 0.0,
|
|
195
|
-
gc_time: float = 300.0,
|
|
196
|
-
retries: int = 3,
|
|
197
|
-
retry_delay: float = RETRY_DELAY_DEFAULT,
|
|
198
|
-
initial: T | Callable[[TState], T] | None = MISSING,
|
|
199
|
-
key: Callable[[TState], QueryKey] | None = None,
|
|
200
|
-
on_success: OnSuccessFn[TState, T] | None = None,
|
|
201
|
-
on_error: OnErrorFn[TState] | None = None,
|
|
202
|
-
):
|
|
203
|
-
self.name = name
|
|
204
|
-
self._fetch_fn = fetch_fn
|
|
205
|
-
self._key_fn = None
|
|
206
|
-
self._on_success_fn = on_success # pyright: ignore[reportAttributeAccessIssue]
|
|
207
|
-
self._on_error_fn = on_error # pyright: ignore[reportAttributeAccessIssue]
|
|
208
|
-
self._keep_previous_data = keep_previous_data
|
|
209
|
-
self._stale_time = stale_time
|
|
210
|
-
self._gc_time = gc_time
|
|
211
|
-
self._retries = retries
|
|
212
|
-
self._retry_delay = retry_delay
|
|
213
|
-
self._initial_data = initial
|
|
214
|
-
self._priv_result = f"__query_{name}"
|
|
215
|
-
|
|
216
|
-
# Decorator to attach a key function
|
|
217
|
-
def key(self, fn: Callable[[TState], QueryKey]):
|
|
218
|
-
if self._key_fn is not None:
|
|
219
|
-
raise RuntimeError(
|
|
220
|
-
f"Duplicate key() decorator for query '{self.name}'. Only one is allowed."
|
|
221
|
-
)
|
|
222
|
-
self._key_fn = fn
|
|
223
|
-
return fn
|
|
224
|
-
|
|
225
|
-
# Decorator to attach a function providing initial data
|
|
226
|
-
def initial_data(self, fn: Callable[[TState], T]):
|
|
227
|
-
if self._initial_data is not MISSING:
|
|
228
|
-
raise RuntimeError(
|
|
229
|
-
f"Duplicate initial_data() decorator for query '{self.name}'. Only one is allowed."
|
|
230
|
-
)
|
|
231
|
-
self._initial_data = fn
|
|
232
|
-
return fn
|
|
233
|
-
|
|
234
|
-
# Decorator to attach an on-success handler (sync or async)
|
|
235
|
-
def on_success(self, fn: OnSuccessFn[TState, T]):
|
|
236
|
-
if self._on_success_fn is not None:
|
|
237
|
-
raise RuntimeError(
|
|
238
|
-
f"Duplicate on_success() decorator for query '{self.name}'. Only one is allowed."
|
|
239
|
-
)
|
|
240
|
-
self._on_success_fn = fn # pyright: ignore[reportAttributeAccessIssue]
|
|
241
|
-
return fn
|
|
242
|
-
|
|
243
|
-
# Decorator to attach an on-error handler (sync or async)
|
|
244
|
-
def on_error(self, fn: OnErrorFn[TState]):
|
|
245
|
-
if self._on_error_fn is not None:
|
|
246
|
-
raise RuntimeError(
|
|
247
|
-
f"Duplicate on_error() decorator for query '{self.name}'. Only one is allowed."
|
|
248
|
-
)
|
|
249
|
-
self._on_error_fn = fn # pyright: ignore[reportAttributeAccessIssue]
|
|
250
|
-
return fn
|
|
251
|
-
|
|
252
|
-
@override
|
|
253
|
-
def initialize(self, state: Any, name: str) -> QueryResult[T]:
|
|
254
|
-
# Return cached query instance if present
|
|
255
|
-
result: QueryResult[T] | None = getattr(state, self._priv_result, None)
|
|
256
|
-
if result:
|
|
257
|
-
# Don't re-initialize, just return the cached instance
|
|
258
|
-
return result
|
|
259
|
-
|
|
260
|
-
# Bind methods to this instance
|
|
261
|
-
fetch_fn = bind_state(state, self._fetch_fn)
|
|
262
|
-
initial_data = cast(
|
|
263
|
-
T | None,
|
|
264
|
-
(
|
|
265
|
-
self._initial_data(state)
|
|
266
|
-
if callable(self._initial_data)
|
|
267
|
-
and len(inspect.signature(self._initial_data).parameters) == 1
|
|
268
|
-
else self._initial_data
|
|
269
|
-
),
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if self._key_fn:
|
|
273
|
-
# Keyed query: use session-wide QueryStore
|
|
274
|
-
query = self._resolve_keyed(state, fetch_fn, initial_data)
|
|
275
|
-
else:
|
|
276
|
-
# Unkeyed query: create private Query
|
|
277
|
-
query = self._resolve_unkeyed(fetch_fn, initial_data)
|
|
278
|
-
|
|
279
|
-
# Wrap query in QueryResult
|
|
280
|
-
result = QueryResult[T](
|
|
281
|
-
query=query,
|
|
282
|
-
stale_time=self._stale_time,
|
|
283
|
-
keep_previous_data=self._keep_previous_data,
|
|
284
|
-
gc_time=self._gc_time,
|
|
285
|
-
on_success=bind_state(state, self._on_success_fn)
|
|
286
|
-
if self._on_success_fn
|
|
287
|
-
else None,
|
|
288
|
-
on_error=bind_state(state, self._on_error_fn)
|
|
289
|
-
if self._on_error_fn
|
|
290
|
-
else None,
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
# Store result on the instance
|
|
294
|
-
setattr(state, self._priv_result, result)
|
|
295
|
-
return result
|
|
296
|
-
|
|
297
|
-
def _resolve_keyed(
|
|
298
|
-
self,
|
|
299
|
-
state: TState,
|
|
300
|
-
fetch_fn: Callable[[], Awaitable[T]],
|
|
301
|
-
initial_data: T | None,
|
|
302
|
-
) -> Computed[Query[T]]:
|
|
303
|
-
"""Create or get a keyed query from the session store using a Computed."""
|
|
304
|
-
assert self._key_fn is not None
|
|
305
|
-
|
|
306
|
-
key_computed = Computed(
|
|
307
|
-
bind_state(state, self._key_fn), name=f"query.key.{self.name}"
|
|
308
|
-
)
|
|
309
|
-
render = PulseContext.get().render
|
|
310
|
-
if render is None:
|
|
311
|
-
raise RuntimeError("No render session available")
|
|
312
|
-
store = render.query_store
|
|
313
|
-
|
|
314
|
-
def query() -> Query[T]:
|
|
315
|
-
key = key_computed()
|
|
316
|
-
return store.ensure(
|
|
317
|
-
key,
|
|
318
|
-
fetch_fn,
|
|
319
|
-
initial_data,
|
|
320
|
-
gc_time=self._gc_time,
|
|
321
|
-
retries=self._retries,
|
|
322
|
-
retry_delay=self._retry_delay,
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
return Computed(query, name=f"query.{self.name}")
|
|
326
|
-
|
|
327
|
-
def _resolve_unkeyed(
|
|
328
|
-
self,
|
|
329
|
-
fetch_fn: Callable[[], Awaitable[T]],
|
|
330
|
-
initial_data: T | None,
|
|
331
|
-
) -> Computed[Query[T]]:
|
|
332
|
-
"""Create a private unkeyed query."""
|
|
333
|
-
query = Query[T](
|
|
334
|
-
key=None,
|
|
335
|
-
fn=fetch_fn,
|
|
336
|
-
initial_data=initial_data,
|
|
337
|
-
gc_time=self._gc_time,
|
|
338
|
-
retries=self._retries,
|
|
339
|
-
retry_delay=self._retry_delay,
|
|
340
|
-
)
|
|
341
|
-
return Computed(lambda: query, name=f"query.{self.name}")
|
|
342
|
-
|
|
343
|
-
def __get__(self, obj: Any, objtype: Any = None) -> QueryResult[T]:
|
|
344
|
-
if obj is None:
|
|
345
|
-
return self # pyright: ignore[reportReturnType]
|
|
346
|
-
return self.initialize(obj, self.name)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
class QueryResultWithInitial(QueryResult[T]):
|
|
350
|
-
@property
|
|
351
|
-
@override
|
|
352
|
-
def data(self) -> T:
|
|
353
|
-
return cast(T, super().data)
|
|
354
|
-
|
|
355
|
-
@property
|
|
356
|
-
@override
|
|
357
|
-
def has_loaded(self) -> bool: # mirror base for completeness
|
|
358
|
-
return super().has_loaded
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
class QueryPropertyWithInitial(QueryProperty[T, TState]):
|
|
362
|
-
@override
|
|
363
|
-
def __get__(self, obj: Any, objtype: Any = None) -> QueryResultWithInitial[T]:
|
|
364
|
-
# Reuse base initialization but narrow the return type for type-checkers
|
|
365
|
-
return cast(QueryResultWithInitial[T], super().__get__(obj, objtype))
|
|
File without changes
|
|
File without changes
|