pulse-framework 0.1.70__tar.gz → 0.1.71__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.70 → pulse_framework-0.1.71}/PKG-INFO +1 -1
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/pyproject.toml +1 -1
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/__init__.py +4 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/client.py +64 -56
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/common.py +54 -3
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/infinite_query.py +47 -16
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/query.py +28 -10
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/store.py +13 -11
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/README.md +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/_examples.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/app.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/logging.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/code_analysis.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/component.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/elements.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/events.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/props.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/svg.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/tags.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/dom/tags.pyi +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/forms.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/init.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/hooks/state.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/__init__.pyi +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/_types.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/array.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/console.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/date.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/document.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/error.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/json.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/map.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/math.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/navigator.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/number.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/obj.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/object.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/promise.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/pulse.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/react.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/regexp.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/set.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/string.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/weakmap.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/weakset.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/js/window.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/proxy.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/effect.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/mutation.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/queries/protocol.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/render_session.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/requirements.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/scheduling.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/test_helpers.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/assets.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/builtins.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/dynamic_import.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/emit_context.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/errors.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/function.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/id.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/imports.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/js_module.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/asyncio.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/json.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/math.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/pulse/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/pulse/tags.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/modules/typing.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/nodes.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/py_module.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/transpiler.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/transpiler/vdom.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.70 → pulse_framework-0.1.71}/src/pulse/version.py +0 -0
|
@@ -1328,8 +1328,12 @@ from pulse.queries.client import queries as queries
|
|
|
1328
1328
|
from pulse.queries.common import ActionError as ActionError
|
|
1329
1329
|
from pulse.queries.common import ActionResult as ActionResult
|
|
1330
1330
|
from pulse.queries.common import ActionSuccess as ActionSuccess
|
|
1331
|
+
from pulse.queries.common import Key as Key
|
|
1331
1332
|
from pulse.queries.common import QueryKey as QueryKey
|
|
1333
|
+
from pulse.queries.common import QueryKeys as QueryKeys
|
|
1332
1334
|
from pulse.queries.common import QueryStatus as QueryStatus
|
|
1335
|
+
from pulse.queries.common import keys as keys
|
|
1336
|
+
from pulse.queries.common import normalize_key as normalize_key
|
|
1333
1337
|
from pulse.queries.infinite_query import infinite_query as infinite_query
|
|
1334
1338
|
from pulse.queries.mutation import mutation as mutation
|
|
1335
1339
|
from pulse.queries.protocol import QueryResult as QueryResult
|
|
@@ -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:
|
|
@@ -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,62 @@ 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:
|
|
38
|
+
raise TypeError("QueryKey values must be hashable") from None
|
|
39
|
+
obj = super().__new__(cls, parts)
|
|
40
|
+
obj._hash = key_hash
|
|
41
|
+
return obj
|
|
42
|
+
raise TypeError("QueryKey must be a list or tuple of hashable values")
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def __hash__(self) -> int:
|
|
46
|
+
return self._hash
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
QueryKey: TypeAlias = tuple[Hashable, ...] | list[Hashable] | Key
|
|
50
|
+
"""List/tuple of hashable values identifying a query in the store.
|
|
23
51
|
|
|
24
52
|
Used to uniquely identify queries for caching, deduplication, and invalidation.
|
|
25
|
-
Keys are hierarchical tuples like ``("user", user_id)`` or ``
|
|
53
|
+
Keys are hierarchical lists/tuples like ``("user", user_id)`` or ``["posts", "feed"]``.
|
|
54
|
+
Lists are normalized to a tuple-backed Key internally.
|
|
26
55
|
"""
|
|
27
56
|
|
|
57
|
+
|
|
58
|
+
def normalize_key(key: QueryKey) -> Key:
|
|
59
|
+
"""Convert a query key to a normalized key for use as a dict key."""
|
|
60
|
+
return Key(key)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@final
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class QueryKeys:
|
|
66
|
+
"""Wrapper for selecting multiple query keys."""
|
|
67
|
+
|
|
68
|
+
keys: tuple[Key, ...]
|
|
69
|
+
|
|
70
|
+
def __init__(self, *keys: QueryKey):
|
|
71
|
+
object.__setattr__(self, "keys", tuple(normalize_key(key) for key in keys))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def keys(*query_keys: QueryKey) -> QueryKeys:
|
|
75
|
+
"""Create a QueryKeys wrapper for filtering by multiple keys."""
|
|
76
|
+
return QueryKeys(*query_keys)
|
|
77
|
+
|
|
78
|
+
|
|
28
79
|
QueryStatus: TypeAlias = Literal["loading", "success", "error"]
|
|
29
80
|
"""Current status of a query.
|
|
30
81
|
|
|
@@ -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."""
|
|
@@ -703,8 +708,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
703
708
|
|
|
704
709
|
page = await action.fetch_fn(action.param)
|
|
705
710
|
|
|
706
|
-
if idx is None:
|
|
707
|
-
#
|
|
711
|
+
if action.clear or idx is None:
|
|
712
|
+
# clear=True or page doesn't exist - replace all pages with just this one
|
|
708
713
|
self.pages.clear()
|
|
709
714
|
self.pages.append(Page(page, action.param))
|
|
710
715
|
else:
|
|
@@ -782,12 +787,13 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
782
787
|
*,
|
|
783
788
|
observer: "InfiniteQueryResult[T, TParam] | None" = None,
|
|
784
789
|
cancel_fetch: bool = False,
|
|
790
|
+
clear: bool = False,
|
|
785
791
|
) -> ActionResult[T | None]:
|
|
786
792
|
"""
|
|
787
793
|
Refetch a page by its param. Queued for sequential execution.
|
|
788
794
|
|
|
789
|
-
If the page doesn't exist, clears existing pages and loads
|
|
790
|
-
page as the new starting point.
|
|
795
|
+
If the page doesn't exist or clear=True, clears existing pages and loads
|
|
796
|
+
the requested page as the new starting point.
|
|
791
797
|
|
|
792
798
|
Note: Prefer calling refetch_page() on InfiniteQueryResult to ensure the
|
|
793
799
|
correct fetch function is used. When called directly on InfiniteQuery, uses
|
|
@@ -795,7 +801,7 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
|
|
|
795
801
|
"""
|
|
796
802
|
fn = fetch_fn if fetch_fn is not None else self.fn
|
|
797
803
|
action: RefetchPage[T, TParam] = RefetchPage(
|
|
798
|
-
fetch_fn=fn, param=param, observer=observer
|
|
804
|
+
fetch_fn=fn, param=param, observer=observer, clear=clear
|
|
799
805
|
)
|
|
800
806
|
return await self._enqueue(action, cancel_fetch=cancel_fetch)
|
|
801
807
|
|
|
@@ -1019,12 +1025,22 @@ class InfiniteQueryResult(Generic[T, TParam], Disposable):
|
|
|
1019
1025
|
page_param: TParam,
|
|
1020
1026
|
*,
|
|
1021
1027
|
cancel_fetch: bool = False,
|
|
1028
|
+
clear: bool = False,
|
|
1022
1029
|
) -> ActionResult[T | None]:
|
|
1030
|
+
"""Fetch a specific page by its param.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
page_param: The page parameter to fetch.
|
|
1034
|
+
cancel_fetch: Cancel any in-flight fetches before starting.
|
|
1035
|
+
clear: If True, clears all other pages and keeps only the fetched page.
|
|
1036
|
+
Useful for resetting pagination to a specific page.
|
|
1037
|
+
"""
|
|
1023
1038
|
return await self._query().refetch_page(
|
|
1024
1039
|
page_param,
|
|
1025
1040
|
fetch_fn=self._fetch_fn,
|
|
1026
1041
|
observer=self,
|
|
1027
1042
|
cancel_fetch=cancel_fetch,
|
|
1043
|
+
clear=clear,
|
|
1028
1044
|
)
|
|
1029
1045
|
|
|
1030
1046
|
def set_initial_data(
|
|
@@ -1149,7 +1165,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1149
1165
|
Callable[[TState, list[Page[T, TParam]]], TParam | None] | None
|
|
1150
1166
|
)
|
|
1151
1167
|
_max_pages: int
|
|
1152
|
-
_key:
|
|
1168
|
+
_key: Key | Callable[[TState], Key] | None
|
|
1153
1169
|
# Not using OnSuccessFn and OnErrorFn since unions of callables are not well
|
|
1154
1170
|
# supported in the type system. We just need to be careful to use
|
|
1155
1171
|
# call_flexible to invoke these functions.
|
|
@@ -1193,7 +1209,17 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1193
1209
|
self._on_success_fn = None
|
|
1194
1210
|
self._on_error_fn = None
|
|
1195
1211
|
self._initial_data = MISSING
|
|
1196
|
-
|
|
1212
|
+
if key is None:
|
|
1213
|
+
self._key = None
|
|
1214
|
+
elif callable(key):
|
|
1215
|
+
key_fn = key
|
|
1216
|
+
|
|
1217
|
+
def normalized_key(state: TState) -> Key:
|
|
1218
|
+
return normalize_key(key_fn(state))
|
|
1219
|
+
|
|
1220
|
+
self._key = normalized_key
|
|
1221
|
+
else:
|
|
1222
|
+
self._key = normalize_key(key)
|
|
1197
1223
|
self._initial_data_updated_at = initial_data_updated_at
|
|
1198
1224
|
self._enabled = enabled
|
|
1199
1225
|
self._fetch_on_mount = fetch_on_mount
|
|
@@ -1204,7 +1230,11 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1204
1230
|
raise RuntimeError(
|
|
1205
1231
|
f"Cannot use @{self.name}.key decorator when a key is already provided to @infinite_query(key=...)."
|
|
1206
1232
|
)
|
|
1207
|
-
|
|
1233
|
+
|
|
1234
|
+
def normalized_key(state: TState) -> Key:
|
|
1235
|
+
return normalize_key(fn(state))
|
|
1236
|
+
|
|
1237
|
+
self._key = normalized_key
|
|
1208
1238
|
return fn
|
|
1209
1239
|
|
|
1210
1240
|
def on_success(self, fn: OnSuccessFn[TState, list[T]]):
|
|
@@ -1384,6 +1414,7 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
|
|
|
1384
1414
|
def infinite_query(
|
|
1385
1415
|
fn: Callable[[TState, TParam], Awaitable[T]],
|
|
1386
1416
|
*,
|
|
1417
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1387
1418
|
initial_page_param: TParam,
|
|
1388
1419
|
max_pages: int = 0,
|
|
1389
1420
|
stale_time: float = 0.0,
|
|
@@ -1395,7 +1426,6 @@ def infinite_query(
|
|
|
1395
1426
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1396
1427
|
enabled: bool = True,
|
|
1397
1428
|
fetch_on_mount: bool = True,
|
|
1398
|
-
key: QueryKey | None = None,
|
|
1399
1429
|
) -> InfiniteQueryProperty[T, TParam, TState]: ...
|
|
1400
1430
|
|
|
1401
1431
|
|
|
@@ -1403,6 +1433,7 @@ def infinite_query(
|
|
|
1403
1433
|
def infinite_query(
|
|
1404
1434
|
fn: None = None,
|
|
1405
1435
|
*,
|
|
1436
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1406
1437
|
initial_page_param: TParam,
|
|
1407
1438
|
max_pages: int = 0,
|
|
1408
1439
|
stale_time: float = 0.0,
|
|
@@ -1414,7 +1445,6 @@ def infinite_query(
|
|
|
1414
1445
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1415
1446
|
enabled: bool = True,
|
|
1416
1447
|
fetch_on_mount: bool = True,
|
|
1417
|
-
key: QueryKey | None = None,
|
|
1418
1448
|
) -> Callable[
|
|
1419
1449
|
[Callable[[TState, Any], Awaitable[T]]],
|
|
1420
1450
|
InfiniteQueryProperty[T, TParam, TState],
|
|
@@ -1424,6 +1454,7 @@ def infinite_query(
|
|
|
1424
1454
|
def infinite_query(
|
|
1425
1455
|
fn: Callable[[TState, TParam], Awaitable[T]] | None = None,
|
|
1426
1456
|
*,
|
|
1457
|
+
key: QueryKey | Callable[[TState], QueryKey] | None = None,
|
|
1427
1458
|
initial_page_param: TParam,
|
|
1428
1459
|
max_pages: int = 0,
|
|
1429
1460
|
stale_time: float = 0.0,
|
|
@@ -1435,7 +1466,6 @@ def infinite_query(
|
|
|
1435
1466
|
initial_data_updated_at: float | dt.datetime | None = None,
|
|
1436
1467
|
enabled: bool = True,
|
|
1437
1468
|
fetch_on_mount: bool = True,
|
|
1438
|
-
key: QueryKey | None = None,
|
|
1439
1469
|
) -> (
|
|
1440
1470
|
InfiniteQueryProperty[T, TParam, TState]
|
|
1441
1471
|
| Callable[
|
|
@@ -1449,7 +1479,8 @@ def infinite_query(
|
|
|
1449
1479
|
pagination. Data is stored as a list of pages, each with its data and the
|
|
1450
1480
|
parameter used to fetch it.
|
|
1451
1481
|
|
|
1452
|
-
Requires
|
|
1482
|
+
Requires a key (``key=`` or ``@query_prop.key``) and
|
|
1483
|
+
``@query_prop.get_next_page_param`` decorator.
|
|
1453
1484
|
|
|
1454
1485
|
Args:
|
|
1455
1486
|
fn: The async method to decorate (when used without parentheses).
|