reflex 0.4.1a1__py3-none-any.whl → 0.4.2a1__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/.templates/jinja/web/pages/_app.js.jinja2 +2 -2
- reflex/.templates/jinja/web/utils/context.js.jinja2 +9 -2
- reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +5 -3
- reflex/.templates/web/utils/state.js +35 -8
- reflex/app.py +5 -4
- reflex/base.py +4 -0
- reflex/compiler/compiler.py +14 -4
- reflex/compiler/templates.py +1 -0
- reflex/components/base/bare.pyi +84 -0
- reflex/components/component.py +23 -4
- reflex/components/core/cond.py +2 -2
- reflex/components/core/debounce.py +1 -1
- reflex/components/core/upload.py +2 -2
- reflex/components/core/upload.pyi +2 -2
- reflex/components/radix/primitives/accordion.py +5 -2
- reflex/components/radix/primitives/accordion.pyi +1 -1
- reflex/components/radix/primitives/progress.py +40 -8
- reflex/components/radix/primitives/progress.pyi +71 -2
- reflex/components/radix/themes/base.py +9 -2
- reflex/components/radix/themes/base.pyi +3 -1
- reflex/components/radix/themes/color_mode.py +1 -1
- reflex/components/radix/themes/layout/stack.py +6 -5
- reflex/components/radix/themes/layout/stack.pyi +6 -5
- reflex/constants/base.pyi +94 -0
- reflex/constants/compiler.py +2 -0
- reflex/constants/event.pyi +59 -0
- reflex/constants/installer.py +2 -2
- reflex/constants/route.pyi +50 -0
- reflex/constants/style.pyi +20 -0
- reflex/event.py +10 -0
- reflex/middleware/hydrate_middleware.py +0 -8
- reflex/state.py +194 -13
- reflex/testing.py +1 -0
- reflex/utils/prerequisites.py +31 -4
- reflex/utils/processes.py +12 -1
- reflex/utils/serializers.py +18 -2
- reflex/utils/types.py +6 -1
- reflex/vars.py +20 -1
- reflex/vars.pyi +2 -0
- {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/METADATA +2 -1
- {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/RECORD +44 -39
- {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/WHEEL +1 -1
- {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/LICENSE +0 -0
- {reflex-0.4.1a1.dist-info → reflex-0.4.2a1.dist-info}/entry_points.txt +0 -0
reflex/state.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Define the reflex state specification."""
|
|
2
|
+
|
|
2
3
|
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
import asyncio
|
|
@@ -213,21 +214,29 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
213
214
|
# The router data for the current page
|
|
214
215
|
router: RouterData = RouterData()
|
|
215
216
|
|
|
216
|
-
def __init__(
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
*args,
|
|
220
|
+
parent_state: BaseState | None = None,
|
|
221
|
+
init_substates: bool = True,
|
|
222
|
+
**kwargs,
|
|
223
|
+
):
|
|
217
224
|
"""Initialize the state.
|
|
218
225
|
|
|
219
226
|
Args:
|
|
220
227
|
*args: The args to pass to the Pydantic init method.
|
|
221
228
|
parent_state: The parent state.
|
|
229
|
+
init_substates: Whether to initialize the substates in this instance.
|
|
222
230
|
**kwargs: The kwargs to pass to the Pydantic init method.
|
|
223
231
|
|
|
224
232
|
"""
|
|
225
233
|
kwargs["parent_state"] = parent_state
|
|
226
234
|
super().__init__(*args, **kwargs)
|
|
227
235
|
|
|
228
|
-
# Setup the substates.
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
# Setup the substates (for memory state manager only).
|
|
237
|
+
if init_substates:
|
|
238
|
+
for substate in self.get_substates():
|
|
239
|
+
self.substates[substate.get_name()] = substate(parent_state=self)
|
|
231
240
|
# Convert the event handlers to functions.
|
|
232
241
|
self._init_event_handlers()
|
|
233
242
|
|
|
@@ -1002,7 +1011,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1002
1011
|
for substate in self.substates.values():
|
|
1003
1012
|
substate._reset_client_storage()
|
|
1004
1013
|
|
|
1005
|
-
def get_substate(self, path: Sequence[str]) -> BaseState
|
|
1014
|
+
def get_substate(self, path: Sequence[str]) -> BaseState:
|
|
1006
1015
|
"""Get the substate.
|
|
1007
1016
|
|
|
1008
1017
|
Args:
|
|
@@ -1257,6 +1266,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1257
1266
|
# Recursively find the substate deltas.
|
|
1258
1267
|
substates = self.substates
|
|
1259
1268
|
for substate in self.dirty_substates.union(self._always_dirty_substates):
|
|
1269
|
+
if substate not in substates:
|
|
1270
|
+
continue # substate not loaded at this time, no delta
|
|
1260
1271
|
delta.update(substates[substate].get_delta())
|
|
1261
1272
|
|
|
1262
1273
|
# Format the delta.
|
|
@@ -1284,6 +1295,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1284
1295
|
for var in self.dirty_vars:
|
|
1285
1296
|
for substate_name in self._substate_var_dependencies[var]:
|
|
1286
1297
|
self.dirty_substates.add(substate_name)
|
|
1298
|
+
if substate_name not in substates:
|
|
1299
|
+
continue
|
|
1287
1300
|
substate = substates[substate_name]
|
|
1288
1301
|
substate.dirty_vars.add(var)
|
|
1289
1302
|
substate._mark_dirty()
|
|
@@ -1292,6 +1305,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1292
1305
|
"""Reset the dirty vars."""
|
|
1293
1306
|
# Recursively clean the substates.
|
|
1294
1307
|
for substate in self.dirty_substates:
|
|
1308
|
+
if substate not in self.substates:
|
|
1309
|
+
continue
|
|
1295
1310
|
self.substates[substate]._clean()
|
|
1296
1311
|
|
|
1297
1312
|
# Clean this state.
|
|
@@ -1377,6 +1392,24 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1377
1392
|
"""
|
|
1378
1393
|
pass
|
|
1379
1394
|
|
|
1395
|
+
def __getstate__(self):
|
|
1396
|
+
"""Get the state for redis serialization.
|
|
1397
|
+
|
|
1398
|
+
This method is called by cloudpickle to serialize the object.
|
|
1399
|
+
|
|
1400
|
+
It explicitly removes parent_state and substates because those are serialized separately
|
|
1401
|
+
by the StateManagerRedis to allow for better horizontal scaling as state size increases.
|
|
1402
|
+
|
|
1403
|
+
Returns:
|
|
1404
|
+
The state dict for serialization.
|
|
1405
|
+
"""
|
|
1406
|
+
state = super().__getstate__()
|
|
1407
|
+
# Never serialize parent_state or substates
|
|
1408
|
+
state["__dict__"] = state["__dict__"].copy()
|
|
1409
|
+
state["__dict__"]["parent_state"] = None
|
|
1410
|
+
state["__dict__"]["substates"] = {}
|
|
1411
|
+
return state
|
|
1412
|
+
|
|
1380
1413
|
|
|
1381
1414
|
class State(BaseState):
|
|
1382
1415
|
"""The app Base State."""
|
|
@@ -1405,6 +1438,23 @@ class State(BaseState):
|
|
|
1405
1438
|
type(self).set_is_hydrated(True), # type: ignore
|
|
1406
1439
|
]
|
|
1407
1440
|
|
|
1441
|
+
def update_vars_internal(self, vars: dict[str, Any]) -> None:
|
|
1442
|
+
"""Apply updates to fully qualified state vars.
|
|
1443
|
+
|
|
1444
|
+
The keys in `vars` should be in the form of `{state.get_full_name()}.{var_name}`,
|
|
1445
|
+
and each value will be set on the appropriate substate instance.
|
|
1446
|
+
|
|
1447
|
+
This function is primarily used to apply cookie and local storage
|
|
1448
|
+
updates from the frontend to the appropriate substate.
|
|
1449
|
+
|
|
1450
|
+
Args:
|
|
1451
|
+
vars: The fully qualified vars and values to update.
|
|
1452
|
+
"""
|
|
1453
|
+
for var, value in vars.items():
|
|
1454
|
+
state_name, _, var_name = var.rpartition(".")
|
|
1455
|
+
var_state = self.get_substate(state_name.split("."))
|
|
1456
|
+
setattr(var_state, var_name, value)
|
|
1457
|
+
|
|
1408
1458
|
|
|
1409
1459
|
class StateProxy(wrapt.ObjectProxy):
|
|
1410
1460
|
"""Proxy of a state instance to control mutability of vars for a background task.
|
|
@@ -1459,6 +1509,8 @@ class StateProxy(wrapt.ObjectProxy):
|
|
|
1459
1509
|
"""
|
|
1460
1510
|
self._self_actx = self._self_app.modify_state(
|
|
1461
1511
|
self.__wrapped__.router.session.client_token
|
|
1512
|
+
+ "_"
|
|
1513
|
+
+ ".".join(self._self_substate_path)
|
|
1462
1514
|
)
|
|
1463
1515
|
mutable_state = await self._self_actx.__aenter__()
|
|
1464
1516
|
super().__setattr__(
|
|
@@ -1655,6 +1707,8 @@ class StateManagerMemory(StateManager):
|
|
|
1655
1707
|
Returns:
|
|
1656
1708
|
The state for the token.
|
|
1657
1709
|
"""
|
|
1710
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
1711
|
+
token = token.partition("_")[0]
|
|
1658
1712
|
if token not in self.states:
|
|
1659
1713
|
self.states[token] = self.state()
|
|
1660
1714
|
return self.states[token]
|
|
@@ -1678,6 +1732,8 @@ class StateManagerMemory(StateManager):
|
|
|
1678
1732
|
Yields:
|
|
1679
1733
|
The state for the token.
|
|
1680
1734
|
"""
|
|
1735
|
+
# Memory state manager ignores the substate suffix and always returns the top-level state.
|
|
1736
|
+
token = token.partition("_")[0]
|
|
1681
1737
|
if token not in self._states_locks:
|
|
1682
1738
|
async with self._state_manager_lock:
|
|
1683
1739
|
if token not in self._states_locks:
|
|
@@ -1717,23 +1773,104 @@ class StateManagerRedis(StateManager):
|
|
|
1717
1773
|
b"evicted",
|
|
1718
1774
|
}
|
|
1719
1775
|
|
|
1720
|
-
async def get_state(
|
|
1776
|
+
async def get_state(
|
|
1777
|
+
self,
|
|
1778
|
+
token: str,
|
|
1779
|
+
top_level: bool = True,
|
|
1780
|
+
get_substates: bool = True,
|
|
1781
|
+
parent_state: BaseState | None = None,
|
|
1782
|
+
) -> BaseState:
|
|
1721
1783
|
"""Get the state for a token.
|
|
1722
1784
|
|
|
1723
1785
|
Args:
|
|
1724
1786
|
token: The token to get the state for.
|
|
1787
|
+
top_level: If true, return an instance of the top-level state.
|
|
1788
|
+
get_substates: If true, also retrieve substates
|
|
1789
|
+
parent_state: If provided, use this parent_state instead of getting it from redis.
|
|
1725
1790
|
|
|
1726
1791
|
Returns:
|
|
1727
1792
|
The state for the token.
|
|
1793
|
+
|
|
1794
|
+
Raises:
|
|
1795
|
+
RuntimeError: when the state_cls is not specified in the token
|
|
1728
1796
|
"""
|
|
1797
|
+
# Split the actual token from the fully qualified substate name.
|
|
1798
|
+
client_token, _, state_path = token.partition("_")
|
|
1799
|
+
if state_path:
|
|
1800
|
+
# Get the State class associated with the given path.
|
|
1801
|
+
state_cls = self.state.get_class_substate(tuple(state_path.split(".")))
|
|
1802
|
+
else:
|
|
1803
|
+
raise RuntimeError(
|
|
1804
|
+
"StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}"
|
|
1805
|
+
)
|
|
1806
|
+
|
|
1807
|
+
# Fetch the serialized substate from redis.
|
|
1729
1808
|
redis_state = await self.redis.get(token)
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1809
|
+
|
|
1810
|
+
if redis_state is not None:
|
|
1811
|
+
# Deserialize the substate.
|
|
1812
|
+
state = cloudpickle.loads(redis_state)
|
|
1813
|
+
|
|
1814
|
+
# Populate parent and substates if requested.
|
|
1815
|
+
if parent_state is None:
|
|
1816
|
+
# Retrieve the parent state from redis.
|
|
1817
|
+
parent_state_name = state_path.rpartition(".")[0]
|
|
1818
|
+
if parent_state_name:
|
|
1819
|
+
parent_state_key = token.rpartition(".")[0]
|
|
1820
|
+
parent_state = await self.get_state(
|
|
1821
|
+
parent_state_key, top_level=False, get_substates=False
|
|
1822
|
+
)
|
|
1823
|
+
# Set up Bidirectional linkage between this state and its parent.
|
|
1824
|
+
if parent_state is not None:
|
|
1825
|
+
parent_state.substates[state.get_name()] = state
|
|
1826
|
+
state.parent_state = parent_state
|
|
1827
|
+
if get_substates:
|
|
1828
|
+
# Retrieve all substates from redis.
|
|
1829
|
+
for substate_cls in state_cls.get_substates():
|
|
1830
|
+
substate_name = substate_cls.get_name()
|
|
1831
|
+
substate_key = token + "." + substate_name
|
|
1832
|
+
state.substates[substate_name] = await self.get_state(
|
|
1833
|
+
substate_key, top_level=False, parent_state=state
|
|
1834
|
+
)
|
|
1835
|
+
# To retain compatibility with previous implementation, by default, we return
|
|
1836
|
+
# the top-level state by chasing `parent_state` pointers up the tree.
|
|
1837
|
+
if top_level:
|
|
1838
|
+
while type(state) != self.state and state.parent_state is not None:
|
|
1839
|
+
state = state.parent_state
|
|
1840
|
+
return state
|
|
1841
|
+
|
|
1842
|
+
# Key didn't exist so we have to create a new entry for this token.
|
|
1843
|
+
if parent_state is None:
|
|
1844
|
+
parent_state_name = state_path.rpartition(".")[0]
|
|
1845
|
+
if parent_state_name:
|
|
1846
|
+
# Retrieve the parent state to populate event handlers onto this substate.
|
|
1847
|
+
parent_state_key = client_token + "_" + parent_state_name
|
|
1848
|
+
parent_state = await self.get_state(
|
|
1849
|
+
parent_state_key, top_level=False, get_substates=False
|
|
1850
|
+
)
|
|
1851
|
+
# Persist the new state class to redis.
|
|
1852
|
+
await self.set_state(
|
|
1853
|
+
token,
|
|
1854
|
+
state_cls(
|
|
1855
|
+
parent_state=parent_state,
|
|
1856
|
+
init_substates=False,
|
|
1857
|
+
),
|
|
1858
|
+
)
|
|
1859
|
+
# After creating the state key, recursively call `get_state` to populate substates.
|
|
1860
|
+
return await self.get_state(
|
|
1861
|
+
token,
|
|
1862
|
+
top_level=top_level,
|
|
1863
|
+
get_substates=get_substates,
|
|
1864
|
+
parent_state=parent_state,
|
|
1865
|
+
)
|
|
1734
1866
|
|
|
1735
1867
|
async def set_state(
|
|
1736
|
-
self,
|
|
1868
|
+
self,
|
|
1869
|
+
token: str,
|
|
1870
|
+
state: BaseState,
|
|
1871
|
+
lock_id: bytes | None = None,
|
|
1872
|
+
set_substates: bool = True,
|
|
1873
|
+
set_parent_state: bool = True,
|
|
1737
1874
|
):
|
|
1738
1875
|
"""Set the state for a token.
|
|
1739
1876
|
|
|
@@ -1741,11 +1878,13 @@ class StateManagerRedis(StateManager):
|
|
|
1741
1878
|
token: The token to set the state for.
|
|
1742
1879
|
state: The state to set.
|
|
1743
1880
|
lock_id: If provided, the lock_key must be set to this value to set the state.
|
|
1881
|
+
set_substates: If True, write substates to redis
|
|
1882
|
+
set_parent_state: If True, write parent state to redis
|
|
1744
1883
|
|
|
1745
1884
|
Raises:
|
|
1746
1885
|
LockExpiredError: If lock_id is provided and the lock for the token is not held by that ID.
|
|
1747
1886
|
"""
|
|
1748
|
-
#
|
|
1887
|
+
# Check that we're holding the lock.
|
|
1749
1888
|
if (
|
|
1750
1889
|
lock_id is not None
|
|
1751
1890
|
and await self.redis.get(self._lock_key(token)) != lock_id
|
|
@@ -1755,6 +1894,27 @@ class StateManagerRedis(StateManager):
|
|
|
1755
1894
|
f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) "
|
|
1756
1895
|
"or use `@rx.background` decorator for long-running tasks."
|
|
1757
1896
|
)
|
|
1897
|
+
# Find the substate associated with the token.
|
|
1898
|
+
state_path = token.partition("_")[2]
|
|
1899
|
+
if state_path and state.get_full_name() != state_path:
|
|
1900
|
+
state = state.get_substate(tuple(state_path.split(".")))
|
|
1901
|
+
# Persist the parent state separately, if requested.
|
|
1902
|
+
if state.parent_state is not None and set_parent_state:
|
|
1903
|
+
parent_state_key = token.rpartition(".")[0]
|
|
1904
|
+
await self.set_state(
|
|
1905
|
+
parent_state_key,
|
|
1906
|
+
state.parent_state,
|
|
1907
|
+
lock_id=lock_id,
|
|
1908
|
+
set_substates=False,
|
|
1909
|
+
)
|
|
1910
|
+
# Persist the substates separately, if requested.
|
|
1911
|
+
if set_substates:
|
|
1912
|
+
for substate_name, substate in state.substates.items():
|
|
1913
|
+
substate_key = token + "." + substate_name
|
|
1914
|
+
await self.set_state(
|
|
1915
|
+
substate_key, substate, lock_id=lock_id, set_parent_state=False
|
|
1916
|
+
)
|
|
1917
|
+
# Persist only the given state (parents or substates are excluded by BaseState.__getstate__).
|
|
1758
1918
|
await self.redis.set(token, cloudpickle.dumps(state), ex=self.token_expiration)
|
|
1759
1919
|
|
|
1760
1920
|
@contextlib.asynccontextmanager
|
|
@@ -1782,7 +1942,9 @@ class StateManagerRedis(StateManager):
|
|
|
1782
1942
|
Returns:
|
|
1783
1943
|
The redis lock key for the token.
|
|
1784
1944
|
"""
|
|
1785
|
-
|
|
1945
|
+
# All substates share the same lock domain, so ignore any substate path suffix.
|
|
1946
|
+
client_token = token.partition("_")[0]
|
|
1947
|
+
return f"{client_token}_lock".encode()
|
|
1786
1948
|
|
|
1787
1949
|
async def _try_get_lock(self, lock_key: bytes, lock_id: bytes) -> bool | None:
|
|
1788
1950
|
"""Try to get a redis lock for a token.
|
|
@@ -1949,6 +2111,7 @@ class LocalStorage(ClientStorageBase, str):
|
|
|
1949
2111
|
"""Represents a state Var that is stored in localStorage in the browser."""
|
|
1950
2112
|
|
|
1951
2113
|
name: str | None
|
|
2114
|
+
sync: bool = False
|
|
1952
2115
|
|
|
1953
2116
|
def __new__(
|
|
1954
2117
|
cls,
|
|
@@ -1957,6 +2120,7 @@ class LocalStorage(ClientStorageBase, str):
|
|
|
1957
2120
|
errors: str | None = None,
|
|
1958
2121
|
/,
|
|
1959
2122
|
name: str | None = None,
|
|
2123
|
+
sync: bool = False,
|
|
1960
2124
|
) -> "LocalStorage":
|
|
1961
2125
|
"""Create a client-side localStorage (str).
|
|
1962
2126
|
|
|
@@ -1965,6 +2129,7 @@ class LocalStorage(ClientStorageBase, str):
|
|
|
1965
2129
|
encoding: The encoding to use.
|
|
1966
2130
|
errors: The error handling scheme to use.
|
|
1967
2131
|
name: The name of the storage key on the client side.
|
|
2132
|
+
sync: Whether changes should be propagated to other tabs.
|
|
1968
2133
|
|
|
1969
2134
|
Returns:
|
|
1970
2135
|
The client-side localStorage object.
|
|
@@ -1974,6 +2139,7 @@ class LocalStorage(ClientStorageBase, str):
|
|
|
1974
2139
|
else:
|
|
1975
2140
|
inst = super().__new__(cls, object)
|
|
1976
2141
|
inst.name = name
|
|
2142
|
+
inst.sync = sync
|
|
1977
2143
|
return inst
|
|
1978
2144
|
|
|
1979
2145
|
|
|
@@ -2197,6 +2363,21 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
2197
2363
|
"""
|
|
2198
2364
|
return copy.deepcopy(self.__wrapped__, memo=memo)
|
|
2199
2365
|
|
|
2366
|
+
def __reduce_ex__(self, protocol_version):
|
|
2367
|
+
"""Get the state for redis serialization.
|
|
2368
|
+
|
|
2369
|
+
This method is called by cloudpickle to serialize the object.
|
|
2370
|
+
|
|
2371
|
+
It explicitly serializes the wrapped object, stripping off the mutable proxy.
|
|
2372
|
+
|
|
2373
|
+
Args:
|
|
2374
|
+
protocol_version: The protocol version.
|
|
2375
|
+
|
|
2376
|
+
Returns:
|
|
2377
|
+
Tuple of (wrapped class, empty args, class __getstate__)
|
|
2378
|
+
"""
|
|
2379
|
+
return self.__wrapped__.__reduce_ex__(protocol_version)
|
|
2380
|
+
|
|
2200
2381
|
|
|
2201
2382
|
@serializer
|
|
2202
2383
|
def serialize_mutable_proxy(mp: MutableProxy) -> SerializedType:
|
reflex/testing.py
CHANGED
|
@@ -220,6 +220,7 @@ class AppHarness:
|
|
|
220
220
|
reflex.config.get_config(reload=True)
|
|
221
221
|
# reset rx.State subclasses
|
|
222
222
|
State.class_subclasses.clear()
|
|
223
|
+
State.get_class_substate.cache_clear()
|
|
223
224
|
# Ensure the AppHarness test does not skip State assignment due to running via pytest
|
|
224
225
|
os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None)
|
|
225
226
|
# self.app_module.app.
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -24,6 +24,7 @@ import pkg_resources
|
|
|
24
24
|
import typer
|
|
25
25
|
from alembic.util.exc import CommandError
|
|
26
26
|
from packaging import version
|
|
27
|
+
from redis import Redis as RedisSync
|
|
27
28
|
from redis.asyncio import Redis
|
|
28
29
|
|
|
29
30
|
import reflex
|
|
@@ -189,16 +190,42 @@ def get_compiled_app(reload: bool = False) -> ModuleType:
|
|
|
189
190
|
|
|
190
191
|
|
|
191
192
|
def get_redis() -> Redis | None:
|
|
192
|
-
"""Get the redis client.
|
|
193
|
+
"""Get the asynchronous redis client.
|
|
193
194
|
|
|
194
195
|
Returns:
|
|
195
|
-
The redis client.
|
|
196
|
+
The asynchronous redis client.
|
|
197
|
+
"""
|
|
198
|
+
if isinstance((redis_url_or_options := parse_redis_url()), str):
|
|
199
|
+
return Redis.from_url(redis_url_or_options)
|
|
200
|
+
elif isinstance(redis_url_or_options, dict):
|
|
201
|
+
return Redis(**redis_url_or_options)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_redis_sync() -> RedisSync | None:
|
|
206
|
+
"""Get the synchronous redis client.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The synchronous redis client.
|
|
210
|
+
"""
|
|
211
|
+
if isinstance((redis_url_or_options := parse_redis_url()), str):
|
|
212
|
+
return RedisSync.from_url(redis_url_or_options)
|
|
213
|
+
elif isinstance(redis_url_or_options, dict):
|
|
214
|
+
return RedisSync(**redis_url_or_options)
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_redis_url() -> str | dict | None:
|
|
219
|
+
"""Parse the REDIS_URL in config if applicable.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
If redis-py syntax, return the URL as it is. Otherwise, return the host/port/db as a dict.
|
|
196
223
|
"""
|
|
197
224
|
config = get_config()
|
|
198
225
|
if not config.redis_url:
|
|
199
226
|
return None
|
|
200
227
|
if config.redis_url.startswith(("redis://", "rediss://", "unix://")):
|
|
201
|
-
return
|
|
228
|
+
return config.redis_url
|
|
202
229
|
console.deprecate(
|
|
203
230
|
feature_name="host[:port] style redis urls",
|
|
204
231
|
reason="redis-py url syntax is now being used",
|
|
@@ -209,7 +236,7 @@ def get_redis() -> Redis | None:
|
|
|
209
236
|
if not has_port:
|
|
210
237
|
redis_port = 6379
|
|
211
238
|
console.info(f"Using redis at {config.redis_url}")
|
|
212
|
-
return
|
|
239
|
+
return dict(host=redis_url, port=int(redis_port), db=0)
|
|
213
240
|
|
|
214
241
|
|
|
215
242
|
def get_production_backend_url() -> str:
|
reflex/utils/processes.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Callable, Generator, List, Optional, Tuple, Union
|
|
|
12
12
|
|
|
13
13
|
import psutil
|
|
14
14
|
import typer
|
|
15
|
+
from redis.exceptions import RedisError
|
|
15
16
|
|
|
16
17
|
from reflex.utils import console, path_ops, prerequisites
|
|
17
18
|
|
|
@@ -28,10 +29,20 @@ def kill(pid):
|
|
|
28
29
|
def get_num_workers() -> int:
|
|
29
30
|
"""Get the number of backend worker processes.
|
|
30
31
|
|
|
32
|
+
Raises:
|
|
33
|
+
Exit: If unable to connect to Redis.
|
|
34
|
+
|
|
31
35
|
Returns:
|
|
32
36
|
The number of backend worker processes.
|
|
33
37
|
"""
|
|
34
|
-
|
|
38
|
+
if (redis_client := prerequisites.get_redis_sync()) is None:
|
|
39
|
+
return 1
|
|
40
|
+
try:
|
|
41
|
+
redis_client.ping()
|
|
42
|
+
except RedisError as re:
|
|
43
|
+
console.error(f"Unable to connect to Redis: {re}")
|
|
44
|
+
raise typer.Exit(1) from re
|
|
45
|
+
return (os.cpu_count() or 1) * 2 + 1
|
|
35
46
|
|
|
36
47
|
|
|
37
48
|
def get_process_on_port(port) -> Optional[psutil.Process]:
|
reflex/utils/serializers.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import types as builtin_types
|
|
7
|
+
import warnings
|
|
7
8
|
from datetime import date, datetime, time, timedelta
|
|
8
9
|
from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union, get_type_hints
|
|
9
10
|
|
|
@@ -303,6 +304,7 @@ try:
|
|
|
303
304
|
import base64
|
|
304
305
|
import io
|
|
305
306
|
|
|
307
|
+
from PIL.Image import MIME
|
|
306
308
|
from PIL.Image import Image as Img
|
|
307
309
|
|
|
308
310
|
@serializer
|
|
@@ -316,10 +318,24 @@ try:
|
|
|
316
318
|
The serialized image.
|
|
317
319
|
"""
|
|
318
320
|
buff = io.BytesIO()
|
|
319
|
-
|
|
321
|
+
image_format = getattr(image, "format", None) or "PNG"
|
|
322
|
+
image.save(buff, format=image_format)
|
|
320
323
|
image_bytes = buff.getvalue()
|
|
321
324
|
base64_image = base64.b64encode(image_bytes).decode("utf-8")
|
|
322
|
-
|
|
325
|
+
try:
|
|
326
|
+
# Newer method to get the mime type, but does not always work.
|
|
327
|
+
mime_type = image.get_format_mimetype() # type: ignore
|
|
328
|
+
except AttributeError:
|
|
329
|
+
try:
|
|
330
|
+
# Fallback method
|
|
331
|
+
mime_type = MIME[image_format]
|
|
332
|
+
except KeyError:
|
|
333
|
+
# Unknown mime_type: warn and return image/png and hope the browser can sort it out.
|
|
334
|
+
warnings.warn(
|
|
335
|
+
f"Unknown mime type for {image} {image_format}. Defaulting to image/png"
|
|
336
|
+
)
|
|
337
|
+
mime_type = "image/png"
|
|
338
|
+
|
|
323
339
|
return f"data:{mime_type};base64,{base64_image}"
|
|
324
340
|
|
|
325
341
|
except ImportError:
|
reflex/utils/types.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import (
|
|
|
10
10
|
Any,
|
|
11
11
|
Callable,
|
|
12
12
|
Iterable,
|
|
13
|
+
List,
|
|
13
14
|
Literal,
|
|
14
15
|
Optional,
|
|
15
16
|
Type,
|
|
@@ -164,7 +165,11 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
|
|
|
164
165
|
prop = descriptor.property
|
|
165
166
|
if not isinstance(prop, Relationship):
|
|
166
167
|
return None
|
|
167
|
-
|
|
168
|
+
class_ = prop.mapper.class_
|
|
169
|
+
if prop.uselist:
|
|
170
|
+
return List[class_]
|
|
171
|
+
else:
|
|
172
|
+
return class_
|
|
168
173
|
elif isinstance(cls, type) and issubclass(cls, Model):
|
|
169
174
|
# Check in the annotations directly (for sqlmodel.Relationship)
|
|
170
175
|
hints = get_type_hints(cls)
|
reflex/vars.py
CHANGED
|
@@ -623,7 +623,9 @@ class Var:
|
|
|
623
623
|
|
|
624
624
|
# Get the type of the indexed var.
|
|
625
625
|
if types.is_generic_alias(self._var_type):
|
|
626
|
-
|
|
626
|
+
index = i if not isinstance(i, Var) else 0
|
|
627
|
+
type_ = types.get_args(self._var_type)
|
|
628
|
+
type_ = type_[index % len(type_)]
|
|
627
629
|
elif types._issubclass(self._var_type, str):
|
|
628
630
|
type_ = str
|
|
629
631
|
|
|
@@ -2003,3 +2005,20 @@ class CallableVar(BaseVar):
|
|
|
2003
2005
|
The Var returned from calling the function.
|
|
2004
2006
|
"""
|
|
2005
2007
|
return self.fn(*args, **kwargs)
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def get_uuid_string_var() -> Var:
|
|
2011
|
+
"""Return a var that generates UUIDs via .web/utils/state.js.
|
|
2012
|
+
|
|
2013
|
+
Returns:
|
|
2014
|
+
the var to generate UUIDs at runtime.
|
|
2015
|
+
"""
|
|
2016
|
+
from reflex.utils.imports import ImportVar
|
|
2017
|
+
|
|
2018
|
+
unique_uuid_var_data = VarData(
|
|
2019
|
+
imports={f"/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}} # type: ignore
|
|
2020
|
+
)
|
|
2021
|
+
|
|
2022
|
+
return BaseVar(
|
|
2023
|
+
_var_name="generateUUID()", _var_type=str, _var_data=unique_uuid_var_data
|
|
2024
|
+
)
|
reflex/vars.pyi
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: reflex
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2a1
|
|
4
4
|
Summary: Web apps in pure Python.
|
|
5
5
|
Home-page: https://reflex.dev
|
|
6
6
|
License: Apache-2.0
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
19
|
Requires-Dist: alembic (>=1.11.1,<2.0.0)
|
|
19
20
|
Requires-Dist: charset-normalizer (>=3.3.2,<4.0.0)
|
|
20
21
|
Requires-Dist: cloudpickle (>=2.2.1,<3.0.0)
|