pulse-framework 0.1.55__py3-none-any.whl → 0.1.57__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.
Files changed (70) hide show
  1. pulse/__init__.py +5 -6
  2. pulse/app.py +144 -57
  3. pulse/channel.py +139 -7
  4. pulse/cli/cmd.py +16 -2
  5. pulse/codegen/codegen.py +43 -12
  6. pulse/component.py +104 -0
  7. pulse/components/for_.py +30 -4
  8. pulse/components/if_.py +28 -5
  9. pulse/components/react_router.py +61 -3
  10. pulse/context.py +39 -5
  11. pulse/cookies.py +108 -4
  12. pulse/decorators.py +193 -24
  13. pulse/env.py +56 -2
  14. pulse/form.py +198 -5
  15. pulse/helpers.py +7 -1
  16. pulse/hooks/core.py +135 -5
  17. pulse/hooks/effects.py +61 -77
  18. pulse/hooks/init.py +60 -1
  19. pulse/hooks/runtime.py +241 -0
  20. pulse/hooks/setup.py +77 -0
  21. pulse/hooks/stable.py +58 -1
  22. pulse/hooks/state.py +107 -20
  23. pulse/js/__init__.py +40 -24
  24. pulse/js/array.py +9 -6
  25. pulse/js/console.py +15 -12
  26. pulse/js/date.py +9 -6
  27. pulse/js/document.py +5 -2
  28. pulse/js/error.py +7 -4
  29. pulse/js/json.py +9 -6
  30. pulse/js/map.py +8 -5
  31. pulse/js/math.py +9 -6
  32. pulse/js/navigator.py +5 -2
  33. pulse/js/number.py +9 -6
  34. pulse/js/obj.py +16 -13
  35. pulse/js/object.py +9 -6
  36. pulse/js/promise.py +19 -13
  37. pulse/js/pulse.py +28 -25
  38. pulse/js/react.py +94 -55
  39. pulse/js/regexp.py +7 -4
  40. pulse/js/set.py +8 -5
  41. pulse/js/string.py +9 -6
  42. pulse/js/weakmap.py +8 -5
  43. pulse/js/weakset.py +8 -5
  44. pulse/js/window.py +6 -3
  45. pulse/messages.py +5 -0
  46. pulse/middleware.py +147 -76
  47. pulse/plugin.py +76 -5
  48. pulse/queries/client.py +186 -39
  49. pulse/queries/common.py +52 -3
  50. pulse/queries/infinite_query.py +154 -2
  51. pulse/queries/mutation.py +127 -7
  52. pulse/queries/query.py +112 -11
  53. pulse/react_component.py +66 -3
  54. pulse/reactive.py +314 -30
  55. pulse/reactive_extensions.py +106 -26
  56. pulse/render_session.py +304 -173
  57. pulse/request.py +46 -11
  58. pulse/routing.py +140 -4
  59. pulse/serializer.py +71 -0
  60. pulse/state.py +177 -9
  61. pulse/test_helpers.py +15 -0
  62. pulse/transpiler/__init__.py +0 -3
  63. pulse/transpiler/py_module.py +1 -7
  64. pulse/user_session.py +119 -18
  65. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/METADATA +5 -5
  66. pulse_framework-0.1.57.dist-info/RECORD +127 -0
  67. pulse/transpiler/react_component.py +0 -44
  68. pulse_framework-0.1.55.dist-info/RECORD +0 -127
  69. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/WHEEL +0 -0
  70. {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.57.dist-info}/entry_points.txt +0 -0
pulse/queries/mutation.py CHANGED
@@ -22,9 +22,28 @@ P = ParamSpec("P")
22
22
 
23
23
 
24
24
  class MutationResult(Generic[T, P]):
25
- """
26
- Result object for mutations that provides reactive access to mutation state
27
- and is callable to execute the mutation.
25
+ """Result object for mutations providing reactive state and execution.
26
+
27
+ MutationResult wraps an async mutation function and provides reactive
28
+ access to its execution state. It is callable to execute the mutation.
29
+
30
+ Attributes:
31
+ data: The last successful mutation result, or None.
32
+ is_running: Whether the mutation is currently executing.
33
+ error: The last error encountered, or None.
34
+
35
+ Example:
36
+
37
+ ```python
38
+ # Access mutation state
39
+ if state.update_name.is_running:
40
+ show_loading()
41
+ if state.update_name.error:
42
+ show_error(state.update_name.error)
43
+
44
+ # Execute mutation
45
+ result = await state.update_name("New Name")
46
+ ```
28
47
  """
29
48
 
30
49
  _data: Signal[T | None]
@@ -40,6 +59,13 @@ class MutationResult(Generic[T, P]):
40
59
  on_success: Callable[[T], Any] | None = None,
41
60
  on_error: Callable[[Exception], Any] | None = None,
42
61
  ):
62
+ """Initialize the mutation result.
63
+
64
+ Args:
65
+ fn: The bound async function to execute.
66
+ on_success: Optional callback invoked on successful completion.
67
+ on_error: Optional callback invoked on error.
68
+ """
43
69
  self._data = Signal(None, name="mutation.data")
44
70
  self._is_running = Signal(False, name="mutation.is_running")
45
71
  self._error = Signal(None, name="mutation.error")
@@ -49,14 +75,17 @@ class MutationResult(Generic[T, P]):
49
75
 
50
76
  @property
51
77
  def data(self) -> T | None:
78
+ """The last successful mutation result, or None if never completed."""
52
79
  return self._data()
53
80
 
54
81
  @property
55
82
  def is_running(self) -> bool:
83
+ """Whether the mutation is currently executing."""
56
84
  return self._is_running()
57
85
 
58
86
  @property
59
87
  def error(self) -> Exception | None:
88
+ """The last error encountered, or None if no error."""
60
89
  return self._error()
61
90
 
62
91
  async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
@@ -78,6 +107,37 @@ class MutationResult(Generic[T, P]):
78
107
 
79
108
 
80
109
  class MutationProperty(Generic[T, TState, P], InitializableProperty):
110
+ """Descriptor for state-bound mutations created by the @mutation decorator.
111
+
112
+ MutationProperty is the return type of the ``@mutation`` decorator. It acts
113
+ as a descriptor that creates and manages MutationResult instances for each
114
+ State object.
115
+
116
+ When accessed on a State instance, returns a MutationResult that can be
117
+ called to execute the mutation and provides reactive state properties.
118
+
119
+ Supports additional decorators for customization:
120
+ - ``@mutation_prop.on_success``: Handle successful mutation.
121
+ - ``@mutation_prop.on_error``: Handle mutation errors.
122
+
123
+ Example:
124
+
125
+ ```python
126
+ class UserState(ps.State):
127
+ @ps.mutation
128
+ async def update_name(self, name: str) -> User:
129
+ return await api.update_user(name=name)
130
+
131
+ @update_name.on_success
132
+ def _on_success(self, data: User):
133
+ self.user.invalidate() # Refresh user query
134
+
135
+ @update_name.on_error
136
+ def _on_error(self, error: Exception):
137
+ logger.error(f"Update failed: {error}")
138
+ ```
139
+ """
140
+
81
141
  _on_success_fn: Callable[[TState, T], Any] | None
82
142
  _on_error_fn: Callable[[TState, Exception], Any] | None
83
143
  name: str
@@ -90,13 +150,28 @@ class MutationProperty(Generic[T, TState, P], InitializableProperty):
90
150
  on_success: OnSuccessFn[TState, T] | None = None,
91
151
  on_error: OnErrorFn[TState] | None = None,
92
152
  ):
153
+ """Initialize the mutation property.
154
+
155
+ Args:
156
+ name: The method name.
157
+ fn: The async method to wrap.
158
+ on_success: Optional success callback.
159
+ on_error: Optional error callback.
160
+ """
93
161
  self.name = name
94
162
  self.fn = fn
95
163
  self._on_success_fn = on_success # pyright: ignore[reportAttributeAccessIssue]
96
164
  self._on_error_fn = on_error # pyright: ignore[reportAttributeAccessIssue]
97
165
 
98
- # Decorator to attach an on-success handler (sync or async)
99
- def on_success(self, fn: OnSuccessFn[TState, T]):
166
+ def on_success(self, fn: OnSuccessFn[TState, T]) -> OnSuccessFn[TState, T]:
167
+ """Decorator to attach an on-success handler (sync or async).
168
+
169
+ Args:
170
+ fn: Callback receiving (self, data) on successful mutation.
171
+
172
+ Returns:
173
+ The callback function unchanged.
174
+ """
100
175
  if self._on_success_fn is not None:
101
176
  raise RuntimeError(
102
177
  f"Duplicate on_success() decorator for mutation '{self.name}'. Only one is allowed."
@@ -104,8 +179,15 @@ class MutationProperty(Generic[T, TState, P], InitializableProperty):
104
179
  self._on_success_fn = fn # pyright: ignore[reportAttributeAccessIssue]
105
180
  return fn
106
181
 
107
- # Decorator to attach an on-error handler (sync or async)
108
- def on_error(self, fn: OnErrorFn[TState]):
182
+ def on_error(self, fn: OnErrorFn[TState]) -> OnErrorFn[TState]:
183
+ """Decorator to attach an on-error handler (sync or async).
184
+
185
+ Args:
186
+ fn: Callback receiving (self, error) on mutation failure.
187
+
188
+ Returns:
189
+ The callback function unchanged.
190
+ """
109
191
  if self._on_error_fn is not None:
110
192
  raise RuntimeError(
111
193
  f"Duplicate on_error() decorator for mutation '{self.name}'. Only one is allowed."
@@ -160,7 +242,45 @@ def mutation(
160
242
 
161
243
  def mutation(
162
244
  fn: Callable[Concatenate[TState, P], Awaitable[T]] | None = None,
245
+ ) -> (
246
+ MutationProperty[T, TState, P]
247
+ | Callable[
248
+ [Callable[Concatenate[TState, P], Awaitable[T]]],
249
+ MutationProperty[T, TState, P],
250
+ ]
163
251
  ):
252
+ """Decorator for async mutations (write operations) on State methods.
253
+
254
+ Creates a mutation wrapper that tracks execution state and provides
255
+ callbacks for success/error handling. Unlike queries, mutations are
256
+ not cached and must be explicitly called.
257
+
258
+ Args:
259
+ fn: The async method to decorate (when used without parentheses).
260
+
261
+ Returns:
262
+ MutationProperty that creates MutationResult instances when accessed.
263
+
264
+ Example:
265
+
266
+ ```python
267
+ class UserState(ps.State):
268
+ @ps.mutation
269
+ async def update_name(self, name: str) -> User:
270
+ return await api.update_user(name=name)
271
+
272
+ @update_name.on_success
273
+ def _on_success(self, data: User):
274
+ self.user.invalidate()
275
+ ```
276
+
277
+ Calling the mutation:
278
+
279
+ ```python
280
+ result = await state.update_name("New Name")
281
+ ```
282
+ """
283
+
164
284
  def decorator(func: Callable[Concatenate[TState, P], Awaitable[T]], /):
165
285
  sig = inspect.signature(func)
166
286
  params = list(sig.parameters.values())
pulse/queries/query.py CHANGED
@@ -48,6 +48,20 @@ RETRY_DELAY_DEFAULT = 2.0 if not is_pytest() else 0.01
48
48
 
49
49
  @dataclass(slots=True)
50
50
  class QueryConfig(Generic[T]):
51
+ """Configuration options for a query.
52
+
53
+ Stores immutable configuration that controls query behavior including
54
+ retry logic, caching, and lifecycle callbacks.
55
+
56
+ Attributes:
57
+ retries: Number of retry attempts on failure (default 3).
58
+ retry_delay: Delay in seconds between retry attempts (default 2.0).
59
+ initial_data: Initial data value or factory function.
60
+ initial_data_updated_at: Timestamp for initial data staleness calculation.
61
+ gc_time: Seconds to keep unused query in cache before garbage collection.
62
+ on_dispose: Callback invoked when query is disposed.
63
+ """
64
+
51
65
  retries: int
52
66
  retry_delay: float
53
67
  initial_data: T | Callable[[], T] | None
@@ -57,9 +71,20 @@ class QueryConfig(Generic[T]):
57
71
 
58
72
 
59
73
  class QueryState(Generic[T]):
60
- """
61
- Container for query state signals and manipulation methods.
74
+ """Container for query state signals and manipulation methods.
75
+
76
+ Manages reactive signals for query data, status, errors, and retry state.
62
77
  Used by both KeyedQuery and UnkeyedQuery via composition.
78
+
79
+ Attributes:
80
+ cfg: Query configuration options.
81
+ data: Signal containing the fetched data or None.
82
+ error: Signal containing the last error or None.
83
+ last_updated: Signal with timestamp of last successful update.
84
+ status: Signal with current QueryStatus ("loading", "success", "error").
85
+ is_fetching: Signal indicating if a fetch is in progress.
86
+ retries: Signal with current retry attempt count.
87
+ retry_reason: Signal with exception from last failed retry.
63
88
  """
64
89
 
65
90
  cfg: QueryConfig[T]
@@ -900,17 +925,38 @@ class KeyedQueryResult(Generic[T], Disposable):
900
925
 
901
926
 
902
927
  class QueryProperty(Generic[T, TState], InitializableProperty):
903
- """
904
- Descriptor for state-bound queries.
928
+ """Descriptor for state-bound queries created by the @query decorator.
929
+
930
+ QueryProperty is the return type of the ``@query`` decorator. It acts as a
931
+ descriptor that creates and manages query instances for each State object.
932
+
933
+ When accessed on a State instance, returns a QueryResult with reactive
934
+ properties (data, status, error) and methods (refetch, invalidate, etc.).
935
+
936
+ Supports additional decorators for customization:
937
+ - ``@query_prop.key``: Define dynamic query key for sharing.
938
+ - ``@query_prop.initial_data``: Provide initial/placeholder data.
939
+ - ``@query_prop.on_success``: Handle successful fetch.
940
+ - ``@query_prop.on_error``: Handle fetch errors.
905
941
 
906
- Usage:
907
- class S(ps.State):
908
- @ps.query()
909
- async def user(self) -> User: ...
942
+ Example:
910
943
 
911
- @user.key
912
- def _user_key(self):
913
- return ("user", self.user_id)
944
+ ```python
945
+ class UserState(ps.State):
946
+ user_id: str = ""
947
+
948
+ @ps.query
949
+ async def user(self) -> User:
950
+ return await api.get_user(self.user_id)
951
+
952
+ @user.key
953
+ def _user_key(self):
954
+ return ("user", self.user_id)
955
+
956
+ @user.on_success
957
+ def _on_user_loaded(self, data: User):
958
+ print(f"Loaded user: {data.name}")
959
+ ```
914
960
  """
915
961
 
916
962
  name: str
@@ -1183,7 +1229,62 @@ def query(
1183
1229
  enabled: bool = True,
1184
1230
  fetch_on_mount: bool = True,
1185
1231
  key: QueryKey | None = None,
1232
+ ) -> (
1233
+ QueryProperty[T, TState]
1234
+ | Callable[[Callable[[TState], Awaitable[T]]], QueryProperty[T, TState]]
1186
1235
  ):
1236
+ """Decorator for async data fetching on State methods.
1237
+
1238
+ Creates a reactive query that automatically fetches data, handles loading
1239
+ states, retries on failure, and caches results. Queries can be shared
1240
+ across components using keys.
1241
+
1242
+ Args:
1243
+ fn: The async method to decorate (when used without parentheses).
1244
+ stale_time: Seconds before data is considered stale (default 0.0).
1245
+ gc_time: Seconds to keep unused query in cache (default 300.0, None to disable).
1246
+ refetch_interval: Auto-refetch interval in seconds (default None, disabled).
1247
+ keep_previous_data: Keep previous data while refetching (default False).
1248
+ retries: Number of retry attempts on failure (default 3).
1249
+ retry_delay: Delay between retries in seconds (default 2.0).
1250
+ initial_data_updated_at: Timestamp for initial data staleness calculation.
1251
+ enabled: Whether query is enabled (default True).
1252
+ fetch_on_mount: Fetch when component mounts (default True).
1253
+ key: Static query key for sharing across instances.
1254
+
1255
+ Returns:
1256
+ QueryProperty that creates QueryResult instances when accessed.
1257
+
1258
+ Example:
1259
+
1260
+ Basic usage:
1261
+
1262
+ ```python
1263
+ class UserState(ps.State):
1264
+ user_id: str = ""
1265
+
1266
+ @ps.query
1267
+ async def user(self) -> User:
1268
+ return await api.get_user(self.user_id)
1269
+ ```
1270
+
1271
+ With options:
1272
+
1273
+ ```python
1274
+ @ps.query(stale_time=60, refetch_interval=300)
1275
+ async def user(self) -> User:
1276
+ return await api.get_user(self.user_id)
1277
+ ```
1278
+
1279
+ Keyed query (shared across instances):
1280
+
1281
+ ```python
1282
+ @ps.query(key=("users", "current"))
1283
+ async def current_user(self) -> User:
1284
+ return await api.get_current_user()
1285
+ ```
1286
+ """
1287
+
1187
1288
  def decorator(
1188
1289
  func: Callable[[TState], Awaitable[T]], /
1189
1290
  ) -> QueryProperty[T, TState]:
pulse/react_component.py CHANGED
@@ -1,5 +1,68 @@
1
- """Thin wrappers for transpiler react_component integration."""
1
+ """React component helpers for Python API."""
2
2
 
3
- from pulse.transpiler.react_component import default_signature, react_component
3
+ from __future__ import annotations
4
4
 
5
- __all__ = ["react_component", "default_signature"]
5
+ from collections.abc import Callable
6
+ from typing import Any, ParamSpec, overload
7
+
8
+ from pulse.transpiler.imports import Import
9
+ from pulse.transpiler.nodes import Element, Expr, Jsx, Node
10
+
11
+ P = ParamSpec("P")
12
+
13
+
14
+ def default_signature(
15
+ *children: Node, key: str | None = None, **props: Any
16
+ ) -> Element: ...
17
+
18
+
19
+ class ReactComponent(Jsx):
20
+ """JSX wrapper for React components with runtime call support."""
21
+
22
+ def __init__(self, expr: Expr) -> None:
23
+ if not isinstance(expr, Expr):
24
+ raise TypeError("ReactComponent expects an Expr")
25
+ if isinstance(expr, Jsx):
26
+ expr = expr.expr
27
+ super().__init__(expr)
28
+
29
+
30
+ @overload
31
+ def react_component(
32
+ expr_or_name: Expr,
33
+ ) -> Callable[[Callable[P, Any]], Callable[P, Element]]: ...
34
+
35
+
36
+ @overload
37
+ def react_component(
38
+ expr_or_name: str, src: str, *, lazy: bool = False
39
+ ) -> Callable[[Callable[P, Any]], Callable[P, Element]]: ...
40
+
41
+
42
+ def react_component(
43
+ expr_or_name: Expr | str,
44
+ src: str | None = None,
45
+ *,
46
+ lazy: bool = False,
47
+ ) -> Callable[[Callable[P, Any]], Callable[P, Element]]:
48
+ """Decorator for typed React component bindings."""
49
+ if isinstance(expr_or_name, Expr):
50
+ if src is not None:
51
+ raise TypeError("react_component expects (expr) or (name, src)")
52
+ if lazy:
53
+ raise TypeError("react_component lazy only supported with (name, src)")
54
+ component = ReactComponent(expr_or_name)
55
+ elif isinstance(expr_or_name, str):
56
+ if src is None:
57
+ raise TypeError("react_component expects (name, src)")
58
+ component = ReactComponent(Import(expr_or_name, src, lazy=lazy))
59
+ else:
60
+ raise TypeError("react_component expects an Expr or (name, src)")
61
+
62
+ def decorator(fn: Callable[P, Any]) -> Callable[P, Element]:
63
+ return component.as_(fn)
64
+
65
+ return decorator
66
+
67
+
68
+ __all__ = ["ReactComponent", "react_component", "default_signature"]