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 +35 -11
- reflex/components/base/error_boundary.py +60 -43
- reflex/components/lucide/icon.py +3 -1
- reflex/components/lucide/icon.pyi +4 -1
- reflex/constants/installer.py +1 -1
- reflex/istate/manager/redis.py +1 -1
- reflex/plugins/tailwind_v4.py +2 -2
- reflex/state.py +12 -6
- reflex/utils/prerequisites.py +4 -1
- reflex/utils/token_manager.py +286 -33
- reflex/utils/types.py +44 -12
- {reflex-0.8.17a1.dist-info → reflex-0.8.18a1.dist-info}/METADATA +3 -3
- {reflex-0.8.17a1.dist-info → reflex-0.8.18a1.dist-info}/RECORD +16 -16
- {reflex-0.8.17a1.dist-info → reflex-0.8.18a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.17a1.dist-info → reflex-0.8.18a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.17a1.dist-info → reflex-0.8.18a1.dist-info}/licenses/LICENSE +0 -0
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) ->
|
|
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
|
|
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
|
-
|
|
2104
|
-
if
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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,
|
|
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="
|
|
87
|
+
font_size="5vmin",
|
|
71
88
|
font_weight="bold",
|
|
72
89
|
),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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="
|
|
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%",
|
reflex/components/lucide/icon.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|
reflex/constants/installer.py
CHANGED
reflex/istate/manager/redis.py
CHANGED
|
@@ -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
|
}
|
reflex/plugins/tailwind_v4.py
CHANGED
|
@@ -17,7 +17,7 @@ class Constants(SimpleNamespace):
|
|
|
17
17
|
"""Tailwind constants."""
|
|
18
18
|
|
|
19
19
|
# The Tailwindcss version
|
|
20
|
-
VERSION = "tailwindcss@4.1.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2196
|
-
|
|
2194
|
+
Returns:
|
|
2195
|
+
The unmodified state (self)
|
|
2197
2196
|
"""
|
|
2198
|
-
|
|
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
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
reflex/utils/token_manager.py
CHANGED
|
@@ -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
|
|
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
|
-
#
|
|
30
|
-
self.
|
|
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]] =
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
self.
|
|
194
|
-
|
|
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
|
-
#
|
|
324
|
+
# Store in Redis if possible
|
|
197
325
|
try:
|
|
198
|
-
await self.redis.set(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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<
|
|
34
|
-
Requires-Dist: reflex-hosting-cli>=0.1.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
143
|
-
reflex/components/lucide/icon.pyi,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
379
|
-
reflex/utils/types.py,sha256=
|
|
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.
|
|
391
|
-
reflex-0.8.
|
|
392
|
-
reflex-0.8.
|
|
393
|
-
reflex-0.8.
|
|
394
|
-
reflex-0.8.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|