pulse-framework 0.1.41__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/react_component.py CHANGED
@@ -535,10 +535,11 @@ class ComponentRegistry:
535
535
  exc_type: type[BaseException] | None,
536
536
  exc_val: BaseException | None,
537
537
  exc_tb: Any,
538
- ):
538
+ ) -> Literal[False]:
539
539
  if self._token:
540
540
  COMPONENT_REGISTRY.reset(self._token)
541
541
  self._token = None
542
+ return False
542
543
 
543
544
 
544
545
  COMPONENT_REGISTRY: ContextVar[ComponentRegistry] = ContextVar(
pulse/reactive.py CHANGED
@@ -6,6 +6,7 @@ from contextvars import ContextVar, Token
6
6
  from typing import (
7
7
  Any,
8
8
  Generic,
9
+ Literal,
9
10
  ParamSpec,
10
11
  TypeVar,
11
12
  override,
@@ -20,6 +21,7 @@ from pulse.helpers import (
20
21
  )
21
22
 
22
23
  T = TypeVar("T")
24
+ T_co = TypeVar("T_co", covariant=True)
23
25
  P = ParamSpec("P")
24
26
 
25
27
 
@@ -94,16 +96,16 @@ class Signal(Generic[T]):
94
96
  obs.push_change()
95
97
 
96
98
 
97
- class Computed(Generic[T]):
98
- fn: Callable[..., T]
99
+ class Computed(Generic[T_co]):
100
+ fn: Callable[..., T_co]
99
101
  name: str | None
100
102
  dirty: bool
101
103
  on_stack: bool
102
104
  accepts_prev_value: bool
103
105
 
104
- def __init__(self, fn: Callable[..., T], name: str | None = None):
106
+ def __init__(self, fn: Callable[..., T_co], name: str | None = None):
105
107
  self.fn = fn
106
- self.value: T = None # pyright: ignore[reportAttributeAccessIssue]
108
+ self.value: T_co = None # pyright: ignore[reportAttributeAccessIssue]
107
109
  self.name = name
108
110
  self.dirty = False
109
111
  self.on_stack = False
@@ -125,7 +127,7 @@ class Computed(Generic[T]):
125
127
  for p in params
126
128
  )
127
129
 
128
- def read(self) -> T:
130
+ def read(self) -> T_co:
129
131
  if self.on_stack:
130
132
  raise RuntimeError("Circular dependency detected")
131
133
 
@@ -138,10 +140,10 @@ class Computed(Generic[T]):
138
140
  rc.scope.register_dep(self)
139
141
  return self.value
140
142
 
141
- def __call__(self) -> T:
143
+ def __call__(self) -> T_co:
142
144
  return self.read()
143
145
 
144
- def unwrap(self) -> T:
146
+ def unwrap(self) -> T_co:
145
147
  """Return the current value while registering subscriptions."""
146
148
  return self.read()
147
149
 
@@ -268,6 +270,8 @@ class Effect(Disposable):
268
270
  last_run: int
269
271
  immediate: bool
270
272
  _lazy: bool
273
+ _interval: float | None
274
+ _interval_handle: asyncio.TimerHandle | None
271
275
  explicit_deps: bool
272
276
  batch: "Batch | None"
273
277
 
@@ -279,6 +283,7 @@ class Effect(Disposable):
279
283
  lazy: bool = False,
280
284
  on_error: Callable[[Exception], None] | None = None,
281
285
  deps: list[Signal[Any] | Computed[Any]] | None = None,
286
+ interval: float | None = None,
282
287
  ):
283
288
  self.fn = fn # type: ignore[assignment]
284
289
  self.name = name
@@ -294,6 +299,8 @@ class Effect(Disposable):
294
299
  self.explicit_deps = deps is not None
295
300
  self.immediate = immediate
296
301
  self._lazy = lazy
302
+ self._interval = interval
303
+ self._interval_handle = None
297
304
 
298
305
  if immediate and lazy:
299
306
  raise ValueError("An effect cannot be boht immediate and lazy")
@@ -321,7 +328,7 @@ class Effect(Disposable):
321
328
 
322
329
  @override
323
330
  def dispose(self):
324
- self.unschedule()
331
+ self.cancel(cancel_interval=True)
325
332
  for child in self.children.copy():
326
333
  child.dispose()
327
334
  if self.cleanup_fn:
@@ -331,6 +338,26 @@ class Effect(Disposable):
331
338
  if self.parent:
332
339
  self.parent.children.remove(self)
333
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
+
334
361
  def schedule(self):
335
362
  # Immediate effects run right away when scheduled and do not enter a batch
336
363
  if self.immediate:
@@ -341,12 +368,26 @@ class Effect(Disposable):
341
368
  batch.register_effect(self)
342
369
  self.batch = batch
343
370
 
344
- 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
+ """
345
379
  if self.batch is not None:
346
380
  self.batch.effects.remove(self)
347
381
  self.batch = None
382
+ if cancel_interval:
383
+ self._cancel_interval()
348
384
 
349
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
350
391
  self.schedule()
351
392
 
352
393
  def should_run(self):
@@ -426,6 +467,7 @@ class Effect(Disposable):
426
467
  "lazy": self._lazy,
427
468
  "on_error": self.on_error,
428
469
  "deps": deps,
470
+ "interval": self._interval,
429
471
  }
430
472
 
431
473
  def __copy__(self):
@@ -471,12 +513,16 @@ class Effect(Disposable):
471
513
  self.runs += 1
472
514
  self.last_run = execution_epoch
473
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()
474
519
 
475
520
 
476
521
  class AsyncEffect(Effect):
477
522
  fn: AsyncEffectFn # pyright: ignore[reportIncompatibleMethodOverride]
478
523
  batch: None # pyright: ignore[reportIncompatibleVariableOverride]
479
524
  _task: asyncio.Task[None] | None
525
+ _task_started: bool
480
526
 
481
527
  def __init__(
482
528
  self,
@@ -485,9 +531,11 @@ class AsyncEffect(Effect):
485
531
  lazy: bool = False,
486
532
  on_error: Callable[[Exception], None] | None = None,
487
533
  deps: list[Signal[Any] | Computed[Any]] | None = None,
534
+ interval: float | None = None,
488
535
  ):
489
536
  # Track an async task when running async effects
490
537
  self._task = None
538
+ self._task_started = False
491
539
  super().__init__(
492
540
  fn=fn, # pyright: ignore[reportArgumentType]
493
541
  name=name,
@@ -495,8 +543,19 @@ class AsyncEffect(Effect):
495
543
  lazy=lazy,
496
544
  on_error=on_error,
497
545
  deps=deps,
546
+ interval=interval,
498
547
  )
499
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
+
500
559
  @override
501
560
  def schedule(self):
502
561
  """
@@ -524,13 +583,14 @@ class AsyncEffect(Effect):
524
583
  """
525
584
  execution_epoch = epoch()
526
585
 
527
- # Cancel any previous run still in flight
528
- self.cancel()
586
+ # Cancel any previous run still in flight, but preserve the interval
587
+ self.cancel(cancel_interval=False)
529
588
  this_task: asyncio.Task[None] | None = None
530
589
 
531
590
  async def _runner():
532
591
  nonlocal execution_epoch, this_task
533
592
  try:
593
+ self._task_started = True
534
594
  # Perform cleanups in the new task
535
595
  with Untrack():
536
596
  try:
@@ -557,10 +617,14 @@ class AsyncEffect(Effect):
557
617
  self.runs += 1
558
618
  self.last_run = execution_epoch
559
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()
560
623
  finally:
561
624
  # Clear the task reference when it finishes
562
625
  if self._task is this_task:
563
626
  self._task = None
627
+ self._task_started = False
564
628
 
565
629
  this_task = create_task(_runner(), name=f"effect:{self.name or 'unnamed'}")
566
630
  self._task = this_task
@@ -570,23 +634,34 @@ class AsyncEffect(Effect):
570
634
  async def __call__(self): # pyright: ignore[reportIncompatibleMethodOverride]
571
635
  await self.run()
572
636
 
573
- def cancel(self) -> None:
574
- # 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
+ """
575
645
  if self._task:
576
646
  t = self._task
577
647
  self._task = None
578
648
  if not t.cancelled():
579
649
  t.cancel()
650
+ if cancel_interval:
651
+ self._cancel_interval()
580
652
 
581
653
  async def wait(self) -> None:
582
654
  """
583
- Wait until the completion of the current task if it's already running,
584
- or start a run if it's not running. In case of cancellation, awaits
585
- 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.
586
658
  """
587
659
  while True:
660
+ if self._task is None or self._task.done():
661
+ # No task running, return immediately
662
+ return
588
663
  try:
589
- await (self._task or self.run())
664
+ await self._task
590
665
  return
591
666
  except asyncio.CancelledError:
592
667
  # If wait() itself is cancelled, propagate it
@@ -595,13 +670,14 @@ class AsyncEffect(Effect):
595
670
  current_task.cancelling() > 0 or current_task.cancelled()
596
671
  ):
597
672
  raise
598
- # 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
599
675
  continue
600
676
 
601
677
  @override
602
678
  def dispose(self):
603
- # Run children cleanups first, then cancel in-flight task
604
- self.cancel()
679
+ # Run children cleanups first, then cancel in-flight task and interval
680
+ self.cancel(cancel_interval=True)
605
681
  for child in self.children.copy():
606
682
  child.dispose()
607
683
  if self.cleanup_fn:
@@ -675,11 +751,12 @@ class Batch:
675
751
  exc_type: type[BaseException] | None,
676
752
  exc_value: BaseException | None,
677
753
  exc_traceback: Any,
678
- ):
754
+ ) -> Literal[False]:
679
755
  self.flush()
680
756
  # Restore previous reactive context
681
757
  if self._token:
682
758
  REACTIVE_CONTEXT.reset(self._token)
759
+ return False
683
760
 
684
761
 
685
762
  class GlobalBatch(Batch):
@@ -755,10 +832,11 @@ class Scope:
755
832
  exc_type: type[BaseException] | None,
756
833
  exc_value: BaseException | None,
757
834
  exc_traceback: Any,
758
- ):
835
+ ) -> Literal[False]:
759
836
  # Restore previous reactive context
760
837
  if self._token:
761
838
  REACTIVE_CONTEXT.reset(self._token)
839
+ return False
762
840
 
763
841
 
764
842
  class Untrack(Scope): ...
@@ -801,8 +879,9 @@ class ReactiveContext:
801
879
  exc_type: type[BaseException] | None,
802
880
  exc_value: BaseException | None,
803
881
  exc_tb: Any,
804
- ):
882
+ ) -> Literal[False]:
805
883
  REACTIVE_CONTEXT.reset(self._tokens.pop())
884
+ return False
806
885
 
807
886
 
808
887
  def epoch():
@@ -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
 
pulse/vdom.py CHANGED
@@ -23,6 +23,7 @@ from typing import (
23
23
  )
24
24
 
25
25
  from pulse.hooks.core import HookContext
26
+ from pulse.hooks.init import rewrite_init_blocks
26
27
 
27
28
  # ============================================================================
28
29
  # Core VDOM
@@ -291,7 +292,8 @@ def component(
291
292
  fn: "Callable[P, Element] | None" = None, *, name: str | None = None
292
293
  ) -> "Component[P] | Callable[[Callable[P, Element]], Component[P]]":
293
294
  def decorator(fn: Callable[P, Element]):
294
- return Component(fn, name)
295
+ rewritten = rewrite_init_blocks(fn)
296
+ return Component(rewritten, name)
295
297
 
296
298
  if fn is not None:
297
299
  return decorator(fn)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.41
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=aqZV3hIDf07lrvBN-SFFr37Inunj-V68yR8DSUCU4xI,32102
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
@@ -24,16 +24,17 @@ pulse/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
24
24
  pulse/components/for_.py,sha256=LUyJEUlDM6b9oPjvUFgSsddxu6b6usF4BQdXe8FIiGI,1302
25
25
  pulse/components/if_.py,sha256=rQywsmdirNpkb-61ZEdF-tgzUh-37JWd4YFGblkzIdQ,1624
26
26
  pulse/components/react_router.py,sha256=TbRec-NVliUqrvAMeFXCrnDWV1rh6TGTPfRhqLuLubk,1129
27
- pulse/context.py,sha256=x_nCbCEUGygAdCZiTfko5uuYxVSAeCNhYa59zBq015M,1692
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
- pulse/hooks/core.py,sha256=dIo4axztiuaRZpWhp1a3PdhGoMCYf-Z5lxGHDUaIwp0,7389
35
+ pulse/hooks/core.py,sha256=JTZbVxNOEs_GAeK6Bh6hemSTkgwZPtEi_wt55cvOdik,7381
36
36
  pulse/hooks/effects.py,sha256=CQvt5viAweGLSxaGGlWm155GlEQiwQnGussw7OfiCGc,2393
37
+ pulse/hooks/init.py,sha256=snTy3PJtkSnnKBrAjcNOJbem2896xJzHD0DHLVVeyAo,11924
37
38
  pulse/hooks/runtime.py,sha256=k5LZ8hnlNBMKOiEkQcAvs8BKwYxV6gwea2WCfju5K7Y,5106
38
39
  pulse/hooks/setup.py,sha256=GJLSE6hLBNKHR9aLhvsS6KXwpOXQiSx1V3E2IkGADWM,4461
39
40
  pulse/hooks/stable.py,sha256=uHEJ2E22r2kHx4uFjWjDepQ6OtPjLd7tT5ju-yKlkCU,1702
@@ -51,26 +52,28 @@ pulse/plugin.py,sha256=RfGl6Vtr7VRHb8bp4Ob4dOX9dVzvc4Riu7HWnStMPpk,580
51
52
  pulse/proxy.py,sha256=zh4v5lmYNg5IBE_xdHHmGPwbMQNSXb2npeLXvw_O1Oc,6591
52
53
  pulse/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
54
  pulse/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- pulse/queries/common.py,sha256=2_11SEOFrDbe_ULNSA9EWeOkvz-xym5hCuX4bW455t0,556
55
- pulse/queries/mutation.py,sha256=_0-o2g2yux52hTsRLGuWwFUdGrBx9YJqi67oBw9iNcc,4209
56
- pulse/queries/query.py,sha256=WxBEaEjtzDGWWKpSi8-xl9xBvPvREYH1Tonl_lOY-VQ,7347
57
- pulse/queries/query_observer.py,sha256=Wd3pk5OsZB_ze6DnpOkASv4Ny5EuSbwUlUEPXIXxSgk,10229
58
- pulse/queries/store.py,sha256=ylSCOHiXp8vEyEWc5Et8zLWkyHj5OQYKOVfs0OehpX8,1465
59
- pulse/react_component.py,sha256=Rw1J6cHOX8-K3BnkswVOu2COgneVvRz1OYmyXkX17RM,25993
60
- pulse/reactive.py,sha256=rJap9AL9Af-kMTZ_bVpE0_9M9fGVy-1J4oa3nTzMHXw,21078
61
- pulse/reactive_extensions.py,sha256=gTLkQ0urwANjWNHWMkg-P9zvpevHCnNKd5BSM8G0pno,31521
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
62
+ pulse/react_component.py,sha256=hPibKBEkVdpBKNSpMQ6bZ-7GnJQcNQwcw2SvfY1chHA,26026
63
+ pulse/reactive.py,sha256=0CqzeowZHAENMxM2PFhi_Q6KkDzG844JofzDk0Wxckw,23961
64
+ pulse/reactive_extensions.py,sha256=WAx4hlB1ByZLFVpgtRnaWXAQ3N2QQplf647oQXDL5vg,31901
62
65
  pulse/render_session.py,sha256=kqLfZ9AxCrB2mIJqegATL1KA7CI-LZSBQwRYr7Uxo9g,14581
63
66
  pulse/renderer.py,sha256=dJiX9VeHr9kC1UBw5oaKB8Mv-3OCMGTrHiKgLJ5FL50,16759
64
67
  pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
65
68
  pulse/routing.py,sha256=RlrGHyK4F28_zUHMYNeKp4pNbvqrt4GY4t5xNdhzunI,13926
66
69
  pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
67
- pulse/state.py,sha256=mytXlQjmLIBjB2XDgCg9E1fHCcyoNQ02cBqZ_vldxuc,10636
70
+ pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
68
71
  pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
72
  pulse/types/event_handler.py,sha256=tfKa6OEA5XvzuYbllQZJ03ooN7rGSYOtaPBstSL4OLU,1642
70
73
  pulse/user_session.py,sha256=Nn9ZZha1Rruw31OSoK14QaEL0erGVFbryFhJYrtMZsQ,7599
71
- pulse/vdom.py,sha256=KTNBh2dVvDy9eXRzhneBJgk7F35MyWec8R_puQ4tSRY,12420
74
+ pulse/vdom.py,sha256=1UAjOYSmpdZeSVELqejh47Jer4mA73T_q2HtAogOphs,12514
72
75
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
73
- pulse_framework-0.1.41.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
74
- pulse_framework-0.1.41.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
75
- pulse_framework-0.1.41.dist-info/METADATA,sha256=vpFfqYHPFqBUL86mDFhOUwcQvgKK36xMYSYijrOhpFU,580
76
- pulse_framework-0.1.41.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,,