pulse-framework 0.1.70__py3-none-any.whl → 0.1.72__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 +7 -0
- pulse/app.py +27 -24
- pulse/cli/cmd.py +1 -1
- pulse/cli/folder_lock.py +25 -6
- pulse/codegen/templates/layout.py +3 -1
- pulse/hooks/effects.py +15 -1
- pulse/proxy.py +719 -185
- pulse/queries/client.py +64 -56
- pulse/queries/common.py +66 -3
- pulse/queries/infinite_query.py +59 -18
- pulse/queries/query.py +30 -11
- pulse/queries/store.py +13 -11
- pulse/render_session.py +5 -2
- {pulse_framework-0.1.70.dist-info → pulse_framework-0.1.72.dist-info}/METADATA +3 -3
- {pulse_framework-0.1.70.dist-info → pulse_framework-0.1.72.dist-info}/RECORD +17 -17
- {pulse_framework-0.1.70.dist-info → pulse_framework-0.1.72.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.70.dist-info → pulse_framework-0.1.72.dist-info}/entry_points.txt +0 -0
pulse/queries/client.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, TypeVar, overload
|
|
|
4
4
|
|
|
5
5
|
from pulse.context import PulseContext
|
|
6
6
|
from pulse.helpers import MISSING
|
|
7
|
-
from pulse.queries.common import ActionResult, QueryKey
|
|
7
|
+
from pulse.queries.common import ActionResult, Key, QueryKey, QueryKeys, normalize_key
|
|
8
8
|
from pulse.queries.infinite_query import InfiniteQuery, Page
|
|
9
9
|
from pulse.queries.query import KeyedQuery
|
|
10
10
|
from pulse.queries.store import QueryStore
|
|
@@ -13,34 +13,32 @@ T = TypeVar("T")
|
|
|
13
13
|
|
|
14
14
|
# Query filter types
|
|
15
15
|
QueryFilter = (
|
|
16
|
-
QueryKey # exact key match
|
|
17
|
-
|
|
|
18
|
-
| Callable[[
|
|
16
|
+
QueryKey # exact key match (tuple or list)
|
|
17
|
+
| QueryKeys # explicit set of keys
|
|
18
|
+
| Callable[[Key], bool] # predicate function
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _normalize_filter(
|
|
23
23
|
filter: QueryFilter | None,
|
|
24
|
-
) -> Callable[[
|
|
25
|
-
"""
|
|
24
|
+
) -> tuple[Key | None, Callable[[Key], bool] | None]:
|
|
25
|
+
"""Return normalized exact key (if any) and a predicate for filtering."""
|
|
26
26
|
if filter is None:
|
|
27
|
-
return None
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _prefix_filter(prefix: tuple[Any, ...]) -> Callable[[QueryKey], bool]:
|
|
27
|
+
return None, None
|
|
28
|
+
if callable(filter):
|
|
29
|
+
return None, filter
|
|
30
|
+
if isinstance(filter, QueryKeys):
|
|
31
|
+
key_set = set(filter.keys)
|
|
32
|
+
return None, lambda k: k in key_set
|
|
33
|
+
exact_key = normalize_key(filter)
|
|
34
|
+
return exact_key, lambda k: k == exact_key
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _prefix_filter(prefix: QueryKey) -> Callable[[Key], bool]:
|
|
41
38
|
"""Create a predicate that matches keys starting with the given prefix."""
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
normalized = normalize_key(prefix)
|
|
40
|
+
prefix_len = len(normalized)
|
|
41
|
+
return lambda k: len(k) >= prefix_len and k[:prefix_len] == normalized
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
class QueryClient:
|
|
@@ -120,7 +118,7 @@ class QueryClient:
|
|
|
120
118
|
Get all queries matching the filter.
|
|
121
119
|
|
|
122
120
|
Args:
|
|
123
|
-
filter: Optional filter -
|
|
121
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
124
122
|
If None, returns all queries.
|
|
125
123
|
include_infinite: Whether to include infinite queries (default True).
|
|
126
124
|
|
|
@@ -128,9 +126,16 @@ class QueryClient:
|
|
|
128
126
|
List of matching Query or InfiniteQuery instances.
|
|
129
127
|
"""
|
|
130
128
|
store = self._get_store()
|
|
131
|
-
predicate = _normalize_filter(filter)
|
|
129
|
+
exact_key, predicate = _normalize_filter(filter)
|
|
132
130
|
results: list[KeyedQuery[Any] | InfiniteQuery[Any, Any]] = []
|
|
133
131
|
|
|
132
|
+
if exact_key is not None:
|
|
133
|
+
if include_infinite:
|
|
134
|
+
entry = store.get_any(exact_key)
|
|
135
|
+
else:
|
|
136
|
+
entry = store.get(exact_key)
|
|
137
|
+
return [entry] if entry is not None else []
|
|
138
|
+
|
|
134
139
|
for key, entry in store.items():
|
|
135
140
|
if predicate is not None and not predicate(key):
|
|
136
141
|
continue
|
|
@@ -144,16 +149,20 @@ class QueryClient:
|
|
|
144
149
|
"""Get all regular queries matching the filter.
|
|
145
150
|
|
|
146
151
|
Args:
|
|
147
|
-
filter: Optional filter - exact key,
|
|
152
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
148
153
|
If None, returns all regular queries.
|
|
149
154
|
|
|
150
155
|
Returns:
|
|
151
156
|
List of matching KeyedQuery instances (excludes infinite queries).
|
|
152
157
|
"""
|
|
153
158
|
store = self._get_store()
|
|
154
|
-
predicate = _normalize_filter(filter)
|
|
159
|
+
exact_key, predicate = _normalize_filter(filter)
|
|
155
160
|
results: list[KeyedQuery[Any]] = []
|
|
156
161
|
|
|
162
|
+
if exact_key is not None:
|
|
163
|
+
entry = store.get(exact_key)
|
|
164
|
+
return [entry] if entry is not None else []
|
|
165
|
+
|
|
157
166
|
for key, entry in store.items():
|
|
158
167
|
if isinstance(entry, InfiniteQuery):
|
|
159
168
|
continue
|
|
@@ -169,16 +178,20 @@ class QueryClient:
|
|
|
169
178
|
"""Get all infinite queries matching the filter.
|
|
170
179
|
|
|
171
180
|
Args:
|
|
172
|
-
filter: Optional filter - exact key,
|
|
181
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
173
182
|
If None, returns all infinite queries.
|
|
174
183
|
|
|
175
184
|
Returns:
|
|
176
185
|
List of matching InfiniteQuery instances.
|
|
177
186
|
"""
|
|
178
187
|
store = self._get_store()
|
|
179
|
-
predicate = _normalize_filter(filter)
|
|
188
|
+
exact_key, predicate = _normalize_filter(filter)
|
|
180
189
|
results: list[InfiniteQuery[Any, Any]] = []
|
|
181
190
|
|
|
191
|
+
if exact_key is not None:
|
|
192
|
+
entry = store.get_infinite(exact_key)
|
|
193
|
+
return [entry] if entry is not None else []
|
|
194
|
+
|
|
182
195
|
for key, entry in store.items():
|
|
183
196
|
if not isinstance(entry, InfiniteQuery):
|
|
184
197
|
continue
|
|
@@ -239,7 +252,7 @@ class QueryClient:
|
|
|
239
252
|
@overload
|
|
240
253
|
def set_data(
|
|
241
254
|
self,
|
|
242
|
-
key_or_filter:
|
|
255
|
+
key_or_filter: QueryKeys | Callable[[Key], bool],
|
|
243
256
|
data: Callable[[Any], Any],
|
|
244
257
|
*,
|
|
245
258
|
updated_at: float | dt.datetime | None = None,
|
|
@@ -247,7 +260,7 @@ class QueryClient:
|
|
|
247
260
|
|
|
248
261
|
def set_data(
|
|
249
262
|
self,
|
|
250
|
-
key_or_filter: QueryKey |
|
|
263
|
+
key_or_filter: QueryKey | QueryKeys | Callable[[Key], bool],
|
|
251
264
|
data: Any | Callable[[Any], Any],
|
|
252
265
|
*,
|
|
253
266
|
updated_at: float | dt.datetime | None = None,
|
|
@@ -266,16 +279,15 @@ class QueryClient:
|
|
|
266
279
|
Returns:
|
|
267
280
|
bool if exact key, int count if filter.
|
|
268
281
|
"""
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
query = self.get(
|
|
282
|
+
exact_key, predicate = _normalize_filter(key_or_filter)
|
|
283
|
+
if exact_key is not None:
|
|
284
|
+
query = self.get(exact_key)
|
|
272
285
|
if query is None:
|
|
273
286
|
return False
|
|
274
287
|
query.set_data(data, updated_at=updated_at)
|
|
275
288
|
return True
|
|
276
289
|
|
|
277
|
-
|
|
278
|
-
queries = self.get_queries(key_or_filter)
|
|
290
|
+
queries = self.get_queries(predicate)
|
|
279
291
|
for q in queries:
|
|
280
292
|
q.set_data(data, updated_at=updated_at)
|
|
281
293
|
return len(queries)
|
|
@@ -319,17 +331,14 @@ class QueryClient:
|
|
|
319
331
|
@overload
|
|
320
332
|
def invalidate(
|
|
321
333
|
self,
|
|
322
|
-
key_or_filter:
|
|
334
|
+
key_or_filter: QueryKeys | Callable[[Key], bool] | None = None,
|
|
323
335
|
*,
|
|
324
336
|
cancel_refetch: bool = False,
|
|
325
337
|
) -> int: ...
|
|
326
338
|
|
|
327
339
|
def invalidate(
|
|
328
340
|
self,
|
|
329
|
-
key_or_filter: QueryKey
|
|
330
|
-
| list[QueryKey]
|
|
331
|
-
| Callable[[QueryKey], bool]
|
|
332
|
-
| None = None,
|
|
341
|
+
key_or_filter: QueryKey | QueryKeys | Callable[[Key], bool] | None = None,
|
|
333
342
|
*,
|
|
334
343
|
cancel_refetch: bool = False,
|
|
335
344
|
) -> bool | int:
|
|
@@ -346,20 +355,19 @@ class QueryClient:
|
|
|
346
355
|
Returns:
|
|
347
356
|
bool if exact key, int count if filter/None.
|
|
348
357
|
"""
|
|
349
|
-
|
|
350
|
-
if
|
|
351
|
-
query = self.get(
|
|
358
|
+
exact_key, predicate = _normalize_filter(key_or_filter)
|
|
359
|
+
if exact_key is not None:
|
|
360
|
+
query = self.get(exact_key)
|
|
352
361
|
if query is not None:
|
|
353
362
|
query.invalidate(cancel_refetch=cancel_refetch)
|
|
354
363
|
return True
|
|
355
|
-
inf_query = self.get_infinite(
|
|
364
|
+
inf_query = self.get_infinite(exact_key)
|
|
356
365
|
if inf_query is not None:
|
|
357
366
|
inf_query.invalidate(cancel_fetch=cancel_refetch)
|
|
358
367
|
return True
|
|
359
368
|
return False
|
|
360
369
|
|
|
361
|
-
|
|
362
|
-
queries = self.get_all(key_or_filter)
|
|
370
|
+
queries = self.get_all(predicate)
|
|
363
371
|
for q in queries:
|
|
364
372
|
if isinstance(q, InfiniteQuery):
|
|
365
373
|
q.invalidate(cancel_fetch=cancel_refetch)
|
|
@@ -369,14 +377,14 @@ class QueryClient:
|
|
|
369
377
|
|
|
370
378
|
def invalidate_prefix(
|
|
371
379
|
self,
|
|
372
|
-
prefix:
|
|
380
|
+
prefix: QueryKey,
|
|
373
381
|
*,
|
|
374
382
|
cancel_refetch: bool = False,
|
|
375
383
|
) -> int:
|
|
376
384
|
"""Invalidate all queries whose keys start with the given prefix.
|
|
377
385
|
|
|
378
386
|
Args:
|
|
379
|
-
prefix:
|
|
387
|
+
prefix: Key prefix to match against query keys.
|
|
380
388
|
cancel_refetch: Cancel in-flight requests before refetch.
|
|
381
389
|
|
|
382
390
|
Returns:
|
|
@@ -429,7 +437,7 @@ class QueryClient:
|
|
|
429
437
|
"""Refetch all queries matching the filter.
|
|
430
438
|
|
|
431
439
|
Args:
|
|
432
|
-
filter: Optional filter - exact key,
|
|
440
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
433
441
|
If None, refetches all queries.
|
|
434
442
|
cancel_refetch: Cancel in-flight requests before refetching.
|
|
435
443
|
|
|
@@ -450,14 +458,14 @@ class QueryClient:
|
|
|
450
458
|
|
|
451
459
|
async def refetch_prefix(
|
|
452
460
|
self,
|
|
453
|
-
prefix:
|
|
461
|
+
prefix: QueryKey,
|
|
454
462
|
*,
|
|
455
463
|
cancel_refetch: bool = True,
|
|
456
464
|
) -> list[ActionResult[Any]]:
|
|
457
465
|
"""Refetch all queries whose keys start with the given prefix.
|
|
458
466
|
|
|
459
467
|
Args:
|
|
460
|
-
prefix:
|
|
468
|
+
prefix: Key prefix to match against query keys.
|
|
461
469
|
cancel_refetch: Cancel in-flight requests before refetching.
|
|
462
470
|
|
|
463
471
|
Returns:
|
|
@@ -524,7 +532,7 @@ class QueryClient:
|
|
|
524
532
|
"""Remove all queries matching the filter.
|
|
525
533
|
|
|
526
534
|
Args:
|
|
527
|
-
filter: Optional filter - exact key,
|
|
535
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
528
536
|
If None, removes all queries.
|
|
529
537
|
|
|
530
538
|
Returns:
|
|
@@ -535,11 +543,11 @@ class QueryClient:
|
|
|
535
543
|
q.dispose()
|
|
536
544
|
return len(queries)
|
|
537
545
|
|
|
538
|
-
def remove_prefix(self, prefix:
|
|
546
|
+
def remove_prefix(self, prefix: QueryKey) -> int:
|
|
539
547
|
"""Remove all queries whose keys start with the given prefix.
|
|
540
548
|
|
|
541
549
|
Args:
|
|
542
|
-
prefix:
|
|
550
|
+
prefix: Key prefix to match against query keys.
|
|
543
551
|
|
|
544
552
|
Returns:
|
|
545
553
|
Count of removed queries.
|
|
@@ -554,7 +562,7 @@ class QueryClient:
|
|
|
554
562
|
"""Check if any query matching the filter is currently fetching.
|
|
555
563
|
|
|
556
564
|
Args:
|
|
557
|
-
filter: Optional filter - exact key,
|
|
565
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
558
566
|
If None, checks all queries.
|
|
559
567
|
|
|
560
568
|
Returns:
|
|
@@ -570,7 +578,7 @@ class QueryClient:
|
|
|
570
578
|
"""Check if any query matching the filter is in loading state.
|
|
571
579
|
|
|
572
580
|
Args:
|
|
573
|
-
filter: Optional filter - exact key,
|
|
581
|
+
filter: Optional filter - exact key, QueryKeys, or predicate.
|
|
574
582
|
If None, checks all queries.
|
|
575
583
|
|
|
576
584
|
Returns:
|
pulse/queries/common.py
CHANGED
|
@@ -9,6 +9,8 @@ from typing import (
|
|
|
9
9
|
ParamSpec,
|
|
10
10
|
TypeAlias,
|
|
11
11
|
TypeVar,
|
|
12
|
+
final,
|
|
13
|
+
override,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
from pulse.state import State
|
|
@@ -18,13 +20,74 @@ TState = TypeVar("TState", bound="State")
|
|
|
18
20
|
P = ParamSpec("P")
|
|
19
21
|
R = TypeVar("R")
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
|
|
24
|
+
@final
|
|
25
|
+
class Key(tuple[Hashable, ...]):
|
|
26
|
+
"""Normalized query key with a precomputed hash."""
|
|
27
|
+
|
|
28
|
+
_hash: int = 0
|
|
29
|
+
|
|
30
|
+
def __new__(cls, key: "QueryKey"):
|
|
31
|
+
if isinstance(key, Key):
|
|
32
|
+
return key
|
|
33
|
+
if isinstance(key, (list, tuple)):
|
|
34
|
+
parts = tuple(key)
|
|
35
|
+
try:
|
|
36
|
+
key_hash = hash(parts)
|
|
37
|
+
except TypeError as e:
|
|
38
|
+
raise TypeError(
|
|
39
|
+
f"Query key contains unhashable value: {e}.\n\n"
|
|
40
|
+
+ "Keys must contain only hashable values (strings, numbers, tuples).\n"
|
|
41
|
+
+ f"Got: {key!r}\n\n"
|
|
42
|
+
+ "If using a dict or list inside the key, convert it to a tuple:\n"
|
|
43
|
+
+ " key=('users', tuple(user_ids)) # instead of list"
|
|
44
|
+
) from None
|
|
45
|
+
obj = super().__new__(cls, parts)
|
|
46
|
+
obj._hash = key_hash
|
|
47
|
+
return obj
|
|
48
|
+
raise TypeError(
|
|
49
|
+
f"Query key must be a tuple or list, got {type(key).__name__}: {key!r}\n\n"
|
|
50
|
+
+ "Examples of valid keys:\n"
|
|
51
|
+
+ " key=('users',) # single-element tuple\n"
|
|
52
|
+
+ " key=('user', user_id) # tuple with dynamic value\n"
|
|
53
|
+
+ " key=['posts', 'feed'] # list form also works"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def __hash__(self) -> int:
|
|
58
|
+
return self._hash
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
QueryKey: TypeAlias = tuple[Hashable, ...] | list[Hashable] | Key # pyright: ignore[reportImplicitStringConcatenation]
|
|
62
|
+
"""List/tuple of hashable values identifying a query in the store.
|
|
23
63
|
|
|
24
64
|
Used to uniquely identify queries for caching, deduplication, and invalidation.
|
|
25
|
-
Keys are hierarchical tuples like ``("user", user_id)`` or ``
|
|
65
|
+
Keys are hierarchical lists/tuples like ``("user", user_id)`` or ``["posts", "feed"]``.
|
|
66
|
+
Lists are normalized to a tuple-backed Key internally.
|
|
26
67
|
"""
|
|
27
68
|
|
|
69
|
+
|
|
70
|
+
def normalize_key(key: QueryKey) -> Key:
|
|
71
|
+
"""Convert a query key to a normalized key for use as a dict key."""
|
|
72
|
+
return Key(key)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@final
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class QueryKeys:
|
|
78
|
+
"""Wrapper for selecting multiple query keys."""
|
|
79
|
+
|
|
80
|
+
keys: tuple[Key, ...]
|
|
81
|
+
|
|
82
|
+
def __init__(self, *keys: QueryKey):
|
|
83
|
+
object.__setattr__(self, "keys", tuple(normalize_key(key) for key in keys))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def keys(*query_keys: QueryKey) -> QueryKeys:
|
|
87
|
+
"""Create a QueryKeys wrapper for filtering by multiple keys."""
|
|
88
|
+
return QueryKeys(*query_keys)
|
|
89
|
+
|
|
90
|
+
|
|
28
91
|
QueryStatus: TypeAlias = Literal["loading", "success", "error"]
|
|
29
92
|
"""Current status of a query.
|
|
30
93
|
|
pulse/queries/infinite_query.py
CHANGED
|
@@ -27,11 +27,13 @@ from pulse.queries.common import (
|
|
|
27
27
|
ActionError,
|
|
28
28
|
ActionResult,
|
|
29
29
|
ActionSuccess,
|
|
30
|
+
Key,
|
|
30
31
|
OnErrorFn,
|
|
31
32
|
OnSuccessFn,
|
|
32
33
|
QueryKey,
|
|
33
34
|
QueryStatus,
|
|
34
35
|
bind_state,
|
|
36
|
+
normalize_key,
|
|
35
37
|
)
|
|
36
38
|
from pulse.queries.query import RETRY_DELAY_DEFAULT, QueryConfig
|
|
37
39
|
from pulse.reactive import Computed, Effect, Signal, Untrack
|
|
@@ -115,6 +117,7 @@ class RefetchPage(Generic[T, TParam]):
|
|
|
115
117
|
fetch_fn: Callable[[TParam], Awaitable[T]]
|
|
116
118
|
param: TParam
|
|
117
119
|
observer: "InfiniteQueryResult[T, TParam] | None" = None
|
|
120
|
+
clear: bool = False
|
|
118
121
|
future: "asyncio.Future[ActionResult[T | None]]" = field(
|
|
119
122
|
default_factory=asyncio.Future
|
|
120
123
|
)
|
|
@@ -141,7 +144,7 @@ class InfiniteQueryConfig(QueryConfig[list[Page[T, TParam]]], Generic[T, TParam]
|
|
|
141
144
|
class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
142
145
|
"""Paginated query that stores data as a list of Page(data, param)."""
|
|
143
146
|
|
|
144
|
-
key:
|
|
147
|
+
key: Key
|
|
145
148
|
cfg: InfiniteQueryConfig[T, TParam]
|
|
146
149
|
|
|
147
150
|
@property
|
|
@@ -248,7 +251,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
248
251
|
gc_time: float = 300.0,
|
|
249
252
|
on_dispose: Callable[[Any], None] | None = None,
|
|
250
253
|
):
|
|
251
|
-
self.key = key
|
|
254
|
+
self.key = normalize_key(key)
|
|
252
255
|
|
|
253
256
|
self.cfg = InfiniteQueryConfig(
|
|
254
257
|
retries=retries,
|
|
@@ -305,7 +308,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
305
308
|
|
|
306
309
|
for obs in self._observers:
|
|
307
310
|
if obs._on_success is not None: # pyright: ignore[reportPrivateUsage]
|
|
308
|
-
|
|
311
|
+
with Untrack():
|
|
312
|
+
await maybe_await(call_flexible(obs._on_success, self.pages)) # pyright: ignore[reportPrivateUsage]
|
|
309
313
|
|
|
310
314
|
async def _commit_error(self, error: Exception):
|
|
311
315
|
"""Commit error state and run error callbacks."""
|
|
@@ -313,7 +317,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
313
317
|
|
|
314
318
|
for obs in self._observers:
|
|
315
319
|
if obs._on_error is not None: # pyright: ignore[reportPrivateUsage]
|
|
316
|
-
|
|
320
|
+
with Untrack():
|
|
321
|
+
await maybe_await(call_flexible(obs._on_error, error)) # pyright: ignore[reportPrivateUsage]
|
|
317
322
|
|
|
318
323
|
def _commit_sync(self):
|
|
319
324
|
"""Synchronous commit - updates state based on current pages."""
|
|
@@ -427,7 +432,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
427
432
|
self._cancel_observer_actions(observer)
|
|
428
433
|
|
|
429
434
|
if len(self._observers) == 0:
|
|
430
|
-
self.
|
|
435
|
+
if not self.__disposed__:
|
|
436
|
+
self.schedule_gc()
|
|
431
437
|
|
|
432
438
|
def invalidate(
|
|
433
439
|
self,
|
|
@@ -703,8 +709,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
703
709
|
|
|
704
710
|
page = await action.fetch_fn(action.param)
|
|
705
711
|
|
|
706
|
-
if idx is None:
|
|
707
|
-
#
|
|
712
|
+
if action.clear or idx is None:
|
|
713
|
+
# clear=True or page doesn't exist - replace all pages with just this one
|
|
708
714
|
self.pages.clear()
|
|
709
715
|
self.pages.append(Page(page, action.param))
|
|
710
716
|
else:
|
|
@@ -782,12 +788,13 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
782
788
|
*,
|
|
783
789
|
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
784
790
|
cancel_fetch: bool = False,
|
|
791
|
+
clear: bool = False,
|
|
785
792
|
) -> ActionResult[T | None]:
|
|
786
793
|
"""
|
|
787
794
|
Refetch a page by its param. Queued for sequential execution.
|
|
788
795
|
|
|
789
|
-
If the page doesn't exist, clears existing pages and loads
|
|
790
|
-
page as the new starting point.
|
|
796
|
+
If the page doesn't exist or clear=True, clears existing pages and loads
|
|
797
|
+
the requested page as the new starting point.
|
|
791
798
|
|
|
792
799
|
Note: Prefer calling refetch_page() on InfiniteQueryResult to ensure the
|
|
793
800
|
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
@@ -795,7 +802,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
795
802
|
"""
|
|
796
803
|
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
797
804
|
action: RefetchPage[T, TParam] = RefetchPage(
|
|
798
|
-
fetch_fn=fn, param=param, observer=observer
|
|
805
|
+
fetch_fn=fn, param=param, observer=observer, clear=clear
|
|
799
806
|
)
|
|
800
807
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
801
808
|
|
|
@@ -1019,12 +1026,22 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
1019
1026
|
page_param: TParam,
|
|
1020
1027
|
*,
|
|
1021
1028
|
cancel_fetch: bool = False,
|
|
1029
|
+
clear: bool = False,
|
|
1022
1030
|
) -> ActionResult[T | None]:
|
|
1031
|
+
"""Fetch a specific page by its param.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
page_param: The page parameter to fetch.
|
|
1035
|
+
cancel_fetch: Cancel any in-flight fetches before starting.
|
|
1036
|
+
clear: If True, clears all other pages and keeps only the fetched page.
|
|
1037
|
+
Useful for resetting pagination to a specific page.
|
|
1038
|
+
"""
|
|
1023
1039
|
return await self._query().refetch_page(
|
|
1024
1040
|
page_param,
|
|
1025
1041
|
fetch_fn=self._fetch_fn,
|
|
1026
1042
|
observer=self,
|
|
1027
1043
|
cancel_fetch=cancel_fetch,
|
|
1044
|
+
clear=clear,
|
|
1028
1045
|
)
|
|
1029
1046
|
|
|
1030
1047
|
def set_initial_data(
|
|
@@ -1149,7 +1166,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1149
1166
|
Callable[[TState, list[Page[T, TParam]]], TParam | None] | None
|
|
1150
1167
|
)
|
|
1151
1168
|
_max_pages: int
|
|
1152
|
-
_key:
|
|
1169
|
+
_key: Key | Callable[[TState], Key] | None
|
|
1153
1170
|
# Not using OnSuccessFn and OnErrorFn since unions of callables are not well
|
|
1154
1171
|
# supported in the type system. We just need to be careful to use
|
|
1155
1172
|
# call_flexible to invoke these functions.
|
|
@@ -1193,7 +1210,17 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1193
1210
|
self._on_success_fn = None
|
|
1194
1211
|
self._on_error_fn = None
|
|
1195
1212
|
self._initial_data = MISSING
|
|
1196
|
-
|
|
1213
|
+
if key is None:
|
|
1214
|
+
self._key = None
|
|
1215
|
+
elif callable(key):
|
|
1216
|
+
key_fn = key
|
|
1217
|
+
|
|
1218
|
+
def normalized_key(state: TState) -> Key:
|
|
1219
|
+
return normalize_key(key_fn(state))
|
|
1220
|
+
|
|
1221
|
+
self._key = normalized_key
|
|
1222
|
+
else:
|
|
1223
|
+
self._key = normalize_key(key)
|
|
1197
1224
|
self._initial_data_updated_at = initial_data_updated_at
|
|
1198
1225
|
self._enabled = enabled
|
|
1199
1226
|
self._fetch_on_mount = fetch_on_mount
|
|
@@ -1204,7 +1231,11 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1204
1231
|
raise RuntimeError(
|
|
1205
1232
|
f"Cannot use @{self.name}.key decorator when a key is already provided to @infinite_query(key=...)."
|
|
1206
1233
|
)
|
|
1207
|
-
|
|
1234
|
+
|
|
1235
|
+
def normalized_key(state: TState) -> Key:
|
|
1236
|
+
return normalize_key(fn(state))
|
|
1237
|
+
|
|
1238
|
+
self._key = normalized_key
|
|
1208
1239
|
return fn
|
|
1209
1240
|
|
|
1210
1241
|
def on_success(self, fn: OnSuccessFn[TState, list[T]]):
|
|
@@ -1278,8 +1309,17 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1278
1309
|
)
|
|
1279
1310
|
|
|
1280
1311
|
if self._key is None:
|
|
1312
|
+
# pyright: ignore[reportImplicitStringConcatenation]
|
|
1281
1313
|
raise RuntimeError(
|
|
1282
|
-
f"
|
|
1314
|
+
f"Missing query key for @infinite_query '{self.name}'. "
|
|
1315
|
+
+ "A key is required to cache and share query results.\n\n"
|
|
1316
|
+
+ f"Fix: Add key=(...) to the decorator or use the @{self.name}.key decorator:\n\n"
|
|
1317
|
+
+ " @ps.infinite_query(initial_page_param=..., key=('my_query',))\n"
|
|
1318
|
+
+ f" async def {self.name}(self, param): ...\n\n"
|
|
1319
|
+
+ "Or with a dynamic key:\n\n"
|
|
1320
|
+
+ f" @{self.name}.key\n"
|
|
1321
|
+
+ f" def _{self.name}_key(self):\n"
|
|
1322
|
+
+ " return ('my_query', self.some_param)"
|
|
1283
1323
|
)
|
|
1284
1324
|
raw_initial = (
|
|
1285
1325
|
call_flexible(self._initial_data, state)
|
|
@@ -1384,6 +1424,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1384
1424
|
def infinite_query(
|
|
1385
1425
|
fn: Callable[[TState, TParam], Awaitable[T]],
|
|
1386
1426
|
*,
|
|
1427
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1387
1428
|
initial_page_param: TParam,
|
|
1388
1429
|
max_pages: int = 0,
|
|
1389
1430
|
stale_time: float = 0.0,
|
|
@@ -1395,7 +1436,6 @@ def infinite_query(
|
|
|
1395
1436
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1396
1437
|
enabled: bool = True,
|
|
1397
1438
|
fetch_on_mount: bool = True,
|
|
1398
|
-
key: QueryKey | None = None,
|
|
1399
1439
|
) -> InfiniteQueryProperty[T, TParam, TState]: ...
|
|
1400
1440
|
|
|
1401
1441
|
|
|
@@ -1403,6 +1443,7 @@ def infinite_query(
|
|
|
1403
1443
|
def infinite_query(
|
|
1404
1444
|
fn: None = None,
|
|
1405
1445
|
*,
|
|
1446
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1406
1447
|
initial_page_param: TParam,
|
|
1407
1448
|
max_pages: int = 0,
|
|
1408
1449
|
stale_time: float = 0.0,
|
|
@@ -1414,7 +1455,6 @@ def infinite_query(
|
|
|
1414
1455
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1415
1456
|
enabled: bool = True,
|
|
1416
1457
|
fetch_on_mount: bool = True,
|
|
1417
|
-
key: QueryKey | None = None,
|
|
1418
1458
|
) -> Callable[
|
|
1419
1459
|
[Callable[[TState, Any], Awaitable[T]]],
|
|
1420
1460
|
InfiniteQueryProperty[T, TParam, TState],
|
|
@@ -1424,6 +1464,7 @@ def infinite_query(
|
|
|
1424
1464
|
def infinite_query(
|
|
1425
1465
|
fn: Callable[[TState, TParam], Awaitable[T]] | None = None,
|
|
1426
1466
|
*,
|
|
1467
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1427
1468
|
initial_page_param: TParam,
|
|
1428
1469
|
max_pages: int = 0,
|
|
1429
1470
|
stale_time: float = 0.0,
|
|
@@ -1435,7 +1476,6 @@ def infinite_query(
|
|
|
1435
1476
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1436
1477
|
enabled: bool = True,
|
|
1437
1478
|
fetch_on_mount: bool = True,
|
|
1438
|
-
key: QueryKey | None = None,
|
|
1439
1479
|
) -> (
|
|
1440
1480
|
InfiniteQueryProperty[T, TParam, TState]
|
|
1441
1481
|
| Callable[
|
|
@@ -1449,7 +1489,8 @@ def infinite_query(
|
|
|
1449
1489
|
pagination. Data is stored as a list of pages, each with its data and the
|
|
1450
1490
|
parameter used to fetch it.
|
|
1451
1491
|
|
|
1452
|
-
Requires
|
|
1492
|
+
Requires a key (``key=`` or ``@query_prop.key``) and
|
|
1493
|
+
``@query_prop.get_next_page_param`` decorator.
|
|
1453
1494
|
|
|
1454
1495
|
Args:
|
|
1455
1496
|
fn: The async method to decorate (when used without parentheses).
|