pulse-framework 0.1.71__py3-none-any.whl → 0.1.72__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/queries/common.py CHANGED
@@ -34,19 +34,31 @@ class Key(tuple[Hashable, ...]):
34
34
  parts = tuple(key)
35
35
  try:
36
36
  key_hash = hash(parts)
37
- except TypeError:
38
- raise TypeError("QueryKey values must be hashable") from None
37
+ except TypeError as e:
38
+ raise TypeError(
39
+ f"Query key contains unhashable value: {e}.\n\n"
40
+ + "Keys must contain only hashable values (strings, numbers, tuples).\n"
41
+ + f"Got: {key!r}\n\n"
42
+ + "If using a dict or list inside the key, convert it to a tuple:\n"
43
+ + " key=('users', tuple(user_ids)) # instead of list"
44
+ ) from None
39
45
  obj = super().__new__(cls, parts)
40
46
  obj._hash = key_hash
41
47
  return obj
42
- raise TypeError("QueryKey must be a list or tuple of hashable values")
48
+ raise TypeError(
49
+ f"Query key must be a tuple or list, got {type(key).__name__}: {key!r}\n\n"
50
+ + "Examples of valid keys:\n"
51
+ + " key=('users',) # single-element tuple\n"
52
+ + " key=('user', user_id) # tuple with dynamic value\n"
53
+ + " key=['posts', 'feed'] # list form also works"
54
+ )
43
55
 
44
56
  @override
45
57
  def __hash__(self) -> int:
46
58
  return self._hash
47
59
 
48
60
 
49
- QueryKey: TypeAlias = tuple[Hashable, ...] | list[Hashable] | Key
61
+ QueryKey: TypeAlias = tuple[Hashable, ...] | list[Hashable] | Key # pyright: ignore[reportImplicitStringConcatenation]
50
62
  """List/tuple of hashable values identifying a query in the store.
51
63
 
52
64
  Used to uniquely identify queries for caching, deduplication, and invalidation.
@@ -432,7 +432,8 @@ class InfiniteQuery(Generic[T, TParam], Disposable):
432
432
  self._cancel_observer_actions(observer)
433
433
 
434
434
  if len(self._observers) == 0:
435
- self.schedule_gc()
435
+ if not self.__disposed__:
436
+ self.schedule_gc()
436
437
 
437
438
  def invalidate(
438
439
  self,
@@ -1308,8 +1309,17 @@ class InfiniteQueryProperty(Generic[T, TParam, TState], InitializableProperty):
1308
1309
  )
1309
1310
 
1310
1311
  if self._key is None:
1312
+ # pyright: ignore[reportImplicitStringConcatenation]
1311
1313
  raise RuntimeError(
1312
- f"key is required for infinite query '{self.name}'. Provide a key via @infinite_query(key=...) or @{self.name}.key decorator."
1314
+ f"Missing query key for @infinite_query '{self.name}'. "
1315
+ + "A key is required to cache and share query results.\n\n"
1316
+ + f"Fix: Add key=(...) to the decorator or use the @{self.name}.key decorator:\n\n"
1317
+ + " @ps.infinite_query(initial_page_param=..., key=('my_query',))\n"
1318
+ + f" async def {self.name}(self, param): ...\n\n"
1319
+ + "Or with a dynamic key:\n\n"
1320
+ + f" @{self.name}.key\n"
1321
+ + f" def _{self.name}_key(self):\n"
1322
+ + " return ('my_query', self.some_param)"
1313
1323
  )
1314
1324
  raw_initial = (
1315
1325
  call_flexible(self._initial_data, state)
pulse/queries/query.py CHANGED
@@ -563,7 +563,8 @@ class KeyedQuery(Generic[T], Disposable):
563
563
  )
564
564
 
565
565
  if len(self.observers) == 0:
566
- self.schedule_gc()
566
+ if not self.__disposed__:
567
+ self.schedule_gc()
567
568
 
568
569
  def schedule_gc(self):
569
570
  self.cancel_gc()
pulse/render_session.py CHANGED
@@ -285,7 +285,6 @@ class RenderSession:
285
285
  self._send_message = None
286
286
  self._global_states = {}
287
287
  self._global_queue = []
288
- self.query_store = QueryStore()
289
288
  self.connected = False
290
289
  self.channels = ChannelsManager(self)
291
290
  self.forms = FormRegistry(self)
@@ -293,6 +292,7 @@ class RenderSession:
293
292
  self._pending_js_results = {}
294
293
  self._tasks = TaskRegistry(name=f"render:{id}")
295
294
  self._timers = TimerRegistry(tasks=self._tasks, name=f"render:{id}")
295
+ self.query_store = QueryStore()
296
296
  self.prerender_queue_timeout = prerender_queue_timeout
297
297
  self.detach_queue_timeout = detach_queue_timeout
298
298
  self.disconnect_queue_timeout = disconnect_queue_timeout
@@ -574,9 +574,10 @@ class RenderSession:
574
574
  # ---- Helpers ----
575
575
 
576
576
  def close(self):
577
+ # Close all pending timers at the start, to avoid anything firing while we clean up
578
+ self._timers.cancel_all()
577
579
  self.forms.dispose()
578
580
  self._tasks.cancel_all()
579
- self._timers.cancel_all()
580
581
  for path in list(self.route_mounts.keys()):
581
582
  self.detach(path, timeout=0)
582
583
  self.route_mounts.clear()
@@ -597,6 +598,8 @@ class RenderSession:
597
598
  if not fut.done():
598
599
  fut.cancel()
599
600
  self._pending_js_results.clear()
601
+ # Close any timer that may have been scheduled during cleanup (ex: query GC)
602
+ self._timers.cancel_all()
600
603
  self._global_queue = []
601
604
  self._send_message = None
602
605
  self.connected = False
@@ -1,20 +1,20 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.71
3
+ Version: 0.1.72
4
4
  Summary: Pulse - Full-stack framework for building real-time React applications in Python
5
- Requires-Dist: websockets>=12.0
6
5
  Requires-Dist: fastapi>=0.128.0
7
6
  Requires-Dist: uvicorn>=0.24.0
8
7
  Requires-Dist: mako>=1.3.10
9
8
  Requires-Dist: typer>=0.16.0
10
9
  Requires-Dist: python-socketio>=5.16.0
11
10
  Requires-Dist: rich>=13.7.1
12
- Requires-Dist: python-multipart>=0.0.20
11
+ Requires-Dist: python-multipart>=0.0.22
13
12
  Requires-Dist: python-dateutil>=2.9.0.post0
14
13
  Requires-Dist: starlette>=0.50.0,<0.51.0
15
14
  Requires-Dist: urllib3>=2.6.3
16
15
  Requires-Dist: watchfiles>=1.1.0
17
16
  Requires-Dist: httpx>=0.28.1
17
+ Requires-Dist: aiohttp>=3.12.0
18
18
  Requires-Python: >=3.11
19
19
  Description-Content-Type: text/markdown
20
20
 
@@ -1,11 +1,11 @@
1
- pulse/__init__.py,sha256=yeGbAC9Nkp5ug4sfi3U1epaRfI1_D0zBbMtPQpPEBHA,32393
1
+ pulse/__init__.py,sha256=VV1oD_y0xnhQ_yBj1XqGWjIaw4Hi-xGHYQfEMv-Z1Bo,32453
2
2
  pulse/_examples.py,sha256=dFuhD2EVXsbvAeexoG57s4VuN4gWLaTMOEMNYvlPm9A,561
3
- pulse/app.py,sha256=TjMP21lpwYpRnkMkeKL8EA22V9xSKTA6_5GQI9Btw_o,36059
3
+ pulse/app.py,sha256=Bi94rYG-MoldkGa-_CscLMstjTEV8BHVAgDbvapRGzI,36167
4
4
  pulse/channel.py,sha256=ePpvD2mDbddt_LMxxxDjNRgOLbVi8Ed6TmJFgkrALB0,15790
5
5
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- pulse/cli/cmd.py,sha256=zh3Ah6c16cNg3o_v_If_S58Qe8rvxNe5M2VrTkwvDU8,15957
6
+ pulse/cli/cmd.py,sha256=LQK_B6iANOAqcQCM0KMTfRbpqGYRaPDkEBvvaAS3qNI,15985
7
7
  pulse/cli/dependencies.py,sha256=qU-rF7QyP0Rl1Fl0YKQubrGNBzj84BAbH1uUT3ehxik,4283
8
- pulse/cli/folder_lock.py,sha256=-AKld2iM91G0uHB3F5ARD0QAjOw0TmsYYGaFgy_V350,3477
8
+ pulse/cli/folder_lock.py,sha256=CGJCovbSxZ59dllwNUqtPDx6YCED8k3ct5ZsEL0gzfw,4035
9
9
  pulse/cli/helpers.py,sha256=XXRRXeGFgeq-jbp0QGFFVq_aGg_Kp7_AkYsTK8LfSdg,7810
10
10
  pulse/cli/logging.py,sha256=3uuB1dqI-lHJkodNUURN6UMWdKF5UQ9spNG-hBG7bA4,2516
11
11
  pulse/cli/models.py,sha256=NBV5byBDNoAQSk0vKwibLjoxuA85XBYIyOVJn64L8oU,858
@@ -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=xz2ImZrbpu-tUkHZ38U6npIDzeYhrR_s41BuNPJUnOU,4470
20
+ pulse/codegen/templates/layout.py,sha256=nJ9k0MDUHOGjtwbt8QAo9XdCPR06rqJSU9E8Qi4Uot4,4597
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
@@ -41,7 +41,7 @@ pulse/forms.py,sha256=0irpErCMJk8-YO1BrxjMkFb8dnvSz3rfzTywmMeib7g,14042
41
41
  pulse/helpers.py,sha256=imVA9XzkYrYmdeqEdD7ot0g99adL1SVKv5bQGkKb-aQ,9504
42
42
  pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  pulse/hooks/core.py,sha256=tDEcB_CTD4yI5bNKn7CtB40sRKIanGNqPD5_qLgSzf4,10982
44
- pulse/hooks/effects.py,sha256=cEPurXwRQoFmxasI0tZE1cQsZYnZr6VB5mWMUK0Db8c,2604
44
+ pulse/hooks/effects.py,sha256=IRb37K9p0egBqlPvAnlW5dPohPvQGzmcyLSgbuNk5FU,3005
45
45
  pulse/hooks/init.py,sha256=PhjVBLlHpodPzVrRcx_QfEUrsx_6gEX_NuVhe6ojiYI,17834
46
46
  pulse/hooks/runtime.py,sha256=nxqwl8ByclIh5i3ZcCN6L3f0X3ZpwOBWajLb_FSbcDw,11839
47
47
  pulse/hooks/setup.py,sha256=ILbn2v6UFJPFBOWnJef1X2A9JLpIagEHN9Mx0d9947I,6925
@@ -74,21 +74,21 @@ pulse/js/window.py,sha256=yC1BjyH2jqp1x-CXJCUFta-ASyZ5668ozQ0AmAjZcxA,4097
74
74
  pulse/messages.py,sha256=hz5EUFVHbzXHkcByZcV_Y199vb-M9cGjMMBL1HXPctE,4024
75
75
  pulse/middleware.py,sha256=2syzmJ0r9fEa0k1pD7zC_1DHUMs9qLSWRzo5XXtKqsA,10896
76
76
  pulse/plugin.py,sha256=bu90qaUVFtZsIsW41dpshVK1vvIGHUsg6mFoiF0Wfso,2370
77
- pulse/proxy.py,sha256=tj30GIJJVNKMxMwPNJ39-gn3PAXbx5wua4PiQ7XupvQ,7857
77
+ pulse/proxy.py,sha256=c13b0fE3sq82sFo46vv0emWLQ_ePwRkI7hiPZrnQDCE,22780
78
78
  pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
79
  pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
80
  pulse/queries/client.py,sha256=KMGT92dESMrzpLlhd701fyh7Wrs3VKmM5cZoRQ0AEzg,18994
81
- pulse/queries/common.py,sha256=24EK5AWeunTeC7xObG3xufOjqLUNvnLN7PEECtlgqGU,3705
81
+ pulse/queries/common.py,sha256=tjW5pqpWvRYUy3gsj-KxhUB9c5KBOgBqRKIuKJkob9A,4278
82
82
  pulse/queries/effect.py,sha256=1ePUi2TwP49L9LhlkKI2qV_HhIO4jKj1r5jyPaWiUn8,1508
83
- pulse/queries/infinite_query.py,sha256=sfZ_nwznD8kO_zWOvJ9uiyhJRb8slN1QMYWgzUnRvVQ,50025
83
+ pulse/queries/infinite_query.py,sha256=QZmXN2G8r0FA7cCez5bjPxEEvWZTB2Sl5_Pob3d5E6E,50489
84
84
  pulse/queries/mutation.py,sha256=fhEpOZ7CuHImH4Y02QapYdTJrwe6K52-keb0d67wmms,8274
85
85
  pulse/queries/protocol.py,sha256=TOrUiI4QK55xuh0i4ch1u96apNl12QeYafkf6RVDd08,3544
86
- pulse/queries/query.py,sha256=R5XXQ32pCSNX5tpedietIlI4saT1MVIJnR6ElnKQyVk,41819
86
+ pulse/queries/query.py,sha256=Ap1PRKoc1U1ddhnnHgBOuNX9oUcYD9nInpq87uTLnb8,41849
87
87
  pulse/queries/store.py,sha256=iw05_EFpyfiXv5_FV_x4aHtCo00mk0dDPFD461cajcg,3850
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=RJgxJp_yRpDBr3l9ZFW0Eb6-dSp_urTzzPhPcHd0_Vo,23224
91
+ pulse/render_session.py,sha256=65mhbKZ9o1vIDSRrM2p3VNPmo6Z5vn1Aoz7we5mw7P8,23417
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.71.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
126
- pulse_framework-0.1.71.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
127
- pulse_framework-0.1.71.dist-info/METADATA,sha256=emzemQv95geCAUlozNCamUpuVbiVyo2REoNCYsAx3JI,8300
128
- pulse_framework-0.1.71.dist-info/RECORD,,
125
+ pulse_framework-0.1.72.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
126
+ pulse_framework-0.1.72.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
127
+ pulse_framework-0.1.72.dist-info/METADATA,sha256=Vn_d62C23dTizl4uuQ05BAkhQUDrBmnELQI3OrHkkE8,8299
128
+ pulse_framework-0.1.72.dist-info/RECORD,,