reflex 0.8.15a0__py3-none-any.whl → 0.8.15a1__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/__init__.py +11 -6
- reflex/__init__.pyi +9 -2
- reflex/base.py +8 -11
- reflex/components/field.py +3 -1
- reflex/components/markdown/markdown.py +101 -27
- reflex/constants/base.py +5 -0
- reflex/constants/installer.py +2 -2
- reflex/event.py +3 -0
- reflex/istate/manager/__init__.py +120 -0
- reflex/istate/manager/disk.py +210 -0
- reflex/istate/manager/memory.py +76 -0
- reflex/istate/{manager.py → manager/redis.py} +5 -372
- reflex/model.py +5 -1
- reflex/state.py +14 -9
- reflex/testing.py +4 -8
- reflex/utils/compat.py +49 -1
- reflex/utils/misc.py +2 -1
- reflex/utils/monitoring.py +1 -2
- reflex/utils/prerequisites.py +17 -3
- reflex/utils/processes.py +3 -1
- reflex/utils/redir.py +21 -37
- reflex/utils/templates.py +4 -4
- reflex/utils/types.py +1 -0
- reflex/vars/base.py +106 -25
- reflex/vars/color.py +28 -8
- reflex/vars/datetime.py +6 -2
- reflex/vars/dep_tracking.py +2 -2
- reflex/vars/number.py +26 -0
- reflex/vars/object.py +23 -6
- reflex/vars/sequence.py +32 -1
- {reflex-0.8.15a0.dist-info → reflex-0.8.15a1.dist-info}/METADATA +4 -3
- {reflex-0.8.15a0.dist-info → reflex-0.8.15a1.dist-info}/RECORD +35 -32
- {reflex-0.8.15a0.dist-info → reflex-0.8.15a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.15a0.dist-info → reflex-0.8.15a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.15a0.dist-info → reflex-0.8.15a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""A state manager that stores states in memory."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import dataclasses
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from reflex.istate.manager import StateManager
|
|
11
|
+
from reflex.state import BaseState, _split_substate_key
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class StateManagerMemory(StateManager):
|
|
16
|
+
"""A state manager that stores states in memory."""
|
|
17
|
+
|
|
18
|
+
# The mapping of client ids to states.
|
|
19
|
+
states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
# The mutex ensures the dict of mutexes is updated exclusively
|
|
22
|
+
_state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
|
|
23
|
+
|
|
24
|
+
# The dict of mutexes for each client
|
|
25
|
+
_states_locks: dict[str, asyncio.Lock] = dataclasses.field(
|
|
26
|
+
default_factory=dict, init=False
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
async def get_state(self, token: str) -> BaseState:
|
|
31
|
+
"""Get the state for a token.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
token: The token to get the state for.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
The state for the token.
|
|
38
|
+
"""
|
|
39
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
40
|
+
token = _split_substate_key(token)[0]
|
|
41
|
+
if token not in self.states:
|
|
42
|
+
self.states[token] = self.state(_reflex_internal_init=True)
|
|
43
|
+
return self.states[token]
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
async def set_state(self, token: str, state: BaseState):
|
|
47
|
+
"""Set the state for a token.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
token: The token to set the state for.
|
|
51
|
+
state: The state to set.
|
|
52
|
+
"""
|
|
53
|
+
token = _split_substate_key(token)[0]
|
|
54
|
+
self.states[token] = state
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
@contextlib.asynccontextmanager
|
|
58
|
+
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
59
|
+
"""Modify the state for a token while holding exclusive lock.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
token: The token to modify the state for.
|
|
63
|
+
|
|
64
|
+
Yields:
|
|
65
|
+
The state for the token.
|
|
66
|
+
"""
|
|
67
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
68
|
+
token = _split_substate_key(token)[0]
|
|
69
|
+
if token not in self._states_locks:
|
|
70
|
+
async with self._state_manager_lock:
|
|
71
|
+
if token not in self._states_locks:
|
|
72
|
+
self._states_locks[token] = asyncio.Lock()
|
|
73
|
+
|
|
74
|
+
async with self._states_locks[token]:
|
|
75
|
+
state = await self.get_state(token)
|
|
76
|
+
yield state
|
|
@@ -1,387 +1,29 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""A state manager that stores states in redis."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
5
|
import dataclasses
|
|
6
|
-
import functools
|
|
7
6
|
import time
|
|
8
7
|
import uuid
|
|
9
|
-
from abc import ABC, abstractmethod
|
|
10
8
|
from collections.abc import AsyncIterator
|
|
11
|
-
from hashlib import md5
|
|
12
|
-
from pathlib import Path
|
|
13
9
|
|
|
14
10
|
from redis import ResponseError
|
|
15
11
|
from redis.asyncio import Redis
|
|
16
12
|
from redis.asyncio.client import PubSub
|
|
17
13
|
from typing_extensions import override
|
|
18
14
|
|
|
19
|
-
from reflex import constants
|
|
20
15
|
from reflex.config import get_config
|
|
21
16
|
from reflex.environment import environment
|
|
17
|
+
from reflex.istate.manager import StateManager, _default_token_expiration
|
|
22
18
|
from reflex.state import BaseState, _split_substate_key, _substate_key
|
|
23
|
-
from reflex.utils import console
|
|
19
|
+
from reflex.utils import console
|
|
24
20
|
from reflex.utils.exceptions import (
|
|
25
21
|
InvalidLockWarningThresholdError,
|
|
26
|
-
InvalidStateManagerModeError,
|
|
27
22
|
LockExpiredError,
|
|
28
23
|
StateSchemaMismatchError,
|
|
29
24
|
)
|
|
30
25
|
|
|
31
26
|
|
|
32
|
-
@dataclasses.dataclass
|
|
33
|
-
class StateManager(ABC):
|
|
34
|
-
"""A class to manage many client states."""
|
|
35
|
-
|
|
36
|
-
# The state class to use.
|
|
37
|
-
state: type[BaseState]
|
|
38
|
-
|
|
39
|
-
@classmethod
|
|
40
|
-
def create(cls, state: type[BaseState]):
|
|
41
|
-
"""Create a new state manager.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
state: The state class to use.
|
|
45
|
-
|
|
46
|
-
Raises:
|
|
47
|
-
InvalidStateManagerModeError: If the state manager mode is invalid.
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
The state manager (either disk, memory or redis).
|
|
51
|
-
"""
|
|
52
|
-
config = get_config()
|
|
53
|
-
if prerequisites.parse_redis_url() is not None:
|
|
54
|
-
config.state_manager_mode = constants.StateManagerMode.REDIS
|
|
55
|
-
if config.state_manager_mode == constants.StateManagerMode.MEMORY:
|
|
56
|
-
return StateManagerMemory(state=state)
|
|
57
|
-
if config.state_manager_mode == constants.StateManagerMode.DISK:
|
|
58
|
-
return StateManagerDisk(state=state)
|
|
59
|
-
if config.state_manager_mode == constants.StateManagerMode.REDIS:
|
|
60
|
-
redis = prerequisites.get_redis()
|
|
61
|
-
if redis is not None:
|
|
62
|
-
# make sure expiration values are obtained only from the config object on creation
|
|
63
|
-
return StateManagerRedis(
|
|
64
|
-
state=state,
|
|
65
|
-
redis=redis,
|
|
66
|
-
token_expiration=config.redis_token_expiration,
|
|
67
|
-
lock_expiration=config.redis_lock_expiration,
|
|
68
|
-
lock_warning_threshold=config.redis_lock_warning_threshold,
|
|
69
|
-
)
|
|
70
|
-
msg = f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
|
|
71
|
-
raise InvalidStateManagerModeError(msg)
|
|
72
|
-
|
|
73
|
-
@abstractmethod
|
|
74
|
-
async def get_state(self, token: str) -> BaseState:
|
|
75
|
-
"""Get the state for a token.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
token: The token to get the state for.
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
The state for the token.
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
@abstractmethod
|
|
85
|
-
async def set_state(self, token: str, state: BaseState):
|
|
86
|
-
"""Set the state for a token.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
token: The token to set the state for.
|
|
90
|
-
state: The state to set.
|
|
91
|
-
"""
|
|
92
|
-
|
|
93
|
-
@abstractmethod
|
|
94
|
-
@contextlib.asynccontextmanager
|
|
95
|
-
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
96
|
-
"""Modify the state for a token while holding exclusive lock.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
token: The token to modify the state for.
|
|
100
|
-
|
|
101
|
-
Yields:
|
|
102
|
-
The state for the token.
|
|
103
|
-
"""
|
|
104
|
-
yield self.state()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@dataclasses.dataclass
|
|
108
|
-
class StateManagerMemory(StateManager):
|
|
109
|
-
"""A state manager that stores states in memory."""
|
|
110
|
-
|
|
111
|
-
# The mapping of client ids to states.
|
|
112
|
-
states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
|
|
113
|
-
|
|
114
|
-
# The mutex ensures the dict of mutexes is updated exclusively
|
|
115
|
-
_state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
|
|
116
|
-
|
|
117
|
-
# The dict of mutexes for each client
|
|
118
|
-
_states_locks: dict[str, asyncio.Lock] = dataclasses.field(
|
|
119
|
-
default_factory=dict, init=False
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
@override
|
|
123
|
-
async def get_state(self, token: str) -> BaseState:
|
|
124
|
-
"""Get the state for a token.
|
|
125
|
-
|
|
126
|
-
Args:
|
|
127
|
-
token: The token to get the state for.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
The state for the token.
|
|
131
|
-
"""
|
|
132
|
-
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
133
|
-
token = _split_substate_key(token)[0]
|
|
134
|
-
if token not in self.states:
|
|
135
|
-
self.states[token] = self.state(_reflex_internal_init=True)
|
|
136
|
-
return self.states[token]
|
|
137
|
-
|
|
138
|
-
@override
|
|
139
|
-
async def set_state(self, token: str, state: BaseState):
|
|
140
|
-
"""Set the state for a token.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
token: The token to set the state for.
|
|
144
|
-
state: The state to set.
|
|
145
|
-
"""
|
|
146
|
-
token = _split_substate_key(token)[0]
|
|
147
|
-
self.states[token] = state
|
|
148
|
-
|
|
149
|
-
@override
|
|
150
|
-
@contextlib.asynccontextmanager
|
|
151
|
-
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
152
|
-
"""Modify the state for a token while holding exclusive lock.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
token: The token to modify the state for.
|
|
156
|
-
|
|
157
|
-
Yields:
|
|
158
|
-
The state for the token.
|
|
159
|
-
"""
|
|
160
|
-
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
161
|
-
token = _split_substate_key(token)[0]
|
|
162
|
-
if token not in self._states_locks:
|
|
163
|
-
async with self._state_manager_lock:
|
|
164
|
-
if token not in self._states_locks:
|
|
165
|
-
self._states_locks[token] = asyncio.Lock()
|
|
166
|
-
|
|
167
|
-
async with self._states_locks[token]:
|
|
168
|
-
state = await self.get_state(token)
|
|
169
|
-
yield state
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _default_token_expiration() -> int:
|
|
173
|
-
"""Get the default token expiration time.
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
The default token expiration time.
|
|
177
|
-
"""
|
|
178
|
-
return get_config().redis_token_expiration
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def reset_disk_state_manager():
|
|
182
|
-
"""Reset the disk state manager."""
|
|
183
|
-
console.debug("Resetting disk state manager.")
|
|
184
|
-
states_directory = prerequisites.get_states_dir()
|
|
185
|
-
if states_directory.exists():
|
|
186
|
-
for path in states_directory.iterdir():
|
|
187
|
-
path.unlink()
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@dataclasses.dataclass
|
|
191
|
-
class StateManagerDisk(StateManager):
|
|
192
|
-
"""A state manager that stores states in memory."""
|
|
193
|
-
|
|
194
|
-
# The mapping of client ids to states.
|
|
195
|
-
states: dict[str, BaseState] = dataclasses.field(default_factory=dict)
|
|
196
|
-
|
|
197
|
-
# The mutex ensures the dict of mutexes is updated exclusively
|
|
198
|
-
_state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock())
|
|
199
|
-
|
|
200
|
-
# The dict of mutexes for each client
|
|
201
|
-
_states_locks: dict[str, asyncio.Lock] = dataclasses.field(
|
|
202
|
-
default_factory=dict,
|
|
203
|
-
init=False,
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# The token expiration time (s).
|
|
207
|
-
token_expiration: int = dataclasses.field(default_factory=_default_token_expiration)
|
|
208
|
-
|
|
209
|
-
def __post_init_(self):
|
|
210
|
-
"""Create a new state manager."""
|
|
211
|
-
path_ops.mkdir(self.states_directory)
|
|
212
|
-
|
|
213
|
-
self._purge_expired_states()
|
|
214
|
-
|
|
215
|
-
@functools.cached_property
|
|
216
|
-
def states_directory(self) -> Path:
|
|
217
|
-
"""Get the states directory.
|
|
218
|
-
|
|
219
|
-
Returns:
|
|
220
|
-
The states directory.
|
|
221
|
-
"""
|
|
222
|
-
return prerequisites.get_states_dir()
|
|
223
|
-
|
|
224
|
-
def _purge_expired_states(self):
|
|
225
|
-
"""Purge expired states from the disk."""
|
|
226
|
-
import time
|
|
227
|
-
|
|
228
|
-
for path in path_ops.ls(self.states_directory):
|
|
229
|
-
# check path is a pickle file
|
|
230
|
-
if path.suffix != ".pkl":
|
|
231
|
-
continue
|
|
232
|
-
|
|
233
|
-
# load last edited field from file
|
|
234
|
-
last_edited = path.stat().st_mtime
|
|
235
|
-
|
|
236
|
-
# check if the file is older than the token expiration time
|
|
237
|
-
if time.time() - last_edited > self.token_expiration:
|
|
238
|
-
# remove the file
|
|
239
|
-
path.unlink()
|
|
240
|
-
|
|
241
|
-
def token_path(self, token: str) -> Path:
|
|
242
|
-
"""Get the path for a token.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
token: The token to get the path for.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
The path for the token.
|
|
249
|
-
"""
|
|
250
|
-
return (
|
|
251
|
-
self.states_directory / f"{md5(token.encode()).hexdigest()}.pkl"
|
|
252
|
-
).absolute()
|
|
253
|
-
|
|
254
|
-
async def load_state(self, token: str) -> BaseState | None:
|
|
255
|
-
"""Load a state object based on the provided token.
|
|
256
|
-
|
|
257
|
-
Args:
|
|
258
|
-
token: The token used to identify the state object.
|
|
259
|
-
|
|
260
|
-
Returns:
|
|
261
|
-
The loaded state object or None.
|
|
262
|
-
"""
|
|
263
|
-
token_path = self.token_path(token)
|
|
264
|
-
|
|
265
|
-
if token_path.exists():
|
|
266
|
-
try:
|
|
267
|
-
with token_path.open(mode="rb") as file:
|
|
268
|
-
return BaseState._deserialize(fp=file)
|
|
269
|
-
except Exception:
|
|
270
|
-
pass
|
|
271
|
-
return None
|
|
272
|
-
|
|
273
|
-
async def populate_substates(
|
|
274
|
-
self, client_token: str, state: BaseState, root_state: BaseState
|
|
275
|
-
):
|
|
276
|
-
"""Populate the substates of a state object.
|
|
277
|
-
|
|
278
|
-
Args:
|
|
279
|
-
client_token: The client token.
|
|
280
|
-
state: The state object to populate.
|
|
281
|
-
root_state: The root state object.
|
|
282
|
-
"""
|
|
283
|
-
for substate in state.get_substates():
|
|
284
|
-
substate_token = _substate_key(client_token, substate)
|
|
285
|
-
|
|
286
|
-
fresh_instance = await root_state.get_state(substate)
|
|
287
|
-
instance = await self.load_state(substate_token)
|
|
288
|
-
if instance is not None:
|
|
289
|
-
# Ensure all substates exist, even if they weren't serialized previously.
|
|
290
|
-
instance.substates = fresh_instance.substates
|
|
291
|
-
else:
|
|
292
|
-
instance = fresh_instance
|
|
293
|
-
state.substates[substate.get_name()] = instance
|
|
294
|
-
instance.parent_state = state
|
|
295
|
-
|
|
296
|
-
await self.populate_substates(client_token, instance, root_state)
|
|
297
|
-
|
|
298
|
-
@override
|
|
299
|
-
async def get_state(
|
|
300
|
-
self,
|
|
301
|
-
token: str,
|
|
302
|
-
) -> BaseState:
|
|
303
|
-
"""Get the state for a token.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
token: The token to get the state for.
|
|
307
|
-
|
|
308
|
-
Returns:
|
|
309
|
-
The state for the token.
|
|
310
|
-
"""
|
|
311
|
-
client_token = _split_substate_key(token)[0]
|
|
312
|
-
root_state = self.states.get(client_token)
|
|
313
|
-
if root_state is not None:
|
|
314
|
-
# Retrieved state from memory.
|
|
315
|
-
return root_state
|
|
316
|
-
|
|
317
|
-
# Deserialize root state from disk.
|
|
318
|
-
root_state = await self.load_state(_substate_key(client_token, self.state))
|
|
319
|
-
# Create a new root state tree with all substates instantiated.
|
|
320
|
-
fresh_root_state = self.state(_reflex_internal_init=True)
|
|
321
|
-
if root_state is None:
|
|
322
|
-
root_state = fresh_root_state
|
|
323
|
-
else:
|
|
324
|
-
# Ensure all substates exist, even if they were not serialized previously.
|
|
325
|
-
root_state.substates = fresh_root_state.substates
|
|
326
|
-
self.states[client_token] = root_state
|
|
327
|
-
await self.populate_substates(client_token, root_state, root_state)
|
|
328
|
-
return root_state
|
|
329
|
-
|
|
330
|
-
async def set_state_for_substate(self, client_token: str, substate: BaseState):
|
|
331
|
-
"""Set the state for a substate.
|
|
332
|
-
|
|
333
|
-
Args:
|
|
334
|
-
client_token: The client token.
|
|
335
|
-
substate: The substate to set.
|
|
336
|
-
"""
|
|
337
|
-
substate_token = _substate_key(client_token, substate)
|
|
338
|
-
|
|
339
|
-
if substate._get_was_touched():
|
|
340
|
-
substate._was_touched = False # Reset the touched flag after serializing.
|
|
341
|
-
pickle_state = substate._serialize()
|
|
342
|
-
if pickle_state:
|
|
343
|
-
if not self.states_directory.exists():
|
|
344
|
-
self.states_directory.mkdir(parents=True, exist_ok=True)
|
|
345
|
-
self.token_path(substate_token).write_bytes(pickle_state)
|
|
346
|
-
|
|
347
|
-
for substate_substate in substate.substates.values():
|
|
348
|
-
await self.set_state_for_substate(client_token, substate_substate)
|
|
349
|
-
|
|
350
|
-
@override
|
|
351
|
-
async def set_state(self, token: str, state: BaseState):
|
|
352
|
-
"""Set the state for a token.
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
token: The token to set the state for.
|
|
356
|
-
state: The state to set.
|
|
357
|
-
"""
|
|
358
|
-
client_token, _ = _split_substate_key(token)
|
|
359
|
-
await self.set_state_for_substate(client_token, state)
|
|
360
|
-
|
|
361
|
-
@override
|
|
362
|
-
@contextlib.asynccontextmanager
|
|
363
|
-
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
364
|
-
"""Modify the state for a token while holding exclusive lock.
|
|
365
|
-
|
|
366
|
-
Args:
|
|
367
|
-
token: The token to modify the state for.
|
|
368
|
-
|
|
369
|
-
Yields:
|
|
370
|
-
The state for the token.
|
|
371
|
-
"""
|
|
372
|
-
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
373
|
-
client_token, _ = _split_substate_key(token)
|
|
374
|
-
if client_token not in self._states_locks:
|
|
375
|
-
async with self._state_manager_lock:
|
|
376
|
-
if client_token not in self._states_locks:
|
|
377
|
-
self._states_locks[client_token] = asyncio.Lock()
|
|
378
|
-
|
|
379
|
-
async with self._states_locks[client_token]:
|
|
380
|
-
state = await self.get_state(token)
|
|
381
|
-
yield state
|
|
382
|
-
await self.set_state(token, state)
|
|
383
|
-
|
|
384
|
-
|
|
385
27
|
def _default_lock_expiration() -> int:
|
|
386
28
|
"""Get the default lock expiration time.
|
|
387
29
|
|
|
@@ -748,7 +390,7 @@ class StateManagerRedis(StateManager):
|
|
|
748
390
|
if timeout is None:
|
|
749
391
|
timeout = self.lock_expiration / 1000.0
|
|
750
392
|
|
|
751
|
-
started = time.
|
|
393
|
+
started = time.monotonic()
|
|
752
394
|
message = await pubsub.get_message(
|
|
753
395
|
ignore_subscribe_messages=True,
|
|
754
396
|
timeout=timeout,
|
|
@@ -757,7 +399,7 @@ class StateManagerRedis(StateManager):
|
|
|
757
399
|
message is None
|
|
758
400
|
or message["data"] not in self._redis_keyspace_lock_release_events
|
|
759
401
|
):
|
|
760
|
-
remaining = timeout - (time.
|
|
402
|
+
remaining = timeout - (time.monotonic() - started)
|
|
761
403
|
if remaining <= 0:
|
|
762
404
|
return
|
|
763
405
|
await self._get_pubsub_message(pubsub, timeout=remaining)
|
|
@@ -847,12 +489,3 @@ class StateManagerRedis(StateManager):
|
|
|
847
489
|
Note: Connections will be automatically reopened when needed.
|
|
848
490
|
"""
|
|
849
491
|
await self.redis.aclose(close_connection_pool=True)
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
def get_state_manager() -> StateManager:
|
|
853
|
-
"""Get the state manager for the app that is currently running.
|
|
854
|
-
|
|
855
|
-
Returns:
|
|
856
|
-
The state manager.
|
|
857
|
-
"""
|
|
858
|
-
return prerequisites.get_and_validate_app().app.state_manager
|
reflex/model.py
CHANGED
|
@@ -71,6 +71,10 @@ if find_spec("sqlalchemy"):
|
|
|
71
71
|
"echo": environment.SQLALCHEMY_ECHO.get(),
|
|
72
72
|
# Check connections before returning them.
|
|
73
73
|
"pool_pre_ping": environment.SQLALCHEMY_POOL_PRE_PING.get(),
|
|
74
|
+
"pool_size": environment.SQLALCHEMY_POOL_SIZE.get(),
|
|
75
|
+
"max_overflow": environment.SQLALCHEMY_MAX_OVERFLOW.get(),
|
|
76
|
+
"pool_recycle": environment.SQLALCHEMY_POOL_RECYCLE.get(),
|
|
77
|
+
"pool_timeout": environment.SQLALCHEMY_POOL_TIMEOUT.get(),
|
|
74
78
|
}
|
|
75
79
|
conf = get_config()
|
|
76
80
|
url = url or conf.db_url
|
|
@@ -363,7 +367,7 @@ if find_spec("sqlmodel") and find_spec("sqlalchemy") and find_spec("pydantic"):
|
|
|
363
367
|
reason=(
|
|
364
368
|
"Register sqlmodel.SQLModel classes with `@rx.ModelRegistry.register`"
|
|
365
369
|
),
|
|
366
|
-
deprecation_version="0.8.
|
|
370
|
+
deprecation_version="0.8.15",
|
|
367
371
|
removal_version="0.9.0",
|
|
368
372
|
)
|
|
369
373
|
super().__pydantic_init_subclass__()
|
reflex/state.py
CHANGED
|
@@ -16,6 +16,7 @@ import time
|
|
|
16
16
|
import typing
|
|
17
17
|
import warnings
|
|
18
18
|
from collections.abc import AsyncIterator, Callable, Sequence
|
|
19
|
+
from enum import Enum
|
|
19
20
|
from hashlib import md5
|
|
20
21
|
from importlib.util import find_spec
|
|
21
22
|
from types import FunctionType
|
|
@@ -246,7 +247,7 @@ class EventHandlerSetVar(EventHandler):
|
|
|
246
247
|
msg = f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`"
|
|
247
248
|
raise AttributeError(msg)
|
|
248
249
|
|
|
249
|
-
if
|
|
250
|
+
if inspect.iscoroutinefunction(handler.fn):
|
|
250
251
|
msg = f"Setter for {args[0]} is async, which is not supported."
|
|
251
252
|
raise NotImplementedError(msg)
|
|
252
253
|
|
|
@@ -287,7 +288,7 @@ async def _resolve_delta(delta: Delta) -> Delta:
|
|
|
287
288
|
tasks = {}
|
|
288
289
|
for state_name, state_delta in delta.items():
|
|
289
290
|
for var_name, value in state_delta.items():
|
|
290
|
-
if
|
|
291
|
+
if inspect.iscoroutine(value):
|
|
291
292
|
tasks[state_name, var_name] = asyncio.create_task(
|
|
292
293
|
value,
|
|
293
294
|
name=f"reflex_resolve_delta|{state_name}|{var_name}|{time.time()}",
|
|
@@ -852,7 +853,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
852
853
|
ComputedVarShadowsBaseVarsError: When a computed var shadows a base var.
|
|
853
854
|
"""
|
|
854
855
|
for name, computed_var_ in cls._get_computed_vars():
|
|
855
|
-
if name in cls
|
|
856
|
+
if name in get_type_hints(cls):
|
|
856
857
|
msg = f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
|
|
857
858
|
raise ComputedVarShadowsBaseVarsError(msg)
|
|
858
859
|
|
|
@@ -1554,6 +1555,8 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1554
1555
|
RuntimeError: If redis is not used in this backend process.
|
|
1555
1556
|
StateMismatchError: If the state instance is not of the expected type.
|
|
1556
1557
|
"""
|
|
1558
|
+
from reflex.istate.manager.redis import StateManagerRedis
|
|
1559
|
+
|
|
1557
1560
|
# Then get the target state and all its substates.
|
|
1558
1561
|
state_manager = get_state_manager()
|
|
1559
1562
|
if not isinstance(state_manager, StateManagerRedis):
|
|
@@ -1733,7 +1736,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1733
1736
|
except TypeError:
|
|
1734
1737
|
pass
|
|
1735
1738
|
|
|
1736
|
-
coroutines = [e for e in events if
|
|
1739
|
+
coroutines = [e for e in events if inspect.iscoroutine(e)]
|
|
1737
1740
|
|
|
1738
1741
|
for coroutine in coroutines:
|
|
1739
1742
|
coroutine_name = coroutine.__qualname__
|
|
@@ -1878,6 +1881,12 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1878
1881
|
hinted_args is tuple or hinted_args is tuple
|
|
1879
1882
|
):
|
|
1880
1883
|
payload[arg] = tuple(value)
|
|
1884
|
+
elif isinstance(hinted_args, type) and issubclass(hinted_args, Enum):
|
|
1885
|
+
try:
|
|
1886
|
+
payload[arg] = hinted_args(value)
|
|
1887
|
+
except ValueError:
|
|
1888
|
+
msg = f"Received an invalid enum value ({value}) for {arg} of type {hinted_args}"
|
|
1889
|
+
raise ValueError(msg) from None
|
|
1881
1890
|
elif (
|
|
1882
1891
|
isinstance(value, str)
|
|
1883
1892
|
and (deserializer := _deserializers.get(hinted_args)) is not None
|
|
@@ -1895,7 +1904,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1895
1904
|
# Wrap the function in a try/except block.
|
|
1896
1905
|
try:
|
|
1897
1906
|
# Handle async functions.
|
|
1898
|
-
if
|
|
1907
|
+
if inspect.iscoroutinefunction(fn.func):
|
|
1899
1908
|
events = await fn(**payload)
|
|
1900
1909
|
|
|
1901
1910
|
# Handle regular functions.
|
|
@@ -2738,11 +2747,7 @@ def reload_state_module(
|
|
|
2738
2747
|
state.get_class_substate.cache_clear()
|
|
2739
2748
|
|
|
2740
2749
|
|
|
2741
|
-
from reflex.istate.manager import LockExpiredError as LockExpiredError # noqa: E402
|
|
2742
2750
|
from reflex.istate.manager import StateManager as StateManager # noqa: E402
|
|
2743
|
-
from reflex.istate.manager import StateManagerDisk as StateManagerDisk # noqa: E402
|
|
2744
|
-
from reflex.istate.manager import StateManagerMemory as StateManagerMemory # noqa: E402
|
|
2745
|
-
from reflex.istate.manager import StateManagerRedis as StateManagerRedis # noqa: E402
|
|
2746
2751
|
from reflex.istate.manager import get_state_manager as get_state_manager # noqa: E402
|
|
2747
2752
|
from reflex.istate.manager import ( # noqa: E402
|
|
2748
2753
|
reset_disk_state_manager as reset_disk_state_manager,
|
reflex/testing.py
CHANGED
|
@@ -38,14 +38,10 @@ import reflex.utils.processes
|
|
|
38
38
|
from reflex.components.component import CustomComponent
|
|
39
39
|
from reflex.config import get_config
|
|
40
40
|
from reflex.environment import environment
|
|
41
|
-
from reflex.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
StateManagerMemory,
|
|
46
|
-
StateManagerRedis,
|
|
47
|
-
reload_state_module,
|
|
48
|
-
)
|
|
41
|
+
from reflex.istate.manager.disk import StateManagerDisk
|
|
42
|
+
from reflex.istate.manager.memory import StateManagerMemory
|
|
43
|
+
from reflex.istate.manager.redis import StateManagerRedis
|
|
44
|
+
from reflex.state import BaseState, StateManager, reload_state_module
|
|
49
45
|
from reflex.utils import console, js_runtimes
|
|
50
46
|
from reflex.utils.export import export
|
|
51
47
|
from reflex.utils.token_manager import TokenManager
|
reflex/utils/compat.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"""Compatibility hacks and helpers."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from importlib.util import find_spec
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
4
7
|
|
|
5
8
|
if TYPE_CHECKING:
|
|
6
9
|
from pydantic.fields import FieldInfo
|
|
@@ -30,6 +33,51 @@ async def windows_hot_reload_lifespan_hack():
|
|
|
30
33
|
pass
|
|
31
34
|
|
|
32
35
|
|
|
36
|
+
def annotations_from_namespace(namespace: Mapping[str, Any]) -> dict[str, Any]:
|
|
37
|
+
"""Get the annotations from a class namespace.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
namespace: The class namespace.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The (forward-ref) annotations from the class namespace.
|
|
44
|
+
"""
|
|
45
|
+
if sys.version_info >= (3, 14) and "__annotations__" not in namespace:
|
|
46
|
+
from annotationlib import (
|
|
47
|
+
Format,
|
|
48
|
+
call_annotate_function,
|
|
49
|
+
get_annotate_from_class_namespace,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if annotate := get_annotate_from_class_namespace(namespace):
|
|
53
|
+
return call_annotate_function(annotate, format=Format.FORWARDREF)
|
|
54
|
+
return namespace.get("__annotations__", {})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if find_spec("pydantic") and find_spec("pydantic.v1"):
|
|
58
|
+
from pydantic.v1.main import ModelMetaclass
|
|
59
|
+
|
|
60
|
+
class ModelMetaclassLazyAnnotations(ModelMetaclass):
|
|
61
|
+
"""Compatibility metaclass to resolve python3.14 style lazy annotations."""
|
|
62
|
+
|
|
63
|
+
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs):
|
|
64
|
+
"""Resolve python3.14 style lazy annotations before passing off to pydantic v1.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
name: The class name.
|
|
68
|
+
bases: The base classes.
|
|
69
|
+
namespace: The class namespace.
|
|
70
|
+
**kwargs: Additional keyword arguments.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The created class.
|
|
74
|
+
"""
|
|
75
|
+
namespace["__annotations__"] = annotations_from_namespace(namespace)
|
|
76
|
+
return super().__new__(mcs, name, bases, namespace, **kwargs)
|
|
77
|
+
else:
|
|
78
|
+
ModelMetaclassLazyAnnotations = type # type: ignore[assignment]
|
|
79
|
+
|
|
80
|
+
|
|
33
81
|
def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool:
|
|
34
82
|
"""Determines if a field is a primary.
|
|
35
83
|
|