pulse-framework 0.1.64__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/scheduling.py ADDED
@@ -0,0 +1,448 @@
1
+ import asyncio
2
+ import os
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any, ParamSpec, Protocol, TypeVar, override
5
+
6
+ from anyio import from_thread
7
+
8
+ T = TypeVar("T")
9
+ P = ParamSpec("P")
10
+
11
+
12
+ class TimerHandleLike(Protocol):
13
+ def cancel(self) -> None: ...
14
+ def cancelled(self) -> bool: ...
15
+ def when(self) -> float: ...
16
+
17
+
18
+ def is_pytest() -> bool:
19
+ """Detect if running inside pytest using environment variables."""
20
+ return bool(os.environ.get("PYTEST_CURRENT_TEST")) or (
21
+ "PYTEST_XDIST_TESTRUNUID" in os.environ
22
+ )
23
+
24
+
25
+ def _resolve_registries() -> tuple["TaskRegistry", "TimerRegistry"]:
26
+ from pulse.context import PulseContext
27
+
28
+ ctx = PulseContext.get()
29
+ if ctx.render is not None:
30
+ return ctx.render._tasks, ctx.render._timers # pyright: ignore[reportPrivateUsage]
31
+ return ctx.app._tasks, ctx.app._timers # pyright: ignore[reportPrivateUsage]
32
+
33
+
34
+ def call_soon(
35
+ fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs
36
+ ) -> TimerHandleLike | None:
37
+ """Schedule a callback to run ASAP on the main event loop from any thread."""
38
+ _, timer_registry = _resolve_registries()
39
+ return timer_registry.call_soon(fn, *args, **kwargs)
40
+
41
+
42
+ def create_task(
43
+ coroutine: Awaitable[T],
44
+ *,
45
+ name: str | None = None,
46
+ on_done: Callable[[asyncio.Task[T]], None] | None = None,
47
+ ) -> asyncio.Task[T]:
48
+ """Create a tracked task on the active session/app registry."""
49
+ task_registry, _ = _resolve_registries()
50
+ return task_registry.create_task(coroutine, name=name, on_done=on_done)
51
+
52
+
53
+ def create_future() -> asyncio.Future[Any]:
54
+ """Create an asyncio Future on the main event loop from any thread."""
55
+ task_registry, _ = _resolve_registries()
56
+ return task_registry.create_future()
57
+
58
+
59
+ def later(
60
+ delay: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs
61
+ ) -> TimerHandleLike:
62
+ """
63
+ Schedule `fn(*args, **kwargs)` to run after `delay` seconds.
64
+ Works with sync or async functions. Returns a handle; call .cancel() to cancel.
65
+
66
+ The callback runs with no reactive scope to avoid accidentally capturing
67
+ reactive dependencies from the calling context. Other context vars (like
68
+ PulseContext) are preserved normally.
69
+ """
70
+
71
+ _, timer_registry = _resolve_registries()
72
+ return timer_registry.later(delay, fn, *args, **kwargs)
73
+
74
+
75
+ class RepeatHandle:
76
+ task: asyncio.Task[None] | None
77
+ cancelled: bool
78
+
79
+ def __init__(self) -> None:
80
+ self.task = None
81
+ self.cancelled = False
82
+
83
+ def cancel(self):
84
+ if self.cancelled:
85
+ return
86
+ self.cancelled = True
87
+ if self.task is not None and not self.task.done():
88
+ self.task.cancel()
89
+
90
+
91
+ def repeat(interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs):
92
+ """
93
+ Repeatedly run `fn(*args, **kwargs)` every `interval` seconds.
94
+ Works with sync or async functions.
95
+ For async functions, waits for completion before starting the next delay.
96
+ Returns a handle with .cancel() to stop future runs.
97
+
98
+ The callback runs with no reactive scope to avoid accidentally capturing
99
+ reactive dependencies from the calling context. Other context vars (like
100
+ PulseContext) are preserved normally.
101
+
102
+ Optional kwargs:
103
+ - immediate: bool = False # run once immediately before the first interval
104
+ """
105
+
106
+ _, timer_registry = _resolve_registries()
107
+ return timer_registry.repeat(interval, fn, *args, **kwargs)
108
+
109
+
110
+ class TaskRegistry:
111
+ _tasks: set[asyncio.Task[Any]]
112
+ name: str | None
113
+
114
+ def __init__(self, name: str | None = None) -> None:
115
+ self._tasks = set()
116
+ self.name = name
117
+
118
+ def track(self, task: asyncio.Task[T]) -> asyncio.Task[T]:
119
+ self._tasks.add(task)
120
+ task.add_done_callback(self._tasks.discard)
121
+ return task
122
+
123
+ def create_task(
124
+ self,
125
+ coroutine: Awaitable[T],
126
+ *,
127
+ name: str | None = None,
128
+ on_done: Callable[[asyncio.Task[T]], None] | None = None,
129
+ ) -> asyncio.Task[T]:
130
+ """Create and schedule a coroutine task on the main loop from any thread."""
131
+ try:
132
+ asyncio.get_running_loop()
133
+ task = asyncio.ensure_future(coroutine)
134
+ if name is not None:
135
+ task.set_name(name)
136
+ if on_done:
137
+ task.add_done_callback(on_done)
138
+ except RuntimeError:
139
+
140
+ async def _runner():
141
+ asyncio.get_running_loop()
142
+ task = asyncio.ensure_future(coroutine)
143
+ if name is not None:
144
+ task.set_name(name)
145
+ if on_done:
146
+ task.add_done_callback(on_done)
147
+ return task
148
+
149
+ task = from_thread.run(_runner)
150
+
151
+ return self.track(task)
152
+
153
+ def create_future(self) -> asyncio.Future[Any]:
154
+ """Create an asyncio Future on the main event loop from any thread."""
155
+ try:
156
+ return asyncio.get_running_loop().create_future()
157
+ except RuntimeError:
158
+
159
+ async def _create():
160
+ return asyncio.get_running_loop().create_future()
161
+
162
+ return from_thread.run(_create)
163
+
164
+ def cancel_all(self) -> None:
165
+ for task in list(self._tasks):
166
+ if not task.done():
167
+ task.cancel()
168
+ self._tasks.clear()
169
+
170
+
171
+ class TimerRegistry:
172
+ _handles: set[TimerHandleLike]
173
+ _tasks: TaskRegistry
174
+ name: str | None
175
+
176
+ def __init__(self, *, tasks: TaskRegistry, name: str | None = None) -> None:
177
+ self._handles = set()
178
+ self._tasks = tasks
179
+ self.name = name
180
+
181
+ def track(self, handle: TimerHandleLike) -> TimerHandleLike:
182
+ self._handles.add(handle)
183
+ return handle
184
+
185
+ def discard(self, handle: TimerHandleLike | None) -> None:
186
+ if handle is None:
187
+ return
188
+ self._handles.discard(handle)
189
+
190
+ def later(
191
+ self,
192
+ delay: float,
193
+ fn: Callable[P, Any],
194
+ *args: P.args,
195
+ **kwargs: P.kwargs,
196
+ ) -> TimerHandleLike:
197
+ return self._schedule(delay, fn, args, dict(kwargs), untrack=True)
198
+
199
+ def call_soon(
200
+ self, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs
201
+ ) -> TimerHandleLike | None:
202
+ def _schedule():
203
+ return self._schedule_soon(fn, args, dict(kwargs))
204
+
205
+ try:
206
+ asyncio.get_running_loop()
207
+ return _schedule()
208
+ except RuntimeError:
209
+
210
+ async def _runner():
211
+ return _schedule()
212
+
213
+ try:
214
+ return from_thread.run(_runner)
215
+ except RuntimeError:
216
+ if not is_pytest():
217
+ raise
218
+ return None
219
+
220
+ def repeat(
221
+ self, interval: float, fn: Callable[P, Any], *args: P.args, **kwargs: P.kwargs
222
+ ) -> RepeatHandle:
223
+ from pulse.reactive import Untrack
224
+
225
+ loop = asyncio.get_running_loop()
226
+ handle = RepeatHandle()
227
+
228
+ async def _runner():
229
+ nonlocal handle
230
+ try:
231
+ while not handle.cancelled:
232
+ # Start counting the next interval AFTER the previous execution completes
233
+ await asyncio.sleep(interval)
234
+ if handle.cancelled:
235
+ break
236
+ try:
237
+ with Untrack():
238
+ result = fn(*args, **kwargs)
239
+ if asyncio.iscoroutine(result):
240
+ await result
241
+ except asyncio.CancelledError:
242
+ # Propagate to outer handler to finish cleanly
243
+ raise
244
+ except Exception as exc:
245
+ # Surface exceptions via the loop's exception handler and continue
246
+ loop.call_exception_handler(
247
+ {
248
+ "message": "Unhandled exception in repeat() callback",
249
+ "exception": exc,
250
+ "context": {"callback": fn},
251
+ }
252
+ )
253
+ except asyncio.CancelledError:
254
+ # Swallow task cancellation to avoid noisy "exception was never retrieved"
255
+ pass
256
+
257
+ handle.task = self._tasks.create_task(_runner())
258
+ return handle
259
+
260
+ def cancel_all(self) -> None:
261
+ for handle in list(self._handles):
262
+ handle.cancel()
263
+ self._handles.clear()
264
+
265
+ def _schedule(
266
+ self,
267
+ delay: float,
268
+ fn: Callable[..., Any],
269
+ args: tuple[Any, ...],
270
+ kwargs: dict[str, Any],
271
+ *,
272
+ untrack: bool,
273
+ ) -> TimerHandleLike:
274
+ """
275
+ Schedule `fn(*args, **kwargs)` to run after `delay` seconds.
276
+ Works with sync or async functions. Returns a TimerHandle; call .cancel() to cancel.
277
+
278
+ The callback can run without a reactive scope to avoid accidentally capturing
279
+ reactive dependencies from the calling context. Other context vars (like
280
+ PulseContext) are preserved normally.
281
+ """
282
+ try:
283
+ loop = asyncio.get_running_loop()
284
+ except RuntimeError:
285
+ try:
286
+ loop = asyncio.get_event_loop()
287
+ except RuntimeError as exc:
288
+ raise RuntimeError("later() requires an event loop") from exc
289
+
290
+ tracked_box: list[TimerHandleLike] = []
291
+ _run = self._prepare_run(loop, tracked_box, fn, args, kwargs, untrack=untrack)
292
+
293
+ handle = loop.call_later(delay, _run)
294
+ tracked = _TrackedTimerHandle(handle, self)
295
+ tracked_box.append(tracked)
296
+ self._handles.add(tracked)
297
+ return tracked
298
+
299
+ def _schedule_soon(
300
+ self,
301
+ fn: Callable[..., Any],
302
+ args: tuple[Any, ...],
303
+ kwargs: dict[str, Any],
304
+ ) -> TimerHandleLike:
305
+ try:
306
+ loop = asyncio.get_running_loop()
307
+ except RuntimeError:
308
+ try:
309
+ loop = asyncio.get_event_loop()
310
+ except RuntimeError as exc:
311
+ raise RuntimeError("call_soon() requires an event loop") from exc
312
+
313
+ tracked_box: list[TimerHandleLike] = []
314
+ _run = self._prepare_run(loop, tracked_box, fn, args, kwargs, untrack=False)
315
+
316
+ handle = loop.call_soon(_run)
317
+ tracked = _TrackedHandle(handle, self, when=loop.time())
318
+ tracked_box.append(tracked)
319
+ self._handles.add(tracked)
320
+ return tracked
321
+
322
+ def _prepare_run(
323
+ self,
324
+ loop: asyncio.AbstractEventLoop,
325
+ tracked_box: list[TimerHandleLike],
326
+ fn: Callable[..., Any],
327
+ args: tuple[Any, ...],
328
+ kwargs: dict[str, Any],
329
+ *,
330
+ untrack: bool,
331
+ ) -> Callable[[], None]:
332
+ def _run():
333
+ from pulse.reactive import Untrack
334
+
335
+ try:
336
+ if untrack:
337
+ with Untrack():
338
+ res = fn(*args, **kwargs)
339
+ else:
340
+ res = fn(*args, **kwargs)
341
+ if asyncio.iscoroutine(res):
342
+ task = self._tasks.create_task(res)
343
+
344
+ def _log_task_exception(t: asyncio.Task[Any]):
345
+ try:
346
+ t.result()
347
+ except asyncio.CancelledError:
348
+ # Normal cancellation path
349
+ pass
350
+ except Exception as exc:
351
+ loop.call_exception_handler(
352
+ {
353
+ "message": "Unhandled exception in later() task",
354
+ "exception": exc,
355
+ "context": {"callback": fn},
356
+ }
357
+ )
358
+
359
+ task.add_done_callback(_log_task_exception)
360
+ except Exception as exc:
361
+ # Surface exceptions via the loop's exception handler and continue
362
+ loop.call_exception_handler(
363
+ {
364
+ "message": "Unhandled exception in later() callback",
365
+ "exception": exc,
366
+ "context": {"callback": fn},
367
+ }
368
+ )
369
+ finally:
370
+ self.discard(tracked_box[0] if tracked_box else None)
371
+
372
+ return _run
373
+
374
+
375
+ class _TrackedTimerHandle:
376
+ __slots__: tuple[str, ...] = ("_handle", "_registry")
377
+ _handle: asyncio.TimerHandle
378
+ _registry: "TimerRegistry"
379
+
380
+ def __init__(self, handle: asyncio.TimerHandle, registry: "TimerRegistry") -> None:
381
+ self._handle = handle
382
+ self._registry = registry
383
+
384
+ def cancel(self) -> None:
385
+ if not self._handle.cancelled():
386
+ self._handle.cancel()
387
+ self._registry.discard(self)
388
+
389
+ def cancelled(self) -> bool:
390
+ return self._handle.cancelled()
391
+
392
+ def when(self) -> float:
393
+ return self._handle.when()
394
+
395
+ def __getattr__(self, name: str):
396
+ return getattr(self._handle, name)
397
+
398
+ @override
399
+ def __hash__(self) -> int:
400
+ return hash(self._handle)
401
+
402
+ @override
403
+ def __eq__(self, other: object) -> bool:
404
+ if isinstance(other, _TrackedTimerHandle):
405
+ return self._handle is other._handle
406
+ return self._handle is other
407
+
408
+
409
+ class _TrackedHandle:
410
+ __slots__: tuple[str, ...] = ("_handle", "_registry", "_when")
411
+ _handle: asyncio.Handle
412
+ _registry: "TimerRegistry"
413
+ _when: float
414
+
415
+ def __init__(
416
+ self,
417
+ handle: asyncio.Handle,
418
+ registry: "TimerRegistry",
419
+ *,
420
+ when: float,
421
+ ) -> None:
422
+ self._handle = handle
423
+ self._registry = registry
424
+ self._when = when
425
+
426
+ def cancel(self) -> None:
427
+ if not self._handle.cancelled():
428
+ self._handle.cancel()
429
+ self._registry.discard(self)
430
+
431
+ def cancelled(self) -> bool:
432
+ return self._handle.cancelled()
433
+
434
+ def when(self) -> float:
435
+ return self._when
436
+
437
+ def __getattr__(self, name: str):
438
+ return getattr(self._handle, name)
439
+
440
+ @override
441
+ def __hash__(self) -> int:
442
+ return hash(self._handle)
443
+
444
+ @override
445
+ def __eq__(self, other: object) -> bool:
446
+ if isinstance(other, _TrackedHandle):
447
+ return self._handle is other._handle
448
+ return self._handle is other
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.64
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,7 +1,7 @@
1
- pulse/__init__.py,sha256=hf1FFA4EDRxFNiPQoKwQfNHtnCf4UT7STqzgbjJ7dgI,32043
1
+ pulse/__init__.py,sha256=cXNVXz0aizbkOG1aj2zytgzodyVNv7nNylsXcWmH-Lc,32183
2
2
  pulse/_examples.py,sha256=dFuhD2EVXsbvAeexoG57s4VuN4gWLaTMOEMNYvlPm9A,561
3
- pulse/app.py,sha256=KnP6U8uHgfBbFMguDcVk0KgjakY1UC1NJk2rS5l6Sas,35145
4
- pulse/channel.py,sha256=sQrDLh3k9Z8CyJQkEHzKu4h-yR4XSTgAA3OCQax3Ciw,15766
3
+ pulse/app.py,sha256=oVwe7ioAGl7OXIEiT8J0bj5GBxEkBroOJw6qgdyA7Bg,37343
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
7
7
  pulse/cli/dependencies.py,sha256=qU-rF7QyP0Rl1Fl0YKQubrGNBzj84BAbH1uUT3ehxik,4283
@@ -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
@@ -37,8 +37,8 @@ pulse/dom/svg.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  pulse/dom/tags.py,sha256=U6mKmwB9JAFM6LTESMJcoIejNfnyxIdQo2-TLM5OaZ0,7585
38
38
  pulse/dom/tags.pyi,sha256=0BC7zTh22roPBuMQawL8hgI6IrfN8xJZuDIoKMd4QKc,14393
39
39
  pulse/env.py,sha256=etfubfwq7VWlo2iKy_972WtHZSiVe4StAnjFga0xgj8,4244
40
- pulse/form.py,sha256=dzzcxuIkA8EOAETrbMV916Jv_tlgVuoBKfgL3P2MySo,14058
41
- pulse/helpers.py,sha256=feHkC2g3DgfGN7FrvgRPZjmXzBjBYXwFu6wRPdy3w_I,15056
40
+ pulse/forms.py,sha256=0irpErCMJk8-YO1BrxjMkFb8dnvSz3rfzTywmMeib7g,14042
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
44
  pulse/hooks/effects.py,sha256=cEPurXwRQoFmxasI0tZE1cQsZYnZr6VB5mWMUK0Db8c,2604
@@ -74,25 +74,26 @@ 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=Rj0hOVnwyI36lrkK9fEKAgFK-WCwt5X526J87A2EvMs,7773
77
+ pulse/proxy.py,sha256=tj30GIJJVNKMxMwPNJ39-gn3PAXbx5wua4PiQ7XupvQ,7857
78
78
  pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
79
  pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- pulse/queries/client.py,sha256=AW42ZPdZJfDkCERpoxQnsiU5cYLfOlZX0sIb9BdIL4E,18495
80
+ pulse/queries/client.py,sha256=52T4MvorDr-T5UsJlndcEgnrPDs3QxIYP-MZtBSEuvc,18583
81
81
  pulse/queries/common.py,sha256=TYhn6LyldfmOKYYurxINgCEr3C3WSEwB0cIki1a5iBM,2488
82
- pulse/queries/effect.py,sha256=7KvV_yK7OHTWhfQbZFGzg_pRhyI2mn25pKIF9AmSmcU,1471
83
- pulse/queries/infinite_query.py,sha256=3_gv_pDTShNDf_y2yQ-QLDskJOxXhg1dOmQbU2_QxhE,48101
82
+ pulse/queries/effect.py,sha256=1ePUi2TwP49L9LhlkKI2qV_HhIO4jKj1r5jyPaWiUn8,1508
83
+ pulse/queries/infinite_query.py,sha256=xWmFl5UqW7DXkquR-8EUPqlS6Z-ZF4l7qarEUUXjdN0,49091
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=bKtsbsYGy53s8Fq-33OY0od8p9GKbXmiKR0Ob-NRcPs,40735
87
- pulse/queries/store.py,sha256=Ct7a-h1-Cq07zEfe9vw-LM85Fm7jIJx7CLAIlsiznlU,3444
86
+ pulse/queries/query.py,sha256=2VlYMeLqHfEokoEtocKjld8zi6Oy-_lieV4baTfF5DU,41332
87
+ pulse/queries/store.py,sha256=4pWTDSl71LUM7YqhWanKjZkFh3t8F_04o48js_H4ttQ,3728
88
88
  pulse/react_component.py,sha256=8RLg4Bi7IcjqbnbEnp4hJpy8t1UsE7mG0UR1Q655LDk,2332
89
- pulse/reactive.py,sha256=FxxpH7NBtQr7G89iCVN7y1EG21f23GcRi1M-XIxcRQA,31280
89
+ pulse/reactive.py,sha256=GSh9wSH3THCBjDTafwWttyx7djeKBWV_KqjaKRYUNsA,31393
90
90
  pulse/reactive_extensions.py,sha256=yQ1PpdAh4kMvll7R15T72FOg8NFdG_HGBsGc63dawYk,33754
91
- pulse/render_session.py,sha256=9gfwuBZRCWuQMN_nFuaAi__1UPN3I3C1mKWtAXyA3-A,21340
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
95
95
  pulse/routing.py,sha256=LzTITvGgaLI1w7qTDZjFwoBcWAb4O8Dz7AmXeTNYrFU,16903
96
+ pulse/scheduling.py,sha256=D-L5mVsbakK_-1NCq62dU6wEJpq6I_HxI4PCmR9aj9w,11714
96
97
  pulse/serializer.py,sha256=HmQZgxQiaCx2SL2XwmEQLd_xsk_P8XfLtGciLLLOxx0,7616
97
98
  pulse/state.py,sha256=VMphVpYNU1CyHMMg1_kNJO3cfqLXJPAuq9gr9RYyUAw,15922
98
99
  pulse/test_helpers.py,sha256=4iO5Ymy3SMvSjh-UaAaSdqm1I_SAJMNjdY2iYVro5f8,436
@@ -121,7 +122,7 @@ pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
121
122
  pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
122
123
  pulse/user_session.py,sha256=nsnsMgqq2xGJZLpbHRMHUHcLrElMP8WcA4gjGMrcoBk,10208
123
124
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
124
- pulse_framework-0.1.64.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
125
- pulse_framework-0.1.64.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
126
- pulse_framework-0.1.64.dist-info/METADATA,sha256=vngWZz-tQp2X0ijnpp71j-4W1pRv8_JZCYlU0smMZE4,8300
127
- pulse_framework-0.1.64.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,,