pulse-framework 0.1.65__py3-none-any.whl → 0.1.66a1__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/app.py CHANGED
@@ -107,8 +107,8 @@ PulseMode = Literal["subdomains", "single-server"]
107
107
  """Deployment mode for the application.
108
108
 
109
109
  Values:
110
- "single-server": Python and React served from the same origin (default).
111
- "subdomains": Python API on a subdomain (e.g., api.example.com).
110
+ "single-server": Python and React served from the same origin (default).
111
+ "subdomains": Python API on a subdomain (e.g., api.example.com).
112
112
  """
113
113
 
114
114
 
@@ -118,12 +118,12 @@ class ConnectionStatusConfig:
118
118
  Configuration for connection status message delays.
119
119
 
120
120
  Attributes:
121
- initial_connecting_delay: Delay in seconds before showing "Connecting..." message
122
- on initial connection attempt. Default: 2.0
123
- initial_error_delay: Additional delay in seconds before showing error message
124
- on initial connection attempt (after connecting message). Default: 8.0
125
- reconnect_error_delay: Delay in seconds before showing error message when
126
- reconnecting after losing connection. Default: 8.0
121
+ initial_connecting_delay: Delay in seconds before showing "Connecting..." message
122
+ on initial connection attempt. Default: 2.0
123
+ initial_error_delay: Additional delay in seconds before showing error message
124
+ on initial connection attempt (after connecting message). Default: 8.0
125
+ reconnect_error_delay: Delay in seconds before showing error message when
126
+ reconnecting after losing connection. Default: 8.0
127
127
  """
128
128
 
129
129
  initial_connecting_delay: float = 2.0
@@ -171,15 +171,15 @@ class App:
171
171
  import pulse as ps
172
172
 
173
173
  app = ps.App(
174
- routes=[
175
- ps.Route("/", render=home),
176
- ps.Route("/users/:id", render=user_detail),
177
- ],
178
- session_timeout=120.0,
174
+ routes=[
175
+ ps.Route("/", render=home),
176
+ ps.Route("/users/:id", render=user_detail),
177
+ ],
178
+ session_timeout=120.0,
179
179
  )
180
180
 
181
181
  if __name__ == "__main__":
182
- app.run(port=8000)
182
+ app.run(port=8000)
183
183
  ```
184
184
  """
185
185
 
@@ -544,10 +544,21 @@ class App:
544
544
  paths = [ensure_absolute_path(path) for path in paths]
545
545
  payload["paths"] = paths
546
546
  route_info = payload.get("routeInfo")
547
+ debug = os.environ.get("PULSE_DEBUG_RENDER")
548
+ if debug:
549
+ logger.info(
550
+ "[PulseDebug][prerender] session=%s header_render_id=%s payload_render_id=%s paths=%s route_info=%s",
551
+ session.sid,
552
+ request.headers.get("x-pulse-render-id"),
553
+ payload.get("renderId"),
554
+ paths,
555
+ route_info,
556
+ )
547
557
 
548
558
  client_addr: str | None = get_client_address(request)
549
559
  # Reuse render session from header (set by middleware) or create new one
550
560
  render = PulseContext.get().render
561
+ reused = render is not None
551
562
  if render is not None:
552
563
  render_id = render.id
553
564
  else:
@@ -556,9 +567,19 @@ class App:
556
567
  render = self.create_render(
557
568
  render_id, session, client_address=client_addr
558
569
  )
570
+ if debug:
571
+ logger.info(
572
+ "[PulseDebug][prerender] session=%s render=%s reused=%s connected=%s",
573
+ session.sid,
574
+ render_id,
575
+ reused,
576
+ render.connected,
577
+ )
578
+ print(f"Prerendering for RenderSession {render_id}")
559
579
 
560
580
  # Schedule cleanup timeout (will cancel/reschedule on activity)
561
- self._schedule_render_cleanup(render_id)
581
+ if not render.connected:
582
+ self._schedule_render_cleanup(render_id)
562
583
 
563
584
  def _normalize_prerender_result(
564
585
  captured: ServerInitMessage | ServerNavigateToMessage,
@@ -698,6 +719,13 @@ class App:
698
719
  if cookie is None:
699
720
  raise ConnectionRefusedError("Socket connect missing cookie")
700
721
  session = await self.get_or_create_session(cookie)
722
+ debug = os.environ.get("PULSE_DEBUG_RENDER")
723
+ if debug:
724
+ logger.info(
725
+ "[PulseDebug][connect] session=%s render_id=%s",
726
+ session.sid,
727
+ rid,
728
+ )
701
729
 
702
730
  if not rid:
703
731
  # Still refuse connections without a renderId
@@ -708,6 +736,13 @@ class App:
708
736
  # Allow reconnects where the provided renderId no longer exists by creating a new RenderSession
709
737
  render = self.render_sessions.get(rid)
710
738
  if render is None:
739
+ # The client will try to attach to a non-existing RouteMount, which will cause a reload down the line
740
+ if debug:
741
+ logger.info(
742
+ "[PulseDebug][connect] render_missing session=%s render_id=%s creating=true",
743
+ session.sid,
744
+ rid,
745
+ )
711
746
  render = self.create_render(
712
747
  rid, session, client_address=get_client_address_socketio(environ)
713
748
  )
@@ -718,6 +753,15 @@ class App:
718
753
  f"Socket connect session mismatch render={render.id} "
719
754
  + f"owner={owner} session={session.sid}"
720
755
  )
756
+ if debug:
757
+ logger.info(
758
+ "[PulseDebug][connect] render_found session=%s render_id=%s owner=%s connected=%s",
759
+ session.sid,
760
+ render.id,
761
+ owner,
762
+ render.connected,
763
+ )
764
+ print(f"Connected to RenderSession {render.id}")
721
765
 
722
766
  def on_message(message: ServerMessage):
723
767
  payload = serialize(message)
@@ -830,6 +874,20 @@ class App:
830
874
  async def _handle_pulse_message(
831
875
  self, render: RenderSession, session: UserSession, msg: ClientPulseMessage
832
876
  ) -> None:
877
+ if os.environ.get("PULSE_DEBUG_RENDER") and msg["type"] in (
878
+ "attach",
879
+ "update",
880
+ "detach",
881
+ ):
882
+ logger.info(
883
+ "[PulseDebug][client-message] session=%s render=%s type=%s path=%s route_info=%s",
884
+ session.sid,
885
+ render.id,
886
+ msg["type"],
887
+ msg.get("path"),
888
+ msg.get("routeInfo"),
889
+ )
890
+
833
891
  async def _next() -> Ok[None]:
834
892
  if msg["type"] == "attach":
835
893
  render.attach(msg["path"], msg["routeInfo"])
@@ -53,15 +53,11 @@ export async function loader(args: LoaderFunctionArgs) {
53
53
  return data(prerenderData, { headers });
54
54
  }
55
55
 
56
- // Client loader: re-prerender on navigation while reusing renderId
56
+ // Client loader: re-prerender on navigation while reusing directives
57
57
  export async function clientLoader(args: ClientLoaderFunctionArgs) {
58
58
  const url = new URL(args.request.url);
59
59
  const matches = matchRoutes(rrPulseRouteTree, url.pathname) ?? [];
60
60
  const paths = matches.map(m => m.route.uniquePath);
61
- const renderId =
62
- typeof window !== "undefined" && typeof sessionStorage !== "undefined"
63
- ? (sessionStorage.getItem("__PULSE_RENDER_ID") ?? undefined)
64
- : undefined;
65
61
  const directives =
66
62
  typeof window !== "undefined" && typeof sessionStorage !== "undefined"
67
63
  ? (JSON.parse(sessionStorage.getItem("__PULSE_DIRECTIVES") ?? "{}"))
@@ -76,7 +72,7 @@ export async function clientLoader(args: ClientLoaderFunctionArgs) {
76
72
  method: "POST",
77
73
  headers,
78
74
  credentials: "include",
79
- body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args), renderId }),
75
+ body: JSON.stringify({ paths, routeInfo: extractServerRouteInfo(args) }),
80
76
  });
81
77
  if (!res.ok) throw new Error("Failed to prerender batch:" + res.status);
82
78
  const body = await res.json();
@@ -92,7 +88,6 @@ export async function clientLoader(args: ClientLoaderFunctionArgs) {
92
88
  export default function PulseLayout() {
93
89
  const data = useLoaderData<typeof loader>();
94
90
  if (typeof window !== "undefined" && typeof sessionStorage !== "undefined") {
95
- sessionStorage.setItem("__PULSE_RENDER_ID", data.renderId);
96
91
  sessionStorage.setItem("__PULSE_DIRECTIVES", JSON.stringify(data.directives));
97
92
  }
98
93
  return (
@@ -101,6 +96,6 @@ export default function PulseLayout() {
101
96
  </PulseProvider>
102
97
  );
103
98
  }
104
- // Persist renderId and directives in sessionStorage for reuse in clientLoader is handled within the component
99
+ // Persist directives in sessionStorage for reuse in clientLoader is handled within the component
105
100
  """
106
101
  )
pulse/render_session.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
+ import os
3
4
  import traceback
4
5
  import uuid
5
6
  from asyncio import iscoroutine
@@ -137,6 +138,14 @@ class RouteMount:
137
138
  if self.state != "pending":
138
139
  return
139
140
  action = self.pending_action
141
+ if os.environ.get("PULSE_DEBUG_RENDER"):
142
+ logger.info(
143
+ "[PulseDebug][mount-timeout] render=%s path=%s action=%s state=%s",
144
+ self.render.id,
145
+ self.path,
146
+ action,
147
+ self.state,
148
+ )
140
149
  self.pending_action = None
141
150
  if action == "dispose":
142
151
  self.render.dispose_mount(self.path, self)
@@ -144,6 +153,15 @@ class RouteMount:
144
153
  self.to_idle()
145
154
 
146
155
  def start_pending(self, timeout: float, *, action: PendingAction = "idle") -> None:
156
+ if os.environ.get("PULSE_DEBUG_RENDER"):
157
+ logger.info(
158
+ "[PulseDebug][mount-pending] render=%s path=%s state=%s action=%s timeout=%s",
159
+ self.render.id,
160
+ self.path,
161
+ self.state,
162
+ action,
163
+ timeout,
164
+ )
147
165
  if self.state == "pending":
148
166
  prev_action = self.pending_action
149
167
  next_action: PendingAction = (
@@ -166,6 +184,14 @@ class RouteMount:
166
184
  )
167
185
 
168
186
  def activate(self, send_message: Callable[[ServerMessage], Any]) -> None:
187
+ if os.environ.get("PULSE_DEBUG_RENDER"):
188
+ logger.info(
189
+ "[PulseDebug][mount-activate] render=%s path=%s state=%s queued=%s",
190
+ self.render.id,
191
+ self.path,
192
+ self.state,
193
+ 0 if not self.queue else len(self.queue),
194
+ )
169
195
  if self.state != "pending":
170
196
  return
171
197
  self._cancel_pending_timeout()
@@ -192,6 +218,12 @@ class RouteMount:
192
218
  def to_idle(self) -> None:
193
219
  if self.state != "pending":
194
220
  return
221
+ if os.environ.get("PULSE_DEBUG_RENDER"):
222
+ logger.info(
223
+ "[PulseDebug][mount-idle] render=%s path=%s",
224
+ self.render.id,
225
+ self.path,
226
+ )
195
227
  self.state = "idle"
196
228
  self.queue = None
197
229
  self._cancel_pending_timeout()
@@ -317,11 +349,15 @@ class RenderSession:
317
349
 
318
350
  def connect(self, send_message: Callable[[ServerMessage], Any]):
319
351
  """WebSocket connected. Set sender, don't auto-flush (attach does that)."""
352
+ if os.environ.get("PULSE_DEBUG_RENDER"):
353
+ logger.info("[PulseDebug][render-connect] render=%s", self.id)
320
354
  self._send_message = send_message
321
355
  self.connected = True
322
356
 
323
357
  def disconnect(self):
324
358
  """WebSocket disconnected. Start queuing briefly before pausing."""
359
+ if os.environ.get("PULSE_DEBUG_RENDER"):
360
+ logger.info("[PulseDebug][render-disconnect] render=%s", self.id)
325
361
  self._send_message = None
326
362
  self.connected = False
327
363
 
@@ -395,6 +431,13 @@ class RenderSession:
395
431
  - Creates mounts in PENDING state and starts queue
396
432
  """
397
433
  normalized = [ensure_absolute_path(path) for path in paths]
434
+ if os.environ.get("PULSE_DEBUG_RENDER"):
435
+ logger.info(
436
+ "[PulseDebug][prerender] render=%s paths=%s route_info=%s",
437
+ self.id,
438
+ normalized,
439
+ route_info,
440
+ )
398
441
 
399
442
  results: dict[str, ServerInitMessage | ServerNavigateToMessage] = {}
400
443
 
@@ -402,6 +445,15 @@ class RenderSession:
402
445
  route = self.routes.find(path)
403
446
  info = route_info or route.default_route_info()
404
447
  mount = self.route_mounts.get(path)
448
+ if os.environ.get("PULSE_DEBUG_RENDER"):
449
+ route_label = repr(route)
450
+ logger.info(
451
+ "[PulseDebug][prerender] render=%s path=%s mount_state=%s route=%s",
452
+ self.id,
453
+ path,
454
+ mount.state if mount else None,
455
+ route_label,
456
+ )
405
457
 
406
458
  if mount is None:
407
459
  mount = RouteMount(self, path, route, info)
@@ -440,6 +492,15 @@ class RenderSession:
440
492
  mount = self.route_mounts.get(path)
441
493
 
442
494
  if mount is None or mount.state == "idle":
495
+ if os.environ.get("PULSE_DEBUG_RENDER"):
496
+ logger.info(
497
+ "[PulseDebug][attach] render=%s path=%s mount_state=%s mounts=%s route_info=%s",
498
+ self.id,
499
+ path,
500
+ mount.state if mount else None,
501
+ {key: value.state for key, value in self.route_mounts.items()},
502
+ route_info,
503
+ )
443
504
  # Initial render must come from prerender
444
505
  print(f"[DEBUG] Missing or idle route '{path}', reloading")
445
506
  self.send({"type": "reload"})
@@ -448,6 +509,12 @@ class RenderSession:
448
509
  # Update route info for active and pending mounts
449
510
  mount.update_route(route_info)
450
511
  if mount.state == "pending" and self._send_message:
512
+ if os.environ.get("PULSE_DEBUG_RENDER"):
513
+ logger.info(
514
+ "[PulseDebug][attach] render=%s path=%s activating=true",
515
+ self.id,
516
+ path,
517
+ )
451
518
  mount.activate(self._send_message)
452
519
 
453
520
  def update_route(self, path: str, route_info: RouteInfo):
@@ -463,6 +530,13 @@ class RenderSession:
463
530
  current = self.route_mounts.get(path)
464
531
  if current is not mount:
465
532
  return
533
+ if os.environ.get("PULSE_DEBUG_RENDER"):
534
+ logger.info(
535
+ "[PulseDebug][mount-dispose] render=%s path=%s state=%s",
536
+ self.id,
537
+ path,
538
+ mount.state,
539
+ )
466
540
  try:
467
541
  self.route_mounts.pop(path, None)
468
542
  mount.dispose()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.65
3
+ Version: 0.1.66a1
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.128.0
@@ -1,6 +1,6 @@
1
1
  pulse/__init__.py,sha256=cXNVXz0aizbkOG1aj2zytgzodyVNv7nNylsXcWmH-Lc,32183
2
2
  pulse/_examples.py,sha256=dFuhD2EVXsbvAeexoG57s4VuN4gWLaTMOEMNYvlPm9A,561
3
- pulse/app.py,sha256=hYqCgesH1T4qTTcU0yDNPrx6zIgL7_BGKzVdJhLNmbM,35777
3
+ pulse/app.py,sha256=oVwe7ioAGl7OXIEiT8J0bj5GBxEkBroOJw6qgdyA7Bg,37343
4
4
  pulse/channel.py,sha256=ePpvD2mDbddt_LMxxxDjNRgOLbVi8Ed6TmJFgkrALB0,15790
5
5
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  pulse/cli/cmd.py,sha256=zh3Ah6c16cNg3o_v_If_S58Qe8rvxNe5M2VrTkwvDU8,15957
@@ -17,7 +17,7 @@ pulse/code_analysis.py,sha256=NBba_7VtOxZYMyfku_p-bWkG0O_1pi1AxcaNyVM1nmY,1024
17
17
  pulse/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  pulse/codegen/codegen.py,sha256=Zw55vzevg_17hFtSi6KLl-EWSiABKRfZe6fB-cWpLAk,10330
19
19
  pulse/codegen/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- pulse/codegen/templates/layout.py,sha256=nmWPQcO9SRXc3mCCVLCmykreSF96TqQfdDY7dvUBxRg,4737
20
+ pulse/codegen/templates/layout.py,sha256=xz2ImZrbpu-tUkHZ38U6npIDzeYhrR_s41BuNPJUnOU,4470
21
21
  pulse/codegen/templates/route.py,sha256=UjBrb3e_8tMkd1OjBjEsnYmK6PCQqOYZBWDuU59FcrI,9234
22
22
  pulse/codegen/templates/routes_ts.py,sha256=nPgKCvU0gzue2k6KlOL1TJgrBqqRLmyy7K_qKAI8zAE,1129
23
23
  pulse/codegen/utils.py,sha256=QoXcV-h-DLLmq_t03hDNUePS0fNnofUQLoR-TXzDFCY,539
@@ -88,7 +88,7 @@ pulse/queries/store.py,sha256=4pWTDSl71LUM7YqhWanKjZkFh3t8F_04o48js_H4ttQ,3728
88
88
  pulse/react_component.py,sha256=8RLg4Bi7IcjqbnbEnp4hJpy8t1UsE7mG0UR1Q655LDk,2332
89
89
  pulse/reactive.py,sha256=GSh9wSH3THCBjDTafwWttyx7djeKBWV_KqjaKRYUNsA,31393
90
90
  pulse/reactive_extensions.py,sha256=yQ1PpdAh4kMvll7R15T72FOg8NFdG_HGBsGc63dawYk,33754
91
- pulse/render_session.py,sha256=-ILYIkI2N9W2fJEVSw4u7UMVEyuXNPkduYBLXsaW65U,22807
91
+ pulse/render_session.py,sha256=w6eJcMt4VFu84HeBAmBeEogxPtiwRHnRgQSTabrSXQM,24889
92
92
  pulse/renderer.py,sha256=fjSsUvCqV12jyN7Y5XspKUfjQJJzKX-Chha5oF5PrAk,16001
93
93
  pulse/request.py,sha256=N0oFOLiGxpbgSgxznjvu64lG3YyOcZPKC8JFyKx6X7w,6023
94
94
  pulse/requirements.py,sha256=nMnE25Uu-TUuQd88jW7m2xwus6fD-HvXxQ9UNb7OOGc,1254
@@ -122,7 +122,7 @@ pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
122
122
  pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
123
123
  pulse/user_session.py,sha256=nsnsMgqq2xGJZLpbHRMHUHcLrElMP8WcA4gjGMrcoBk,10208
124
124
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
125
- pulse_framework-0.1.65.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
126
- pulse_framework-0.1.65.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
127
- pulse_framework-0.1.65.dist-info/METADATA,sha256=sVBQbxUQWt5E7xaAiRpuPi-3kzMeDyO4Z7u8uQrZdGk,8300
128
- pulse_framework-0.1.65.dist-info/RECORD,,
125
+ pulse_framework-0.1.66a1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
126
+ pulse_framework-0.1.66a1.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
127
+ pulse_framework-0.1.66a1.dist-info/METADATA,sha256=5eOY1sXHWh-_WZB1EUlHxRKoqU3jIAJH0_iqQ2niZhI,8302
128
+ pulse_framework-0.1.66a1.dist-info/RECORD,,