pulse-framework 0.1.42__tar.gz → 0.1.44__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.
Files changed (83) hide show
  1. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/PKG-INFO +1 -1
  2. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/pyproject.toml +1 -1
  3. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/__init__.py +12 -3
  4. pulse_framework-0.1.44/src/pulse/decorators.py +175 -0
  5. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/helpers.py +39 -23
  6. pulse_framework-0.1.44/src/pulse/queries/client.py +462 -0
  7. pulse_framework-0.1.44/src/pulse/queries/common.py +52 -0
  8. pulse_framework-0.1.44/src/pulse/queries/effect.py +39 -0
  9. pulse_framework-0.1.44/src/pulse/queries/infinite_query.py +1157 -0
  10. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/queries/mutation.py +47 -0
  11. pulse_framework-0.1.44/src/pulse/queries/query.py +777 -0
  12. pulse_framework-0.1.44/src/pulse/queries/store.py +123 -0
  13. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/reactive.py +95 -20
  14. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/reactive_extensions.py +19 -7
  15. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/state.py +5 -0
  16. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/user_session.py +7 -3
  17. pulse_framework-0.1.42/src/pulse/decorators.py +0 -339
  18. pulse_framework-0.1.42/src/pulse/queries/common.py +0 -24
  19. pulse_framework-0.1.42/src/pulse/queries/query.py +0 -270
  20. pulse_framework-0.1.42/src/pulse/queries/query_observer.py +0 -365
  21. pulse_framework-0.1.42/src/pulse/queries/store.py +0 -60
  22. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/README.md +0 -0
  23. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/app.py +0 -0
  24. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/channel.py +0 -0
  25. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/__init__.py +0 -0
  26. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/cmd.py +0 -0
  27. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/dependencies.py +0 -0
  28. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/folder_lock.py +0 -0
  29. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/helpers.py +0 -0
  30. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/models.py +0 -0
  31. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/packages.py +0 -0
  32. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/processes.py +0 -0
  33. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/secrets.py +0 -0
  34. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cli/uvicorn_log_config.py +0 -0
  35. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/__init__.py +0 -0
  36. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/codegen.py +0 -0
  37. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/imports.py +0 -0
  38. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/js.py +0 -0
  39. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/templates/__init__.py +0 -0
  40. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/templates/layout.py +0 -0
  41. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/templates/route.py +0 -0
  42. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/templates/routes_ts.py +0 -0
  43. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/codegen/utils.py +0 -0
  44. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/components/__init__.py +0 -0
  45. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/components/for_.py +0 -0
  46. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/components/if_.py +0 -0
  47. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/components/react_router.py +0 -0
  48. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/context.py +0 -0
  49. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/cookies.py +0 -0
  50. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/css.py +0 -0
  51. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/env.py +0 -0
  52. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/form.py +0 -0
  53. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/__init__.py +0 -0
  54. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/core.py +0 -0
  55. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/effects.py +0 -0
  56. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/init.py +0 -0
  57. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/runtime.py +0 -0
  58. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/setup.py +0 -0
  59. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/stable.py +0 -0
  60. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/hooks/states.py +0 -0
  61. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/__init__.py +0 -0
  62. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/elements.py +0 -0
  63. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/events.py +0 -0
  64. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/props.py +0 -0
  65. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/svg.py +0 -0
  66. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/tags.py +0 -0
  67. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/html/tags.pyi +0 -0
  68. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/messages.py +0 -0
  69. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/middleware.py +0 -0
  70. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/plugin.py +0 -0
  71. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/proxy.py +0 -0
  72. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/py.typed +0 -0
  73. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/queries/__init__.py +0 -0
  74. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/react_component.py +0 -0
  75. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/render_session.py +0 -0
  76. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/renderer.py +0 -0
  77. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/request.py +0 -0
  78. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/routing.py +0 -0
  79. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/serializer.py +0 -0
  80. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/types/__init__.py +0 -0
  81. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/types/event_handler.py +0 -0
  82. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/vdom.py +0 -0
  83. {pulse_framework-0.1.42 → pulse_framework-0.1.44}/src/pulse/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.42
3
+ Version: 0.1.44
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
5
  Requires-Dist: websockets>=12.0
6
6
  Requires-Dist: fastapi>=0.104.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pulse-framework"
3
- version = "0.1.42"
3
+ version = "0.1.44"
4
4
  description = "Pulse - Full-stack framework for building real-time React applications in Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -72,8 +72,6 @@ from pulse.css import (
72
72
  # Decorators
73
73
  from pulse.decorators import computed as computed
74
74
  from pulse.decorators import effect as effect
75
- from pulse.decorators import mutation as mutation
76
- from pulse.decorators import query as query
77
75
 
78
76
  # Environment
79
77
  from pulse.env import PulseEnv as PulseEnv
@@ -1349,7 +1347,18 @@ from pulse.middleware import (
1349
1347
 
1350
1348
  # Plugin
1351
1349
  from pulse.plugin import Plugin as Plugin
1352
- from pulse.queries.query import QueryStatus as QueryStatus
1350
+ from pulse.queries.client import QueryClient as QueryClient
1351
+ from pulse.queries.client import QueryFilter as QueryFilter
1352
+ from pulse.queries.client import queries as queries
1353
+ from pulse.queries.common import ActionError as ActionError
1354
+ from pulse.queries.common import ActionResult as ActionResult
1355
+ from pulse.queries.common import ActionSuccess as ActionSuccess
1356
+ from pulse.queries.common import QueryKey as QueryKey
1357
+ from pulse.queries.common import QueryStatus as QueryStatus
1358
+ from pulse.queries.infinite_query import infinite_query as infinite_query
1359
+ from pulse.queries.mutation import mutation as mutation
1360
+ from pulse.queries.query import QueryResult as QueryResult
1361
+ from pulse.queries.query import query as query
1353
1362
 
1354
1363
  # React component registry
1355
1364
  from pulse.react_component import (
@@ -0,0 +1,175 @@
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, overload
6
+
7
+ from pulse.reactive import (
8
+ AsyncEffect,
9
+ AsyncEffectFn,
10
+ Computed,
11
+ Effect,
12
+ EffectCleanup,
13
+ EffectFn,
14
+ Signal,
15
+ )
16
+ from pulse.state import ComputedProperty, State, StateEffect
17
+
18
+ T = TypeVar("T")
19
+ TState = TypeVar("TState", bound=State)
20
+ P = ParamSpec("P")
21
+
22
+
23
+ # -> @ps.computed The chalenge is:
24
+ # - We want to turn regular functions with no arguments into a Computed object
25
+ # - We want to turn state methods into a ComputedProperty (which wraps a
26
+ # Computed, but gives it access to the State object).
27
+ @overload
28
+ def computed(fn: Callable[[], T], *, name: str | None = None) -> Computed[T]: ...
29
+ @overload
30
+ def computed(
31
+ fn: Callable[[TState], T], *, name: str | None = None
32
+ ) -> ComputedProperty[T]: ...
33
+ @overload
34
+ def computed(
35
+ fn: None = None, *, name: str | None = None
36
+ ) -> Callable[[Callable[[], T]], Computed[T]]: ...
37
+
38
+
39
+ def computed(fn: Callable[..., Any] | None = None, *, name: str | None = None):
40
+ # The type checker is not happy if I don't specify the `/` here.
41
+ def decorator(fn: Callable[..., Any], /):
42
+ sig = inspect.signature(fn)
43
+ params = list(sig.parameters.values())
44
+ # Check if it's a method with exactly one argument called 'self'
45
+ if len(params) == 1 and params[0].name == "self":
46
+ return ComputedProperty(fn.__name__, fn)
47
+ # If it has any arguments at all, it's not allowed (except for 'self')
48
+ if len(params) > 0:
49
+ raise TypeError(
50
+ f"@computed: Function '{fn.__name__}' must take no arguments or a single 'self' argument"
51
+ )
52
+ return Computed(fn, name=name or fn.__name__)
53
+
54
+ if fn is not None:
55
+ return decorator(fn)
56
+ else:
57
+ return decorator
58
+
59
+
60
+ StateEffectFn = Callable[[TState], EffectCleanup | None]
61
+ AsyncStateEffectFn = Callable[[TState], Awaitable[EffectCleanup | None]]
62
+
63
+
64
+ class EffectBuilder(Protocol):
65
+ @overload
66
+ def __call__(self, fn: EffectFn | StateEffectFn[Any]) -> Effect: ...
67
+ @overload
68
+ def __call__(self, fn: AsyncEffectFn | AsyncStateEffectFn[Any]) -> AsyncEffect: ...
69
+ def __call__(
70
+ self,
71
+ fn: EffectFn | StateEffectFn[Any] | AsyncEffectFn | AsyncStateEffectFn[Any],
72
+ ) -> Effect | AsyncEffect: ...
73
+
74
+
75
+ @overload
76
+ def effect(
77
+ fn: EffectFn,
78
+ *,
79
+ name: str | None = None,
80
+ immediate: bool = False,
81
+ lazy: bool = False,
82
+ on_error: Callable[[Exception], None] | None = None,
83
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
84
+ interval: float | None = None,
85
+ ) -> Effect: ...
86
+
87
+
88
+ @overload
89
+ def effect(
90
+ fn: AsyncEffectFn,
91
+ *,
92
+ name: str | None = None,
93
+ immediate: bool = False,
94
+ lazy: bool = False,
95
+ on_error: Callable[[Exception], None] | None = None,
96
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
97
+ interval: float | None = None,
98
+ ) -> AsyncEffect: ...
99
+ # In practice this overload returns a StateEffect, but it gets converted into an
100
+ # Effect at state instantiation.
101
+ @overload
102
+ def effect(fn: StateEffectFn[Any]) -> Effect: ...
103
+ @overload
104
+ def effect(fn: AsyncStateEffectFn[Any]) -> AsyncEffect: ...
105
+ @overload
106
+ def effect(
107
+ fn: None = None,
108
+ *,
109
+ name: str | None = None,
110
+ immediate: bool = False,
111
+ lazy: bool = False,
112
+ on_error: Callable[[Exception], None] | None = None,
113
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
114
+ interval: float | None = None,
115
+ ) -> EffectBuilder: ...
116
+
117
+
118
+ def effect(
119
+ fn: Callable[..., Any] | None = None,
120
+ *,
121
+ name: str | None = None,
122
+ immediate: bool = False,
123
+ lazy: bool = False,
124
+ on_error: Callable[[Exception], None] | None = None,
125
+ deps: list[Signal[Any] | Computed[Any]] | None = None,
126
+ interval: float | None = None,
127
+ ):
128
+ # The type checker is not happy if I don't specify the `/` here.
129
+ def decorator(func: Callable[..., Any], /):
130
+ sig = inspect.signature(func)
131
+ params = list(sig.parameters.values())
132
+
133
+ # Disallow intermediate + async
134
+ if immediate and inspect.iscoroutinefunction(func):
135
+ raise ValueError("Async effects cannot have immediate=True")
136
+
137
+ if len(params) == 1 and params[0].name == "self":
138
+ return StateEffect(
139
+ func,
140
+ name=name,
141
+ immediate=immediate,
142
+ lazy=lazy,
143
+ on_error=on_error,
144
+ deps=deps,
145
+ interval=interval,
146
+ )
147
+
148
+ if len(params) > 0:
149
+ raise TypeError(
150
+ f"@effect: Function '{func.__name__}' must take no arguments or a single 'self' argument"
151
+ )
152
+
153
+ # This is a standalone effect function. Choose subclass based on async-ness
154
+ if inspect.iscoroutinefunction(func):
155
+ return AsyncEffect(
156
+ func, # type: ignore[arg-type]
157
+ name=name or func.__name__,
158
+ lazy=lazy,
159
+ on_error=on_error,
160
+ deps=deps,
161
+ interval=interval,
162
+ )
163
+ return Effect(
164
+ func, # type: ignore[arg-type]
165
+ name=name or func.__name__,
166
+ immediate=immediate,
167
+ lazy=lazy,
168
+ on_error=on_error,
169
+ deps=deps,
170
+ interval=interval,
171
+ )
172
+
173
+ if fn:
174
+ return decorator(fn)
175
+ return decorator
@@ -212,31 +212,39 @@ def later(
212
212
  """
213
213
  Schedule `fn(*args, **kwargs)` to run after `delay` seconds.
214
214
  Works with sync or async functions. Returns a TimerHandle; call .cancel() to cancel.
215
+
216
+ The callback runs with no reactive scope to avoid accidentally capturing
217
+ reactive dependencies from the calling context. Other context vars (like
218
+ PulseContext) are preserved normally.
215
219
  """
220
+
221
+ from pulse.reactive import Untrack
222
+
216
223
  loop = asyncio.get_running_loop()
217
224
 
218
225
  def _run():
219
226
  try:
220
- res = fn(*args, **kwargs)
221
- if asyncio.iscoroutine(res):
222
- task = loop.create_task(res)
223
-
224
- def _log_task_exception(t: asyncio.Task[Any]):
225
- try:
226
- t.result()
227
- except asyncio.CancelledError:
228
- # Normal cancellation path
229
- pass
230
- except Exception as exc:
231
- loop.call_exception_handler(
232
- {
233
- "message": "Unhandled exception in later() task",
234
- "exception": exc,
235
- "context": {"callback": fn},
236
- }
237
- )
238
-
239
- task.add_done_callback(_log_task_exception)
227
+ with Untrack():
228
+ res = fn(*args, **kwargs)
229
+ if asyncio.iscoroutine(res):
230
+ task = loop.create_task(res)
231
+
232
+ def _log_task_exception(t: asyncio.Task[Any]):
233
+ try:
234
+ t.result()
235
+ except asyncio.CancelledError:
236
+ # Normal cancellation path
237
+ pass
238
+ except Exception as exc:
239
+ loop.call_exception_handler(
240
+ {
241
+ "message": "Unhandled exception in later() task",
242
+ "exception": exc,
243
+ "context": {"callback": fn},
244
+ }
245
+ )
246
+
247
+ task.add_done_callback(_log_task_exception)
240
248
  except Exception as exc:
241
249
  # Surface exceptions via the loop's exception handler and continue
242
250
  loop.call_exception_handler(
@@ -273,9 +281,16 @@ def repeat(interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwa
273
281
  For async functions, waits for completion before starting the next delay.
274
282
  Returns a handle with .cancel() to stop future runs.
275
283
 
284
+ The callback runs with no reactive scope to avoid accidentally capturing
285
+ reactive dependencies from the calling context. Other context vars (like
286
+ PulseContext) are preserved normally.
287
+
276
288
  Optional kwargs:
277
289
  - immediate: bool = False # run once immediately before the first interval
278
290
  """
291
+
292
+ from pulse.reactive import Untrack
293
+
279
294
  loop = asyncio.get_running_loop()
280
295
  handle = RepeatHandle()
281
296
 
@@ -288,9 +303,10 @@ def repeat(interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwa
288
303
  if handle.cancelled:
289
304
  break
290
305
  try:
291
- result = fn(*args, **kwargs)
292
- if asyncio.iscoroutine(result):
293
- await result
306
+ with Untrack():
307
+ result = fn(*args, **kwargs)
308
+ if asyncio.iscoroutine(result):
309
+ await result
294
310
  except asyncio.CancelledError:
295
311
  # Propagate to outer handler to finish cleanly
296
312
  raise