pulse-framework 0.1.54__py3-none-any.whl → 0.1.56__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 (80) 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/code_analysis.py +38 -0
  6. pulse/codegen/codegen.py +61 -62
  7. pulse/codegen/templates/route.py +100 -56
  8. pulse/component.py +128 -6
  9. pulse/components/for_.py +30 -4
  10. pulse/components/if_.py +28 -5
  11. pulse/components/react_router.py +61 -3
  12. pulse/context.py +39 -5
  13. pulse/cookies.py +108 -4
  14. pulse/decorators.py +193 -24
  15. pulse/env.py +56 -2
  16. pulse/form.py +198 -5
  17. pulse/helpers.py +7 -1
  18. pulse/hooks/core.py +135 -5
  19. pulse/hooks/effects.py +61 -77
  20. pulse/hooks/init.py +60 -1
  21. pulse/hooks/runtime.py +241 -0
  22. pulse/hooks/setup.py +77 -0
  23. pulse/hooks/stable.py +58 -1
  24. pulse/hooks/state.py +107 -20
  25. pulse/js/__init__.py +41 -25
  26. pulse/js/array.py +9 -6
  27. pulse/js/console.py +15 -12
  28. pulse/js/date.py +9 -6
  29. pulse/js/document.py +5 -2
  30. pulse/js/error.py +7 -4
  31. pulse/js/json.py +9 -6
  32. pulse/js/map.py +8 -5
  33. pulse/js/math.py +9 -6
  34. pulse/js/navigator.py +5 -2
  35. pulse/js/number.py +9 -6
  36. pulse/js/obj.py +16 -13
  37. pulse/js/object.py +9 -6
  38. pulse/js/promise.py +19 -13
  39. pulse/js/pulse.py +28 -25
  40. pulse/js/react.py +190 -44
  41. pulse/js/regexp.py +7 -4
  42. pulse/js/set.py +8 -5
  43. pulse/js/string.py +9 -6
  44. pulse/js/weakmap.py +8 -5
  45. pulse/js/weakset.py +8 -5
  46. pulse/js/window.py +6 -3
  47. pulse/messages.py +5 -0
  48. pulse/middleware.py +147 -76
  49. pulse/plugin.py +76 -5
  50. pulse/queries/client.py +186 -39
  51. pulse/queries/common.py +52 -3
  52. pulse/queries/infinite_query.py +154 -2
  53. pulse/queries/mutation.py +127 -7
  54. pulse/queries/query.py +112 -11
  55. pulse/react_component.py +66 -3
  56. pulse/reactive.py +314 -30
  57. pulse/reactive_extensions.py +106 -26
  58. pulse/render_session.py +304 -173
  59. pulse/request.py +46 -11
  60. pulse/routing.py +140 -4
  61. pulse/serializer.py +71 -0
  62. pulse/state.py +177 -9
  63. pulse/test_helpers.py +15 -0
  64. pulse/transpiler/__init__.py +13 -3
  65. pulse/transpiler/assets.py +66 -0
  66. pulse/transpiler/dynamic_import.py +131 -0
  67. pulse/transpiler/emit_context.py +49 -0
  68. pulse/transpiler/function.py +6 -2
  69. pulse/transpiler/imports.py +33 -27
  70. pulse/transpiler/js_module.py +64 -8
  71. pulse/transpiler/py_module.py +1 -7
  72. pulse/transpiler/transpiler.py +4 -0
  73. pulse/user_session.py +119 -18
  74. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/METADATA +5 -5
  75. pulse_framework-0.1.56.dist-info/RECORD +127 -0
  76. pulse/js/react_dom.py +0 -30
  77. pulse/transpiler/react_component.py +0 -51
  78. pulse_framework-0.1.54.dist-info/RECORD +0 -124
  79. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/WHEEL +0 -0
  80. {pulse_framework-0.1.54.dist-info → pulse_framework-0.1.56.dist-info}/entry_points.txt +0 -0
pulse/middleware.py CHANGED
@@ -9,25 +9,56 @@ from pulse.messages import (
9
9
  ClientMessage,
10
10
  Prerender,
11
11
  PrerenderPayload,
12
- ServerInitMessage,
13
12
  )
14
13
  from pulse.request import PulseRequest
15
- from pulse.routing import RouteInfo
16
14
 
17
15
  T = TypeVar("T")
18
16
 
19
17
 
20
18
  class Redirect:
19
+ """Redirect response. Causes navigation to the specified path.
20
+
21
+ Attributes:
22
+ path: The path to redirect to.
23
+
24
+ Example:
25
+
26
+ ```python
27
+ return ps.Redirect("/login")
28
+ ```
29
+ """
30
+
21
31
  path: str
22
32
 
23
33
  def __init__(self, path: str) -> None:
34
+ """Initialize a redirect response.
35
+
36
+ Args:
37
+ path: The path to redirect to.
38
+ """
24
39
  self.path = path
25
40
 
26
41
 
27
- class NotFound: ...
42
+ class NotFound:
43
+ """Not found response. Returns 404."""
28
44
 
29
45
 
30
46
  class Ok(Generic[T]):
47
+ """Success response wrapper.
48
+
49
+ Use ``Ok(None)`` for void success, or ``Ok(payload)`` to wrap a value.
50
+
51
+ Attributes:
52
+ payload: The wrapped success value.
53
+
54
+ Example:
55
+
56
+ ```python
57
+ return ps.Ok(None) # Allow request
58
+ return ps.Ok(prerender_result) # Return with payload
59
+ ```
60
+ """
61
+
31
62
  payload: T
32
63
 
33
64
  @overload
@@ -35,22 +66,56 @@ class Ok(Generic[T]):
35
66
  @overload
36
67
  def __init__(self, payload: None = None) -> None: ...
37
68
  def __init__(self, payload: T | None = None) -> None:
69
+ """Initialize a success response.
70
+
71
+ Args:
72
+ payload: The success value (optional, defaults to None).
73
+ """
38
74
  self.payload = payload # pyright: ignore[reportAttributeAccessIssue]
39
75
 
40
76
 
41
- class Deny: ...
77
+ class Deny:
78
+ """Denial response. Blocks the request."""
42
79
 
43
80
 
44
- RoutePrerenderResponse = Ok[ServerInitMessage] | Redirect | NotFound
45
81
  PrerenderResponse = Ok[Prerender] | Redirect | NotFound
82
+ """Response type for batch prerender: ``Ok[Prerender] | Redirect | NotFound``."""
83
+
46
84
  ConnectResponse = Ok[None] | Deny
85
+ """Response type for WebSocket connection: ``Ok[None] | Deny``."""
47
86
 
48
87
 
49
88
  class PulseMiddleware:
50
- """Base middleware with pass-through defaults and short-circuiting.
89
+ """Base middleware class with pass-through defaults.
51
90
 
52
- Subclass and override any of the hooks. Mutate `context` to attach values
53
- for later use. Return a decision to allow or short-circuit the flow.
91
+ Subclass and override hooks to implement custom behavior. Each hook receives
92
+ a ``next`` callable to continue the middleware chain.
93
+
94
+ Attributes:
95
+ dev: If True, middleware is only active in dev environments.
96
+
97
+ Example:
98
+
99
+ ```python
100
+ class AuthMiddleware(ps.PulseMiddleware):
101
+ async def prerender_route(
102
+ self,
103
+ *,
104
+ path: str,
105
+ request: ps.PulseRequest,
106
+ route_info: ps.RouteInfo,
107
+ session: dict[str, Any],
108
+ next,
109
+ ):
110
+ if path.startswith("/admin") and not session.get("is_admin"):
111
+ return ps.Redirect("/login")
112
+ return await next()
113
+
114
+ async def connect(self, *, request, session, next):
115
+ if not session.get("user_id"):
116
+ return ps.Deny()
117
+ return await next()
118
+ ```
54
119
  """
55
120
 
56
121
  dev: bool
@@ -71,24 +136,13 @@ class PulseMiddleware:
71
136
  session: dict[str, Any],
72
137
  next: Callable[[], Awaitable[PrerenderResponse]],
73
138
  ) -> PrerenderResponse:
74
- """Handle batch prerender at the top level.
139
+ """Handle batch prerender for the full request.
75
140
 
76
- Receives the full PrerenderPayload. Call next() to get the PrerenderResult
77
- and can modify it (views and directives) before returning to the client.
141
+ Receives the full PrerenderPayload (all paths). Call next() to get the
142
+ Prerender result and can modify it (views and directives) before returning.
78
143
  """
79
144
  return await next()
80
145
 
81
- async def prerender_route(
82
- self,
83
- *,
84
- path: str,
85
- request: PulseRequest,
86
- route_info: RouteInfo,
87
- session: dict[str, Any],
88
- next: Callable[[], Awaitable[RoutePrerenderResponse]],
89
- ) -> RoutePrerenderResponse:
90
- return await next()
91
-
92
146
  async def connect(
93
147
  self,
94
148
  *,
@@ -96,6 +150,16 @@ class PulseMiddleware:
96
150
  session: dict[str, Any],
97
151
  next: Callable[[], Awaitable[ConnectResponse]],
98
152
  ) -> ConnectResponse:
153
+ """Handle WebSocket connection establishment.
154
+
155
+ Args:
156
+ request: Normalized request object.
157
+ session: Session data dictionary.
158
+ next: Callable to continue the middleware chain.
159
+
160
+ Returns:
161
+ ``Ok[None]`` to allow, ``Deny`` to reject.
162
+ """
99
163
  return await next()
100
164
 
101
165
  async def message(
@@ -107,7 +171,13 @@ class PulseMiddleware:
107
171
  ) -> Ok[None] | Deny:
108
172
  """Handle per-message authorization.
109
173
 
110
- Return Deny() to block, Ok(None) to allow.
174
+ Args:
175
+ data: Client message data.
176
+ session: Session data dictionary.
177
+ next: Callable to continue the middleware chain.
178
+
179
+ Returns:
180
+ ``Ok[None]`` to allow, ``Deny`` to block.
111
181
  """
112
182
  return await next()
113
183
 
@@ -121,17 +191,49 @@ class PulseMiddleware:
121
191
  session: dict[str, Any],
122
192
  next: Callable[[], Awaitable[Ok[None]]],
123
193
  ) -> Ok[None] | Deny:
194
+ """Handle channel message authorization.
195
+
196
+ Args:
197
+ channel_id: Channel identifier.
198
+ event: Event name.
199
+ payload: Event payload.
200
+ request_id: Request ID if awaiting response.
201
+ session: Session data dictionary.
202
+ next: Callable to continue the middleware chain.
203
+
204
+ Returns:
205
+ ``Ok[None]`` to allow, ``Deny`` to block.
206
+ """
124
207
  return await next()
125
208
 
126
209
 
127
210
  class MiddlewareStack(PulseMiddleware):
128
- """Composable stack of `PulseMiddleware` executed in order.
211
+ """Composable stack of ``PulseMiddleware`` executed in order.
129
212
 
130
- Each middleware receives a `next` callable that advances the chain. If a
131
- middleware returns without calling `next`, the chain short-circuits.
213
+ Each middleware receives a ``next`` callable that advances the chain. If a
214
+ middleware returns without calling ``next``, the chain short-circuits.
215
+
216
+ Args:
217
+ middlewares: Sequence of middleware instances.
218
+
219
+ Example:
220
+
221
+ ```python
222
+ app = ps.App(
223
+ middleware=ps.stack(
224
+ AuthMiddleware(),
225
+ LoggingMiddleware(),
226
+ )
227
+ )
228
+ ```
132
229
  """
133
230
 
134
231
  def __init__(self, middlewares: Sequence[PulseMiddleware]) -> None:
232
+ """Initialize middleware stack.
233
+
234
+ Args:
235
+ middlewares: Sequence of middleware instances.
236
+ """
135
237
  super().__init__(dev=False)
136
238
  # Filter out dev middlewares when not in dev environment
137
239
  if env.pulse_env != "dev":
@@ -164,34 +266,6 @@ class MiddlewareStack(PulseMiddleware):
164
266
 
165
267
  return await dispatch(0)
166
268
 
167
- @override
168
- async def prerender_route(
169
- self,
170
- *,
171
- path: str,
172
- request: PulseRequest,
173
- route_info: RouteInfo,
174
- session: dict[str, Any],
175
- next: Callable[[], Awaitable[RoutePrerenderResponse]],
176
- ) -> RoutePrerenderResponse:
177
- async def dispatch(index: int) -> RoutePrerenderResponse:
178
- if index >= len(self._middlewares):
179
- return await next()
180
- mw = self._middlewares[index]
181
-
182
- async def _next() -> RoutePrerenderResponse:
183
- return await dispatch(index + 1)
184
-
185
- return await mw.prerender_route(
186
- path=path,
187
- route_info=route_info,
188
- request=request,
189
- session=session,
190
- next=_next,
191
- )
192
-
193
- return await dispatch(0)
194
-
195
269
  @override
196
270
  async def connect(
197
271
  self,
@@ -276,10 +350,26 @@ class MiddlewareStack(PulseMiddleware):
276
350
 
277
351
 
278
352
  def stack(*middlewares: PulseMiddleware) -> PulseMiddleware:
279
- """Helper to build a middleware stack in code.
353
+ """Compose multiple middlewares into a single middleware stack.
354
+
355
+ Args:
356
+ *middlewares: Middleware instances to compose.
357
+
358
+ Returns:
359
+ ``MiddlewareStack`` instance.
360
+
361
+ Example:
280
362
 
281
- Example: `app = App(..., middleware=stack(Auth(), Logging()))`
282
- Prefer passing a `list`/`tuple` to `App` directly.
363
+ ```python
364
+ import pulse as ps
365
+
366
+ app = ps.App(
367
+ middleware=ps.stack(
368
+ AuthMiddleware(),
369
+ LoggingMiddleware(),
370
+ )
371
+ )
372
+ ```
283
373
  """
284
374
  return MiddlewareStack(list(middlewares))
285
375
 
@@ -302,7 +392,6 @@ class LatencyMiddleware(PulseMiddleware):
302
392
  """
303
393
 
304
394
  prerender_ms: float
305
- prerender_route_ms: float
306
395
  connect_ms: float
307
396
  message_ms: float
308
397
  channel_ms: float
@@ -311,7 +400,6 @@ class LatencyMiddleware(PulseMiddleware):
311
400
  self,
312
401
  *,
313
402
  prerender_ms: float = 80.0,
314
- prerender_route_ms: float = 60.0,
315
403
  connect_ms: float = 40.0,
316
404
  message_ms: float = 25.0,
317
405
  channel_ms: float = 20.0,
@@ -320,15 +408,12 @@ class LatencyMiddleware(PulseMiddleware):
320
408
 
321
409
  Args:
322
410
  prerender_ms: Latency for batch prerender requests (HTTP). Default: 80ms
323
- prerender_route_ms: Latency for individual route prerenders. Default: 60ms
324
411
  connect_ms: Latency for WebSocket connections. Default: 40ms
325
412
  message_ms: Latency for WebSocket messages (including API calls). Default: 25ms
326
413
  channel_ms: Latency for channel messages. Default: 20ms
327
- dev: If True, only active in dev environments. Default: True
328
414
  """
329
415
  super().__init__(dev=True)
330
416
  self.prerender_ms = prerender_ms
331
- self.prerender_route_ms = prerender_route_ms
332
417
  self.connect_ms = connect_ms
333
418
  self.message_ms = message_ms
334
419
  self.channel_ms = channel_ms
@@ -346,20 +431,6 @@ class LatencyMiddleware(PulseMiddleware):
346
431
  await asyncio.sleep(self.prerender_ms / 1000.0)
347
432
  return await next()
348
433
 
349
- @override
350
- async def prerender_route(
351
- self,
352
- *,
353
- path: str,
354
- request: PulseRequest,
355
- route_info: RouteInfo,
356
- session: dict[str, Any],
357
- next: Callable[[], Awaitable[RoutePrerenderResponse]],
358
- ) -> RoutePrerenderResponse:
359
- if self.prerender_route_ms > 0:
360
- await asyncio.sleep(self.prerender_route_ms / 1000.0)
361
- return await next()
362
-
363
434
  @override
364
435
  async def connect(
365
436
  self,
pulse/plugin.py CHANGED
@@ -10,16 +10,87 @@ if TYPE_CHECKING:
10
10
 
11
11
 
12
12
  class Plugin:
13
+ """Base class for application plugins.
14
+
15
+ Plugins extend application functionality by contributing routes,
16
+ middleware, and lifecycle hooks. Create a subclass and override
17
+ the methods you need.
18
+
19
+ Attributes:
20
+ priority: Plugin execution order. Higher values run first.
21
+ Defaults to 0. Use positive values for plugins that should
22
+ initialize early (e.g., auth), negative for late initialization.
23
+
24
+ Example:
25
+ ```python
26
+ class AuthPlugin(ps.Plugin):
27
+ priority = 10 # Higher priority runs first
28
+
29
+ def routes(self):
30
+ return [ps.Route("/login", render=login_page)]
31
+
32
+ def middleware(self):
33
+ return [AuthMiddleware()]
34
+
35
+ def on_startup(self, app):
36
+ print("Auth plugin started")
37
+ ```
38
+ """
39
+
13
40
  priority: int = 0
14
41
 
15
- # Optional: return a sequence; return None or [] if not contributing
16
42
  def routes(self) -> list[Route | Layout]:
43
+ """Return routes to add to the application.
44
+
45
+ Override to contribute routes from this plugin. Routes are added
46
+ after user-defined routes in the App constructor.
47
+
48
+ Returns:
49
+ List of Route or Layout objects to register.
50
+ """
17
51
  return []
18
52
 
19
53
  def middleware(self) -> list[PulseMiddleware]:
54
+ """Return middleware to add to the application.
55
+
56
+ Override to contribute middleware from this plugin. Middleware
57
+ is added after user-defined middleware.
58
+
59
+ Returns:
60
+ List of PulseMiddleware instances to register.
61
+ """
20
62
  return []
21
63
 
22
- # Optional lifecycle
23
- def on_setup(self, app: App) -> None: ...
24
- def on_startup(self, app: App) -> None: ...
25
- def on_shutdown(self, app: App) -> None: ...
64
+ def on_setup(self, app: App) -> None:
65
+ """Called after FastAPI routes are configured.
66
+
67
+ Override to perform setup that requires FastAPI routes to exist,
68
+ such as adding custom endpoints or middleware.
69
+
70
+ Args:
71
+ app: The Pulse application instance.
72
+ """
73
+ ...
74
+
75
+ def on_startup(self, app: App) -> None:
76
+ """Called when the application starts.
77
+
78
+ Override to perform initialization when the server begins
79
+ accepting connections, such as connecting to databases or
80
+ initializing caches.
81
+
82
+ Args:
83
+ app: The Pulse application instance.
84
+ """
85
+ ...
86
+
87
+ def on_shutdown(self, app: App) -> None:
88
+ """Called when the application shuts down.
89
+
90
+ Override to perform cleanup when the server is stopping,
91
+ such as closing database connections or flushing caches.
92
+
93
+ Args:
94
+ app: The Pulse application instance.
95
+ """
96
+ ...