pulse-framework 0.1.44__py3-none-any.whl → 0.1.46__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,10 +1,11 @@
1
1
  import datetime as dt
2
- from collections.abc import Awaitable, Callable
2
+ from collections.abc import Callable
3
3
  from typing import Any, TypeVar, cast
4
4
 
5
+ from pulse.helpers import MISSING
5
6
  from pulse.queries.common import QueryKey
6
7
  from pulse.queries.infinite_query import InfiniteQuery, Page
7
- from pulse.queries.query import RETRY_DELAY_DEFAULT, Query
8
+ from pulse.queries.query import RETRY_DELAY_DEFAULT, KeyedQuery
8
9
 
9
10
  T = TypeVar("T")
10
11
 
@@ -15,26 +16,25 @@ class QueryStore:
15
16
  """
16
17
 
17
18
  def __init__(self):
18
- self._entries: dict[QueryKey, Query[Any] | InfiniteQuery[Any, Any]] = {}
19
+ self._entries: dict[QueryKey, KeyedQuery[Any] | InfiniteQuery[Any, Any]] = {}
19
20
 
20
21
  def items(self):
21
22
  """Iterate over all (key, query) pairs in the store."""
22
23
  return self._entries.items()
23
24
 
24
- def get_any(self, key: QueryKey) -> Query[Any] | InfiniteQuery[Any, Any] | None:
25
+ def get_any(self, key: QueryKey):
25
26
  """Get any query (regular or infinite) by key, or None if not found."""
26
27
  return self._entries.get(key)
27
28
 
28
29
  def ensure(
29
30
  self,
30
31
  key: QueryKey,
31
- fetch_fn: Callable[[], Awaitable[T]],
32
- initial_data: T | None = None,
32
+ initial_data: T | None = MISSING,
33
33
  initial_data_updated_at: float | dt.datetime | None = None,
34
34
  gc_time: float = 300.0,
35
35
  retries: int = 3,
36
36
  retry_delay: float = RETRY_DELAY_DEFAULT,
37
- ) -> Query[T]:
37
+ ) -> KeyedQuery[T]:
38
38
  # Return existing entry if present
39
39
  existing = self._entries.get(key)
40
40
  if existing:
@@ -42,15 +42,14 @@ class QueryStore:
42
42
  raise TypeError(
43
43
  "Query key is already used for an infinite query; cannot reuse for regular query"
44
44
  )
45
- return cast(Query[T], existing)
45
+ return cast(KeyedQuery[T], existing)
46
46
 
47
- def _on_dispose(e: Query[Any]) -> None:
47
+ def _on_dispose(e: KeyedQuery[Any]) -> None:
48
48
  if e.key in self._entries and self._entries[e.key] is e:
49
49
  del self._entries[e.key]
50
50
 
51
- entry = Query(
51
+ entry = KeyedQuery(
52
52
  key,
53
- fetch_fn,
54
53
  initial_data=initial_data,
55
54
  initial_data_updated_at=initial_data_updated_at,
56
55
  gc_time=gc_time,
@@ -61,7 +60,7 @@ class QueryStore:
61
60
  self._entries[key] = entry
62
61
  return entry
63
62
 
64
- def get(self, key: QueryKey) -> Query[Any] | None:
63
+ def get(self, key: QueryKey) -> KeyedQuery[Any] | None:
65
64
  """
66
65
  Get an existing regular query by key, or None if not found.
67
66
  """
@@ -82,7 +81,6 @@ class QueryStore:
82
81
  def ensure_infinite(
83
82
  self,
84
83
  key: QueryKey,
85
- query_fn: Callable[[Any], Awaitable[Any]],
86
84
  *,
87
85
  initial_page_param: Any,
88
86
  get_next_page_param: Callable[[list[Page[Any, Any]]], Any | None],
@@ -108,7 +106,6 @@ class QueryStore:
108
106
 
109
107
  entry = InfiniteQuery(
110
108
  key,
111
- query_fn,
112
109
  initial_page_param=initial_page_param,
113
110
  get_next_page_param=get_next_page_param,
114
111
  get_previous_page_param=get_previous_page_param,
pulse/reactive.py CHANGED
@@ -191,7 +191,8 @@ class Computed(Generic[T_co]):
191
191
  if len(scope.effects) > 0:
192
192
  raise RuntimeError(
193
193
  "An effect was created within a computed variable's function. "
194
- + "This behavior is not allowed, computed variables should be pure calculations."
194
+ + "This is most likely unintended. If you need to create an effect here, "
195
+ + "wrap the effect creation with Untrack()."
195
196
  )
196
197
  finally:
197
198
  self.on_stack = False
@@ -274,6 +275,7 @@ class Effect(Disposable):
274
275
  _interval_handle: asyncio.TimerHandle | None
275
276
  explicit_deps: bool
276
277
  batch: "Batch | None"
278
+ paused: bool
277
279
 
278
280
  def __init__(
279
281
  self,
@@ -301,6 +303,7 @@ class Effect(Disposable):
301
303
  self._lazy = lazy
302
304
  self._interval = interval
303
305
  self._interval_handle = None
306
+ self.paused = False
304
307
 
305
308
  if immediate and lazy:
306
309
  raise ValueError("An effect cannot be boht immediate and lazy")
@@ -358,7 +361,20 @@ class Effect(Disposable):
358
361
  self._interval_handle.cancel()
359
362
  self._interval_handle = None
360
363
 
364
+ def pause(self):
365
+ """Pause the effect - it won't run when dependencies change."""
366
+ self.paused = True
367
+ self.cancel(cancel_interval=True)
368
+
369
+ def resume(self):
370
+ """Resume a paused effect and schedule it to run."""
371
+ if self.paused:
372
+ self.paused = False
373
+ self.schedule()
374
+
361
375
  def schedule(self):
376
+ if self.paused:
377
+ return
362
378
  # Immediate effects run right away when scheduled and do not enter a batch
363
379
  if self.immediate:
364
380
  self.run()
@@ -383,6 +399,8 @@ class Effect(Disposable):
383
399
  self._cancel_interval()
384
400
 
385
401
  def push_change(self):
402
+ if self.paused:
403
+ return
386
404
  # Short-circuit if already scheduled in a batch.
387
405
  # This avoids redundant schedule() calls and O(n) list checks
388
406
  # when the same effect is reached through multiple dependency paths.
pulse/render_session.py CHANGED
@@ -43,17 +43,21 @@ class RouteMount:
43
43
  render: "RenderSession"
44
44
  route: RouteContext
45
45
  tree: RenderTree
46
+ effect: Effect | None
47
+ _pulse_ctx: PulseContext | None
48
+ element: Element
49
+ rendered: bool
46
50
 
47
51
  def __init__(
48
52
  self, render: "RenderSession", route: Route | Layout, route_info: RouteInfo
49
53
  ) -> None:
50
54
  self.render = render
51
55
  self.route = RouteContext(route_info, route)
52
- self.effect: Effect | None = None
53
- self._pulse_ctx: PulseContext | None = None
54
- self.element: Element = route.render()
56
+ self.effect = None
57
+ self._pulse_ctx = None
58
+ self.element = route.render()
55
59
  self.tree = RenderTree(self.element)
56
- self.rendered: bool = False
60
+ self.rendered = False
57
61
 
58
62
 
59
63
  class RenderSession:
@@ -62,6 +66,13 @@ class RenderSession:
62
66
  channels: "ChannelsManager"
63
67
  forms: "FormRegistry"
64
68
  query_store: QueryStore
69
+ route_mounts: dict[str, RouteMount]
70
+ _server_address: str | None
71
+ _client_address: str | None
72
+ _send_message: Callable[[ServerMessage], Any] | None
73
+ _pending_api: dict[str, asyncio.Future[dict[str, Any]]]
74
+ _global_states: dict[str, State]
75
+ connected: bool
65
76
 
66
77
  def __init__(
67
78
  self,
@@ -76,20 +87,18 @@ class RenderSession:
76
87
 
77
88
  self.id = id
78
89
  self.routes = routes
79
- self.route_mounts: dict[str, RouteMount] = {}
90
+ self.route_mounts = {}
80
91
  # Base server address for building absolute API URLs (e.g., http://localhost:8000)
81
- self._server_address: str | None = server_address
92
+ self._server_address = server_address
82
93
  # Best-effort client address, captured at prerender or socket connect time
83
- self._client_address: str | None = client_address
84
- self._send_message: Callable[[ServerMessage], Any] | None = None
85
- # Buffer messages emitted before a connection is established
86
- self._message_buffer: list[ServerMessage] = []
87
- self._pending_api: dict[str, asyncio.Future[dict[str, Any]]] = {}
94
+ self._client_address = client_address
95
+ self._send_message = None
96
+ self._pending_api = {}
88
97
  # Registry of per-session global singletons (created via ps.global_state without id)
89
- self._global_states: dict[str, State] = {}
98
+ self._global_states = {}
90
99
  self.query_store = QueryStore()
91
100
  # Connection state
92
- self.connected: bool = False
101
+ self.connected = False
93
102
  self.channels = ChannelsManager(self)
94
103
  self.forms = FormRegistry(self)
95
104
 
@@ -117,19 +126,22 @@ class RenderSession:
117
126
  def connect(self, send_message: Callable[[ServerMessage], Any]):
118
127
  self._send_message = send_message
119
128
  self.connected = True
120
- # Flush any buffered messages now that we can send
121
- if self._message_buffer:
122
- for msg in self._message_buffer:
123
- self._send_message(msg)
124
- self._message_buffer.clear()
129
+ # Don't flush buffer or resume effects here - mount() handles reconnection
130
+ # by resetting mount.rendered and resuming effects to send fresh vdom_init
131
+
132
+ def disconnect(self):
133
+ """Called when client disconnects - pause render effects."""
134
+ self._send_message = None
135
+ self.connected = False
136
+ for mount in self.route_mounts.values():
137
+ if mount.effect:
138
+ mount.effect.pause()
125
139
 
126
140
  def send(self, message: ServerMessage):
127
141
  # If a sender is available (connected or during prerender capture), send immediately.
128
- # Otherwise, buffer until a connection is established.
142
+ # Otherwise, drop the message - we'll send full VDOM state on reconnection.
129
143
  if self._send_message:
130
144
  self._send_message(message)
131
- else:
132
- self._message_buffer.append(message)
133
145
 
134
146
  def report_error(
135
147
  self,
@@ -174,8 +186,6 @@ class RenderSession:
174
186
  self.channels.dispose_channel(channel, reason="render.close")
175
187
  # The effect will be garbage collected, and with it the dependencies
176
188
  self._send_message = None
177
- # Discard any buffered messages on close
178
- self._message_buffer.clear()
179
189
  self.connected = False
180
190
 
181
191
  def execute_callback(self, path: str, key: str, args: list[Any] | tuple[Any, ...]):
@@ -392,8 +402,14 @@ class RenderSession:
392
402
 
393
403
  def mount(self, path: str, route_info: RouteInfo):
394
404
  if path in self.route_mounts:
395
- # No logging, this is bound to happen with React strict mode
396
- # logger.error(f"Route already mounted: '{path}'")
405
+ # Route already mounted - this is a reconnection case.
406
+ # Reset rendered flag so effect sends vdom_init, update route info,
407
+ # and resume the paused effect.
408
+ mount = self.route_mounts[path]
409
+ mount.rendered = False
410
+ mount.route.update(route_info)
411
+ if mount.effect and mount.effect.paused:
412
+ mount.effect.resume()
397
413
  return
398
414
 
399
415
  mount = self.create_route_mount(path, route_info)
pulse/renderer.py CHANGED
@@ -527,21 +527,6 @@ class Renderer:
527
527
  unmount_element(node)
528
528
 
529
529
 
530
- def extract_key(element: Element) -> str | None:
531
- if isinstance(element, ComponentNode):
532
- return element.key
533
- if isinstance(element, Node):
534
- return element.key
535
- return None
536
-
537
-
538
- def child_key(element: Element, index: int) -> str:
539
- key = extract_key(element)
540
- if key is not None:
541
- return key
542
- return f"__idx__{index}"
543
-
544
-
545
530
  def normalize_children(children: Sequence[Element] | None) -> list[Element]:
546
531
  if not children:
547
532
  return []
@@ -573,34 +558,6 @@ def same_node(left: Element, right: Element) -> bool:
573
558
  return False
574
559
 
575
560
 
576
- def lis(seq: list[int]) -> list[int]:
577
- if not seq:
578
- return []
579
- tails: list[int] = []
580
- prev: list[int] = [-1] * len(seq)
581
- for i, v in enumerate(seq):
582
- lo, hi = 0, len(tails)
583
- while lo < hi:
584
- mid = (lo + hi) // 2
585
- if seq[tails[mid]] < v:
586
- lo = mid + 1
587
- else:
588
- hi = mid
589
- if lo > 0:
590
- prev[i] = tails[lo - 1]
591
- if lo == len(tails):
592
- tails.append(i)
593
- else:
594
- tails[lo] = i
595
- lis_indices: list[int] = []
596
- k = tails[-1] if tails else -1
597
- while k != -1:
598
- lis_indices.append(k)
599
- k = prev[k]
600
- lis_indices.reverse()
601
- return lis_indices
602
-
603
-
604
561
  def _css_ref_token(ref: CssReference) -> str:
605
562
  return f"{ref.module.id}:{ref.name}"
606
563
 
@@ -1,9 +1,10 @@
1
- from collections.abc import Awaitable, Callable
1
+ from collections.abc import Callable
2
2
  from typing import (
3
+ Any,
3
4
  TypeVar,
4
5
  )
5
6
 
6
- EventHandlerResult = None | Awaitable[None]
7
+ EventHandlerResult = Any
7
8
 
8
9
  T1 = TypeVar("T1", contravariant=True)
9
10
  T2 = TypeVar("T2", contravariant=True)
pulse/vdom.py CHANGED
@@ -6,6 +6,7 @@ the TypeScript UINode format exactly, eliminating the need for translation.
6
6
  """
7
7
 
8
8
  import functools
9
+ import math
9
10
  import warnings
10
11
  from collections.abc import Callable, Iterable, Sequence
11
12
  from inspect import Parameter, signature
@@ -22,9 +23,60 @@ from typing import (
22
23
  override,
23
24
  )
24
25
 
26
+ from pulse.env import env
25
27
  from pulse.hooks.core import HookContext
26
28
  from pulse.hooks.init import rewrite_init_blocks
27
29
 
30
+ # ============================================================================
31
+ # Validation helpers (dev mode only)
32
+ # ============================================================================
33
+
34
+
35
+ def _check_json_safe_float(value: float, context: str) -> None:
36
+ """Raise ValueError if a float is NaN or Infinity."""
37
+ if math.isnan(value):
38
+ raise ValueError(
39
+ f"Cannot use nan in {context}. "
40
+ + "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
41
+ + "Replace with None or a sentinel value before passing to components."
42
+ )
43
+ if math.isinf(value):
44
+ kind = "inf" if value > 0 else "-inf"
45
+ raise ValueError(
46
+ f"Cannot use {kind} in {context}. "
47
+ + "NaN and Infinity are not supported in Pulse because they cannot be serialized to JSON. "
48
+ + "Replace with None or a sentinel value before passing to components."
49
+ )
50
+
51
+
52
+ def _validate_value(value: Any, context: str) -> None:
53
+ """Recursively validate a value for JSON-unsafe floats (NaN, Infinity)."""
54
+ if isinstance(value, float):
55
+ _check_json_safe_float(value, context)
56
+ elif isinstance(value, dict):
57
+ for v in value.values():
58
+ _validate_value(v, context)
59
+ elif isinstance(value, (list, tuple)):
60
+ for item in value:
61
+ _validate_value(item, context)
62
+ # Skip other types - they'll be handled by the serializer
63
+
64
+
65
+ def _validate_props(props: dict[str, Any] | None, parent_name: str) -> None:
66
+ """Validate all props for JSON-unsafe values."""
67
+ if not props:
68
+ return
69
+ for key, value in props.items():
70
+ _validate_value(value, f"{parent_name} prop '{key}'")
71
+
72
+
73
+ def _validate_children(children: "Sequence[Element]", parent_name: str) -> None:
74
+ """Validate primitive children for JSON-unsafe values."""
75
+ for child in children:
76
+ if isinstance(child, float):
77
+ _check_json_safe_float(child, f"{parent_name} children")
78
+
79
+
28
80
  # ============================================================================
29
81
  # Core VDOM
30
82
  # ============================================================================
@@ -75,6 +127,12 @@ class Node:
75
127
  raise ValueError("key must be a string or None")
76
128
  if not self.allow_children and children:
77
129
  raise ValueError(f"{self.tag} cannot have children")
130
+ # Dev-only validation for JSON-unsafe values
131
+ if env.pulse_env == "dev":
132
+ parent_name = f"<{self.tag}>"
133
+ _validate_props(self.props, parent_name)
134
+ if self.children:
135
+ _validate_children(self.children, parent_name)
78
136
 
79
137
  # --- Pretty printing helpers -------------------------------------------------
80
138
  @override
@@ -199,6 +257,15 @@ class Component(Generic[P]):
199
257
  if key is not None and not isinstance(key, str):
200
258
  raise ValueError("key must be a string or None")
201
259
 
260
+ # Flatten children if component takes children (has *children parameter)
261
+ if self._takes_children and args:
262
+ flattened = _flatten_children(
263
+ args, # pyright: ignore[reportArgumentType]
264
+ parent_name=f"<{self.name}>",
265
+ warn_stacklevel=4,
266
+ )
267
+ args = tuple(flattened) # pyright: ignore[reportAssignmentType]
268
+
202
269
  return ComponentNode(
203
270
  fn=self.fn,
204
271
  key=key,
@@ -244,6 +311,17 @@ class ComponentNode:
244
311
  # Used for rendering
245
312
  self.contents: Element | None = None
246
313
  self.hooks = HookContext()
314
+ # Dev-only validation for JSON-unsafe values
315
+ if env.pulse_env == "dev":
316
+ parent_name = f"<{self.name}>"
317
+ # Validate kwargs (props)
318
+ _validate_props(self.kwargs, parent_name)
319
+ # Validate args (children passed positionally)
320
+ for arg in self.args:
321
+ if isinstance(arg, float):
322
+ _check_json_safe_float(arg, f"{parent_name} children")
323
+ elif isinstance(arg, (dict, list, tuple)):
324
+ _validate_value(arg, f"{parent_name} children")
247
325
 
248
326
  def __getitem__(self, children_arg: "Child | tuple[Child, ...]"):
249
327
  if not self.takes_children:
@@ -259,7 +337,7 @@ class ComponentNode:
259
337
  children_arg = (children_arg,)
260
338
  # Flatten children for ComponentNode as well
261
339
  flattened_children = _flatten_children(
262
- children_arg, parent_name=f"<{self.name}>"
340
+ children_arg, parent_name=f"<{self.name}>", warn_stacklevel=4
263
341
  )
264
342
  result = ComponentNode(
265
343
  fn=self.fn,
@@ -315,28 +393,43 @@ Props = dict[str, Any]
315
393
  # ----------------------------------------------------------------------------
316
394
 
317
395
 
318
- def _flatten_children(children: Children, *, parent_name: str) -> Sequence[Element]:
319
- """Flatten children and emit warnings for unkeyed iterables."""
396
+ def _flatten_children(
397
+ children: Children, *, parent_name: str, warn_stacklevel: int = 5
398
+ ) -> Sequence[Element]:
399
+ """Flatten children and emit warnings for unkeyed iterables (dev mode only).
400
+
401
+ Args:
402
+ children: The children sequence to flatten.
403
+ parent_name: Name of the parent element for error messages.
404
+ warn_stacklevel: Stack level for warnings. Adjust based on call site:
405
+ - 5 for Node.__init__ via tag factory (user -> tag factory -> Node.__init__ -> _flatten_children -> visit -> warn)
406
+ - 4 for ComponentNode.__getitem__ or Component.__call__ (user -> method -> _flatten_children -> visit -> warn)
407
+ """
320
408
  flat: list[Element] = []
321
409
  return_tuple = isinstance(children, tuple)
410
+ is_dev = env.pulse_env == "dev"
322
411
 
323
412
  def visit(item: Child) -> None:
324
413
  if isinstance(item, Iterable) and not isinstance(item, str):
325
414
  # If any Node/ComponentNode yielded by this iterable lacks a key,
326
- # emit a single warning for this iterable.
415
+ # emit a single warning for this iterable (dev mode only).
327
416
  missing_key = False
328
417
  for sub in item:
329
- if isinstance(sub, (Node, ComponentNode)) and sub.key is None:
418
+ if (
419
+ is_dev
420
+ and isinstance(sub, (Node, ComponentNode))
421
+ and sub.key is None
422
+ ):
330
423
  missing_key = True
331
424
  visit(sub)
332
425
  if missing_key:
333
- # Warn once per iterable without keys on its elements
426
+ # Warn once per iterable without keys on its elements.
334
427
  warnings.warn(
335
428
  (
336
429
  f"[Pulse] Iterable children of {parent_name} contain elements without 'key'. "
337
430
  "Add a stable 'key' to each element inside iterables to improve reconciliation."
338
431
  ),
339
- stacklevel=3,
432
+ stacklevel=warn_stacklevel,
340
433
  )
341
434
  else:
342
435
  # Not an iterable child: must be a Element or primitive
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pulse-framework
3
- Version: 0.1.44
3
+ Version: 0.1.46
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,5 +1,5 @@
1
- pulse/__init__.py,sha256=Acb_t45WP9HsPBRBtKsx5rj-u-pl8gRmdwsKKVPokK4,32721
2
- pulse/app.py,sha256=cVEqazFcgSnmZSxqqz6HWk2QsI8rnKbO7Y_L88BcHSc,32082
1
+ pulse/__init__.py,sha256=s5J8i92q5Sy4h_FTPhNhirZdx-R573vtKypJiUkBwWk,32724
2
+ pulse/app.py,sha256=-0BWA8YDlm0Hm6mPbsQKPCC_3NrhbHizoAd0WnoG0Rg,32077
3
3
  pulse/channel.py,sha256=d9eLxgyB0P9UBVkPkXV7MHkC4LWED1Cq3GKsEu_SYy4,13056
4
4
  pulse/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  pulse/cli/cmd.py,sha256=UBT7OoqWRU-idLOKkA9TDN8m8ugi1gwRMiUJTUmkVfU,14853
@@ -52,28 +52,29 @@ 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/client.py,sha256=hZB3rAfH_fl_6ld_s9z81Vxvt8Cg0gXWU4Gj1qAL0LE,15254
55
+ pulse/queries/client.py,sha256=GGckE0P3YCBO4Mj-08AO_I9eXVC4sIDSNw_xTLrBFuE,15224
56
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
57
+ pulse/queries/effect.py,sha256=7KvV_yK7OHTWhfQbZFGzg_pRhyI2mn25pKIF9AmSmcU,1471
58
+ pulse/queries/infinite_query.py,sha256=oUHWjP2OliB7h8VDJooGocefHm4m9TDy4WaJesSrsdI,40457
59
+ pulse/queries/mutation.py,sha256=px1fprFL-RxNfbRSoRtdsOLkEbjSsMrJxGHKBIPYQTM,4959
60
+ pulse/queries/protocol.py,sha256=R8n238Ex9DbYIAVKB83a8FAPtnCiPNhWar-F01K2fTo,3345
61
+ pulse/queries/query.py,sha256=G8eXCaT5wuvVcstlqWU8VBxuuUUS7K1R5Y-VtDpMIG0,35065
62
+ pulse/queries/store.py,sha256=Ct7a-h1-Cq07zEfe9vw-LM85Fm7jIJx7CLAIlsiznlU,3444
62
63
  pulse/react_component.py,sha256=hPibKBEkVdpBKNSpMQ6bZ-7GnJQcNQwcw2SvfY1chHA,26026
63
- pulse/reactive.py,sha256=0CqzeowZHAENMxM2PFhi_Q6KkDzG844JofzDk0Wxckw,23961
64
+ pulse/reactive.py,sha256=v8a9IttkabeWwYrrHAx33zqzW9WC4WlS4iXbIh2KQkU,24374
64
65
  pulse/reactive_extensions.py,sha256=WAx4hlB1ByZLFVpgtRnaWXAQ3N2QQplf647oQXDL5vg,31901
65
- pulse/render_session.py,sha256=kqLfZ9AxCrB2mIJqegATL1KA7CI-LZSBQwRYr7Uxo9g,14581
66
- pulse/renderer.py,sha256=dJiX9VeHr9kC1UBw5oaKB8Mv-3OCMGTrHiKgLJ5FL50,16759
66
+ pulse/render_session.py,sha256=G76r9hKHzAJT2x_BWNmy-gNHyDT2NwUbkxQRrDBcKS4,14928
67
+ pulse/renderer.py,sha256=FnfCl7c0n-YAdWfFyrLY1EceBDL2hP85UQv8PLQL20I,15891
67
68
  pulse/request.py,sha256=sPsSRWi5KvReSPBLIs_kzqomn1wlRk1BTLZ5s0chQr4,4979
68
69
  pulse/routing.py,sha256=RlrGHyK4F28_zUHMYNeKp4pNbvqrt4GY4t5xNdhzunI,13926
69
70
  pulse/serializer.py,sha256=8RAITNoSNm5-U38elHpWmkBpcM_rxZFMCluJSfldfk4,5420
70
71
  pulse/state.py,sha256=ikQbK4R8PieV96qd4uWREUvs0jXo9sCapawY7i6oCYo,10776
71
72
  pulse/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- pulse/types/event_handler.py,sha256=tfKa6OEA5XvzuYbllQZJ03ooN7rGSYOtaPBstSL4OLU,1642
73
+ pulse/types/event_handler.py,sha256=psQCydj-WEtBcFU5JU4mDwvyzkW8V2O0g_VFRU2EOHI,1618
73
74
  pulse/user_session.py,sha256=FITxLSEl3JU-jod6UWuUYC6EpnPG2rbaLCnIOdkQPtg,7803
74
- pulse/vdom.py,sha256=1UAjOYSmpdZeSVELqejh47Jer4mA73T_q2HtAogOphs,12514
75
+ pulse/vdom.py,sha256=_quboxfVcUdpn5tiGrL1NESD7730620M4qA7HR9mC20,15950
75
76
  pulse/version.py,sha256=711vaM1jVIQPgkisGgKZqwmw019qZIsc_QTae75K2pg,1895
76
- pulse_framework-0.1.44.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
77
- pulse_framework-0.1.44.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
78
- pulse_framework-0.1.44.dist-info/METADATA,sha256=MnbisSOFD9QfMar8uD3nDaZBbDe7LSL4z1KO0ZYfCb0,580
79
- pulse_framework-0.1.44.dist-info/RECORD,,
77
+ pulse_framework-0.1.46.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
78
+ pulse_framework-0.1.46.dist-info/entry_points.txt,sha256=i7aohd3QaPu5IcuGKKvsQQEiMYMe5HcF56QEsaLVO64,46
79
+ pulse_framework-0.1.46.dist-info/METADATA,sha256=s22qR5YwwOz1eZ3PPQAbW7hSRvZ-lDuliRKjmcp098k,580
80
+ pulse_framework-0.1.46.dist-info/RECORD,,