pulse-framework 0.1.42__py3-none-any.whl → 0.1.43__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/store.py CHANGED
@@ -1,7 +1,10 @@
1
+ import datetime as dt
1
2
  from collections.abc import Awaitable, Callable
2
- from typing import Any, TypeVar
3
+ from typing import Any, TypeVar, cast
3
4
 
4
- from pulse.queries.query import RETRY_DELAY_DEFAULT, Query, QueryKey
5
+ from pulse.queries.common import QueryKey
6
+ from pulse.queries.infinite_query import InfiniteQuery, Page
7
+ from pulse.queries.query import RETRY_DELAY_DEFAULT, Query
5
8
 
6
9
  T = TypeVar("T")
7
10
 
@@ -12,9 +15,14 @@ class QueryStore:
12
15
  """
13
16
 
14
17
  def __init__(self):
15
- self._entries: dict[QueryKey, Query[Any]] = {}
18
+ self._entries: dict[QueryKey, Query[Any] | InfiniteQuery[Any, Any]] = {}
16
19
 
17
- def get(self, key: QueryKey) -> Query[Any] | None:
20
+ def items(self):
21
+ """Iterate over all (key, query) pairs in the store."""
22
+ return self._entries.items()
23
+
24
+ def get_any(self, key: QueryKey) -> Query[Any] | InfiniteQuery[Any, Any] | None:
25
+ """Get any query (regular or infinite) by key, or None if not found."""
18
26
  return self._entries.get(key)
19
27
 
20
28
  def ensure(
@@ -22,13 +30,19 @@ class QueryStore:
22
30
  key: QueryKey,
23
31
  fetch_fn: Callable[[], Awaitable[T]],
24
32
  initial_data: T | None = None,
33
+ initial_data_updated_at: float | dt.datetime | None = None,
25
34
  gc_time: float = 300.0,
26
35
  retries: int = 3,
27
36
  retry_delay: float = RETRY_DELAY_DEFAULT,
28
37
  ) -> Query[T]:
29
38
  # Return existing entry if present
30
- if key in self._entries:
31
- return self._entries[key]
39
+ existing = self._entries.get(key)
40
+ if existing:
41
+ if isinstance(existing, InfiniteQuery):
42
+ raise TypeError(
43
+ "Query key is already used for an infinite query; cannot reuse for regular query"
44
+ )
45
+ return cast(Query[T], existing)
32
46
 
33
47
  def _on_dispose(e: Query[Any]) -> None:
34
48
  if e.key in self._entries and self._entries[e.key] is e:
@@ -38,6 +52,7 @@ class QueryStore:
38
52
  key,
39
53
  fetch_fn,
40
54
  initial_data=initial_data,
55
+ initial_data_updated_at=initial_data_updated_at,
41
56
  gc_time=gc_time,
42
57
  retries=retries,
43
58
  retry_delay=retry_delay,
@@ -46,15 +61,63 @@ class QueryStore:
46
61
  self._entries[key] = entry
47
62
  return entry
48
63
 
49
- def remove(self, key: QueryKey):
50
- entry = self._entries.get(key)
51
- if entry:
52
- entry.dispose()
53
-
54
- def get_queries(
55
- self, predicate: Callable[[Query[Any]], bool] | None = None
56
- ) -> list[Query[Any]]:
57
- """Get all queries matching the predicate."""
58
- if predicate is None:
59
- return list(self._entries.values())
60
- return [e for e in self._entries.values() if predicate(e)]
64
+ def get(self, key: QueryKey) -> Query[Any] | None:
65
+ """
66
+ Get an existing regular query by key, or None if not found.
67
+ """
68
+ existing = self._entries.get(key)
69
+ if existing and isinstance(existing, InfiniteQuery):
70
+ return None
71
+ return existing
72
+
73
+ def get_infinite(self, key: QueryKey) -> InfiniteQuery[Any, Any] | None:
74
+ """
75
+ Get an existing infinite query by key, or None if not found.
76
+ """
77
+ existing = self._entries.get(key)
78
+ if existing and isinstance(existing, InfiniteQuery):
79
+ return existing
80
+ return None
81
+
82
+ def ensure_infinite(
83
+ self,
84
+ key: QueryKey,
85
+ query_fn: Callable[[Any], Awaitable[Any]],
86
+ *,
87
+ initial_page_param: Any,
88
+ get_next_page_param: Callable[[list[Page[Any, Any]]], Any | None],
89
+ get_previous_page_param: Callable[[list[Page[Any, Any]]], Any | None]
90
+ | None = None,
91
+ max_pages: int = 0,
92
+ initial_data_updated_at: float | dt.datetime | None = None,
93
+ gc_time: float = 300.0,
94
+ retries: int = 3,
95
+ retry_delay: float = RETRY_DELAY_DEFAULT,
96
+ ) -> InfiniteQuery[Any, Any]:
97
+ existing = self._entries.get(key)
98
+ if existing:
99
+ if not isinstance(existing, InfiniteQuery):
100
+ raise TypeError(
101
+ "Query key is already used for a regular query; cannot reuse for infinite query"
102
+ )
103
+ return existing
104
+
105
+ def _on_dispose(e: InfiniteQuery[Any, Any]) -> None:
106
+ if e.key in self._entries and self._entries[e.key] is e:
107
+ del self._entries[e.key]
108
+
109
+ entry = InfiniteQuery(
110
+ key,
111
+ query_fn,
112
+ initial_page_param=initial_page_param,
113
+ get_next_page_param=get_next_page_param,
114
+ get_previous_page_param=get_previous_page_param,
115
+ max_pages=max_pages,
116
+ initial_data_updated_at=initial_data_updated_at,
117
+ gc_time=gc_time,
118
+ retries=retries,
119
+ retry_delay=retry_delay,
120
+ on_dispose=_on_dispose,
121
+ )
122
+ self._entries[key] = entry
123
+ return entry
pulse/reactive.py CHANGED
@@ -21,6 +21,7 @@ from pulse.helpers import (
21
21
  )
22
22
 
23
23
  T = TypeVar("T")
24
+ T_co = TypeVar("T_co", covariant=True)
24
25
  P = ParamSpec("P")
25
26
 
26
27
 
@@ -95,16 +96,16 @@ class Signal(Generic[T]):
95
96
  obs.push_change()
96
97
 
97
98
 
98
- class Computed(Generic[T]):
99
- fn: Callable[..., T]
99
+ class Computed(Generic[T_co]):
100
+ fn: Callable[..., T_co]
100
101
  name: str | None
101
102
  dirty: bool
102
103
  on_stack: bool
103
104
  accepts_prev_value: bool
104
105
 
105
- def __init__(self, fn: Callable[..., T], name: str | None = None):
106
+ def __init__(self, fn: Callable[..., T_co], name: str | None = None):
106
107
  self.fn = fn
107
- self.value: T = None # pyright: ignore[reportAttributeAccessIssue]
108
+ self.value: T_co = None # pyright: ignore[reportAttributeAccessIssue]
108
109
  self.name = name
109
110
  self.dirty = False
110
111
  self.on_stack = False
@@ -126,7 +127,7 @@ class Computed(Generic[T]):
126
127
  for p in params
127
128
  )
128
129
 
129
- def read(self) -> T:
130
+ def read(self) -> T_co:
130
131
  if self.on_stack:
131
132
  raise RuntimeError("Circular dependency detected")
132
133
 
@@ -139,10 +140,10 @@ class Computed(Generic[T]):
139
140
  rc.scope.register_dep(self)
140
141
  return self.value
141
142
 
142
- def __call__(self) -> T:
143
+ def __call__(self) -> T_co:
143
144
  return self.read()
144
145
 
145
- def unwrap(self) -> T:
146
+ def unwrap(self) -> T_co:
146
147
  """Return the current value while registering subscriptions."""
147
148
  return self.read()
148
149
 
@@ -269,6 +270,8 @@ class Effect(Disposable):
269
270
  last_run: int
270
271
  immediate: bool
271
272
  _lazy: bool
273
+ _interval: float | None
274
+ _interval_handle: asyncio.TimerHandle | None
272
275
  explicit_deps: bool
273
276
  batch: "Batch | None"
274
277
 
@@ -280,6 +283,7 @@ class Effect(Disposable):
280
283
  lazy: bool = False,
281
284
  on_error: Callable[[Exception], None] | None = None,
282
285
  deps: list[Signal[Any] | Computed[Any]] | None = None,
286
+ interval: float | None = None,
283
287
  ):
284
288
  self.fn = fn # type: ignore[assignment]
285
289
  self.name = name
@@ -295,6 +299,8 @@ class Effect(Disposable):
295
299
  self.explicit_deps = deps is not None
296
300
  self.immediate = immediate
297
301
  self._lazy = lazy
302
+ self._interval = interval
303
+ self._interval_handle = None
298
304
 
299
305
  if immediate and lazy:
300
306
  raise ValueError("An effect cannot be boht immediate and lazy")
@@ -322,7 +328,7 @@ class Effect(Disposable):
322
328
 
323
329
  @override
324
330
  def dispose(self):
325
- self.unschedule()
331
+ self.cancel(cancel_interval=True)
326
332
  for child in self.children.copy():
327
333
  child.dispose()
328
334
  if self.cleanup_fn:
@@ -332,6 +338,26 @@ class Effect(Disposable):
332
338
  if self.parent:
333
339
  self.parent.children.remove(self)
334
340
 
341
+ def _schedule_interval(self):
342
+ """Schedule the next interval run if interval is set."""
343
+ if self._interval is not None and self._interval > 0:
344
+ from pulse.helpers import later
345
+
346
+ self._interval_handle = later(self._interval, self._on_interval)
347
+
348
+ def _on_interval(self):
349
+ """Called when the interval timer fires."""
350
+ if self._interval is not None:
351
+ # Run directly instead of scheduling - interval runs are unconditional
352
+ self.run()
353
+ self._schedule_interval()
354
+
355
+ def _cancel_interval(self):
356
+ """Cancel the interval timer."""
357
+ if self._interval_handle is not None:
358
+ self._interval_handle.cancel()
359
+ self._interval_handle = None
360
+
335
361
  def schedule(self):
336
362
  # Immediate effects run right away when scheduled and do not enter a batch
337
363
  if self.immediate:
@@ -342,12 +368,26 @@ class Effect(Disposable):
342
368
  batch.register_effect(self)
343
369
  self.batch = batch
344
370
 
345
- def unschedule(self):
371
+ def cancel(self, cancel_interval: bool = True):
372
+ """
373
+ Cancel the effect. For sync effects, removes from batch.
374
+ For async effects (override), also cancels the running task.
375
+
376
+ Args:
377
+ cancel_interval: If True (default), also cancels the interval timer.
378
+ """
346
379
  if self.batch is not None:
347
380
  self.batch.effects.remove(self)
348
381
  self.batch = None
382
+ if cancel_interval:
383
+ self._cancel_interval()
349
384
 
350
385
  def push_change(self):
386
+ # Short-circuit if already scheduled in a batch.
387
+ # This avoids redundant schedule() calls and O(n) list checks
388
+ # when the same effect is reached through multiple dependency paths.
389
+ if self.batch is not None:
390
+ return
351
391
  self.schedule()
352
392
 
353
393
  def should_run(self):
@@ -427,6 +467,7 @@ class Effect(Disposable):
427
467
  "lazy": self._lazy,
428
468
  "on_error": self.on_error,
429
469
  "deps": deps,
470
+ "interval": self._interval,
430
471
  }
431
472
 
432
473
  def __copy__(self):
@@ -472,12 +513,16 @@ class Effect(Disposable):
472
513
  self.runs += 1
473
514
  self.last_run = execution_epoch
474
515
  self._apply_scope_results(scope, captured_last_changes)
516
+ # Start/restart interval if set and not currently scheduled
517
+ if self._interval is not None and self._interval_handle is None:
518
+ self._schedule_interval()
475
519
 
476
520
 
477
521
  class AsyncEffect(Effect):
478
522
  fn: AsyncEffectFn # pyright: ignore[reportIncompatibleMethodOverride]
479
523
  batch: None # pyright: ignore[reportIncompatibleVariableOverride]
480
524
  _task: asyncio.Task[None] | None
525
+ _task_started: bool
481
526
 
482
527
  def __init__(
483
528
  self,
@@ -486,9 +531,11 @@ class AsyncEffect(Effect):
486
531
  lazy: bool = False,
487
532
  on_error: Callable[[Exception], None] | None = None,
488
533
  deps: list[Signal[Any] | Computed[Any]] | None = None,
534
+ interval: float | None = None,
489
535
  ):
490
536
  # Track an async task when running async effects
491
537
  self._task = None
538
+ self._task_started = False
492
539
  super().__init__(
493
540
  fn=fn, # pyright: ignore[reportArgumentType]
494
541
  name=name,
@@ -496,8 +543,19 @@ class AsyncEffect(Effect):
496
543
  lazy=lazy,
497
544
  on_error=on_error,
498
545
  deps=deps,
546
+ interval=interval,
499
547
  )
500
548
 
549
+ @override
550
+ def push_change(self):
551
+ # Short-circuit if task exists but hasn't started executing yet.
552
+ # This avoids cancelling and recreating tasks multiple times when reached
553
+ # through multiple dependency paths before the event loop runs.
554
+ # Once the task starts running, new push_change calls will cancel and restart.
555
+ if self._task is not None and not self._task.done() and not self._task_started:
556
+ return
557
+ self.schedule()
558
+
501
559
  @override
502
560
  def schedule(self):
503
561
  """
@@ -525,13 +583,14 @@ class AsyncEffect(Effect):
525
583
  """
526
584
  execution_epoch = epoch()
527
585
 
528
- # Cancel any previous run still in flight
529
- self.cancel()
586
+ # Cancel any previous run still in flight, but preserve the interval
587
+ self.cancel(cancel_interval=False)
530
588
  this_task: asyncio.Task[None] | None = None
531
589
 
532
590
  async def _runner():
533
591
  nonlocal execution_epoch, this_task
534
592
  try:
593
+ self._task_started = True
535
594
  # Perform cleanups in the new task
536
595
  with Untrack():
537
596
  try:
@@ -558,10 +617,14 @@ class AsyncEffect(Effect):
558
617
  self.runs += 1
559
618
  self.last_run = execution_epoch
560
619
  self._apply_scope_results(scope, captured_last_changes)
620
+ # Start/restart interval if set and not currently scheduled
621
+ if self._interval is not None and self._interval_handle is None:
622
+ self._schedule_interval()
561
623
  finally:
562
624
  # Clear the task reference when it finishes
563
625
  if self._task is this_task:
564
626
  self._task = None
627
+ self._task_started = False
565
628
 
566
629
  this_task = create_task(_runner(), name=f"effect:{self.name or 'unnamed'}")
567
630
  self._task = this_task
@@ -571,23 +634,34 @@ class AsyncEffect(Effect):
571
634
  async def __call__(self): # pyright: ignore[reportIncompatibleMethodOverride]
572
635
  await self.run()
573
636
 
574
- def cancel(self) -> None:
575
- # No batch removal needed as AsyncEffect is not batched
637
+ @override
638
+ def cancel(self, cancel_interval: bool = True) -> None:
639
+ """
640
+ Cancel the async effect. Cancels the running task and optionally the interval.
641
+
642
+ Args:
643
+ cancel_interval: If True (default), also cancels the interval timer.
644
+ """
576
645
  if self._task:
577
646
  t = self._task
578
647
  self._task = None
579
648
  if not t.cancelled():
580
649
  t.cancel()
650
+ if cancel_interval:
651
+ self._cancel_interval()
581
652
 
582
653
  async def wait(self) -> None:
583
654
  """
584
- Wait until the completion of the current task if it's already running,
585
- or start a run if it's not running. In case of cancellation, awaits
586
- the new task by recursively calling itself.
655
+ Wait until the completion of the current task if it's already running.
656
+ Does not start a new task if none is running.
657
+ If the task is cancelled while waiting, waits for a new task if one is started.
587
658
  """
588
659
  while True:
660
+ if self._task is None or self._task.done():
661
+ # No task running, return immediately
662
+ return
589
663
  try:
590
- await (self._task or self.run())
664
+ await self._task
591
665
  return
592
666
  except asyncio.CancelledError:
593
667
  # If wait() itself is cancelled, propagate it
@@ -596,13 +670,14 @@ class AsyncEffect(Effect):
596
670
  current_task.cancelling() > 0 or current_task.cancelled()
597
671
  ):
598
672
  raise
599
- # Effect task was cancelled, continue waiting for new task
673
+ # Effect task was cancelled, check if a new task was started
674
+ # and continue waiting if so
600
675
  continue
601
676
 
602
677
  @override
603
678
  def dispose(self):
604
- # Run children cleanups first, then cancel in-flight task
605
- self.cancel()
679
+ # Run children cleanups first, then cancel in-flight task and interval
680
+ self.cancel(cancel_interval=True)
606
681
  for child in self.children.copy():
607
682
  child.dispose()
608
683
  if self.cleanup_fn:
@@ -405,18 +405,19 @@ class ReactiveList(list[T1]):
405
405
  - Setting an index writes to that index's Signal
406
406
  - Structural operations (append/insert/pop/remove/clear/extend/sort/reverse/slice assigns)
407
407
  trigger a structural version Signal so consumers can listen for changes that affect layout
408
- - Iteration and len are NOT reactive; prefer explicit index reads inside render/effects
408
+ - Iteration subscribes to all item signals and structural changes
409
+ - len() subscribes to structural changes
409
410
  """
410
411
 
411
412
  __slots__: tuple[str, ...] = ("_signals", "_structure")
412
413
 
413
- def __init__(self, initial: Iterable[_Any] | None = None) -> None:
414
+ def __init__(self, initial: Iterable[T1] | None = None) -> None:
414
415
  super().__init__()
415
- self._signals: list[Signal[_Any]] = []
416
+ self._signals: list[Signal[T1]] = []
416
417
  self._structure: Signal[int] = Signal(0)
417
418
  if initial:
418
419
  for item in initial:
419
- v = cast(_Any, reactive(item))
420
+ v = reactive(item)
420
421
  self._signals.append(Signal(v))
421
422
  super().append(v)
422
423
 
@@ -617,7 +618,8 @@ class ReactiveList(list[T1]):
617
618
  @override
618
619
  def __iter__(self) -> Iterator[T1]:
619
620
  self._structure.read()
620
- return super().__iter__()
621
+ for sig in self._signals:
622
+ yield sig.read()
621
623
 
622
624
  def __copy__(self):
623
625
  result = type(self)()
@@ -640,7 +642,7 @@ class ReactiveSet(set[T1]):
640
642
 
641
643
  - `x in s` reads a membership Signal for element `x`
642
644
  - Mutations update membership Signals for affected elements
643
- - Iteration and len are NOT reactive
645
+ - Iteration subscribes to membership signals for all elements
644
646
  """
645
647
 
646
648
  __slots__: tuple[str, ...] = ("_signals",)
@@ -664,6 +666,12 @@ class ReactiveSet(set[T1]):
664
666
  sig = self._signals[element]
665
667
  return bool(sig.read())
666
668
 
669
+ @override
670
+ def __iter__(self) -> Iterator[T1]:
671
+ # Subscribe to membership signals and return present elements
672
+ present = [elem for elem, sig in self._signals.items() if sig.read()]
673
+ return iter(present)
674
+
667
675
  @override
668
676
  def add(self, element: T1) -> None:
669
677
  element = reactive(element)
@@ -1068,7 +1076,11 @@ def unwrap(value: _Any, untrack: bool = False) -> _Any:
1068
1076
  return {k: _unwrap(val) for k, val in v.items()}
1069
1077
  if isinstance(v, Sequence) and not isinstance(v, (str, bytes, bytearray)):
1070
1078
  if isinstance(v, tuple):
1071
- return tuple(_unwrap(val) for val in v)
1079
+ # Preserve namedtuple types
1080
+ if hasattr(v, "_fields"):
1081
+ return type(v)(*(_unwrap(val) for val in v))
1082
+ else:
1083
+ return tuple(_unwrap(val) for val in v)
1072
1084
  return [_unwrap(val) for val in v]
1073
1085
  if isinstance(v, set):
1074
1086
  return {_unwrap(val) for val in v}
pulse/state.py CHANGED
@@ -80,6 +80,7 @@ class StateEffect(Generic[T], InitializableProperty):
80
80
  on_error: "Callable[[Exception], None] | None"
81
81
  lazy: bool
82
82
  deps: "list[Signal[Any] | Computed[Any]] | None"
83
+ interval: float | None
83
84
 
84
85
  def __init__(
85
86
  self,
@@ -89,6 +90,7 @@ class StateEffect(Generic[T], InitializableProperty):
89
90
  lazy: bool = False,
90
91
  on_error: "Callable[[Exception], None] | None" = None,
91
92
  deps: "list[Signal[Any] | Computed[Any]] | None" = None,
93
+ interval: float | None = None,
92
94
  ):
93
95
  self.fn = fn
94
96
  self.name = name
@@ -96,6 +98,7 @@ class StateEffect(Generic[T], InitializableProperty):
96
98
  self.on_error = on_error
97
99
  self.lazy = lazy
98
100
  self.deps = deps
101
+ self.interval = interval
99
102
 
100
103
  @override
101
104
  def initialize(self, state: "State", name: str):
@@ -108,6 +111,7 @@ class StateEffect(Generic[T], InitializableProperty):
108
111
  lazy=self.lazy,
109
112
  on_error=self.on_error,
110
113
  deps=self.deps,
114
+ interval=self.interval,
111
115
  )
112
116
  else:
113
117
  effect = Effect(
@@ -117,6 +121,7 @@ class StateEffect(Generic[T], InitializableProperty):
117
121
  lazy=self.lazy,
118
122
  on_error=self.on_error,
119
123
  deps=self.deps,
124
+ interval=self.interval,
120
125
  )
121
126
  setattr(state, name, effect)
122
127
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.42
3
+ Version: 0.1.43
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.104.0
@@ -1,4 +1,4 @@
1
- pulse/__init__.py,sha256=w7LVrYNiho18v9JyDQ8DZGdBYV4dZYtlEFiRi32ICiw,32166
1
+ pulse/__init__.py,sha256=Acb_t45WP9HsPBRBtKsx5rj-u-pl8gRmdwsKKVPokK4,32721
2
2
  pulse/app.py,sha256=cVEqazFcgSnmZSxqqz6HWk2QsI8rnKbO7Y_L88BcHSc,32082
3
3
  pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
4
4
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -27,10 +27,10 @@ pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuL
27
27
  pulse/context.py,sha256=fMK6GdQY4q_3452v5DJli2f2_urVihnpzb-O-O9cJ1Q,1734
28
28
  pulse/cookies.py,sha256=c7ua1Lv6mNe1nYnA4SFVvewvRQAbYy9fN5G3Hr_Dr5c,5000
29
29
  pulse/css.py,sha256=-FyQQQ0EZI1Ins30qiF3l4z9yDb1V9qWuJKWxHcKGkw,3910
30
- pulse/decorators.py,sha256=hRfgb9XU1yizmtdhuBln_3Gy-Cz2Smo4rYvAqlURrLQ,9348
30
+ pulse/decorators.py,sha256=ywNgLN6VFcKOM5fbFdUUzh-DWk4BuSXdD1BTfd1N-0U,4827
31
31
  pulse/env.py,sha256=p3XI8KG1ZCcXPD3LJP7fW8JPYfyvoYY5ENwae2o0PiA,2889
32
32
  pulse/form.py,sha256=P7W8guUdGbgqNOk8cSUCWuY6qWre0me6_fypv1qOvqw,8987
33
- pulse/helpers.py,sha256=BBtf--LZxvfpwJU8p92QrZWtOKIWfB3DOiAtGxhet90,13232
33
+ pulse/helpers.py,sha256=aF0SD3z2oGWFRt3BUhoI8JU5SwDAy1a4eg9BaW0LTUg,13745
34
34
  pulse/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  pulse/hooks/core.py,sha256=JTZbVxNOEs_GAeK6Bh6hemSTkgwZPtEi_wt55cvOdik,7381
36
36
  pulse/hooks/effects.py,sha256=CQvt5viAweGLSxaGGlWm155GlEQiwQnGussw7OfiCGc,2393
@@ -52,26 +52,28 @@ pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
52
52
  pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
53
53
  pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- pulse/queries/common.py,sha256=2_11SEOFrDbe_ULNSA9EWeOkvz-xym5hCuX4bW455t0,556
56
- pulse/queries/mutation.py,sha256=_0-o2g2yux52hTsRLGuWwFUdGrBx9YJqi67oBw9iNcc,4209
57
- pulse/queries/query.py,sha256=WxBEaEjtzDGWWKpSi8-xl9xBvPvREYH1Tonl_lOY-VQ,7347
58
- pulse/queries/query_observer.py,sha256=Wd3pk5OsZB_ze6DnpOkASv4Ny5EuSbwUlUEPXIXxSgk,10229
59
- pulse/queries/store.py,sha256=ylSCOHiXp8vEyEWc5Et8zLWkyHj5OQYKOVfs0OehpX8,1465
55
+ pulse/queries/client.py,sha256=hZB3rAfH_fl_6ld_s9z81Vxvt8Cg0gXWU4Gj1qAL0LE,15254
56
+ pulse/queries/common.py,sha256=Cr_NV0dWz5DQ7Qg771jvUms1o2-EnTYqjZJe4tVeoVk,1160
57
+ pulse/queries/effect.py,sha256=8U7iAo3B4WB5lIcT6bE7YRnyfEYTLzNESq9KdDQsVYA,835
58
+ pulse/queries/infinite_query.py,sha256=XBMuVApwBnzsB-toQfsyYRziMwtQhQq8wjtBzCq0qv4,36098
59
+ pulse/queries/mutation.py,sha256=_WVRFU4BwkHrHQjEZc2mjyZTUx9Pad73xFWaFPF7I1Y,5315
60
+ pulse/queries/query.py,sha256=MEKV8eKANFQmiWWs12ytgD9szYNbR2226EW_DkoFiNo,22037
61
+ pulse/queries/store.py,sha256=p_WO7X-REytwkEyGjJi6XDv9tlrJX0v6E1sk1bBrPqM,3541
60
62
  pulse/react_component.py,sha256=hPibKBEkVdpBKNSpMQ6bZ-7GnJQcNQwcw2SvfY1chHA,26026
61
- pulse/reactive.py,sha256=cKZDafbUQFdnNRAxI71THnsFZEbWZ5mU06pMuP6spo8,21187
62
- pulse/reactive_extensions.py,sha256=gTLkQ0urwANjWNHWMkg-P9zvpevHCnNKd5BSM8G0pno,31521
63
+ pulse/reactive.py,sha256=0CqzeowZHAENMxM2PFhi_Q6KkDzG844JofzDk0Wxckw,23961
64
+ pulse/reactive_extensions.py,sha256=WAx4hlB1ByZLFVpgtRnaWXAQ3N2QQplf647oQXDL5vg,31901
63
65
  pulse/render_session.py,sha256=kqLfZ9AxCrB2mIJqegATL1KA7CI-LZSBQwRYr7Uxo9g,14581
64
66
  pulse/renderer.py,sha256=dJiX9VeHr9kC1UBw5oaKB8Mv-3OCMGTrHiKgLJ5FL50,16759
65
67
  pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
66
68
  pulse/routing.py,sha256=RlrGHyK4F28_zUHMYNeKp4pNbvqrt4GY4t5xNdhzunI,13926
67
69
  pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
68
- pulse/state.py,sha256=mytXlQjmLIBjB2XDgCg9E1fHCcyoNQ02cBqZ_vldxuc,10636
70
+ pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
69
71
  pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
72
  pulse/types/event_handler.py,sha256=tfKa6OEA5XvzuYbllQZJ03ooN7rGSYOtaPBstSL4OLU,1642
71
73
  pulse/user_session.py,sha256=Nn9ZZha1Rruw31OSoK14QaEL0erGVFbryFhJYrtMZsQ,7599
72
74
  pulse/vdom.py,sha256=1UAjOYSmpdZeSVELqejh47Jer4mA73T_q2HtAogOphs,12514
73
75
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
74
- pulse_framework-0.1.42.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
75
- pulse_framework-0.1.42.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
76
- pulse_framework-0.1.42.dist-info/METADATA,sha256=Pyy5qb02JHkAFwGpy6m8SkrlPH_mmL6BPxDfvQ41ly4,580
77
- pulse_framework-0.1.42.dist-info/RECORD,,
76
+ pulse_framework-0.1.43.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
77
+ pulse_framework-0.1.43.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
78
+ pulse_framework-0.1.43.dist-info/METADATA,sha256=sXMEvuwI6Pa_bnIgQyYUdhhnBpEXXrtQFWtXG13xMls,580
79
+ pulse_framework-0.1.43.dist-info/RECORD,,