pulse-framework 0.1.40__py3-none-any.whl → 0.1.42__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 +19 -4
- pulse/app.py +159 -99
- pulse/channel.py +7 -7
- pulse/cli/cmd.py +81 -45
- pulse/cli/models.py +2 -0
- pulse/cli/processes.py +67 -22
- pulse/cli/uvicorn_log_config.py +1 -1
- pulse/codegen/codegen.py +14 -1
- pulse/codegen/templates/layout.py +10 -2
- pulse/context.py +3 -2
- pulse/decorators.py +132 -40
- pulse/form.py +9 -9
- pulse/helpers.py +75 -11
- pulse/hooks/core.py +7 -8
- pulse/hooks/init.py +460 -0
- pulse/hooks/states.py +91 -54
- pulse/messages.py +1 -1
- pulse/middleware.py +170 -119
- pulse/plugin.py +0 -3
- pulse/proxy.py +134 -16
- pulse/queries/__init__.py +0 -0
- pulse/queries/common.py +24 -0
- pulse/queries/mutation.py +142 -0
- pulse/queries/query.py +270 -0
- pulse/queries/query_observer.py +365 -0
- pulse/queries/store.py +60 -0
- pulse/react_component.py +2 -1
- pulse/reactive.py +153 -53
- pulse/render_session.py +5 -2
- pulse/routing.py +68 -10
- pulse/state.py +8 -7
- pulse/types/event_handler.py +2 -3
- pulse/user_session.py +3 -2
- pulse/vdom.py +3 -1
- {pulse_framework-0.1.40.dist-info → pulse_framework-0.1.42.dist-info}/METADATA +1 -1
- {pulse_framework-0.1.40.dist-info → pulse_framework-0.1.42.dist-info}/RECORD +38 -32
- pulse/query.py +0 -408
- {pulse_framework-0.1.40.dist-info → pulse_framework-0.1.42.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.40.dist-info → pulse_framework-0.1.42.dist-info}/entry_points.txt +0 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
pulse/__init__.py,sha256=
|
|
2
|
-
pulse/app.py,sha256=
|
|
3
|
-
pulse/channel.py,sha256=
|
|
1
|
+
pulse/__init__.py,sha256=w7LVrYNiho18v9JyDQ8DZGdBYV4dZYtlEFiRi32ICiw,32166
|
|
2
|
+
pulse/app.py,sha256=cVEqazFcgSnmZSxqqz6HWk2QsI8rnKbO7Y_L88BcHSc,32082
|
|
3
|
+
pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
|
|
4
4
|
pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
pulse/cli/cmd.py,sha256=
|
|
5
|
+
pulse/cli/cmd.py,sha256=UBT7OoqWRU-idLOKkA9TDN8m8ugi1gwRMiUJTUmkVfU,14853
|
|
6
6
|
pulse/cli/dependencies.py,sha256=ZBqBAfMvMBQUvh4THdPDztTMQ_dyR52S1IuotP_eEZs,5623
|
|
7
7
|
pulse/cli/folder_lock.py,sha256=kvUmZBg869lwCTIZFoge9dhorv8qPXHTWwVv_jQg1k8,3477
|
|
8
8
|
pulse/cli/helpers.py,sha256=8bRlV3d7w3w-jHaFvFYt9Pzue6_CbKOq_Z3jBsBOeUk,8820
|
|
9
|
-
pulse/cli/models.py,sha256=
|
|
9
|
+
pulse/cli/models.py,sha256=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
|
|
10
10
|
pulse/cli/packages.py,sha256=e7ycwwJfdmB4pzrai4DHos6-JzyUgmE4DCZp0BqjdeI,6792
|
|
11
|
-
pulse/cli/processes.py,sha256=
|
|
11
|
+
pulse/cli/processes.py,sha256=C1xU72oUanj-1Mkc9WmqESTsVUn_aUHG8URiPyRHSFM,7016
|
|
12
12
|
pulse/cli/secrets.py,sha256=dNfQe6AzSYhZuWveesjCRHIbvaPd3-F9lEJ-kZA7ROw,921
|
|
13
|
-
pulse/cli/uvicorn_log_config.py,sha256=
|
|
13
|
+
pulse/cli/uvicorn_log_config.py,sha256=f7ikDc5foXh3TmFMrnfnW8yev48ZAdlo8F4F_aMVoVk,2391
|
|
14
14
|
pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
pulse/codegen/codegen.py,sha256=
|
|
15
|
+
pulse/codegen/codegen.py,sha256=lPhgl57Ag3ChEDdhFD7wUCaU6lFiTQ1cD_vpSUgMfKg,11093
|
|
16
16
|
pulse/codegen/imports.py,sha256=13f0uzJsotw069aP_COUUPMuTXXhRKRwUzfxsSCq_6A,6070
|
|
17
17
|
pulse/codegen/js.py,sha256=7MuiECSJ-DulSqKuMZ8z1q_d7e3AbK6MYiNTYALZCLA,881
|
|
18
18
|
pulse/codegen/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
pulse/codegen/templates/layout.py,sha256=
|
|
19
|
+
pulse/codegen/templates/layout.py,sha256=nmWPQcO9SRXc3mCCVLCmykreSF96TqQfdDY7dvUBxRg,4737
|
|
20
20
|
pulse/codegen/templates/route.py,sha256=cwmNHYkuecZ5M986hmm6SxisIoVSc656dtpqAvPjMjM,7824
|
|
21
21
|
pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_qKAI8zAE,1129
|
|
22
22
|
pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
|
|
@@ -24,20 +24,21 @@ pulse/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
24
24
|
pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
|
|
25
25
|
pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
|
|
26
26
|
pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
|
|
27
|
-
pulse/context.py,sha256=
|
|
27
|
+
pulse/context.py,sha256=fMK6GdQY4q_3452v5DJli2f2_urVihnpzb-O-O9cJ1Q,1734
|
|
28
28
|
pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
|
|
29
29
|
pulse/css.py,sha256=-FyQQQ0EZI1Ins30qiF3l4z9yDb1V9qWuJKWxHcKGkw,3910
|
|
30
|
-
pulse/decorators.py,sha256=
|
|
30
|
+
pulse/decorators.py,sha256=hRfgb9XU1yizmtdhuBln_3Gy-Cz2Smo4rYvAqlURrLQ,9348
|
|
31
31
|
pulse/env.py,sha256=p3XI8KG1ZCcXPD3LJP7fW8JPYfyvoYY5ENwae2o0PiA,2889
|
|
32
|
-
pulse/form.py,sha256=
|
|
33
|
-
pulse/helpers.py,sha256=
|
|
32
|
+
pulse/form.py,sha256=P7W8guUdGbgqNOk8cSUCWuY6qWre0me6_fypv1qOvqw,8987
|
|
33
|
+
pulse/helpers.py,sha256=BBtf--LZxvfpwJU8p92QrZWtOKIWfB3DOiAtGxhet90,13232
|
|
34
34
|
pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
pulse/hooks/core.py,sha256=
|
|
35
|
+
pulse/hooks/core.py,sha256=JTZbVxNOEs_GAeK6Bh6hemSTkgwZPtEi_wt55cvOdik,7381
|
|
36
36
|
pulse/hooks/effects.py,sha256=CQvt5viAweGLSxaGGlWm155GlEQiwQnGussw7OfiCGc,2393
|
|
37
|
+
pulse/hooks/init.py,sha256=snTy3PJtkSnnKBrAjcNOJbem2896xJzHD0DHLVVeyAo,11924
|
|
37
38
|
pulse/hooks/runtime.py,sha256=k5LZ8hnlNBMKOiEkQcAvs8BKwYxV6gwea2WCfju5K7Y,5106
|
|
38
39
|
pulse/hooks/setup.py,sha256=GJLSE6hLBNKHR9aLhvsS6KXwpOXQiSx1V3E2IkGADWM,4461
|
|
39
40
|
pulse/hooks/stable.py,sha256=uHEJ2E22r2kHx4uFjWjDepQ6OtPjLd7tT5ju-yKlkCU,1702
|
|
40
|
-
pulse/hooks/states.py,sha256=
|
|
41
|
+
pulse/hooks/states.py,sha256=e9vO8ig8vaw6guY711S6rlY9WXCjDYXsjiR7IvyKrXU,6708
|
|
41
42
|
pulse/html/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
43
|
pulse/html/elements.py,sha256=YHXkVpfMAC4-0o61fK-E0LGTOM3KMCtBfpHHAwLx7dw,23241
|
|
43
44
|
pulse/html/events.py,sha256=SiZxaQV340hc5YGoKWXC5uCmbLsuijuEgnQz1hmdqYg,14700
|
|
@@ -45,27 +46,32 @@ pulse/html/props.py,sha256=XatI6N4Hyef3MAql7jCxCIm6iuusgUXKkwHwIGm_dcc,26646
|
|
|
45
46
|
pulse/html/svg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
47
|
pulse/html/tags.py,sha256=dyG4BY9qthBbO-ihcy9F8mLY6WqQxKFXfpqNYcSMKN0,5182
|
|
47
48
|
pulse/html/tags.pyi,sha256=I8dFoft9w4RvneZ3li1weAdijY1krj9jfO_p2SU6e04,13953
|
|
48
|
-
pulse/messages.py,sha256=
|
|
49
|
-
pulse/middleware.py,sha256=
|
|
50
|
-
pulse/plugin.py,sha256=
|
|
51
|
-
pulse/proxy.py,sha256=
|
|
49
|
+
pulse/messages.py,sha256=Z7iYkOacwca5tU1UgZHt96iojT5Hd7VDc0PkEcru_2Q,3619
|
|
50
|
+
pulse/middleware.py,sha256=9uyAhVUEGMSwqWC3WXqs7x5JMMNEcSTTu3g7DjsR8w8,9812
|
|
51
|
+
pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
|
|
52
|
+
pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
|
|
52
53
|
pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
-
pulse/
|
|
54
|
-
pulse/
|
|
55
|
-
pulse/
|
|
54
|
+
pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
|
+
pulse/queries/common.py,sha256=2_11SEOFrDbe_ULNSA9EWeOkvz-xym5hCuX4bW455t0,556
|
|
56
|
+
pulse/queries/mutation.py,sha256=_0-o2g2yux52hTsRLGuWwFUdGrBx9YJqi67oBw9iNcc,4209
|
|
57
|
+
pulse/queries/query.py,sha256=WxBEaEjtzDGWWKpSi8-xl9xBvPvREYH1Tonl_lOY-VQ,7347
|
|
58
|
+
pulse/queries/query_observer.py,sha256=Wd3pk5OsZB_ze6DnpOkASv4Ny5EuSbwUlUEPXIXxSgk,10229
|
|
59
|
+
pulse/queries/store.py,sha256=ylSCOHiXp8vEyEWc5Et8zLWkyHj5OQYKOVfs0OehpX8,1465
|
|
60
|
+
pulse/react_component.py,sha256=hPibKBEkVdpBKNSpMQ6bZ-7GnJQcNQwcw2SvfY1chHA,26026
|
|
61
|
+
pulse/reactive.py,sha256=cKZDafbUQFdnNRAxI71THnsFZEbWZ5mU06pMuP6spo8,21187
|
|
56
62
|
pulse/reactive_extensions.py,sha256=gTLkQ0urwANjWNHWMkg-P9zvpevHCnNKd5BSM8G0pno,31521
|
|
57
|
-
pulse/render_session.py,sha256=
|
|
63
|
+
pulse/render_session.py,sha256=kqLfZ9AxCrB2mIJqegATL1KA7CI-LZSBQwRYr7Uxo9g,14581
|
|
58
64
|
pulse/renderer.py,sha256=dJiX9VeHr9kC1UBw5oaKB8Mv-3OCMGTrHiKgLJ5FL50,16759
|
|
59
65
|
pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
|
|
60
|
-
pulse/routing.py,sha256=
|
|
66
|
+
pulse/routing.py,sha256=RlrGHyK4F28_zUHMYNeKp4pNbvqrt4GY4t5xNdhzunI,13926
|
|
61
67
|
pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
|
|
62
|
-
pulse/state.py,sha256=
|
|
68
|
+
pulse/state.py,sha256=mytXlQjmLIBjB2XDgCg9E1fHCcyoNQ02cBqZ_vldxuc,10636
|
|
63
69
|
pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
-
pulse/types/event_handler.py,sha256=
|
|
65
|
-
pulse/user_session.py,sha256=
|
|
66
|
-
pulse/vdom.py,sha256=
|
|
70
|
+
pulse/types/event_handler.py,sha256=tfKa6OEA5XvzuYbllQZJ03ooN7rGSYOtaPBstSL4OLU,1642
|
|
71
|
+
pulse/user_session.py,sha256=Nn9ZZha1Rruw31OSoK14QaEL0erGVFbryFhJYrtMZsQ,7599
|
|
72
|
+
pulse/vdom.py,sha256=1UAjOYSmpdZeSVELqejh47Jer4mA73T_q2HtAogOphs,12514
|
|
67
73
|
pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
|
|
68
|
-
pulse_framework-0.1.
|
|
69
|
-
pulse_framework-0.1.
|
|
70
|
-
pulse_framework-0.1.
|
|
71
|
-
pulse_framework-0.1.
|
|
74
|
+
pulse_framework-0.1.42.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
75
|
+
pulse_framework-0.1.42.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
|
|
76
|
+
pulse_framework-0.1.42.dist-info/METADATA,sha256=Pyy5qb02JHkAFwGpy6m8SkrlPH_mmL6BPxDfvQ41ly4,580
|
|
77
|
+
pulse_framework-0.1.42.dist-info/RECORD,,
|
pulse/query.py
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from collections.abc import Awaitable, Callable, Coroutine
|
|
3
|
-
from typing import (
|
|
4
|
-
Any,
|
|
5
|
-
Generic,
|
|
6
|
-
TypeVar,
|
|
7
|
-
TypeVarTuple,
|
|
8
|
-
cast,
|
|
9
|
-
override,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
from pulse.helpers import call_flexible, maybe_await
|
|
13
|
-
from pulse.reactive import AsyncEffect, Computed, EffectCleanup, Signal
|
|
14
|
-
from pulse.state import InitializableProperty, State
|
|
15
|
-
|
|
16
|
-
T = TypeVar("T")
|
|
17
|
-
TState = TypeVar("TState", bound="State")
|
|
18
|
-
Args = TypeVarTuple("Args")
|
|
19
|
-
R = TypeVar("R")
|
|
20
|
-
|
|
21
|
-
# Type alias matching AsyncEffectFn from reactive.py
|
|
22
|
-
AsyncEffectFn = Callable[[], Coroutine[Any, Any, EffectCleanup | None]]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class QueryAsyncEffect(AsyncEffect):
|
|
26
|
-
"""
|
|
27
|
-
Specialized AsyncEffect for queries that synchronously clears query data
|
|
28
|
-
when rescheduled (if keep_previous_data is False).
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
_result: "QueryResult[Any]"
|
|
32
|
-
_keep_previous_data: bool
|
|
33
|
-
|
|
34
|
-
def __init__(
|
|
35
|
-
self,
|
|
36
|
-
fn: AsyncEffectFn,
|
|
37
|
-
result: "QueryResult[Any]",
|
|
38
|
-
keep_previous_data: bool,
|
|
39
|
-
name: str | None = None,
|
|
40
|
-
deps: list[Signal[Any] | Computed[Any]] | None = None,
|
|
41
|
-
):
|
|
42
|
-
super().__init__(fn, name=name, deps=deps)
|
|
43
|
-
self._result = result
|
|
44
|
-
self._keep_previous_data = keep_previous_data
|
|
45
|
-
|
|
46
|
-
@override
|
|
47
|
-
def push_change(self):
|
|
48
|
-
# Synchronously clear data before scheduling the effect to run.
|
|
49
|
-
# This ensures renders see the loading state immediately, not stale data.
|
|
50
|
-
self._result._set_loading(clear_data=not self._keep_previous_data) # pyright: ignore[reportPrivateUsage]
|
|
51
|
-
super().push_change()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class QueryResult(Generic[T]):
|
|
55
|
-
def __init__(self, initial_data: T | None = None):
|
|
56
|
-
# print("[QueryResult] initialize")
|
|
57
|
-
self._is_loading: Signal[bool] = Signal(True, name="query.is_loading")
|
|
58
|
-
self._is_error: Signal[bool] = Signal(False, name="query.is_error")
|
|
59
|
-
self._error: Signal[Exception | None] = Signal(None, name="query.error")
|
|
60
|
-
# Store initial data so we can preserve non-None semantics when requested
|
|
61
|
-
self._initial_data: T | None = initial_data
|
|
62
|
-
self._data: Signal[T | None] = Signal(initial_data, name="query.data")
|
|
63
|
-
# Tracks whether at least one load cycle completed (success or error)
|
|
64
|
-
self._has_loaded: Signal[bool] = Signal(False, name="query.has_loaded")
|
|
65
|
-
# Effect driving this query (attached by QueryProperty)
|
|
66
|
-
self._effect: AsyncEffect | None = None
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def is_loading(self) -> bool:
|
|
70
|
-
# print(f"[QueryResult] Accessing is_loading = {self._is_loading.read()}")
|
|
71
|
-
return self._is_loading.read()
|
|
72
|
-
|
|
73
|
-
@property
|
|
74
|
-
def is_error(self) -> bool:
|
|
75
|
-
# print(f"[QueryResult] Accessing is_error = {self._is_error.read()}")
|
|
76
|
-
return self._is_error.read()
|
|
77
|
-
|
|
78
|
-
@property
|
|
79
|
-
def error(self) -> Exception | None:
|
|
80
|
-
# print(f"[QueryResult] Accessing error = {self._error.read()}")
|
|
81
|
-
return self._error.read()
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def data(self) -> T | None:
|
|
85
|
-
# print(f"[QueryResult] Accessing data = {self._data.read()}")
|
|
86
|
-
return self._data.read()
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def has_loaded(self) -> bool:
|
|
90
|
-
return self._has_loaded.read()
|
|
91
|
-
|
|
92
|
-
def attach_effect(self, effect: AsyncEffect) -> None:
|
|
93
|
-
self._effect = effect
|
|
94
|
-
|
|
95
|
-
def refetch(self) -> None:
|
|
96
|
-
if self._effect is None:
|
|
97
|
-
return
|
|
98
|
-
self._effect.cancel()
|
|
99
|
-
self._effect.run()
|
|
100
|
-
|
|
101
|
-
def dispose(self) -> None:
|
|
102
|
-
if self._effect is None:
|
|
103
|
-
return
|
|
104
|
-
self._effect.dispose()
|
|
105
|
-
|
|
106
|
-
# Internal setters used by the query machinery
|
|
107
|
-
def _set_loading(self, *, clear_data: bool = False):
|
|
108
|
-
# print("[QueryResult] set loading=True")
|
|
109
|
-
self._is_loading.write(True)
|
|
110
|
-
self._is_error.write(False)
|
|
111
|
-
self._error.write(None)
|
|
112
|
-
if clear_data:
|
|
113
|
-
# If there was an explicit initial value, reset to it; otherwise clear
|
|
114
|
-
self._data.write(self._initial_data)
|
|
115
|
-
|
|
116
|
-
def _set_success(self, data: T):
|
|
117
|
-
# print(f"[QueryResult] set success data={data!r}")
|
|
118
|
-
self._data.write(data)
|
|
119
|
-
self._is_loading.write(False)
|
|
120
|
-
self._is_error.write(False)
|
|
121
|
-
self._error.write(None)
|
|
122
|
-
self._has_loaded.write(True)
|
|
123
|
-
|
|
124
|
-
def _set_error(self, err: Exception):
|
|
125
|
-
# print(f"[QueryResult] set error err={err!r}")
|
|
126
|
-
self._error.write(err)
|
|
127
|
-
self._is_loading.write(False)
|
|
128
|
-
self._is_error.write(True)
|
|
129
|
-
self._has_loaded.write(True)
|
|
130
|
-
|
|
131
|
-
# Public mutator useful for optimistic updates; does not change loading/error flags
|
|
132
|
-
def set_data(self, data: T):
|
|
133
|
-
self._data.write(data)
|
|
134
|
-
|
|
135
|
-
# Public mutator to set initial data before the first load completes.
|
|
136
|
-
# If called after the first load, it is ignored.
|
|
137
|
-
def set_initial_data(self, data: T):
|
|
138
|
-
if self._has_loaded.read():
|
|
139
|
-
return
|
|
140
|
-
self._initial_data = data
|
|
141
|
-
self._data.write(data)
|
|
142
|
-
|
|
143
|
-
# Public helpers mirroring internal transitions
|
|
144
|
-
def set_loading(self, *, clear_data: bool = False) -> None:
|
|
145
|
-
self._set_loading(clear_data=clear_data)
|
|
146
|
-
|
|
147
|
-
def set_success(self, data: T) -> None:
|
|
148
|
-
self._set_success(data)
|
|
149
|
-
|
|
150
|
-
def set_error(self, err: Exception) -> None:
|
|
151
|
-
self._set_error(err)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
OnSuccessFn = Callable[[TState], Any] | Callable[[TState, T], Any]
|
|
155
|
-
OnErrorFn = Callable[[TState], Any] | Callable[[TState, Exception], Any]
|
|
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
|
-
_initial: T | None
|
|
177
|
-
_key_fn: Callable[[TState], tuple[Any, ...]] | None
|
|
178
|
-
_initial_data_fn: Callable[[TState], T] | None
|
|
179
|
-
# Not using OnSuccessFn and OnErrorFn since unions of callables are not well
|
|
180
|
-
# supported in the type system. We just need to be careful to use
|
|
181
|
-
# call_flexible to invoke these functions.
|
|
182
|
-
_on_success_fn: Callable[[TState, Exception], Any] | None
|
|
183
|
-
_on_error_fn: Callable[[TState, T], Any] | None
|
|
184
|
-
|
|
185
|
-
_priv_query: str
|
|
186
|
-
_priv_effect: str
|
|
187
|
-
_priv_key_comp: str
|
|
188
|
-
_priv_initial_fn: str
|
|
189
|
-
_priv_initial_applied: str
|
|
190
|
-
|
|
191
|
-
def __init__(
|
|
192
|
-
self,
|
|
193
|
-
name: str,
|
|
194
|
-
fetch_fn: "Callable[[TState], Awaitable[T]]",
|
|
195
|
-
keep_alive: bool = False,
|
|
196
|
-
keep_previous_data: bool = False,
|
|
197
|
-
initial: T | None = None,
|
|
198
|
-
):
|
|
199
|
-
self.name = name
|
|
200
|
-
self._fetch_fn = fetch_fn
|
|
201
|
-
self._key_fn = None
|
|
202
|
-
self._initial_data_fn = None
|
|
203
|
-
# Single handlers; error if set more than once
|
|
204
|
-
self._on_success_fn = None
|
|
205
|
-
self._on_error_fn = None
|
|
206
|
-
self._keep_alive = keep_alive
|
|
207
|
-
self._keep_previous_data = keep_previous_data
|
|
208
|
-
self._initial = initial
|
|
209
|
-
self._priv_query = f"__query_{name}"
|
|
210
|
-
self._priv_effect = f"__query_effect_{name}"
|
|
211
|
-
self._priv_key_comp = f"__query_key_{name}"
|
|
212
|
-
self._priv_initial_fn = f"__query_initial_fn_{name}"
|
|
213
|
-
self._priv_initial_applied = f"__query_initial_applied_{name}"
|
|
214
|
-
|
|
215
|
-
# Decorator to attach a key function
|
|
216
|
-
def key(self, fn: Callable[[TState], tuple[Any, ...]]):
|
|
217
|
-
if self._key_fn is not None:
|
|
218
|
-
raise RuntimeError(
|
|
219
|
-
f"Duplicate key() decorator for query '{self.name}'. Only one is allowed."
|
|
220
|
-
)
|
|
221
|
-
self._key_fn = fn
|
|
222
|
-
return fn
|
|
223
|
-
|
|
224
|
-
# Decorator to attach a function providing initial data
|
|
225
|
-
def initial_data(self, fn: Callable[[TState], T]):
|
|
226
|
-
if self._initial_data_fn is not None:
|
|
227
|
-
raise RuntimeError(
|
|
228
|
-
f"Duplicate initial_data() decorator for query '{self.name}'. Only one is allowed."
|
|
229
|
-
)
|
|
230
|
-
self._initial_data_fn = fn
|
|
231
|
-
return fn
|
|
232
|
-
|
|
233
|
-
# Decorator to attach an on-success handler (sync or async)
|
|
234
|
-
def on_success(self, fn: OnSuccessFn[TState, T]):
|
|
235
|
-
if self._on_success_fn is not None:
|
|
236
|
-
raise RuntimeError(
|
|
237
|
-
f"Duplicate on_success() decorator for query '{self.name}'. Only one is allowed."
|
|
238
|
-
)
|
|
239
|
-
self._on_success_fn = fn # pyright: ignore[reportAttributeAccessIssue]
|
|
240
|
-
return fn
|
|
241
|
-
|
|
242
|
-
# Decorator to attach an on-error handler (sync or async)
|
|
243
|
-
def on_error(self, fn: OnErrorFn[TState]):
|
|
244
|
-
if self._on_error_fn is not None:
|
|
245
|
-
raise RuntimeError(
|
|
246
|
-
f"Duplicate on_error() decorator for query '{self.name}'. Only one is allowed."
|
|
247
|
-
)
|
|
248
|
-
self._on_error_fn = fn # pyright: ignore[reportAttributeAccessIssue]
|
|
249
|
-
return fn
|
|
250
|
-
|
|
251
|
-
@override
|
|
252
|
-
def initialize(self, state: Any, name: str) -> QueryResult[T]:
|
|
253
|
-
# Return cached query instance if present
|
|
254
|
-
query: QueryResult[T] | None = getattr(state, self._priv_query, None)
|
|
255
|
-
if query:
|
|
256
|
-
# print(f"[QueryProperty:{self.name}] return cached StateQuery")
|
|
257
|
-
return query
|
|
258
|
-
|
|
259
|
-
# key_fn being None means auto-tracked mode
|
|
260
|
-
|
|
261
|
-
# Bind methods to this instance
|
|
262
|
-
bound_fetch = bind_state(state, self._fetch_fn)
|
|
263
|
-
bound_on_success = (
|
|
264
|
-
bind_state(state, self._on_success_fn) if self._on_success_fn else None
|
|
265
|
-
)
|
|
266
|
-
bound_on_error = (
|
|
267
|
-
bind_state(state, self._on_error_fn) if self._on_error_fn else None
|
|
268
|
-
)
|
|
269
|
-
bound_initial_data = (
|
|
270
|
-
bind_state(state, self._initial_data_fn) if self._initial_data_fn else None
|
|
271
|
-
)
|
|
272
|
-
# print(f"[QueryProperty:{self.name}] bound fetch and key/handlers")
|
|
273
|
-
|
|
274
|
-
# Defer evaluating initial_data provider until after user __init__ by
|
|
275
|
-
# storing it on the instance and marking it unapplied. Use constructor
|
|
276
|
-
# `initial` as the initial visible value for now.
|
|
277
|
-
setattr(state, self._priv_initial_fn, bound_initial_data)
|
|
278
|
-
setattr(state, self._priv_initial_applied, False)
|
|
279
|
-
initial_value: T | None = self._initial
|
|
280
|
-
|
|
281
|
-
result = QueryResult[T](initial_data=initial_value)
|
|
282
|
-
|
|
283
|
-
key_computed: Computed[tuple[Any, ...]] | None = None
|
|
284
|
-
if self._key_fn:
|
|
285
|
-
bound_key_fn = bind_state(state, self._key_fn)
|
|
286
|
-
key_computed = Computed(bound_key_fn, name=f"query.key.{self.name}")
|
|
287
|
-
setattr(state, self._priv_key_comp, key_computed)
|
|
288
|
-
|
|
289
|
-
inflight_key: tuple[Any, ...] | None = None
|
|
290
|
-
|
|
291
|
-
async def run_effect():
|
|
292
|
-
# print(f"[QueryProperty:{self.name}] effect RUN")
|
|
293
|
-
# In key mode, deduplicate same-key concurrent reruns
|
|
294
|
-
if key_computed:
|
|
295
|
-
key = key_computed()
|
|
296
|
-
|
|
297
|
-
nonlocal inflight_key
|
|
298
|
-
# De-duplicate same-key concurrent reruns
|
|
299
|
-
if inflight_key == key:
|
|
300
|
-
return None
|
|
301
|
-
inflight_key = key
|
|
302
|
-
|
|
303
|
-
# Set loading immediately; optionally clear previous data
|
|
304
|
-
result.set_loading(clear_data=not self._keep_previous_data)
|
|
305
|
-
|
|
306
|
-
try:
|
|
307
|
-
data = await bound_fetch()
|
|
308
|
-
except asyncio.CancelledError:
|
|
309
|
-
# Cancellation is expected during reruns; swallow
|
|
310
|
-
return None
|
|
311
|
-
except Exception as e:
|
|
312
|
-
result.set_error(e)
|
|
313
|
-
# Invoke error handler if provided
|
|
314
|
-
if bound_on_error:
|
|
315
|
-
await maybe_await(call_flexible(bound_on_error, e))
|
|
316
|
-
else:
|
|
317
|
-
result.set_success(data)
|
|
318
|
-
# Invoke success handler if provided
|
|
319
|
-
if bound_on_success:
|
|
320
|
-
await maybe_await(call_flexible(bound_on_success, data))
|
|
321
|
-
finally:
|
|
322
|
-
inflight_key = None
|
|
323
|
-
|
|
324
|
-
# In key mode, depend only on key via explicit deps
|
|
325
|
-
if key_computed is not None:
|
|
326
|
-
effect = QueryAsyncEffect(
|
|
327
|
-
run_effect,
|
|
328
|
-
result=result,
|
|
329
|
-
keep_previous_data=self._keep_previous_data,
|
|
330
|
-
name=f"query.effect.{self.name}",
|
|
331
|
-
deps=[key_computed],
|
|
332
|
-
)
|
|
333
|
-
else:
|
|
334
|
-
effect = QueryAsyncEffect(
|
|
335
|
-
run_effect,
|
|
336
|
-
result=result,
|
|
337
|
-
keep_previous_data=self._keep_previous_data,
|
|
338
|
-
name=f"query.effect.{self.name}",
|
|
339
|
-
)
|
|
340
|
-
# print(f"[QueryProperty:{self.name}] created Effect name={effect.name}")
|
|
341
|
-
|
|
342
|
-
# Expose the effect on the instance so State.effects() sees it
|
|
343
|
-
setattr(state, self._priv_effect, effect)
|
|
344
|
-
# Attach effect to result and expose result directly
|
|
345
|
-
result.attach_effect(effect)
|
|
346
|
-
setattr(state, self._priv_query, result)
|
|
347
|
-
|
|
348
|
-
# if not self.keep_alive:
|
|
349
|
-
|
|
350
|
-
# def on_obs(count: int):
|
|
351
|
-
# if count == 0:
|
|
352
|
-
# # print("[QueryProperty] Disposing of effect due to no observers")
|
|
353
|
-
# effect.dispose()
|
|
354
|
-
|
|
355
|
-
# Stop when no one observes key or data
|
|
356
|
-
# result._data.on_observer_change(on_obs)
|
|
357
|
-
# result._is_error.on_observer_change(on_obs)
|
|
358
|
-
# result._is_loading.on_observer_change(on_obs)
|
|
359
|
-
|
|
360
|
-
return result
|
|
361
|
-
|
|
362
|
-
def __get__(self, obj: Any, objtype: Any = None) -> QueryResult[T]:
|
|
363
|
-
if obj is None:
|
|
364
|
-
return self # pyright: ignore[reportReturnType]
|
|
365
|
-
query = self.initialize(obj, self.name)
|
|
366
|
-
# Apply initial_data provider once, after state __init__, before first load
|
|
367
|
-
try:
|
|
368
|
-
applied = bool(getattr(obj, self._priv_initial_applied, False))
|
|
369
|
-
except Exception:
|
|
370
|
-
applied = True # fail safe: do not attempt if attribute missing
|
|
371
|
-
if not applied and not query.has_loaded:
|
|
372
|
-
bound_initial = getattr(obj, self._priv_initial_fn, None)
|
|
373
|
-
if callable(bound_initial):
|
|
374
|
-
try:
|
|
375
|
-
value = bound_initial()
|
|
376
|
-
if value is not None:
|
|
377
|
-
query.set_initial_data(value) # pyright: ignore[reportArgumentType]
|
|
378
|
-
except Exception:
|
|
379
|
-
pass
|
|
380
|
-
try:
|
|
381
|
-
setattr(obj, self._priv_initial_applied, True)
|
|
382
|
-
except Exception:
|
|
383
|
-
pass
|
|
384
|
-
return query
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
class QueryResultWithInitial(QueryResult[T]):
|
|
388
|
-
@property
|
|
389
|
-
@override
|
|
390
|
-
def data(self) -> T:
|
|
391
|
-
return cast(T, super().data)
|
|
392
|
-
|
|
393
|
-
@property
|
|
394
|
-
@override
|
|
395
|
-
def has_loaded(self) -> bool: # mirror base for completeness
|
|
396
|
-
return super().has_loaded
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
class QueryPropertyWithInitial(QueryProperty[T, TState]):
|
|
400
|
-
@override
|
|
401
|
-
def __get__(self, obj: Any, objtype: Any = None) -> QueryResultWithInitial[T]:
|
|
402
|
-
# Reuse base initialization but narrow the return type for type-checkers
|
|
403
|
-
return cast(QueryResultWithInitial[T], super().__get__(obj, objtype))
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def bind_state(state: TState, fn: Callable[[TState, *Args], R]) -> Callable[[*Args], R]:
|
|
407
|
-
"Type-safe helper to bind a method to a state"
|
|
408
|
-
return fn.__get__(state, state.__class__)
|
|
File without changes
|
|
File without changes
|