reflex 0.8.17a1__py3-none-any.whl → 0.8.18a1__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.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

reflex/app.py CHANGED
@@ -120,7 +120,7 @@ from reflex.utils.exec import (
120
120
  )
121
121
  from reflex.utils.imports import ImportVar
122
122
  from reflex.utils.misc import run_in_thread
123
- from reflex.utils.token_manager import TokenManager
123
+ from reflex.utils.token_manager import RedisTokenManager, TokenManager
124
124
  from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
125
125
 
126
126
  if TYPE_CHECKING:
@@ -2033,11 +2033,13 @@ class EventNamespace(AsyncNamespace):
2033
2033
  self._token_manager = TokenManager.create()
2034
2034
 
2035
2035
  @property
2036
- def token_to_sid(self) -> dict[str, str]:
2036
+ def token_to_sid(self) -> Mapping[str, str]:
2037
2037
  """Get token to SID mapping for backward compatibility.
2038
2038
 
2039
+ Note: this mapping is read-only.
2040
+
2039
2041
  Returns:
2040
- The token to SID mapping dict.
2042
+ The token to SID mapping.
2041
2043
  """
2042
2044
  # For backward compatibility, expose the underlying dict
2043
2045
  return self._token_manager.token_to_sid
@@ -2059,6 +2061,9 @@ class EventNamespace(AsyncNamespace):
2059
2061
  sid: The Socket.IO session id.
2060
2062
  environ: The request information, including HTTP headers.
2061
2063
  """
2064
+ if isinstance(self._token_manager, RedisTokenManager):
2065
+ # Make sure this instance is watching for updates from other instances.
2066
+ self._token_manager.ensure_lost_and_found_task(self.emit_update)
2062
2067
  query_params = urllib.parse.parse_qs(environ.get("QUERY_STRING", ""))
2063
2068
  token_list = query_params.get("token", [])
2064
2069
  if token_list:
@@ -2072,11 +2077,14 @@ class EventNamespace(AsyncNamespace):
2072
2077
  f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
2073
2078
  )
2074
2079
 
2075
- def on_disconnect(self, sid: str):
2080
+ def on_disconnect(self, sid: str) -> asyncio.Task | None:
2076
2081
  """Event for when the websocket disconnects.
2077
2082
 
2078
2083
  Args:
2079
2084
  sid: The Socket.IO session id.
2085
+
2086
+ Returns:
2087
+ An asyncio Task for cleaning up the token, or None.
2080
2088
  """
2081
2089
  # Get token before cleaning up
2082
2090
  disconnect_token = self.sid_to_token.get(sid)
@@ -2091,6 +2099,8 @@ class EventNamespace(AsyncNamespace):
2091
2099
  lambda t: t.exception()
2092
2100
  and console.error(f"Token cleanup error: {t.exception()}")
2093
2101
  )
2102
+ return task
2103
+ return None
2094
2104
 
2095
2105
  async def emit_update(self, update: StateUpdate, token: str) -> None:
2096
2106
  """Emit an update to the client.
@@ -2100,16 +2110,30 @@ class EventNamespace(AsyncNamespace):
2100
2110
  token: The client token (tab) associated with the event.
2101
2111
  """
2102
2112
  client_token, _ = _split_substate_key(token)
2103
- sid = self.token_to_sid.get(client_token)
2104
- if sid is None:
2105
- # If the sid is None, we are not connected to a client. Prevent sending
2106
- # updates to all clients.
2107
- console.warn(f"Attempting to send delta to disconnected client {token!r}")
2113
+ socket_record = self._token_manager.token_to_socket.get(client_token)
2114
+ if (
2115
+ socket_record is None
2116
+ or socket_record.instance_id != self._token_manager.instance_id
2117
+ ):
2118
+ if isinstance(self._token_manager, RedisTokenManager):
2119
+ # The socket belongs to another instance of the app, send it to the lost and found.
2120
+ if not await self._token_manager.emit_lost_and_found(
2121
+ client_token, update
2122
+ ):
2123
+ console.warn(
2124
+ f"Failed to send delta to lost and found for client {token!r}"
2125
+ )
2126
+ else:
2127
+ # If the socket record is None, we are not connected to a client. Prevent sending
2128
+ # updates to all clients.
2129
+ console.warn(
2130
+ f"Attempting to send delta to disconnected client {token!r}"
2131
+ )
2108
2132
  return
2109
2133
  # Creating a task prevents the update from being blocked behind other coroutines.
2110
2134
  await asyncio.create_task(
2111
- self.emit(str(constants.SocketEvent.EVENT), update, to=sid),
2112
- name=f"reflex_emit_event|{token}|{sid}|{time.time()}",
2135
+ self.emit(str(constants.SocketEvent.EVENT), update, to=socket_record.sid),
2136
+ name=f"reflex_emit_event|{token}|{socket_record.sid}|{time.time()}",
2113
2137
  )
2114
2138
 
2115
2139
  async def on_event(self, sid: str, data: Any):
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from reflex.components.component import Component
6
6
  from reflex.components.datadisplay.logo import svg_logo
7
- from reflex.components.el import a, button, details, div, h2, hr, p, pre, summary
7
+ from reflex.components.el import a, button, div, h2, hr, p, pre, svg
8
8
  from reflex.event import EventHandler, set_clipboard
9
9
  from reflex.state import FrontendEventExceptionState
10
10
  from reflex.vars.base import Var
@@ -65,53 +65,67 @@ class ErrorBoundary(Component):
65
65
  div(
66
66
  div(
67
67
  div(
68
+ svg(
69
+ svg.circle(cx="12", cy="12", r="10"),
70
+ svg.path(d="M16 16s-1.5-2-4-2-4 2-4 2"),
71
+ svg.line(x1="9", x2="9.01", y1="9", y2="9"),
72
+ svg.line(x1="15", x2="15.01", y1="9", y2="9"),
73
+ xmlns="http://www.w3.org/2000/svg",
74
+ width="25vmin",
75
+ view_box="0 0 24 24",
76
+ class_name="lucide lucide-frown-icon lucide-frown",
77
+ custom_attrs={
78
+ "fill": "none",
79
+ "stroke": "currentColor",
80
+ "stroke-width": "2",
81
+ "stroke-linecap": "round",
82
+ "stroke-linejoin": "round",
83
+ },
84
+ ),
68
85
  h2(
69
86
  "An error occurred while rendering this page.",
70
- font_size="1.25rem",
87
+ font_size="5vmin",
71
88
  font_weight="bold",
72
89
  ),
73
- p(
74
- "This is an error with the application itself.",
75
- opacity="0.75",
76
- ),
77
- details(
78
- summary("Error message", padding="0.5rem"),
79
- div(
80
- div(
81
- pre(
82
- Var(_js_expr=_ERROR_DISPLAY),
83
- ),
84
- padding="0.5rem",
85
- width="fit-content",
86
- ),
87
- width="100%",
88
- max_height="50vh",
89
- overflow="auto",
90
- background="#000",
91
- color="#fff",
92
- border_radius="0.25rem",
93
- ),
94
- button(
95
- "Copy",
96
- on_click=set_clipboard(
97
- Var(_js_expr=_ERROR_DISPLAY)
98
- ),
99
- padding="0.35rem 0.75rem",
100
- margin="0.5rem",
101
- background="#fff",
102
- color="#000",
103
- border="1px solid #000",
104
- border_radius="0.25rem",
105
- font_weight="bold",
90
+ opacity="0.5",
91
+ display="flex",
92
+ gap="4vmin",
93
+ align_items="center",
94
+ ),
95
+ p(
96
+ "This is an error with the application itself. Refreshing the page might help.",
97
+ opacity="0.75",
98
+ margin_block="1rem",
99
+ ),
100
+ div(
101
+ div(
102
+ pre(
103
+ Var(_js_expr=_ERROR_DISPLAY),
106
104
  ),
105
+ padding="0.5rem",
106
+ width="fit-content",
107
107
  ),
108
- display="flex",
109
- flex_direction="column",
110
- gap="1rem",
111
- max_width="50ch",
112
- border="1px solid #888888",
113
- border_radius="0.25rem",
114
- padding="1rem",
108
+ width="100%",
109
+ background="color-mix(in srgb, currentColor 5%, transparent)",
110
+ max_height="15rem",
111
+ overflow="auto",
112
+ border_radius="0.4rem",
113
+ ),
114
+ button(
115
+ "Copy",
116
+ on_click=set_clipboard(Var(_js_expr=_ERROR_DISPLAY)),
117
+ padding="0.35rem 1.35rem",
118
+ margin_block="0.5rem",
119
+ margin_inline_start="auto",
120
+ background="color-mix(in srgb, currentColor 15%, transparent)",
121
+ border_radius="0.4rem",
122
+ width="fit-content",
123
+ _hover={
124
+ "background": "color-mix(in srgb, currentColor 25%, transparent)"
125
+ },
126
+ _active={
127
+ "background": "color-mix(in srgb, currentColor 35%, transparent)"
128
+ },
115
129
  ),
116
130
  hr(
117
131
  border_color="currentColor",
@@ -131,7 +145,10 @@ class ErrorBoundary(Component):
131
145
  ),
132
146
  display="flex",
133
147
  flex_direction="column",
134
- gap="1rem",
148
+ gap="0.5rem",
149
+ max_width="min(80ch, 90vw)",
150
+ border_radius="0.25rem",
151
+ padding="1rem",
135
152
  ),
136
153
  height="100%",
137
154
  width="100%",
@@ -6,7 +6,7 @@ from reflex.utils.imports import ImportVar
6
6
  from reflex.vars.base import LiteralVar, Var
7
7
  from reflex.vars.sequence import LiteralStringVar, StringVar
8
8
 
9
- LUCIDE_LIBRARY = "lucide-react@0.546.0"
9
+ LUCIDE_LIBRARY = "lucide-react@0.548.0"
10
10
 
11
11
 
12
12
  class LucideIconComponent(Component):
@@ -286,6 +286,7 @@ LUCIDE_ICON_LIST = [
286
286
  "binoculars",
287
287
  "biohazard",
288
288
  "bird",
289
+ "birdhouse",
289
290
  "bitcoin",
290
291
  "blend",
291
292
  "blinds",
@@ -828,6 +829,7 @@ LUCIDE_ICON_LIST = [
828
829
  "gallery_vertical_end",
829
830
  "gallery_vertical",
830
831
  "gamepad_2",
832
+ "gamepad_directional",
831
833
  "gamepad",
832
834
  "gantt_chart",
833
835
  "gauge",
@@ -11,7 +11,7 @@ from reflex.components.core.breakpoints import Breakpoints
11
11
  from reflex.event import EventType, PointerEventInfo
12
12
  from reflex.vars.base import Var
13
13
 
14
- LUCIDE_LIBRARY = "lucide-react@0.545.0"
14
+ LUCIDE_LIBRARY = "lucide-react@0.548.0"
15
15
 
16
16
  class LucideIconComponent(Component):
17
17
  @classmethod
@@ -347,6 +347,7 @@ LUCIDE_ICON_LIST = [
347
347
  "binoculars",
348
348
  "biohazard",
349
349
  "bird",
350
+ "birdhouse",
350
351
  "bitcoin",
351
352
  "blend",
352
353
  "blinds",
@@ -889,6 +890,7 @@ LUCIDE_ICON_LIST = [
889
890
  "gallery_vertical_end",
890
891
  "gallery_vertical",
891
892
  "gamepad_2",
893
+ "gamepad_directional",
892
894
  "gamepad",
893
895
  "gantt_chart",
894
896
  "gauge",
@@ -1176,6 +1178,7 @@ LUCIDE_ICON_LIST = [
1176
1178
  "minimize",
1177
1179
  "minus",
1178
1180
  "monitor_check",
1181
+ "monitor_cloud",
1179
1182
  "monitor_cog",
1180
1183
  "monitor_dot",
1181
1184
  "monitor_down",
@@ -14,7 +14,7 @@ class Bun(SimpleNamespace):
14
14
  """Bun constants."""
15
15
 
16
16
  # The Bun version.
17
- VERSION = "1.3.0"
17
+ VERSION = "1.3.1"
18
18
 
19
19
  # Min Bun Version
20
20
  MIN_VERSION = "1.3.0"
@@ -67,6 +67,7 @@ class StateManagerRedis(StateManager):
67
67
  # The keyspace subscription string when redis is waiting for lock to be released.
68
68
  _redis_notify_keyspace_events: str = dataclasses.field(
69
69
  default="K" # Enable keyspace notifications (target a particular key)
70
+ "$" # For String commands (like setting keys)
70
71
  "g" # For generic commands (DEL, EXPIRE, etc)
71
72
  "x" # For expired events
72
73
  "e" # For evicted events (i.e. maxmemory exceeded)
@@ -76,7 +77,6 @@ class StateManagerRedis(StateManager):
76
77
  _redis_keyspace_lock_release_events: set[bytes] = dataclasses.field(
77
78
  default_factory=lambda: {
78
79
  b"del",
79
- b"expire",
80
80
  b"expired",
81
81
  b"evicted",
82
82
  }
@@ -17,7 +17,7 @@ class Constants(SimpleNamespace):
17
17
  """Tailwind constants."""
18
18
 
19
19
  # The Tailwindcss version
20
- VERSION = "tailwindcss@4.1.15"
20
+ VERSION = "tailwindcss@4.1.16"
21
21
  # The Tailwind config.
22
22
  CONFIG = "tailwind.config.js"
23
23
  # Default Tailwind content paths
@@ -156,7 +156,7 @@ class TailwindV4Plugin(TailwindPlugin):
156
156
  return [
157
157
  *super().get_frontend_development_dependencies(**context),
158
158
  Constants.VERSION,
159
- "@tailwindcss/postcss@4.1.15",
159
+ "@tailwindcss/postcss@4.1.16",
160
160
  ]
161
161
 
162
162
  def pre_compile(self, **context):
reflex/state.py CHANGED
@@ -2189,14 +2189,12 @@ class BaseState(EvenMoreBasicBaseState):
2189
2189
  async def __aenter__(self) -> BaseState:
2190
2190
  """Enter the async context manager protocol.
2191
2191
 
2192
- This should not be used for the State class, but exists for
2193
- type-compatibility with StateProxy.
2192
+ This is a no-op for the State class and mainly used in background-tasks/StateProxy.
2194
2193
 
2195
- Raises:
2196
- TypeError: always, because async contextmanager protocol is only supported for background task.
2194
+ Returns:
2195
+ The unmodified state (self)
2197
2196
  """
2198
- msg = "Only background task should use `async with self` to modify state."
2199
- raise TypeError(msg)
2197
+ return self
2200
2198
 
2201
2199
  async def __aexit__(self, *exc_info: Any) -> None:
2202
2200
  """Exit the async context manager protocol.
@@ -2486,6 +2484,14 @@ class FrontendEventExceptionState(State):
2486
2484
  ),
2487
2485
  re.compile(re.escape("TypeError: null is not an object")), # Safari
2488
2486
  re.compile(r"TypeError: can't access property \".*\" of null"), # Firefox
2487
+ # Firefox: property access is on a function that returns null.
2488
+ re.compile(
2489
+ re.escape("TypeError: can't access property \"")
2490
+ + r".*"
2491
+ + re.escape('", ')
2492
+ + r".*"
2493
+ + re.escape(" is null")
2494
+ ),
2489
2495
  ]
2490
2496
 
2491
2497
  @event
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import contextlib
6
6
  import importlib
7
7
  import importlib.metadata
8
+ import inspect
8
9
  import json
9
10
  import random
10
11
  import re
@@ -435,7 +436,9 @@ async def get_redis_status() -> dict[str, bool | None]:
435
436
  status = True
436
437
  redis_client = get_redis()
437
438
  if redis_client is not None:
438
- await redis_client.ping()
439
+ ping_command = redis_client.ping()
440
+ if inspect.isawaitable(ping_command):
441
+ await ping_command
439
442
  else:
440
443
  status = None
441
444
  except RedisError:
@@ -2,10 +2,17 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
6
+ import dataclasses
7
+ import json
5
8
  import uuid
6
9
  from abc import ABC, abstractmethod
7
- from typing import TYPE_CHECKING
10
+ from collections.abc import AsyncIterator, Callable, Coroutine
11
+ from types import MappingProxyType
12
+ from typing import TYPE_CHECKING, Any, ClassVar
8
13
 
14
+ from reflex.istate.manager.redis import StateManagerRedis
15
+ from reflex.state import BaseState, StateUpdate
9
16
  from reflex.utils import console, prerequisites
10
17
 
11
18
  if TYPE_CHECKING:
@@ -21,16 +28,54 @@ def _get_new_token() -> str:
21
28
  return str(uuid.uuid4())
22
29
 
23
30
 
31
+ @dataclasses.dataclass(frozen=True, kw_only=True)
32
+ class SocketRecord:
33
+ """Record for a connected socket client."""
34
+
35
+ instance_id: str
36
+ sid: str
37
+
38
+
39
+ @dataclasses.dataclass(frozen=True, kw_only=True)
40
+ class LostAndFoundRecord:
41
+ """Record for a StateUpdate for a token with its socket on another instance."""
42
+
43
+ token: str
44
+ update: dict[str, Any]
45
+
46
+
24
47
  class TokenManager(ABC):
25
48
  """Abstract base class for managing client token to session ID mappings."""
26
49
 
27
50
  def __init__(self):
28
51
  """Initialize the token manager with local dictionaries."""
29
- # Keep a mapping between socket ID and client token.
30
- self.token_to_sid: dict[str, str] = {}
52
+ # Each process has an instance_id to identify its own sockets.
53
+ self.instance_id: str = _get_new_token()
31
54
  # Keep a mapping between client token and socket ID.
55
+ self.token_to_socket: dict[str, SocketRecord] = {}
56
+ # Keep a mapping between socket ID and client token.
32
57
  self.sid_to_token: dict[str, str] = {}
33
58
 
59
+ @property
60
+ def token_to_sid(self) -> MappingProxyType[str, str]:
61
+ """Read-only compatibility property for token_to_socket mapping.
62
+
63
+ Returns:
64
+ The token to session ID mapping.
65
+ """
66
+ return MappingProxyType({
67
+ token: sr.sid for token, sr in self.token_to_socket.items()
68
+ })
69
+
70
+ async def enumerate_tokens(self) -> AsyncIterator[str]:
71
+ """Iterate over all tokens in the system.
72
+
73
+ Yields:
74
+ All client tokens known to the TokenManager.
75
+ """
76
+ for token in self.token_to_socket:
77
+ yield token
78
+
34
79
  @abstractmethod
35
80
  async def link_token_to_sid(self, token: str, sid: str) -> str | None:
36
81
  """Link a token to a session ID.
@@ -68,7 +113,9 @@ class TokenManager(ABC):
68
113
 
69
114
  async def disconnect_all(self):
70
115
  """Disconnect all tracked tokens when the server is going down."""
71
- token_sid_pairs: set[tuple[str, str]] = set(self.token_to_sid.items())
116
+ token_sid_pairs: set[tuple[str, str]] = {
117
+ (token, sr.sid) for token, sr in self.token_to_socket.items()
118
+ }
72
119
  token_sid_pairs.update(
73
120
  ((token, sid) for sid, token in self.sid_to_token.items())
74
121
  )
@@ -95,14 +142,20 @@ class LocalTokenManager(TokenManager):
95
142
  New token if duplicate detected and new token generated, None otherwise.
96
143
  """
97
144
  # Check if token is already mapped to a different SID (duplicate tab)
98
- if token in self.token_to_sid and sid != self.token_to_sid.get(token):
145
+ if (
146
+ socket_record := self.token_to_socket.get(token)
147
+ ) is not None and sid != socket_record.sid:
99
148
  new_token = _get_new_token()
100
- self.token_to_sid[new_token] = sid
149
+ self.token_to_socket[new_token] = SocketRecord(
150
+ instance_id=self.instance_id, sid=sid
151
+ )
101
152
  self.sid_to_token[sid] = new_token
102
153
  return new_token
103
154
 
104
155
  # Normal case - link token to SID
105
- self.token_to_sid[token] = sid
156
+ self.token_to_socket[token] = SocketRecord(
157
+ instance_id=self.instance_id, sid=sid
158
+ )
106
159
  self.sid_to_token[sid] = token
107
160
  return None
108
161
 
@@ -114,7 +167,7 @@ class LocalTokenManager(TokenManager):
114
167
  sid: The Socket.IO session ID.
115
168
  """
116
169
  # Clean up both mappings
117
- self.token_to_sid.pop(token, None)
170
+ self.token_to_socket.pop(token, None)
118
171
  self.sid_to_token.pop(sid, None)
119
172
 
120
173
 
@@ -125,6 +178,8 @@ class RedisTokenManager(LocalTokenManager):
125
178
  for cross-worker duplicate detection.
126
179
  """
127
180
 
181
+ _token_socket_record_prefix: ClassVar[str] = "token_manager_socket_record_"
182
+
128
183
  def __init__(self, redis: Redis):
129
184
  """Initialize the Redis token manager.
130
185
 
@@ -142,6 +197,10 @@ class RedisTokenManager(LocalTokenManager):
142
197
  config = get_config()
143
198
  self.token_expiration = config.redis_token_expiration
144
199
 
200
+ # Pub/sub tasks for handling sockets owned by other instances.
201
+ self._socket_record_task: asyncio.Task | None = None
202
+ self._lost_and_found_task: asyncio.Task | None = None
203
+
145
204
  def _get_redis_key(self, token: str) -> str:
146
205
  """Get Redis key for token mapping.
147
206
 
@@ -149,9 +208,78 @@ class RedisTokenManager(LocalTokenManager):
149
208
  token: The client token.
150
209
 
151
210
  Returns:
152
- Redis key following Reflex conventions: {token}_sid
211
+ Redis key following Reflex conventions: token_manager_socket_record_{token}
212
+ """
213
+ return f"{self._token_socket_record_prefix}{token}"
214
+
215
+ async def enumerate_tokens(self) -> AsyncIterator[str]:
216
+ """Iterate over all tokens in the system.
217
+
218
+ Yields:
219
+ All client tokens known to the RedisTokenManager.
153
220
  """
154
- return f"{token}_sid"
221
+ cursor = 0
222
+ while scan_result := await self.redis.scan(
223
+ cursor=cursor, match=self._get_redis_key("*")
224
+ ):
225
+ cursor = int(scan_result[0])
226
+ for key in scan_result[1]:
227
+ yield key.decode().replace(self._token_socket_record_prefix, "")
228
+ if not cursor:
229
+ break
230
+
231
+ def _handle_socket_record_del(self, token: str) -> None:
232
+ """Handle deletion of a socket record from Redis.
233
+
234
+ Args:
235
+ token: The client token whose record was deleted.
236
+ """
237
+ if (
238
+ socket_record := self.token_to_socket.pop(token, None)
239
+ ) is not None and socket_record.instance_id != self.instance_id:
240
+ self.sid_to_token.pop(socket_record.sid, None)
241
+
242
+ async def _subscribe_socket_record_updates(self, redis_db: int) -> None:
243
+ """Subscribe to Redis keyspace notifications for socket record updates."""
244
+ async with self.redis.pubsub() as pubsub:
245
+ await pubsub.psubscribe(
246
+ f"__keyspace@{redis_db}__:{self._get_redis_key('*')}"
247
+ )
248
+ async for message in pubsub.listen():
249
+ if message["type"] == "pmessage":
250
+ key = message["channel"].split(b":", 1)[1].decode()
251
+ token = key.replace(self._token_socket_record_prefix, "")
252
+
253
+ if token not in self.token_to_socket:
254
+ # We don't know about this token, skip
255
+ continue
256
+
257
+ event = message["data"].decode()
258
+ if event in ("del", "expired", "evicted"):
259
+ self._handle_socket_record_del(token)
260
+ elif event == "set":
261
+ await self._get_token_owner(token, refresh=True)
262
+
263
+ async def _socket_record_updates_forever(self) -> None:
264
+ """Background task to monitor Redis keyspace notifications for socket record updates."""
265
+ await StateManagerRedis(
266
+ state=BaseState, redis=self.redis
267
+ )._enable_keyspace_notifications()
268
+ redis_db = self.redis.get_connection_kwargs().get("db", 0)
269
+ while True:
270
+ try:
271
+ await self._subscribe_socket_record_updates(redis_db)
272
+ except asyncio.CancelledError: # noqa: PERF203
273
+ break
274
+ except Exception as e:
275
+ console.error(f"RedisTokenManager socket record update task error: {e}")
276
+
277
+ def _ensure_socket_record_task(self) -> None:
278
+ """Ensure the socket record updates subscriber task is running."""
279
+ if self._socket_record_task is None or self._socket_record_task.done():
280
+ self._socket_record_task = asyncio.create_task(
281
+ self._socket_record_updates_forever()
282
+ )
155
283
 
156
284
  async def link_token_to_sid(self, token: str, sid: str) -> str | None:
157
285
  """Link a token to a session ID with Redis-based duplicate detection.
@@ -164,9 +292,14 @@ class RedisTokenManager(LocalTokenManager):
164
292
  New token if duplicate detected and new token generated, None otherwise.
165
293
  """
166
294
  # Fast local check first (handles reconnections)
167
- if token in self.token_to_sid and self.token_to_sid[token] == sid:
295
+ if (
296
+ socket_record := self.token_to_socket.get(token)
297
+ ) is not None and sid == socket_record.sid:
168
298
  return None # Same token, same SID = reconnection, no Redis check needed
169
299
 
300
+ # Make sure the update subscriber is running
301
+ self._ensure_socket_record_task()
302
+
170
303
  # Check Redis for cross-worker duplicates
171
304
  redis_key = self._get_redis_key(token)
172
305
 
@@ -176,34 +309,29 @@ class RedisTokenManager(LocalTokenManager):
176
309
  console.error(f"Redis error checking token existence: {e}")
177
310
  return await super().link_token_to_sid(token, sid)
178
311
 
312
+ new_token = None
179
313
  if token_exists_in_redis:
180
314
  # Duplicate exists somewhere - generate new token
181
- new_token = _get_new_token()
182
- new_redis_key = self._get_redis_key(new_token)
183
-
184
- try:
185
- # Store in Redis
186
- await self.redis.set(new_redis_key, "1", ex=self.token_expiration)
187
- except Exception as e:
188
- console.error(f"Redis error storing new token: {e}")
189
- # Still update local dicts and continue
315
+ token = new_token = _get_new_token()
316
+ redis_key = self._get_redis_key(new_token)
190
317
 
191
- # Store in local dicts (always do this)
192
- self.token_to_sid[new_token] = sid
193
- self.sid_to_token[sid] = new_token
194
- return new_token
318
+ # Store in local dicts
319
+ socket_record = self.token_to_socket[token] = SocketRecord(
320
+ instance_id=self.instance_id, sid=sid
321
+ )
322
+ self.sid_to_token[sid] = token
195
323
 
196
- # Normal case - store in both Redis and local dicts
324
+ # Store in Redis if possible
197
325
  try:
198
- await self.redis.set(redis_key, "1", ex=self.token_expiration)
326
+ await self.redis.set(
327
+ redis_key,
328
+ json.dumps(dataclasses.asdict(socket_record)),
329
+ ex=self.token_expiration,
330
+ )
199
331
  except Exception as e:
200
332
  console.error(f"Redis error storing token: {e}")
201
- # Continue with local storage
202
-
203
- # Store in local dicts (always do this)
204
- self.token_to_sid[token] = sid
205
- self.sid_to_token[sid] = token
206
- return None
333
+ # Return the new token if one was generated
334
+ return new_token
207
335
 
208
336
  async def disconnect_token(self, token: str, sid: str) -> None:
209
337
  """Clean up token mapping when client disconnects.
@@ -213,7 +341,11 @@ class RedisTokenManager(LocalTokenManager):
213
341
  sid: The Socket.IO session ID.
214
342
  """
215
343
  # Only clean up if we own it locally (fast ownership check)
216
- if self.token_to_sid.get(token) == sid:
344
+ if (
345
+ (socket_record := self.token_to_socket.get(token)) is not None
346
+ and socket_record.sid == sid
347
+ and socket_record.instance_id == self.instance_id
348
+ ):
217
349
  # Clean up Redis
218
350
  redis_key = self._get_redis_key(token)
219
351
  try:
@@ -223,3 +355,124 @@ class RedisTokenManager(LocalTokenManager):
223
355
 
224
356
  # Clean up local dicts (always do this)
225
357
  await super().disconnect_token(token, sid)
358
+
359
+ @staticmethod
360
+ def _get_lost_and_found_key(instance_id: str) -> str:
361
+ """Get the Redis key for lost and found deltas for an instance.
362
+
363
+ Args:
364
+ instance_id: The instance ID.
365
+
366
+ Returns:
367
+ The Redis key for lost and found deltas.
368
+ """
369
+ return f"token_manager_lost_and_found_{instance_id}"
370
+
371
+ async def _subscribe_lost_and_found_updates(
372
+ self,
373
+ emit_update: Callable[[StateUpdate, str], Coroutine[None, None, None]],
374
+ ) -> None:
375
+ """Subscribe to Redis channel notifications for lost and found deltas.
376
+
377
+ Args:
378
+ emit_update: The function to emit state updates.
379
+ """
380
+ async with self.redis.pubsub() as pubsub:
381
+ await pubsub.psubscribe(
382
+ f"channel:{self._get_lost_and_found_key(self.instance_id)}"
383
+ )
384
+ async for message in pubsub.listen():
385
+ if message["type"] == "pmessage":
386
+ record = LostAndFoundRecord(**json.loads(message["data"].decode()))
387
+ await emit_update(StateUpdate(**record.update), record.token)
388
+
389
+ async def _lost_and_found_updates_forever(
390
+ self,
391
+ emit_update: Callable[[StateUpdate, str], Coroutine[None, None, None]],
392
+ ):
393
+ """Background task to monitor Redis lost and found deltas.
394
+
395
+ Args:
396
+ emit_update: The function to emit state updates.
397
+ """
398
+ while True:
399
+ try:
400
+ await self._subscribe_lost_and_found_updates(emit_update)
401
+ except asyncio.CancelledError: # noqa: PERF203
402
+ break
403
+ except Exception as e:
404
+ console.error(f"RedisTokenManager lost and found task error: {e}")
405
+
406
+ def ensure_lost_and_found_task(
407
+ self,
408
+ emit_update: Callable[[StateUpdate, str], Coroutine[None, None, None]],
409
+ ) -> None:
410
+ """Ensure the lost and found subscriber task is running.
411
+
412
+ Args:
413
+ emit_update: The function to emit state updates.
414
+ """
415
+ if self._lost_and_found_task is None or self._lost_and_found_task.done():
416
+ self._lost_and_found_task = asyncio.create_task(
417
+ self._lost_and_found_updates_forever(emit_update)
418
+ )
419
+
420
+ async def _get_token_owner(self, token: str, refresh: bool = False) -> str | None:
421
+ """Get the instance ID of the owner of a token.
422
+
423
+ Args:
424
+ token: The client token.
425
+ refresh: Whether to fetch the latest record from Redis.
426
+
427
+ Returns:
428
+ The instance ID of the owner, or None if not found.
429
+ """
430
+ if (
431
+ not refresh
432
+ and (socket_record := self.token_to_socket.get(token)) is not None
433
+ ):
434
+ return socket_record.instance_id
435
+
436
+ redis_key = self._get_redis_key(token)
437
+ try:
438
+ record_json = await self.redis.get(redis_key)
439
+ if record_json:
440
+ record_data = json.loads(record_json)
441
+ socket_record = SocketRecord(**record_data)
442
+ self.token_to_socket[token] = socket_record
443
+ self.sid_to_token[socket_record.sid] = token
444
+ return socket_record.instance_id
445
+ console.warn(f"Redis token owner not found for token {token}")
446
+ except Exception as e:
447
+ console.error(f"Redis error getting token owner: {e}")
448
+ return None
449
+
450
+ async def emit_lost_and_found(
451
+ self,
452
+ token: str,
453
+ update: StateUpdate,
454
+ ) -> bool:
455
+ """Emit a lost and found delta to Redis.
456
+
457
+ Args:
458
+ token: The client token.
459
+ update: The state update.
460
+
461
+ Returns:
462
+ True if the delta was published, False otherwise.
463
+ """
464
+ # See where this update belongs
465
+ owner_instance_id = await self._get_token_owner(token)
466
+ if owner_instance_id is None:
467
+ return False
468
+ record = LostAndFoundRecord(token=token, update=dataclasses.asdict(update))
469
+ try:
470
+ await self.redis.publish(
471
+ f"channel:{self._get_lost_and_found_key(owner_instance_id)}",
472
+ json.dumps(dataclasses.asdict(record)),
473
+ )
474
+ except Exception as e:
475
+ console.error(f"Redis error publishing lost and found delta: {e}")
476
+ else:
477
+ return True
478
+ return False
reflex/utils/types.py CHANGED
@@ -633,12 +633,22 @@ def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None)
633
633
  raise TypeError(msg) from te
634
634
 
635
635
 
636
- def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
636
+ def does_obj_satisfy_typed_dict(
637
+ obj: Any,
638
+ cls: GenericType,
639
+ *,
640
+ nested: int = 0,
641
+ treat_var_as_type: bool = True,
642
+ treat_mutable_obj_as_immutable: bool = False,
643
+ ) -> bool:
637
644
  """Check if an object satisfies a typed dict.
638
645
 
639
646
  Args:
640
647
  obj: The object to check.
641
648
  cls: The typed dict to check against.
649
+ nested: How many levels deep to check.
650
+ treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type.
651
+ treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change.
642
652
 
643
653
  Returns:
644
654
  Whether the object satisfies the typed dict.
@@ -648,19 +658,35 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
648
658
 
649
659
  key_names_to_values = get_type_hints(cls)
650
660
  required_keys: frozenset[str] = getattr(cls, "__required_keys__", frozenset())
661
+ is_closed = getattr(cls, "__closed__", False)
662
+ extra_items_type = getattr(cls, "__extra_items__", Any)
651
663
 
652
- if not all(
653
- isinstance(key, str)
654
- and key in key_names_to_values
655
- and _isinstance(value, key_names_to_values[key])
656
- for key, value in obj.items()
657
- ):
658
- return False
659
-
660
- # TODO in 3.14: Implement https://peps.python.org/pep-0728/ if it's approved
664
+ for key, value in obj.items():
665
+ if is_closed and key not in key_names_to_values:
666
+ return False
667
+ if nested:
668
+ if key in key_names_to_values:
669
+ expected_type = key_names_to_values[key]
670
+ if not _isinstance(
671
+ value,
672
+ expected_type,
673
+ nested=nested - 1,
674
+ treat_var_as_type=treat_var_as_type,
675
+ treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
676
+ ):
677
+ return False
678
+ else:
679
+ if not _isinstance(
680
+ value,
681
+ extra_items_type,
682
+ nested=nested - 1,
683
+ treat_var_as_type=treat_var_as_type,
684
+ treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
685
+ ):
686
+ return False
661
687
 
662
688
  # required keys are all present
663
- return required_keys.issubset(required_keys)
689
+ return required_keys.issubset(frozenset(obj))
664
690
 
665
691
 
666
692
  def _isinstance(
@@ -721,7 +747,13 @@ def _isinstance(
721
747
  # cls is a typed dict
722
748
  if is_typeddict(cls):
723
749
  if nested:
724
- return does_obj_satisfy_typed_dict(obj, cls)
750
+ return does_obj_satisfy_typed_dict(
751
+ obj,
752
+ cls,
753
+ nested=nested - 1,
754
+ treat_var_as_type=treat_var_as_type,
755
+ treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
756
+ )
725
757
  return isinstance(obj, dict)
726
758
 
727
759
  # cls is a float
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reflex
3
- Version: 0.8.17a1
3
+ Version: 0.8.18a1
4
4
  Summary: Web apps in pure Python.
5
5
  Project-URL: homepage, https://reflex.dev
6
6
  Project-URL: repository, https://github.com/reflex-dev/reflex
@@ -30,8 +30,8 @@ Requires-Dist: psutil<8.0,>=7.0.0; sys_platform == 'win32'
30
30
  Requires-Dist: pydantic<3.0,>=1.10.21
31
31
  Requires-Dist: python-multipart<1.0,>=0.0.20
32
32
  Requires-Dist: python-socketio<6.0,>=5.12.0
33
- Requires-Dist: redis<7.0,>=5.2.1
34
- Requires-Dist: reflex-hosting-cli>=0.1.57
33
+ Requires-Dist: redis<8.0,>=5.2.1
34
+ Requires-Dist: reflex-hosting-cli>=0.1.58
35
35
  Requires-Dist: rich<15,>=13
36
36
  Requires-Dist: sqlmodel<0.1,>=0.0.27
37
37
  Requires-Dist: starlette>=0.47.0
@@ -2,7 +2,7 @@ reflex/__init__.py,sha256=7iJASSyU1dxLM-l6q6gFAkw6FniXvawAekgfwN5zKjM,10328
2
2
  reflex/__init__.pyi,sha256=Yy3exOO_7-O7fCjTKO1VDFbjPyeMM7F12WBnEXWx_tk,10428
3
3
  reflex/__main__.py,sha256=6cVrGEyT3j3tEvlEVUatpaYfbB5EF3UVY-6vc_Z7-hw,108
4
4
  reflex/admin.py,sha256=Nbc38y-M8iaRBvh1W6DQu_D3kEhO8JFvxrog4q2cB_E,434
5
- reflex/app.py,sha256=s3Grrt3p_Lsk9I1gOhEaxyMj0Zj0IgmfFOzmqlQU3IM,79352
5
+ reflex/app.py,sha256=jQCqsNJkR-BhcMKVg7L4j80c_IvHPnzPsNO5lG1tHiI,80468
6
6
  reflex/assets.py,sha256=l5O_mlrTprC0lF7Rc_McOe3a0OtSLnRdNl_PqCpDCBA,3431
7
7
  reflex/base.py,sha256=ROoDZCLWyEdVqfYKnhYKPZINklTl5nHKWo2x0J4MfoE,2327
8
8
  reflex/config.py,sha256=LsHAtdH4nkSn3q_Ie-KNdOGdflLXrFICUQov29oFjVk,21229
@@ -13,7 +13,7 @@ reflex/page.py,sha256=ssCbMVFuIy60vH-YhJUzN0OxzUwXFCCD3ej56dVjp3g,3525
13
13
  reflex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  reflex/reflex.py,sha256=urU9hMWIkmVl6BPuw_-NmfaE8Zlls4s_t31ueFl4uBo,25915
15
15
  reflex/route.py,sha256=TnS4m6Hm-b3LfGFpm37iAMEd-_JISAouPW5FqUxTAfU,7858
16
- reflex/state.py,sha256=IT4IxfpKfvuqKeGX6aqj1DhfJWnRJB8dgUIYxe9jnPk,96723
16
+ reflex/state.py,sha256=xmboaoHRhD4kck15EqZ9lR_Zypdpca0v0Fgjk4eC-ng,96814
17
17
  reflex/style.py,sha256=q5Zyc16ULY9xw9CKPPawknrBXIcNNdaSLALXAgdcm2g,13298
18
18
  reflex/testing.py,sha256=EzFAVOD9iRfTlQ805NJG6vUynRt1TEkPxId5aCK8eAQ,41974
19
19
  reflex/.templates/apps/blank/assets/favicon.ico,sha256=baxxgDAQ2V4-G5Q4S2yK5uUJTUGkv-AOWBQ0xd6myUo,4286
@@ -61,7 +61,7 @@ reflex/components/base/body.py,sha256=KLPOhxVsKyjPwrY9AziCOOG_c9ckOqIhI4n2i3_Q3N
61
61
  reflex/components/base/body.pyi,sha256=u_Y3lBkSwGZlnsHWKr_Rtm49UI2bP8Rx2e99gkshENw,2357
62
62
  reflex/components/base/document.py,sha256=Fr7y22NbeKeiz8kWPH2q5BpFjKdq-AmY-sxZilee_H8,636
63
63
  reflex/components/base/document.pyi,sha256=PlvclB2vyGcQ8zLJpFac_37LHeXLwpe8Qh0UqzXAPfE,11788
64
- reflex/components/base/error_boundary.py,sha256=sp6W7G6uFWdls6nzl17XSizSSm6zS2OAu3ZqTE2W1CE,6438
64
+ reflex/components/base/error_boundary.py,sha256=eXLO4SxNx2QoIC5tXk7f0kMulXNDJJaEkZNJ985FAcw,7456
65
65
  reflex/components/base/error_boundary.pyi,sha256=SvKnZ09jwIL_4-J5BKVZuATclrB4o3Eh457iTmmoNb4,2926
66
66
  reflex/components/base/fragment.py,sha256=ys7wkokq-N8WBxa9fqkEaNIrBlSximyD7vqlFVe02hQ,342
67
67
  reflex/components/base/fragment.pyi,sha256=1vl8p6SCWd0_QzYVAb-Em70IX6FwIWFvJLxMl5OJTwU,2401
@@ -139,8 +139,8 @@ reflex/components/gridjs/__init__.py,sha256=xJwDm1AZ70L5-t9LLqZwGUtDpijbf1KuMYDT
139
139
  reflex/components/gridjs/datatable.py,sha256=7JKrRw1zkpFB0_wwoaIhrVrldsm7-dyi3PASgqLq8Hc,4224
140
140
  reflex/components/gridjs/datatable.pyi,sha256=kFgv82vCgfdWZaUq4bZ73G8X3mkw6ecvSRkZ9G9-28E,5185
141
141
  reflex/components/lucide/__init__.py,sha256=EggTK2MuQKQeOBLKW-mF0VaDK9zdWBImu1HO2dvHZbE,73
142
- reflex/components/lucide/icon.py,sha256=WLEzDeVlS68XWj1y4F0DV6pa8S19g-vwuZxMe6BT2Yo,35396
143
- reflex/components/lucide/icon.pyi,sha256=oFXjvb5SBULQlbeGDY4K5WWkqemktGIaCsiyrentjKY,37987
142
+ reflex/components/lucide/icon.py,sha256=4m82j8mWmgQZ-nl1vzBDjhF5rTAq9ZvlvG9dqNc39uI,35440
143
+ reflex/components/lucide/icon.pyi,sha256=KAdU8jt0vmXokueKPegCSwradW0oWyWB9kV4kPoef-8,38052
144
144
  reflex/components/markdown/__init__.py,sha256=Dfl1At5uYoY7H4ufZU_RY2KOGQDLtj75dsZ2BTqqAns,87
145
145
  reflex/components/markdown/markdown.py,sha256=Sg3AioKZsNn27KdOzR3o53k1bHzPa2pjpHFVYbxBgCg,16464
146
146
  reflex/components/markdown/markdown.pyi,sha256=5SbgUBrklIdxEJOHtOqKVM2aobgulnkWp5DEGUFNmEI,4323
@@ -319,7 +319,7 @@ reflex/constants/compiler.py,sha256=1FXPYQNotaSrTwWcOspA1gCVmEdoiWkNMbbrz_qU0YU,
319
319
  reflex/constants/config.py,sha256=8OIjiBdZZJrRVHsNBheMwopE9AwBFFzau0SXqXKcrPg,1715
320
320
  reflex/constants/custom_components.py,sha256=joJt4CEt1yKy7wsBH6vYo7_QRW0O_fWXrrTf0VY2q14,1317
321
321
  reflex/constants/event.py,sha256=tgoynWQi2L0_Kqc3XhXo7XXL76A-OKhJGHRrNjm7gFw,2885
322
- reflex/constants/installer.py,sha256=iZvdnTIl2zh0_fPr1G-RsOiHbA4vnnse8ykPJVFBUbE,4191
322
+ reflex/constants/installer.py,sha256=JUTIXWqN_IET_kCJzowVVqrzcdrcGtGIErBJbNa3D2s,4191
323
323
  reflex/constants/route.py,sha256=UBjqaAOxiUxlDZCSY4O2JJChKvA4MZrhUU0E5rNvKbM,2682
324
324
  reflex/constants/state.py,sha256=VrEeYxXfE9ss8RmOHIXD4T6EGsV9PDqbtMCQMmZxW3I,383
325
325
  reflex/constants/utils.py,sha256=e1ChEvbHfmE_V2UJvCSUhD_qTVAIhEGPpRJSqdSd6PA,780
@@ -337,7 +337,7 @@ reflex/istate/wrappers.py,sha256=p8uuioXRbR5hperwbOJHUcWdu7hukLikQdoR7qrnKsI,909
337
337
  reflex/istate/manager/__init__.py,sha256=hTg5uqxVbz-xayUZNin-wP51PfAkz1CHDez-jncXTTg,4406
338
338
  reflex/istate/manager/disk.py,sha256=RVWDnPt4d2a0El8RBnWDVCksLKv7rPuWbkUPR6Y7Szc,13736
339
339
  reflex/istate/manager/memory.py,sha256=tnK2JzJNcEbiXAdGIT5tNA0U1-mQZoeXKF8XNJCfnts,2760
340
- reflex/istate/manager/redis.py,sha256=LGR8ilvrCZmxKznhIKBHP4-Oe_JT5htcoJldVH53MlU,19020
340
+ reflex/istate/manager/redis.py,sha256=n2JREGIUwevlvx_Kt-b4i1RSrQZCTZUBVMwNOpIeTec,19052
341
341
  reflex/middleware/__init__.py,sha256=x7xTeDuc73Hjj43k1J63naC9x8vzFxl4sq7cCFBX7sk,111
342
342
  reflex/middleware/hydrate_middleware.py,sha256=1ch7bx2ZhojOR15b-LHD2JztrWCnpPJjTe8MWHJe-5Y,1510
343
343
  reflex/middleware/middleware.py,sha256=p5VVoIgQ_NwOg_GOY6g0S4fmrV76_VE1zt-HiwbMw-s,1158
@@ -347,7 +347,7 @@ reflex/plugins/base.py,sha256=5BgzCM7boj9kJ6FGzVzVlgQk-crJuVmOLCl1PXvv4-E,3372
347
347
  reflex/plugins/shared_tailwind.py,sha256=XPnswswPW3UIeEu5ghecdEeYtpikG5ksD92sM-VwKYM,7221
348
348
  reflex/plugins/sitemap.py,sha256=X_CtH5B1w3CZno-gdPj1rp63WjOuNjFnX4B3fx_-VFQ,6135
349
349
  reflex/plugins/tailwind_v3.py,sha256=jCEZ5UYdr706Mw48L-WSHOUB6O55o1C3uG6AMwXqZoI,4810
350
- reflex/plugins/tailwind_v4.py,sha256=Cjp3vJnrZDYiflwm4imj4SEI0pYBv36mLvmpbI6olao,5230
350
+ reflex/plugins/tailwind_v4.py,sha256=GE-GTFFqBPQ8TmIfvp2-Y3K2wyCtmICEbeYJy6tvISk,5230
351
351
  reflex/utils/__init__.py,sha256=y-AHKiRQAhk2oAkvn7W8cRVTZVK625ff8tTwvZtO7S4,24
352
352
  reflex/utils/build.py,sha256=j-OY90O7gMP_bclVt_6J3Q2GFgOHQH_uFpTfdaWmuqU,9746
353
353
  reflex/utils/codespaces.py,sha256=SIATnmlGCABPvjvRIENUCwP-fcjqKhdoOYiFY_Eua6M,4339
@@ -366,7 +366,7 @@ reflex/utils/misc.py,sha256=emPjhUsL5WV8BFwwN8I0IrYUJyB1VlhsfnTLcCB3xco,4596
366
366
  reflex/utils/monitoring.py,sha256=AZ5KZqaPBOIkfNB___rmXK0zEPjwDXHIlUDwKPVPBmI,5235
367
367
  reflex/utils/net.py,sha256=q3h5pNbAlFiqy8U15S9DTOvzy_OnenVVug5ROBTGRTA,4267
368
368
  reflex/utils/path_ops.py,sha256=_RS17IQDNr5vcoLLGZx2-z1E5WP-JgDHvaRAOgqrZiU,8154
369
- reflex/utils/prerequisites.py,sha256=CKrzD41-F1KA7H3oob-OtWuAHWOqUcPGbEvgYwbrphE,21164
369
+ reflex/utils/prerequisites.py,sha256=oom949r2G_VvYiujD5FSo59LvYSkMPunJSSuJgCHsAM,21273
370
370
  reflex/utils/processes.py,sha256=UzXcQ8Qp8TyOMcHrAG7Q8K2YJcXPXhswzBqDMcok0hc,18131
371
371
  reflex/utils/pyi_generator.py,sha256=dDX7pktR6ERE5TPrM4bGhpECu1FaAFadbSqY3W0TlhU,46105
372
372
  reflex/utils/redir.py,sha256=UuVMCISI9UJTzIkQGZZPtW9wRwTsBUo4LaKPegs8dIo,1197
@@ -375,8 +375,8 @@ reflex/utils/rename.py,sha256=8f3laR0Zr3uizKKDD_1woPz-FZvUPjzD-fDeNHf7wBk,5232
375
375
  reflex/utils/serializers.py,sha256=SBjJ0s6euZYfRsb0gzZy3YQdkYkTNU9_-nzk_LyX2C4,14039
376
376
  reflex/utils/telemetry.py,sha256=_jrI6pT3bKedtPFoZXw-tU9cdIh7-fCXl30c6G2jf2M,10756
377
377
  reflex/utils/templates.py,sha256=hSZXol3_fE7d51yeK30XNsCG7ZhD6F09JNzqmBy_MaU,14099
378
- reflex/utils/token_manager.py,sha256=ZtrYR0X8tTs8FpQHtMb09-H2V1xSoLWwVH8jW8OCrU8,7445
379
- reflex/utils/types.py,sha256=CjGrLXnrPooFCpfzm8TFAf_7tZX7-eThROga4QzDxMY,38781
378
+ reflex/utils/token_manager.py,sha256=tMD6VsPz97Xe-ZAd5vzoSbHiqWD9xt50smngs1heFZw,16796
379
+ reflex/utils/types.py,sha256=Lz2Sf2xS2Oh0g6V0pmlJ_zoZSG7FXTEBnBF0G1NmxeI,40206
380
380
  reflex/vars/__init__.py,sha256=pUzFFkY-brpEoqYHQc41VefaOdPQG6xzjer1RJy9IKo,1264
381
381
  reflex/vars/base.py,sha256=q2YZv-FywQaC-LHvR1U3QTY8ksPYSdj5RSJEzKGOFo0,112774
382
382
  reflex/vars/color.py,sha256=PdZ50n7YqIgueIr8FKBkII-aPpD8x7xqbi3MLgI7iGQ,4856
@@ -387,8 +387,8 @@ reflex/vars/number.py,sha256=Cejba-47shtQt-j0uD_HRfTGOm1IF1uZ1WwpWSrcLSE,28865
387
387
  reflex/vars/object.py,sha256=j3b-j66Qa0XDJofMkcJtb8e-TdNx2_hjyEPnrGJEaFY,17833
388
388
  reflex/vars/sequence.py,sha256=OyCfMsv50Zr6W26DMISWjLX6FzK3rbxNcgKepgYr7Pk,52326
389
389
  scripts/hatch_build.py,sha256=-4pxcLSFmirmujGpQX9UUxjhIC03tQ_fIQwVbHu9kc0,1861
390
- reflex-0.8.17a1.dist-info/METADATA,sha256=iH8TN8Ktq1RI_4JtlJBly7yBfM4JMREcj_aFY9DrE3Q,13102
391
- reflex-0.8.17a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
392
- reflex-0.8.17a1.dist-info/entry_points.txt,sha256=Rxt4dXc7MLBNt5CSHTehVPuSe9Xqow4HLX55nD9tQQ0,45
393
- reflex-0.8.17a1.dist-info/licenses/LICENSE,sha256=dw3zLrp9f5ObD7kqS32vWfhcImfO52PMmRqvtxq_YEE,11358
394
- reflex-0.8.17a1.dist-info/RECORD,,
390
+ reflex-0.8.18a1.dist-info/METADATA,sha256=1IV_Z7FfUs-3MX_TQ2WnPzDm6g1c0AQl42D3ZB-gR6s,13102
391
+ reflex-0.8.18a1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
392
+ reflex-0.8.18a1.dist-info/entry_points.txt,sha256=Rxt4dXc7MLBNt5CSHTehVPuSe9Xqow4HLX55nD9tQQ0,45
393
+ reflex-0.8.18a1.dist-info/licenses/LICENSE,sha256=dw3zLrp9f5ObD7kqS32vWfhcImfO52PMmRqvtxq_YEE,11358
394
+ reflex-0.8.18a1.dist-info/RECORD,,