pulse-framework 0.1.55__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.
- pulse/__init__.py +5 -6
- pulse/app.py +144 -57
- pulse/channel.py +139 -7
- pulse/cli/cmd.py +16 -2
- pulse/codegen/codegen.py +43 -12
- pulse/component.py +104 -0
- pulse/components/for_.py +30 -4
- pulse/components/if_.py +28 -5
- pulse/components/react_router.py +61 -3
- pulse/context.py +39 -5
- pulse/cookies.py +108 -4
- pulse/decorators.py +193 -24
- pulse/env.py +56 -2
- pulse/form.py +198 -5
- pulse/helpers.py +7 -1
- pulse/hooks/core.py +135 -5
- pulse/hooks/effects.py +61 -77
- pulse/hooks/init.py +60 -1
- pulse/hooks/runtime.py +241 -0
- pulse/hooks/setup.py +77 -0
- pulse/hooks/stable.py +58 -1
- pulse/hooks/state.py +107 -20
- pulse/js/__init__.py +40 -24
- pulse/js/array.py +9 -6
- pulse/js/console.py +15 -12
- pulse/js/date.py +9 -6
- pulse/js/document.py +5 -2
- pulse/js/error.py +7 -4
- pulse/js/json.py +9 -6
- pulse/js/map.py +8 -5
- pulse/js/math.py +9 -6
- pulse/js/navigator.py +5 -2
- pulse/js/number.py +9 -6
- pulse/js/obj.py +16 -13
- pulse/js/object.py +9 -6
- pulse/js/promise.py +19 -13
- pulse/js/pulse.py +28 -25
- pulse/js/react.py +94 -55
- pulse/js/regexp.py +7 -4
- pulse/js/set.py +8 -5
- pulse/js/string.py +9 -6
- pulse/js/weakmap.py +8 -5
- pulse/js/weakset.py +8 -5
- pulse/js/window.py +6 -3
- pulse/messages.py +5 -0
- pulse/middleware.py +147 -76
- pulse/plugin.py +76 -5
- pulse/queries/client.py +186 -39
- pulse/queries/common.py +52 -3
- pulse/queries/infinite_query.py +154 -2
- pulse/queries/mutation.py +127 -7
- pulse/queries/query.py +112 -11
- pulse/react_component.py +66 -3
- pulse/reactive.py +314 -30
- pulse/reactive_extensions.py +106 -26
- pulse/render_session.py +304 -173
- pulse/request.py +46 -11
- pulse/routing.py +140 -4
- pulse/serializer.py +71 -0
- pulse/state.py +177 -9
- pulse/test_helpers.py +15 -0
- pulse/transpiler/__init__.py +0 -3
- pulse/transpiler/py_module.py +1 -7
- pulse/user_session.py +119 -18
- {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.56.dist-info}/METADATA +5 -5
- pulse_framework-0.1.56.dist-info/RECORD +127 -0
- pulse/transpiler/react_component.py +0 -44
- pulse_framework-0.1.55.dist-info/RECORD +0 -127
- {pulse_framework-0.1.55.dist-info → pulse_framework-0.1.56.dist-info}/WHEEL +0 -0
- {pulse_framework-0.1.55.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
|
|
89
|
+
"""Base middleware class with pass-through defaults.
|
|
51
90
|
|
|
52
|
-
Subclass and override
|
|
53
|
-
|
|
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
|
|
139
|
+
"""Handle batch prerender for the full request.
|
|
75
140
|
|
|
76
|
-
Receives the full PrerenderPayload. Call next() to get the
|
|
77
|
-
and can modify it (views and directives) before returning
|
|
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
|
-
|
|
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
|
|
211
|
+
"""Composable stack of ``PulseMiddleware`` executed in order.
|
|
129
212
|
|
|
130
|
-
Each middleware receives a
|
|
131
|
-
middleware returns without calling
|
|
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
|
-
"""
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
...
|