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/middleware.py ADDED
@@ -0,0 +1,471 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable, Sequence
5
+ from typing import Any, Generic, TypeVar, overload, override
6
+
7
+ from pulse.env import env
8
+ from pulse.messages import (
9
+ ClientMessage,
10
+ Prerender,
11
+ PrerenderPayload,
12
+ )
13
+ from pulse.request import PulseRequest
14
+
15
+ T = TypeVar("T")
16
+
17
+
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
+
31
+ path: str
32
+
33
+ def __init__(self, path: str) -> None:
34
+ """Initialize a redirect response.
35
+
36
+ Args:
37
+ path: The path to redirect to.
38
+ """
39
+ self.path = path
40
+
41
+
42
+ class NotFound:
43
+ """Not found response. Returns 404."""
44
+
45
+
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
+
62
+ payload: T
63
+
64
+ @overload
65
+ def __init__(self, payload: T) -> None: ...
66
+ @overload
67
+ def __init__(self, payload: None = None) -> None: ...
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
+ """
74
+ self.payload = payload # pyright: ignore[reportAttributeAccessIssue]
75
+
76
+
77
+ class Deny:
78
+ """Denial response. Blocks the request."""
79
+
80
+
81
+ PrerenderResponse = Ok[Prerender] | Redirect | NotFound
82
+ """Response type for batch prerender: ``Ok[Prerender] | Redirect | NotFound``."""
83
+
84
+ ConnectResponse = Ok[None] | Deny
85
+ """Response type for WebSocket connection: ``Ok[None] | Deny``."""
86
+
87
+
88
+ class PulseMiddleware:
89
+ """Base middleware class with pass-through defaults.
90
+
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
+ ```
119
+ """
120
+
121
+ dev: bool
122
+
123
+ def __init__(self, dev: bool = False) -> None:
124
+ """Initialize middleware.
125
+
126
+ Args:
127
+ dev: If True, this middleware is only active in dev environments.
128
+ """
129
+ self.dev = dev
130
+
131
+ async def prerender(
132
+ self,
133
+ *,
134
+ payload: "PrerenderPayload",
135
+ request: PulseRequest,
136
+ session: dict[str, Any],
137
+ next: Callable[[], Awaitable[PrerenderResponse]],
138
+ ) -> PrerenderResponse:
139
+ """Handle batch prerender for the full request.
140
+
141
+ Receives the full PrerenderPayload (all paths). Call next() to get the
142
+ Prerender result and can modify it (views and directives) before returning.
143
+ """
144
+ return await next()
145
+
146
+ async def connect(
147
+ self,
148
+ *,
149
+ request: PulseRequest,
150
+ session: dict[str, Any],
151
+ next: Callable[[], Awaitable[ConnectResponse]],
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
+ """
163
+ return await next()
164
+
165
+ async def message(
166
+ self,
167
+ *,
168
+ data: ClientMessage,
169
+ session: dict[str, Any],
170
+ next: Callable[[], Awaitable[Ok[None]]],
171
+ ) -> Ok[None] | Deny:
172
+ """Handle per-message authorization.
173
+
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.
181
+ """
182
+ return await next()
183
+
184
+ async def channel(
185
+ self,
186
+ *,
187
+ channel_id: str,
188
+ event: str,
189
+ payload: Any,
190
+ request_id: str | None,
191
+ session: dict[str, Any],
192
+ next: Callable[[], Awaitable[Ok[None]]],
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
+ """
207
+ return await next()
208
+
209
+
210
+ class MiddlewareStack(PulseMiddleware):
211
+ """Composable stack of ``PulseMiddleware`` executed in order.
212
+
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
+ ```
229
+ """
230
+
231
+ def __init__(self, middlewares: Sequence[PulseMiddleware]) -> None:
232
+ """Initialize middleware stack.
233
+
234
+ Args:
235
+ middlewares: Sequence of middleware instances.
236
+ """
237
+ super().__init__(dev=False)
238
+ # Filter out dev middlewares when not in dev environment
239
+ if env.pulse_env != "dev":
240
+ middlewares = [mw for mw in middlewares if not mw.dev]
241
+ self._middlewares: list[PulseMiddleware] = list(middlewares)
242
+
243
+ @override
244
+ async def prerender(
245
+ self,
246
+ *,
247
+ payload: "PrerenderPayload",
248
+ request: PulseRequest,
249
+ session: dict[str, Any],
250
+ next: Callable[[], Awaitable[PrerenderResponse]],
251
+ ) -> PrerenderResponse:
252
+ async def dispatch(index: int) -> PrerenderResponse:
253
+ if index >= len(self._middlewares):
254
+ return await next()
255
+ mw = self._middlewares[index]
256
+
257
+ async def _next() -> PrerenderResponse:
258
+ return await dispatch(index + 1)
259
+
260
+ return await mw.prerender(
261
+ payload=payload,
262
+ request=request,
263
+ session=session,
264
+ next=_next,
265
+ )
266
+
267
+ return await dispatch(0)
268
+
269
+ @override
270
+ async def connect(
271
+ self,
272
+ *,
273
+ request: PulseRequest,
274
+ session: dict[str, Any],
275
+ next: Callable[[], Awaitable[ConnectResponse]],
276
+ ) -> ConnectResponse:
277
+ async def dispatch(index: int) -> ConnectResponse:
278
+ if index >= len(self._middlewares):
279
+ return await next()
280
+ mw = self._middlewares[index]
281
+
282
+ async def _next() -> ConnectResponse:
283
+ return await dispatch(index + 1)
284
+
285
+ return await mw.connect(request=request, session=session, next=_next)
286
+
287
+ return await dispatch(0)
288
+
289
+ @override
290
+ async def message(
291
+ self,
292
+ *,
293
+ data: ClientMessage,
294
+ session: dict[str, Any],
295
+ next: Callable[[], Awaitable[Ok[None]]],
296
+ ) -> Ok[None] | Deny:
297
+ async def dispatch(index: int) -> Ok[None] | Deny:
298
+ if index >= len(self._middlewares):
299
+ return await next()
300
+ mw = self._middlewares[index]
301
+
302
+ async def _next() -> Ok[None]:
303
+ result = await dispatch(index + 1)
304
+ # If dispatch returns Deny, the middleware should have short-circuited
305
+ # This should only be called when continuing the chain
306
+ if isinstance(result, Deny):
307
+ # This shouldn't happen, but handle it gracefully
308
+ return Ok(None)
309
+ return result
310
+
311
+ return await mw.message(session=session, data=data, next=_next)
312
+
313
+ return await dispatch(0)
314
+
315
+ @override
316
+ async def channel(
317
+ self,
318
+ *,
319
+ channel_id: str,
320
+ event: str,
321
+ payload: Any,
322
+ request_id: str | None,
323
+ session: dict[str, Any],
324
+ next: Callable[[], Awaitable[Ok[None]]],
325
+ ) -> Ok[None] | Deny:
326
+ async def dispatch(index: int) -> Ok[None] | Deny:
327
+ if index >= len(self._middlewares):
328
+ return await next()
329
+ mw = self._middlewares[index]
330
+
331
+ async def _next() -> Ok[None]:
332
+ result = await dispatch(index + 1)
333
+ # If dispatch returns Deny, the middleware should have short-circuited
334
+ # This should only be called when continuing the chain
335
+ if isinstance(result, Deny):
336
+ # This shouldn't happen, but handle it gracefully
337
+ return Ok(None)
338
+ return result
339
+
340
+ return await mw.channel(
341
+ channel_id=channel_id,
342
+ event=event,
343
+ payload=payload,
344
+ request_id=request_id,
345
+ session=session,
346
+ next=_next,
347
+ )
348
+
349
+ return await dispatch(0)
350
+
351
+
352
+ def stack(*middlewares: PulseMiddleware) -> PulseMiddleware:
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:
362
+
363
+ ```python
364
+ import pulse as ps
365
+
366
+ app = ps.App(
367
+ middleware=ps.stack(
368
+ AuthMiddleware(),
369
+ LoggingMiddleware(),
370
+ )
371
+ )
372
+ ```
373
+ """
374
+ return MiddlewareStack(list(middlewares))
375
+
376
+
377
+ class LatencyMiddleware(PulseMiddleware):
378
+ """Middleware that adds artificial latency to simulate network conditions.
379
+
380
+ Useful for testing and development to simulate real-world network delays.
381
+ Defaults are realistic for typical web applications.
382
+
383
+ Example:
384
+ ```python
385
+ app = ps.App(
386
+ middleware=ps.LatencyMiddleware(
387
+ prerender_ms=100,
388
+ connect_ms=50,
389
+ )
390
+ )
391
+ ```
392
+ """
393
+
394
+ prerender_ms: float
395
+ connect_ms: float
396
+ message_ms: float
397
+ channel_ms: float
398
+
399
+ def __init__(
400
+ self,
401
+ *,
402
+ prerender_ms: float = 80.0,
403
+ connect_ms: float = 40.0,
404
+ message_ms: float = 25.0,
405
+ channel_ms: float = 20.0,
406
+ ) -> None:
407
+ """Initialize latency middleware.
408
+
409
+ Args:
410
+ prerender_ms: Latency for batch prerender requests (HTTP). Default: 80ms
411
+ connect_ms: Latency for WebSocket connections. Default: 40ms
412
+ message_ms: Latency for WebSocket messages (including API calls). Default: 25ms
413
+ channel_ms: Latency for channel messages. Default: 20ms
414
+ """
415
+ super().__init__(dev=True)
416
+ self.prerender_ms = prerender_ms
417
+ self.connect_ms = connect_ms
418
+ self.message_ms = message_ms
419
+ self.channel_ms = channel_ms
420
+
421
+ @override
422
+ async def prerender(
423
+ self,
424
+ *,
425
+ payload: "PrerenderPayload",
426
+ request: PulseRequest,
427
+ session: dict[str, Any],
428
+ next: Callable[[], Awaitable[PrerenderResponse]],
429
+ ) -> PrerenderResponse:
430
+ if self.prerender_ms > 0:
431
+ await asyncio.sleep(self.prerender_ms / 1000.0)
432
+ return await next()
433
+
434
+ @override
435
+ async def connect(
436
+ self,
437
+ *,
438
+ request: PulseRequest,
439
+ session: dict[str, Any],
440
+ next: Callable[[], Awaitable[ConnectResponse]],
441
+ ) -> ConnectResponse:
442
+ if self.connect_ms > 0:
443
+ await asyncio.sleep(self.connect_ms / 1000.0)
444
+ return await next()
445
+
446
+ @override
447
+ async def message(
448
+ self,
449
+ *,
450
+ data: ClientMessage,
451
+ session: dict[str, Any],
452
+ next: Callable[[], Awaitable[Ok[None]]],
453
+ ) -> Ok[None] | Deny:
454
+ if self.message_ms > 0:
455
+ await asyncio.sleep(self.message_ms / 1000.0)
456
+ return await next()
457
+
458
+ @override
459
+ async def channel(
460
+ self,
461
+ *,
462
+ channel_id: str,
463
+ event: str,
464
+ payload: Any,
465
+ request_id: str | None,
466
+ session: dict[str, Any],
467
+ next: Callable[[], Awaitable[Ok[None]]],
468
+ ) -> Ok[None] | Deny:
469
+ if self.channel_ms > 0:
470
+ await asyncio.sleep(self.channel_ms / 1000.0)
471
+ return await next()
pulse/plugin.py ADDED
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from pulse.middleware import PulseMiddleware
6
+ from pulse.routing import Layout, Route
7
+
8
+ if TYPE_CHECKING:
9
+ from pulse.app import App
10
+
11
+
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
+
40
+ priority: int = 0
41
+
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
+ """
51
+ return []
52
+
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
+ """
62
+ return []
63
+
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
+ ...