pulse-framework 0.1.62__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 (126) hide show
  1. pulse/__init__.py +1493 -0
  2. pulse/_examples.py +29 -0
  3. pulse/app.py +1086 -0
  4. pulse/channel.py +607 -0
  5. pulse/cli/__init__.py +0 -0
  6. pulse/cli/cmd.py +575 -0
  7. pulse/cli/dependencies.py +181 -0
  8. pulse/cli/folder_lock.py +134 -0
  9. pulse/cli/helpers.py +271 -0
  10. pulse/cli/logging.py +102 -0
  11. pulse/cli/models.py +35 -0
  12. pulse/cli/packages.py +262 -0
  13. pulse/cli/processes.py +292 -0
  14. pulse/cli/secrets.py +39 -0
  15. pulse/cli/uvicorn_log_config.py +87 -0
  16. pulse/code_analysis.py +38 -0
  17. pulse/codegen/__init__.py +0 -0
  18. pulse/codegen/codegen.py +359 -0
  19. pulse/codegen/templates/__init__.py +0 -0
  20. pulse/codegen/templates/layout.py +106 -0
  21. pulse/codegen/templates/route.py +345 -0
  22. pulse/codegen/templates/routes_ts.py +42 -0
  23. pulse/codegen/utils.py +20 -0
  24. pulse/component.py +237 -0
  25. pulse/components/__init__.py +0 -0
  26. pulse/components/for_.py +83 -0
  27. pulse/components/if_.py +86 -0
  28. pulse/components/react_router.py +94 -0
  29. pulse/context.py +108 -0
  30. pulse/cookies.py +322 -0
  31. pulse/decorators.py +344 -0
  32. pulse/dom/__init__.py +0 -0
  33. pulse/dom/elements.py +1024 -0
  34. pulse/dom/events.py +445 -0
  35. pulse/dom/props.py +1250 -0
  36. pulse/dom/svg.py +0 -0
  37. pulse/dom/tags.py +328 -0
  38. pulse/dom/tags.pyi +480 -0
  39. pulse/env.py +178 -0
  40. pulse/form.py +538 -0
  41. pulse/helpers.py +541 -0
  42. pulse/hooks/__init__.py +0 -0
  43. pulse/hooks/core.py +452 -0
  44. pulse/hooks/effects.py +88 -0
  45. pulse/hooks/init.py +668 -0
  46. pulse/hooks/runtime.py +464 -0
  47. pulse/hooks/setup.py +254 -0
  48. pulse/hooks/stable.py +138 -0
  49. pulse/hooks/state.py +192 -0
  50. pulse/js/__init__.py +125 -0
  51. pulse/js/__init__.pyi +115 -0
  52. pulse/js/_types.py +299 -0
  53. pulse/js/array.py +339 -0
  54. pulse/js/console.py +50 -0
  55. pulse/js/date.py +119 -0
  56. pulse/js/document.py +145 -0
  57. pulse/js/error.py +140 -0
  58. pulse/js/json.py +66 -0
  59. pulse/js/map.py +97 -0
  60. pulse/js/math.py +69 -0
  61. pulse/js/navigator.py +79 -0
  62. pulse/js/number.py +57 -0
  63. pulse/js/obj.py +81 -0
  64. pulse/js/object.py +172 -0
  65. pulse/js/promise.py +172 -0
  66. pulse/js/pulse.py +115 -0
  67. pulse/js/react.py +495 -0
  68. pulse/js/regexp.py +57 -0
  69. pulse/js/set.py +124 -0
  70. pulse/js/string.py +38 -0
  71. pulse/js/weakmap.py +53 -0
  72. pulse/js/weakset.py +48 -0
  73. pulse/js/window.py +205 -0
  74. pulse/messages.py +202 -0
  75. pulse/middleware.py +471 -0
  76. pulse/plugin.py +96 -0
  77. pulse/proxy.py +242 -0
  78. pulse/py.typed +0 -0
  79. pulse/queries/__init__.py +0 -0
  80. pulse/queries/client.py +609 -0
  81. pulse/queries/common.py +101 -0
  82. pulse/queries/effect.py +55 -0
  83. pulse/queries/infinite_query.py +1418 -0
  84. pulse/queries/mutation.py +295 -0
  85. pulse/queries/protocol.py +136 -0
  86. pulse/queries/query.py +1314 -0
  87. pulse/queries/store.py +120 -0
  88. pulse/react_component.py +88 -0
  89. pulse/reactive.py +1208 -0
  90. pulse/reactive_extensions.py +1172 -0
  91. pulse/render_session.py +768 -0
  92. pulse/renderer.py +584 -0
  93. pulse/request.py +205 -0
  94. pulse/routing.py +598 -0
  95. pulse/serializer.py +279 -0
  96. pulse/state.py +556 -0
  97. pulse/test_helpers.py +15 -0
  98. pulse/transpiler/__init__.py +111 -0
  99. pulse/transpiler/assets.py +81 -0
  100. pulse/transpiler/builtins.py +1029 -0
  101. pulse/transpiler/dynamic_import.py +130 -0
  102. pulse/transpiler/emit_context.py +49 -0
  103. pulse/transpiler/errors.py +96 -0
  104. pulse/transpiler/function.py +611 -0
  105. pulse/transpiler/id.py +18 -0
  106. pulse/transpiler/imports.py +341 -0
  107. pulse/transpiler/js_module.py +336 -0
  108. pulse/transpiler/modules/__init__.py +33 -0
  109. pulse/transpiler/modules/asyncio.py +57 -0
  110. pulse/transpiler/modules/json.py +24 -0
  111. pulse/transpiler/modules/math.py +265 -0
  112. pulse/transpiler/modules/pulse/__init__.py +5 -0
  113. pulse/transpiler/modules/pulse/tags.py +250 -0
  114. pulse/transpiler/modules/typing.py +63 -0
  115. pulse/transpiler/nodes.py +1987 -0
  116. pulse/transpiler/py_module.py +135 -0
  117. pulse/transpiler/transpiler.py +1100 -0
  118. pulse/transpiler/vdom.py +256 -0
  119. pulse/types/__init__.py +0 -0
  120. pulse/types/event_handler.py +50 -0
  121. pulse/user_session.py +386 -0
  122. pulse/version.py +69 -0
  123. pulse_framework-0.1.62.dist-info/METADATA +198 -0
  124. pulse_framework-0.1.62.dist-info/RECORD +126 -0
  125. pulse_framework-0.1.62.dist-info/WHEEL +4 -0
  126. pulse_framework-0.1.62.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,295 @@
1
+ import inspect
2
+ from collections.abc import Awaitable, Callable
3
+ from typing import (
4
+ Any,
5
+ Concatenate,
6
+ Generic,
7
+ ParamSpec,
8
+ TypeVar,
9
+ overload,
10
+ override,
11
+ )
12
+
13
+ from pulse.helpers import call_flexible, maybe_await
14
+ from pulse.queries.common import OnErrorFn, OnSuccessFn, bind_state
15
+ from pulse.reactive import Signal
16
+ from pulse.state import InitializableProperty, State
17
+
18
+ T = TypeVar("T")
19
+ TState = TypeVar("TState", bound=State)
20
+ R = TypeVar("R")
21
+ P = ParamSpec("P")
22
+
23
+
24
+ class MutationResult(Generic[T, P]):
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
+ ```
47
+ """
48
+
49
+ _data: Signal[T | None]
50
+ _is_running: Signal[bool]
51
+ _error: Signal[Exception | None]
52
+ _fn: Callable[P, Awaitable[T]]
53
+ _on_success: Callable[[T], Any] | None
54
+ _on_error: Callable[[Exception], Any] | None
55
+
56
+ def __init__(
57
+ self,
58
+ fn: Callable[P, Awaitable[T]],
59
+ on_success: Callable[[T], Any] | None = None,
60
+ on_error: Callable[[Exception], Any] | None = None,
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
+ """
69
+ self._data = Signal(None, name="mutation.data")
70
+ self._is_running = Signal(False, name="mutation.is_running")
71
+ self._error = Signal(None, name="mutation.error")
72
+ self._fn = fn
73
+ self._on_success = on_success
74
+ self._on_error = on_error
75
+
76
+ @property
77
+ def data(self) -> T | None:
78
+ """The last successful mutation result, or None if never completed."""
79
+ return self._data()
80
+
81
+ @property
82
+ def is_running(self) -> bool:
83
+ """Whether the mutation is currently executing."""
84
+ return self._is_running()
85
+
86
+ @property
87
+ def error(self) -> Exception | None:
88
+ """The last error encountered, or None if no error."""
89
+ return self._error()
90
+
91
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
92
+ self._is_running.write(True)
93
+ self._error.write(None)
94
+ try:
95
+ mutation_result = await self._fn(*args, **kwargs)
96
+ self._data.write(mutation_result)
97
+ if self._on_success:
98
+ await maybe_await(call_flexible(self._on_success, mutation_result))
99
+ return mutation_result
100
+ except Exception as e:
101
+ self._error.write(e)
102
+ if self._on_error:
103
+ await maybe_await(call_flexible(self._on_error, e))
104
+ raise e
105
+ finally:
106
+ self._is_running.write(False)
107
+
108
+
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
+
141
+ _on_success_fn: Callable[[TState, T], Any] | None
142
+ _on_error_fn: Callable[[TState, Exception], Any] | None
143
+ name: str
144
+ fn: Callable[Concatenate[TState, P], Awaitable[T]]
145
+
146
+ def __init__(
147
+ self,
148
+ name: str,
149
+ fn: Callable[Concatenate[TState, P], Awaitable[T]],
150
+ on_success: OnSuccessFn[TState, T] | None = None,
151
+ on_error: OnErrorFn[TState] | None = None,
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
+ """
161
+ self.name = name
162
+ self.fn = fn
163
+ self._on_success_fn = on_success # pyright: ignore[reportAttributeAccessIssue]
164
+ self._on_error_fn = on_error # pyright: ignore[reportAttributeAccessIssue]
165
+
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
+ """
175
+ if self._on_success_fn is not None:
176
+ raise RuntimeError(
177
+ f"Duplicate on_success() decorator for mutation '{self.name}'. Only one is allowed."
178
+ )
179
+ self._on_success_fn = fn # pyright: ignore[reportAttributeAccessIssue]
180
+ return fn
181
+
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
+ """
191
+ if self._on_error_fn is not None:
192
+ raise RuntimeError(
193
+ f"Duplicate on_error() decorator for mutation '{self.name}'. Only one is allowed."
194
+ )
195
+ self._on_error_fn = fn # pyright: ignore[reportAttributeAccessIssue]
196
+ return fn
197
+
198
+ def __get__(self, obj: Any, objtype: Any = None) -> MutationResult[T, P]:
199
+ if obj is None:
200
+ return self # pyright: ignore[reportReturnType]
201
+
202
+ # Cache the result on the instance
203
+ cache_key = f"__mutation_{self.name}"
204
+ if not hasattr(obj, cache_key):
205
+ # Bind methods to state
206
+ bound_fn = bind_state(obj, self.fn)
207
+ bound_on_success = (
208
+ bind_state(obj, self._on_success_fn) if self._on_success_fn else None
209
+ )
210
+ bound_on_error = (
211
+ bind_state(obj, self._on_error_fn) if self._on_error_fn else None
212
+ )
213
+
214
+ result = MutationResult[T, P](
215
+ fn=bound_fn,
216
+ on_success=bound_on_success,
217
+ on_error=bound_on_error,
218
+ )
219
+ setattr(obj, cache_key, result)
220
+
221
+ return getattr(obj, cache_key)
222
+
223
+ @override
224
+ def initialize(self, state: State, name: str) -> MutationResult[T, P]:
225
+ # For compatibility with InitializableProperty, but mutations don't need special initialization
226
+ return self.__get__(state, state.__class__)
227
+
228
+
229
+ @overload
230
+ def mutation(
231
+ fn: Callable[Concatenate[TState, P], Awaitable[T]],
232
+ ) -> MutationProperty[T, TState, P]: ...
233
+
234
+
235
+ @overload
236
+ def mutation(
237
+ fn: None = None,
238
+ ) -> Callable[
239
+ [Callable[Concatenate[TState, P], Awaitable[T]]], MutationProperty[T, TState, P]
240
+ ]: ...
241
+
242
+
243
+ def mutation(
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
+ ]
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
+
284
+ def decorator(func: Callable[Concatenate[TState, P], Awaitable[T]], /):
285
+ sig = inspect.signature(func)
286
+ params = list(sig.parameters.values())
287
+
288
+ if len(params) == 0 or params[0].name != "self":
289
+ raise TypeError("@mutation method must have 'self' as first argument")
290
+
291
+ return MutationProperty(func.__name__, func)
292
+
293
+ if fn:
294
+ return decorator(fn)
295
+ return decorator
@@ -0,0 +1,136 @@
1
+ import datetime as dt
2
+ from collections.abc import Callable
3
+ from typing import Protocol, TypeVar, runtime_checkable
4
+
5
+ from pulse.queries.common import ActionResult, QueryStatus
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ @runtime_checkable
11
+ class QueryResult(Protocol[T]):
12
+ """
13
+ Unified query result interface for both keyed and unkeyed queries.
14
+
15
+ This protocol defines the public API that all query results expose,
16
+ regardless of whether they use keyed (cached/shared) or unkeyed
17
+ (dependency-tracked) execution strategies.
18
+
19
+ Keyed queries use a session-wide cache and explicit key functions to
20
+ determine when to refetch. Unkeyed queries automatically track reactive
21
+ dependencies and refetch when those dependencies change.
22
+ """
23
+
24
+ # Status properties
25
+ @property
26
+ def status(self) -> QueryStatus:
27
+ """Current query status: 'loading', 'success', or 'error'."""
28
+ ...
29
+
30
+ @property
31
+ def is_loading(self) -> bool:
32
+ """True if the query has not yet completed its initial fetch."""
33
+ ...
34
+
35
+ @property
36
+ def is_success(self) -> bool:
37
+ """True if the query completed successfully."""
38
+ ...
39
+
40
+ @property
41
+ def is_error(self) -> bool:
42
+ """True if the query completed with an error."""
43
+ ...
44
+
45
+ @property
46
+ def is_fetching(self) -> bool:
47
+ """True if a fetch is currently in progress (including refetches)."""
48
+ ...
49
+
50
+ @property
51
+ def is_scheduled(self) -> bool:
52
+ """True if a fetch is scheduled or currently running."""
53
+ ...
54
+
55
+ # Data properties
56
+ @property
57
+ def data(self) -> T | None:
58
+ """The query result data, or None if not yet available."""
59
+ ...
60
+
61
+ @property
62
+ def error(self) -> Exception | None:
63
+ """The error from the last fetch, or None if no error."""
64
+ ...
65
+
66
+ # Query operations
67
+ def is_stale(self) -> bool:
68
+ """Check if the query data is stale based on stale_time."""
69
+ ...
70
+
71
+ async def refetch(self, cancel_refetch: bool = True) -> ActionResult[T]:
72
+ """
73
+ Refetch the query data.
74
+
75
+ Args:
76
+ cancel_refetch: If True (default), cancels any in-flight request
77
+ before starting a new one. If False, deduplicates requests.
78
+
79
+ Returns:
80
+ ActionResult containing either the data or an error.
81
+ """
82
+ ...
83
+
84
+ async def wait(self) -> ActionResult[T]:
85
+ """
86
+ Wait for the current fetch to complete.
87
+
88
+ Returns:
89
+ ActionResult containing either the data or an error.
90
+ """
91
+ ...
92
+
93
+ def invalidate(self) -> None:
94
+ """Mark the query as stale and trigger a refetch if observed."""
95
+ ...
96
+
97
+ # Data manipulation
98
+ def set_data(self, data: T | Callable[[T | None], T]) -> None:
99
+ """
100
+ Optimistically set data without changing loading/error state.
101
+
102
+ Args:
103
+ data: The new data value, or a function that receives the current
104
+ data and returns the new data.
105
+ """
106
+ ...
107
+
108
+ def set_initial_data(
109
+ self,
110
+ data: T | Callable[[], T],
111
+ *,
112
+ updated_at: float | dt.datetime | None = None,
113
+ ) -> None:
114
+ """
115
+ Set data as if it were provided as initial_data.
116
+
117
+ Only takes effect if the query is still in 'loading' state.
118
+
119
+ Args:
120
+ data: The initial data value, or a function that returns it.
121
+ updated_at: Optional timestamp to seed staleness calculations.
122
+ """
123
+ ...
124
+
125
+ def set_error(self, error: Exception) -> None:
126
+ """Set error state on the query."""
127
+ ...
128
+
129
+ # Enable/disable
130
+ def enable(self) -> None:
131
+ """Enable the query, allowing it to fetch."""
132
+ ...
133
+
134
+ def disable(self) -> None:
135
+ """Disable the query, preventing it from fetching."""
136
+ ...