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
pulse/decorators.py ADDED
@@ -0,0 +1,344 @@
1
+ # Separate file from reactive.py due to needing to import from state too
2
+
3
+ import inspect
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any, ParamSpec, Protocol, TypeVar, cast, overload
6
+
7
+ from pulse.hooks.core import HOOK_CONTEXT
8
+ from pulse.hooks.effects import inline_effect_hook
9
+ from pulse.hooks.state import collect_component_identity
10
+ from pulse.reactive import (
11
+ AsyncEffect,
12
+ AsyncEffectFn,
13
+ Computed,
14
+ Effect,
15
+ EffectCleanup,
16
+ EffectFn,
17
+ Signal,
18
+ )
19
+ from pulse.state import ComputedProperty, State, StateEffect
20
+
21
+ T = TypeVar("T")
22
+ TState = TypeVar("TState", bound=State)
23
+ P = ParamSpec("P")
24
+
25
+
26
+ @overload
27
+ def computed(fn: Callable[[], T], *, name: str | None = None) -> Computed[T]: ...
28
+ @overload
29
+ def computed(
30
+ fn: Callable[[TState], T], *, name: str | None = None
31
+ ) -> ComputedProperty[T]: ...
32
+ @overload
33
+ def computed(
34
+ fn: None = None, *, name: str | None = None
35
+ ) -> Callable[[Callable[[], T]], Computed[T]]: ...
36
+
37
+
38
+ def computed(
39
+ fn: Callable[..., Any] | None = None,
40
+ *,
41
+ name: str | None = None,
42
+ ) -> (
43
+ Computed[T]
44
+ | ComputedProperty[T]
45
+ | Callable[[Callable[..., Any]], Computed[T] | ComputedProperty[T]]
46
+ ):
47
+ """
48
+ Decorator for computed (derived) properties.
49
+
50
+ Creates a cached, reactive value that automatically recalculates when its
51
+ dependencies change. The computed tracks which Signals/Computeds are read
52
+ during execution and subscribes to them.
53
+
54
+ Can be used in two ways:
55
+ 1. On a State method (with single `self` argument) - creates a ComputedProperty
56
+ 2. As a standalone function (with no arguments) - creates a Computed
57
+
58
+ Args:
59
+ fn: The function to compute the value. Must take no arguments (standalone) or only `self` (State method).
60
+ name: Optional debug name for the computed. Defaults to the function name.
61
+
62
+ Returns:
63
+ Computed wrapper or decorator depending on usage.
64
+
65
+ Raises:
66
+ TypeError: If the function takes arguments other than `self`.
67
+
68
+ Example:
69
+ On a State method:
70
+
71
+ class MyState(ps.State):
72
+ count: int = 0
73
+
74
+ @ps.computed
75
+ def doubled(self):
76
+ return self.count * 2
77
+
78
+ As a standalone computed:
79
+
80
+ signal = Signal(5)
81
+
82
+ @ps.computed
83
+ def doubled():
84
+ return signal() * 2
85
+
86
+ With explicit name:
87
+
88
+ @ps.computed(name="my_computed")
89
+ def doubled(self):
90
+ return self.count * 2
91
+ """
92
+
93
+ # The type checker is not happy if I don't specify the `/` here.
94
+ def decorator(fn: Callable[..., Any], /):
95
+ sig = inspect.signature(fn)
96
+ params = list(sig.parameters.values())
97
+ # Check if it's a method with exactly one argument called 'self'
98
+ if len(params) == 1 and params[0].name == "self":
99
+ return ComputedProperty(fn.__name__, fn)
100
+ # If it has any arguments at all, it's not allowed (except for 'self')
101
+ if len(params) > 0:
102
+ raise TypeError(
103
+ f"@computed: Function '{fn.__name__}' must take no arguments or a single 'self' argument"
104
+ )
105
+ return Computed(fn, name=name or fn.__name__)
106
+
107
+ if fn is not None:
108
+ return decorator(fn)
109
+ else:
110
+ return decorator
111
+
112
+
113
+ StateEffectFn = Callable[[TState], EffectCleanup | None]
114
+ AsyncStateEffectFn = Callable[[TState], Awaitable[EffectCleanup | None]]
115
+
116
+
117
+ class EffectBuilder(Protocol):
118
+ @overload
119
+ def __call__(self, fn: EffectFn | StateEffectFn[Any]) -> Effect: ...
120
+ @overload
121
+ def __call__(self, fn: AsyncEffectFn | AsyncStateEffectFn[Any]) -> AsyncEffect: ...
122
+ def __call__(
123
+ self,
124
+ fn: EffectFn | StateEffectFn[Any] | AsyncEffectFn | AsyncStateEffectFn[Any],
125
+ ) -> Effect | AsyncEffect: ...
126
+
127
+
128
+ @overload
129
+ def effect(
130
+ fn: EffectFn,
131
+ *,
132
+ name: str | None = None,
133
+ immediate: bool = False,
134
+ lazy: bool = False,
135
+ on_error: Callable[[Exception], None] | None = None,
136
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
137
+ update_deps: bool | None = None,
138
+ interval: float | None = None,
139
+ key: str | None = None,
140
+ ) -> Effect: ...
141
+
142
+
143
+ @overload
144
+ def effect(
145
+ fn: AsyncEffectFn,
146
+ *,
147
+ name: str | None = None,
148
+ immediate: bool = False,
149
+ lazy: bool = False,
150
+ on_error: Callable[[Exception], None] | None = None,
151
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
152
+ update_deps: bool | None = None,
153
+ interval: float | None = None,
154
+ key: str | None = None,
155
+ ) -> AsyncEffect: ...
156
+ # In practice this overload returns a StateEffect, but it gets converted into an
157
+ # Effect at state instantiation.
158
+ @overload
159
+ def effect(fn: StateEffectFn[Any]) -> Effect: ...
160
+ @overload
161
+ def effect(fn: AsyncStateEffectFn[Any]) -> AsyncEffect: ...
162
+ @overload
163
+ def effect(
164
+ fn: None = None,
165
+ *,
166
+ name: str | None = None,
167
+ immediate: bool = False,
168
+ lazy: bool = False,
169
+ on_error: Callable[[Exception], None] | None = None,
170
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
171
+ update_deps: bool | None = None,
172
+ interval: float | None = None,
173
+ key: str | None = None,
174
+ ) -> EffectBuilder: ...
175
+
176
+
177
+ def effect(
178
+ fn: Callable[..., Any] | None = None,
179
+ *,
180
+ name: str | None = None,
181
+ immediate: bool = False,
182
+ lazy: bool = False,
183
+ on_error: Callable[[Exception], None] | None = None,
184
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
185
+ update_deps: bool | None = None,
186
+ interval: float | None = None,
187
+ key: str | None = None,
188
+ ):
189
+ """
190
+ Decorator for side effects that run when dependencies change.
191
+
192
+ Creates an effect that automatically re-runs when any of its tracked
193
+ dependencies change. Dependencies are automatically tracked by observing
194
+ which Signals/Computeds are read during execution.
195
+
196
+ Can be used in two ways:
197
+ 1. On a State method (with single `self` argument) - creates a StateEffect
198
+ 2. As a standalone function (with no arguments) - creates an Effect
199
+
200
+ Supports both sync and async functions. Async effects cannot use `immediate=True`.
201
+
202
+ Args:
203
+ fn: The effect function. Must take no arguments (standalone) or only
204
+ `self` (State method). Can return a cleanup function.
205
+ name: Optional debug name. Defaults to "ClassName.method_name" or function name.
206
+ immediate: If True, run synchronously when scheduled instead of batching.
207
+ Only valid for sync effects.
208
+ lazy: If True, don't run on creation; wait for first dependency change.
209
+ on_error: Callback invoked if the effect throws an exception.
210
+ deps: Explicit list of dependencies. If provided, auto-tracking is disabled
211
+ and the effect only re-runs when these specific dependencies change.
212
+ interval: Re-run interval in seconds. Creates a polling effect that runs
213
+ periodically regardless of dependency changes.
214
+
215
+ Returns:
216
+ Effect, AsyncEffect, or StateEffect depending on usage.
217
+
218
+ Raises:
219
+ TypeError: If the function takes arguments other than `self`.
220
+ ValueError: If `immediate=True` is used with an async function.
221
+
222
+ Example:
223
+ State method effect:
224
+
225
+ class MyState(ps.State):
226
+ count: int = 0
227
+
228
+ @ps.effect
229
+ def log_changes(self):
230
+ print(f"Count is {self.count}")
231
+
232
+ Async effect:
233
+
234
+ class MyState(ps.State):
235
+ query: str = ""
236
+
237
+ @ps.effect
238
+ async def fetch_data(self):
239
+ data = await api.fetch(self.query)
240
+ self.data = data
241
+
242
+ Effect with cleanup:
243
+
244
+ @ps.effect
245
+ def subscribe(self):
246
+ unsub = event_bus.subscribe(self.handle)
247
+ return unsub # Called before next run or on dispose
248
+
249
+ Polling effect:
250
+
251
+ @ps.effect(interval=5.0)
252
+ async def poll_status(self):
253
+ self.status = await api.get_status()
254
+ """
255
+
256
+ def decorator(func: Callable[..., Any], /):
257
+ sig = inspect.signature(func)
258
+ params = list(sig.parameters.values())
259
+
260
+ # Disallow immediate + async
261
+ if immediate and inspect.iscoroutinefunction(func):
262
+ raise ValueError("Async effects cannot have immediate=True")
263
+
264
+ # State method - unchanged behavior
265
+ if len(params) == 1 and params[0].name == "self":
266
+ return StateEffect(
267
+ func,
268
+ name=name,
269
+ immediate=immediate,
270
+ lazy=lazy,
271
+ on_error=on_error,
272
+ deps=deps,
273
+ update_deps=update_deps,
274
+ interval=interval,
275
+ )
276
+
277
+ # Allow params with defaults (used for variable binding in loops)
278
+ # Reject only if there are required params (no default)
279
+ required_params = [p for p in params if p.default is inspect.Parameter.empty]
280
+ if required_params:
281
+ raise TypeError(
282
+ f"@effect: Function '{func.__name__}' must take no arguments, a single 'self' argument, "
283
+ + "or only arguments with defaults (for variable binding)"
284
+ )
285
+
286
+ # Check if we're in a hook context (component render)
287
+ ctx = HOOK_CONTEXT.get()
288
+
289
+ def create_effect() -> Effect | AsyncEffect:
290
+ if inspect.iscoroutinefunction(func):
291
+ return AsyncEffect(
292
+ func, # type: ignore[arg-type]
293
+ name=name or func.__name__,
294
+ lazy=lazy,
295
+ on_error=on_error,
296
+ deps=deps,
297
+ update_deps=update_deps,
298
+ interval=interval,
299
+ )
300
+ return Effect(
301
+ func, # type: ignore[arg-type]
302
+ name=name or func.__name__,
303
+ immediate=immediate,
304
+ lazy=lazy,
305
+ on_error=on_error,
306
+ deps=deps,
307
+ update_deps=update_deps,
308
+ interval=interval,
309
+ )
310
+
311
+ if ctx is None:
312
+ # Not in component - create standalone effect (current behavior)
313
+ return create_effect()
314
+
315
+ # In component render - use inline caching
316
+
317
+ # Get the frame where the decorator was applied.
318
+ # When called as `@ps.effect` (no parens), the call stack is:
319
+ # decorator -> effect -> component
320
+ # When called as `@ps.effect(...)` (with parens), the stack is:
321
+ # decorator -> component
322
+ # We detect which case by checking if the immediate caller is effect().
323
+ frame = inspect.currentframe()
324
+ assert frame is not None
325
+ caller = frame.f_back
326
+ assert caller is not None
327
+ # If the immediate caller is the effect function itself, go back one more
328
+ if (
329
+ caller.f_code.co_name == "effect"
330
+ and "decorators" in caller.f_code.co_filename
331
+ ):
332
+ caller = caller.f_back
333
+ assert caller is not None
334
+ if key is None:
335
+ identity = collect_component_identity(caller)
336
+ else:
337
+ identity = key
338
+
339
+ state = inline_effect_hook()
340
+ return state.get_or_create(cast(Any, identity), key, create_effect)
341
+
342
+ if fn is not None:
343
+ return decorator(fn)
344
+ return decorator
pulse/dom/__init__.py ADDED
File without changes