reflex 0.8.14a2__py3-none-any.whl → 0.8.15__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/web/utils/state.js +68 -8
- reflex/__init__.py +12 -7
- reflex/__init__.pyi +11 -3
- reflex/app.py +10 -7
- reflex/base.py +58 -33
- reflex/components/datadisplay/dataeditor.py +17 -2
- reflex/components/datadisplay/dataeditor.pyi +6 -2
- reflex/components/field.py +3 -1
- reflex/components/lucide/icon.py +2 -1
- reflex/components/lucide/icon.pyi +2 -1
- reflex/components/markdown/markdown.py +101 -27
- reflex/components/sonner/toast.py +3 -2
- reflex/components/sonner/toast.pyi +3 -2
- reflex/constants/base.py +5 -0
- reflex/constants/installer.py +3 -3
- reflex/environment.py +9 -1
- reflex/event.py +3 -0
- reflex/experimental/client_state.py +1 -1
- 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/istate/proxy.py +35 -24
- reflex/model.py +534 -511
- reflex/plugins/tailwind_v4.py +2 -2
- reflex/reflex.py +16 -10
- reflex/state.py +35 -34
- reflex/testing.py +12 -14
- reflex/utils/build.py +11 -1
- reflex/utils/codespaces.py +30 -1
- reflex/utils/compat.py +51 -48
- reflex/utils/misc.py +2 -1
- reflex/utils/monitoring.py +1 -2
- reflex/utils/prerequisites.py +19 -4
- reflex/utils/processes.py +3 -1
- reflex/utils/redir.py +21 -37
- reflex/utils/serializers.py +21 -20
- reflex/utils/telemetry.py +0 -2
- reflex/utils/templates.py +4 -4
- reflex/utils/types.py +89 -90
- reflex/vars/base.py +108 -41
- 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 +51 -7
- reflex/vars/sequence.py +32 -1
- {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/METADATA +8 -3
- {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/RECORD +52 -49
- {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/WHEEL +0 -0
- {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.14a2.dist-info → reflex-0.8.15.dist-info}/licenses/LICENSE +0 -0
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.14"
|
|
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.14",
|
|
160
160
|
]
|
|
161
161
|
|
|
162
162
|
def pre_compile(self, **context):
|
reflex/reflex.py
CHANGED
|
@@ -11,12 +11,14 @@ from reflex_cli.v2.deployments import hosting_cli
|
|
|
11
11
|
|
|
12
12
|
from reflex import constants
|
|
13
13
|
from reflex.config import get_config
|
|
14
|
-
from reflex.constants.base import LITERAL_ENV
|
|
15
14
|
from reflex.custom_components.custom_components import custom_components_cli
|
|
16
15
|
from reflex.environment import environment
|
|
17
|
-
from reflex.
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
from reflex.utils import console
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from reflex_cli.constants.base import LogLevel as HostingLogLevel
|
|
20
|
+
|
|
21
|
+
from reflex.constants.base import LITERAL_ENV
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def set_loglevel(ctx: click.Context, self: click.Parameter, value: str | None):
|
|
@@ -40,6 +42,8 @@ def cli():
|
|
|
40
42
|
|
|
41
43
|
loglevel_option = click.option(
|
|
42
44
|
"--loglevel",
|
|
45
|
+
"--log-level",
|
|
46
|
+
"loglevel",
|
|
43
47
|
type=click.Choice(
|
|
44
48
|
[loglevel.value for loglevel in constants.LogLevel],
|
|
45
49
|
case_sensitive=False,
|
|
@@ -63,7 +67,9 @@ def _init(
|
|
|
63
67
|
exec.output_system_info()
|
|
64
68
|
|
|
65
69
|
if ai:
|
|
66
|
-
redir
|
|
70
|
+
from reflex.utils.redir import reflex_build_redirect
|
|
71
|
+
|
|
72
|
+
reflex_build_redirect()
|
|
67
73
|
return
|
|
68
74
|
|
|
69
75
|
# Validate the app name.
|
|
@@ -136,7 +142,9 @@ def _run(
|
|
|
136
142
|
"""Run the app in the given directory."""
|
|
137
143
|
import atexit
|
|
138
144
|
|
|
139
|
-
from reflex.
|
|
145
|
+
from reflex.state import reset_disk_state_manager
|
|
146
|
+
from reflex.utils import build, exec, prerequisites, processes, telemetry
|
|
147
|
+
from reflex.utils.exec import should_use_granian
|
|
140
148
|
|
|
141
149
|
config = get_config()
|
|
142
150
|
|
|
@@ -535,6 +543,8 @@ def login():
|
|
|
535
543
|
validated_info = hosting_cli.login()
|
|
536
544
|
if validated_info is not None:
|
|
537
545
|
_skip_compile() # Allow running outside of an app dir
|
|
546
|
+
from reflex.utils import telemetry
|
|
547
|
+
|
|
538
548
|
telemetry.send("login", user_uuid=validated_info.get("user_id"))
|
|
539
549
|
|
|
540
550
|
|
|
@@ -838,10 +848,6 @@ def rename(new_name: str):
|
|
|
838
848
|
rename_app(new_name, get_config().loglevel)
|
|
839
849
|
|
|
840
850
|
|
|
841
|
-
if TYPE_CHECKING:
|
|
842
|
-
from reflex_cli.constants.base import LogLevel as HostingLogLevel
|
|
843
|
-
|
|
844
|
-
|
|
845
851
|
def _convert_reflex_loglevel_to_reflex_cli_loglevel(
|
|
846
852
|
loglevel: constants.LogLevel,
|
|
847
853
|
) -> HostingLogLevel:
|
reflex/state.py
CHANGED
|
@@ -16,20 +16,17 @@ 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
|
|
21
|
+
from importlib.util import find_spec
|
|
20
22
|
from types import FunctionType
|
|
21
23
|
from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar, cast, get_type_hints
|
|
22
24
|
|
|
23
|
-
import pydantic.v1 as pydantic
|
|
24
|
-
from pydantic import BaseModel as BaseModelV2
|
|
25
|
-
from pydantic.v1 import BaseModel as BaseModelV1
|
|
26
|
-
from pydantic.v1.fields import ModelField
|
|
27
25
|
from rich.markup import escape
|
|
28
26
|
from typing_extensions import Self
|
|
29
27
|
|
|
30
28
|
import reflex.istate.dynamic
|
|
31
29
|
from reflex import constants, event
|
|
32
|
-
from reflex.base import Base
|
|
33
30
|
from reflex.constants.state import FIELD_MARKER
|
|
34
31
|
from reflex.environment import PerformanceMode, environment
|
|
35
32
|
from reflex.event import (
|
|
@@ -94,13 +91,11 @@ if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF:
|
|
|
94
91
|
VAR_TYPE = TypeVar("VAR_TYPE")
|
|
95
92
|
|
|
96
93
|
|
|
97
|
-
def _no_chain_background_task(
|
|
98
|
-
state_cls: type[BaseState], name: str, fn: Callable
|
|
99
|
-
) -> Callable:
|
|
94
|
+
def _no_chain_background_task(state: BaseState, name: str, fn: Callable) -> Callable:
|
|
100
95
|
"""Protect against directly chaining a background task from another event handler.
|
|
101
96
|
|
|
102
97
|
Args:
|
|
103
|
-
|
|
98
|
+
state: The state instance the background task is bound to.
|
|
104
99
|
name: The name of the background task.
|
|
105
100
|
fn: The background task coroutine function / generator.
|
|
106
101
|
|
|
@@ -110,7 +105,7 @@ def _no_chain_background_task(
|
|
|
110
105
|
Raises:
|
|
111
106
|
TypeError: If the background task is not async.
|
|
112
107
|
"""
|
|
113
|
-
call = f"{
|
|
108
|
+
call = f"{type(state).__name__}.{name}"
|
|
114
109
|
message = (
|
|
115
110
|
f"Cannot directly call background task {name!r}, use "
|
|
116
111
|
f"`yield {call}` or `return {call}` instead."
|
|
@@ -252,17 +247,13 @@ class EventHandlerSetVar(EventHandler):
|
|
|
252
247
|
msg = f"Variable `{args[0]}` cannot be set on `{self.state_cls.get_full_name()}`"
|
|
253
248
|
raise AttributeError(msg)
|
|
254
249
|
|
|
255
|
-
if
|
|
250
|
+
if inspect.iscoroutinefunction(handler.fn):
|
|
256
251
|
msg = f"Setter for {args[0]} is async, which is not supported."
|
|
257
252
|
raise NotImplementedError(msg)
|
|
258
253
|
|
|
259
254
|
return super().__call__(*args)
|
|
260
255
|
|
|
261
256
|
|
|
262
|
-
if TYPE_CHECKING:
|
|
263
|
-
from pydantic.v1.fields import ModelField
|
|
264
|
-
|
|
265
|
-
|
|
266
257
|
def get_var_for_field(cls: type[BaseState], name: str, f: Field) -> Var:
|
|
267
258
|
"""Get a Var instance for a state field.
|
|
268
259
|
|
|
@@ -297,7 +288,7 @@ async def _resolve_delta(delta: Delta) -> Delta:
|
|
|
297
288
|
tasks = {}
|
|
298
289
|
for state_name, state_delta in delta.items():
|
|
299
290
|
for var_name, value in state_delta.items():
|
|
300
|
-
if
|
|
291
|
+
if inspect.iscoroutine(value):
|
|
301
292
|
tasks[state_name, var_name] = asyncio.create_task(
|
|
302
293
|
value,
|
|
303
294
|
name=f"reflex_resolve_delta|{state_name}|{var_name}|{time.time()}",
|
|
@@ -479,7 +470,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
479
470
|
|
|
480
471
|
Args:
|
|
481
472
|
mixin: Whether the subclass is a mixin and should not be initialized.
|
|
482
|
-
**kwargs: The kwargs to pass to the
|
|
473
|
+
**kwargs: The kwargs to pass to the init_subclass method.
|
|
483
474
|
|
|
484
475
|
Raises:
|
|
485
476
|
StateValueError: If a substate class shadows another.
|
|
@@ -739,7 +730,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
739
730
|
mixin
|
|
740
731
|
for mixin in cls.__mro__
|
|
741
732
|
if (
|
|
742
|
-
mixin not
|
|
733
|
+
mixin is not cls
|
|
743
734
|
and issubclass(mixin, BaseState)
|
|
744
735
|
and mixin._mixin is True
|
|
745
736
|
)
|
|
@@ -862,7 +853,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
862
853
|
ComputedVarShadowsBaseVarsError: When a computed var shadows a base var.
|
|
863
854
|
"""
|
|
864
855
|
for name, computed_var_ in cls._get_computed_vars():
|
|
865
|
-
if name in cls
|
|
856
|
+
if name in get_type_hints(cls):
|
|
866
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"
|
|
867
858
|
raise ComputedVarShadowsBaseVarsError(msg)
|
|
868
859
|
|
|
@@ -1090,7 +1081,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1090
1081
|
_var_data=VarData.from_state(cls, name),
|
|
1091
1082
|
).guess_type()
|
|
1092
1083
|
|
|
1093
|
-
# add the
|
|
1084
|
+
# add the field dynamically (must be done before _init_var)
|
|
1094
1085
|
cls.add_field(name, var, default_value)
|
|
1095
1086
|
|
|
1096
1087
|
cls._init_var(name, var)
|
|
@@ -1188,7 +1179,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1188
1179
|
name: The name of the var.
|
|
1189
1180
|
prop: The var to set the default value for.
|
|
1190
1181
|
"""
|
|
1191
|
-
# Get the
|
|
1182
|
+
# Get the field for the var.
|
|
1192
1183
|
field = cls.get_fields()[name]
|
|
1193
1184
|
|
|
1194
1185
|
if field.default is None and not types.is_optional(prop._var_type):
|
|
@@ -1349,7 +1340,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1349
1340
|
if name in event_handlers:
|
|
1350
1341
|
handler = event_handlers[name]
|
|
1351
1342
|
if handler.is_background:
|
|
1352
|
-
fn = _no_chain_background_task(
|
|
1343
|
+
fn = _no_chain_background_task(self, name, handler.fn)
|
|
1353
1344
|
else:
|
|
1354
1345
|
fn = functools.partial(handler.fn, self)
|
|
1355
1346
|
fn.__module__ = handler.fn.__module__
|
|
@@ -1465,7 +1456,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1465
1456
|
|
|
1466
1457
|
@classmethod
|
|
1467
1458
|
@functools.lru_cache
|
|
1468
|
-
def _is_client_storage(cls, prop_name_or_field: str |
|
|
1459
|
+
def _is_client_storage(cls, prop_name_or_field: str | Field) -> bool:
|
|
1469
1460
|
"""Check if the var is a client storage var.
|
|
1470
1461
|
|
|
1471
1462
|
Args:
|
|
@@ -1564,6 +1555,8 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1564
1555
|
RuntimeError: If redis is not used in this backend process.
|
|
1565
1556
|
StateMismatchError: If the state instance is not of the expected type.
|
|
1566
1557
|
"""
|
|
1558
|
+
from reflex.istate.manager.redis import StateManagerRedis
|
|
1559
|
+
|
|
1567
1560
|
# Then get the target state and all its substates.
|
|
1568
1561
|
state_manager = get_state_manager()
|
|
1569
1562
|
if not isinstance(state_manager, StateManagerRedis):
|
|
@@ -1743,7 +1736,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1743
1736
|
except TypeError:
|
|
1744
1737
|
pass
|
|
1745
1738
|
|
|
1746
|
-
coroutines = [e for e in events if
|
|
1739
|
+
coroutines = [e for e in events if inspect.iscoroutine(e)]
|
|
1747
1740
|
|
|
1748
1741
|
for coroutine in coroutines:
|
|
1749
1742
|
coroutine_name = coroutine.__qualname__
|
|
@@ -1872,16 +1865,28 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1872
1865
|
if key in hinted_args.__fields__
|
|
1873
1866
|
}
|
|
1874
1867
|
)
|
|
1875
|
-
elif dataclasses.is_dataclass(hinted_args)
|
|
1876
|
-
hinted_args, (Base, BaseModelV1, BaseModelV2)
|
|
1877
|
-
):
|
|
1868
|
+
elif dataclasses.is_dataclass(hinted_args):
|
|
1878
1869
|
payload[arg] = hinted_args(**value)
|
|
1870
|
+
elif find_spec("pydantic"):
|
|
1871
|
+
from pydantic import BaseModel as BaseModelV2
|
|
1872
|
+
from pydantic.v1 import BaseModel as BaseModelV1
|
|
1873
|
+
|
|
1874
|
+
if issubclass(hinted_args, BaseModelV1):
|
|
1875
|
+
payload[arg] = hinted_args.parse_obj(value)
|
|
1876
|
+
elif issubclass(hinted_args, BaseModelV2):
|
|
1877
|
+
payload[arg] = hinted_args.model_validate(value)
|
|
1879
1878
|
elif isinstance(value, list) and (hinted_args is set or hinted_args is set):
|
|
1880
1879
|
payload[arg] = set(value)
|
|
1881
1880
|
elif isinstance(value, list) and (
|
|
1882
1881
|
hinted_args is tuple or hinted_args is tuple
|
|
1883
1882
|
):
|
|
1884
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
|
|
1885
1890
|
elif (
|
|
1886
1891
|
isinstance(value, str)
|
|
1887
1892
|
and (deserializer := _deserializers.get(hinted_args)) is not None
|
|
@@ -1899,7 +1904,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1899
1904
|
# Wrap the function in a try/except block.
|
|
1900
1905
|
try:
|
|
1901
1906
|
# Handle async functions.
|
|
1902
|
-
if
|
|
1907
|
+
if inspect.iscoroutinefunction(fn.func):
|
|
1903
1908
|
events = await fn(**payload)
|
|
1904
1909
|
|
|
1905
1910
|
# Handle regular functions.
|
|
@@ -2142,7 +2147,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
2142
2147
|
Args:
|
|
2143
2148
|
include_computed: Whether to include computed vars.
|
|
2144
2149
|
initial: Whether to get the initial value of computed vars.
|
|
2145
|
-
**kwargs: Kwargs to pass to the
|
|
2150
|
+
**kwargs: Kwargs to pass to the dict method.
|
|
2146
2151
|
|
|
2147
2152
|
Returns:
|
|
2148
2153
|
The object as a dictionary.
|
|
@@ -2626,7 +2631,7 @@ class ComponentState(State, mixin=True):
|
|
|
2626
2631
|
|
|
2627
2632
|
Args:
|
|
2628
2633
|
mixin: Whether the subclass is a mixin and should not be initialized.
|
|
2629
|
-
**kwargs: The kwargs to pass to the
|
|
2634
|
+
**kwargs: The kwargs to pass to the init_subclass method.
|
|
2630
2635
|
"""
|
|
2631
2636
|
super().__init_subclass__(mixin=mixin, **kwargs)
|
|
2632
2637
|
|
|
@@ -2742,11 +2747,7 @@ def reload_state_module(
|
|
|
2742
2747
|
state.get_class_substate.cache_clear()
|
|
2743
2748
|
|
|
2744
2749
|
|
|
2745
|
-
from reflex.istate.manager import LockExpiredError as LockExpiredError # noqa: E402
|
|
2746
2750
|
from reflex.istate.manager import StateManager as StateManager # noqa: E402
|
|
2747
|
-
from reflex.istate.manager import StateManagerDisk as StateManagerDisk # noqa: E402
|
|
2748
|
-
from reflex.istate.manager import StateManagerMemory as StateManagerMemory # noqa: E402
|
|
2749
|
-
from reflex.istate.manager import StateManagerRedis as StateManagerRedis # noqa: E402
|
|
2750
2751
|
from reflex.istate.manager import get_state_manager as get_state_manager # noqa: E402
|
|
2751
2752
|
from reflex.istate.manager import ( # noqa: E402
|
|
2752
2753
|
reset_disk_state_manager as reset_disk_state_manager,
|
reflex/testing.py
CHANGED
|
@@ -21,6 +21,7 @@ import time
|
|
|
21
21
|
import types
|
|
22
22
|
from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
|
|
23
23
|
from http.server import SimpleHTTPRequestHandler
|
|
24
|
+
from importlib.util import find_spec
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
26
27
|
|
|
@@ -37,14 +38,10 @@ import reflex.utils.processes
|
|
|
37
38
|
from reflex.components.component import CustomComponent
|
|
38
39
|
from reflex.config import get_config
|
|
39
40
|
from reflex.environment import environment
|
|
40
|
-
from reflex.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
StateManagerMemory,
|
|
45
|
-
StateManagerRedis,
|
|
46
|
-
reload_state_module,
|
|
47
|
-
)
|
|
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
|
|
48
45
|
from reflex.utils import console, js_runtimes
|
|
49
46
|
from reflex.utils.export import export
|
|
50
47
|
from reflex.utils.token_manager import TokenManager
|
|
@@ -320,12 +317,13 @@ class AppHarness:
|
|
|
320
317
|
await self.app_instance.sio.shutdown()
|
|
321
318
|
|
|
322
319
|
# sqlalchemy async engine shutdown handler
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
320
|
+
if find_spec("sqlmodel"):
|
|
321
|
+
try:
|
|
322
|
+
async_engine = reflex.model.get_async_engine(None)
|
|
323
|
+
except ValueError:
|
|
324
|
+
pass
|
|
325
|
+
else:
|
|
326
|
+
await async_engine.dispose()
|
|
329
327
|
|
|
330
328
|
await original_shutdown(*args, **kwargs)
|
|
331
329
|
|
reflex/utils/build.py
CHANGED
|
@@ -187,7 +187,11 @@ def _duplicate_index_html_to_parent_directory(directory: Path):
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
def build():
|
|
190
|
-
"""Build the app for deployment.
|
|
190
|
+
"""Build the app for deployment.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
SystemExit: If the build process fails.
|
|
194
|
+
"""
|
|
191
195
|
wdir = prerequisites.get_web_dir()
|
|
192
196
|
|
|
193
197
|
# Clean the static directory if it exists.
|
|
@@ -214,6 +218,12 @@ def build():
|
|
|
214
218
|
},
|
|
215
219
|
)
|
|
216
220
|
processes.show_progress("Creating Production Build", process, checkpoints)
|
|
221
|
+
process.wait()
|
|
222
|
+
if process.returncode != 0:
|
|
223
|
+
console.error(
|
|
224
|
+
"Failed to build the frontend. Please run with --loglevel debug for more information.",
|
|
225
|
+
)
|
|
226
|
+
raise SystemExit(1)
|
|
217
227
|
_duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC)
|
|
218
228
|
|
|
219
229
|
spa_fallback = wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK
|
reflex/utils/codespaces.py
CHANGED
|
@@ -26,11 +26,40 @@ def redirect_script() -> str:
|
|
|
26
26
|
const thisUrl = new URL(window.location.href);
|
|
27
27
|
const params = new URLSearchParams(thisUrl.search)
|
|
28
28
|
|
|
29
|
+
function sameHostnameDifferentPort(one, two) {{
|
|
30
|
+
const hostnameOne = one.hostname;
|
|
31
|
+
const hostnameTwo = two.hostname;
|
|
32
|
+
const partsOne = hostnameOne.split(".");
|
|
33
|
+
const partsTwo = hostnameTwo.split(".");
|
|
34
|
+
if (partsOne.length !== partsTwo.length) {{ return false; }}
|
|
35
|
+
for (let i = 1; i < partsOne.length; i++) {{
|
|
36
|
+
if (partsOne[i] !== partsTwo[i]) {{ return false; }}
|
|
37
|
+
}}
|
|
38
|
+
const uniqueNameOne = partsOne[0];
|
|
39
|
+
const uniqueNameTwo = partsTwo[0];
|
|
40
|
+
const uniqueNamePartsOne = uniqueNameOne.split("-");
|
|
41
|
+
const uniqueNamePartsTwo = uniqueNameTwo.split("-");
|
|
42
|
+
if (uniqueNamePartsOne.length !== uniqueNamePartsTwo.length) {{ return false; }}
|
|
43
|
+
for (let i = 0; i < uniqueNamePartsOne.length - 1; i++) {{
|
|
44
|
+
if (uniqueNamePartsOne[i] !== uniqueNamePartsTwo[i]) {{ return false; }}
|
|
45
|
+
}}
|
|
46
|
+
return true;
|
|
47
|
+
}}
|
|
48
|
+
|
|
29
49
|
function doRedirect(url) {{
|
|
30
50
|
if (!window.sessionStorage.getItem("authenticated_github_codespaces")) {{
|
|
31
51
|
const a = document.createElement("a");
|
|
32
52
|
if (params.has("redirect_to")) {{
|
|
33
|
-
|
|
53
|
+
const redirect_to = new URL(params.get("redirect_to"));
|
|
54
|
+
if (!sameHostnameDifferentPort(thisUrl, redirect_to)) {{
|
|
55
|
+
console.warn("Reflex: Not redirecting to different hostname");
|
|
56
|
+
return;
|
|
57
|
+
}}
|
|
58
|
+
if (!redirect_to.hostname.endsWith(".app.github.dev")) {{
|
|
59
|
+
console.warn("Reflex: Not redirecting to non .app.github.dev hostname");
|
|
60
|
+
return;
|
|
61
|
+
}}
|
|
62
|
+
a.href = redirect_to.href;
|
|
34
63
|
}} else if (!window.location.href.startsWith(url)) {{
|
|
35
64
|
a.href = url + `?redirect_to=${{window.location.href}}`
|
|
36
65
|
}} else {{
|
reflex/utils/compat.py
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"""Compatibility hacks and helpers."""
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
3
|
import sys
|
|
5
|
-
from
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from importlib.util import find_spec
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pydantic.fields import FieldInfo
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
async def windows_hot_reload_lifespan_hack():
|
|
@@ -29,63 +33,62 @@ async def windows_hot_reload_lifespan_hack():
|
|
|
29
33
|
pass
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
Returns:
|
|
43
|
+
The (forward-ref) annotations from the class namespace.
|
|
38
44
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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."""
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
sys.modules["pydantic.errors"] = pydantic.v1.errors # pyright: ignore [reportAttributeAccessIssue]
|
|
59
|
-
sys.modules["pydantic"] = pydantic.v1
|
|
60
|
-
yield
|
|
61
|
-
except (ImportError, AttributeError):
|
|
62
|
-
# pydantic v1 is already installed
|
|
63
|
-
yield
|
|
64
|
-
finally:
|
|
65
|
-
# Restore the original Pydantic module
|
|
66
|
-
for k, original in originals.items():
|
|
67
|
-
if k in sys.modules:
|
|
68
|
-
if original:
|
|
69
|
-
sys.modules[k] = original
|
|
70
|
-
else:
|
|
71
|
-
del sys.modules[k]
|
|
63
|
+
def __new__(mcs, name: str, bases: tuple, namespace: dict, **kwargs):
|
|
64
|
+
"""Resolve python3.14 style lazy annotations before passing off to pydantic v1.
|
|
72
65
|
|
|
66
|
+
Args:
|
|
67
|
+
name: The class name.
|
|
68
|
+
bases: The base classes.
|
|
69
|
+
namespace: The class namespace.
|
|
70
|
+
**kwargs: Additional keyword arguments.
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
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]
|
|
76
79
|
|
|
77
80
|
|
|
78
|
-
def sqlmodel_field_has_primary_key(
|
|
79
|
-
"""Determines if a field is a
|
|
81
|
+
def sqlmodel_field_has_primary_key(field_info: "FieldInfo") -> bool:
|
|
82
|
+
"""Determines if a field is a primary.
|
|
80
83
|
|
|
81
84
|
Args:
|
|
82
|
-
|
|
85
|
+
field_info: a rx.model field
|
|
83
86
|
|
|
84
87
|
Returns:
|
|
85
|
-
If
|
|
88
|
+
If field_info is a primary key (Bool)
|
|
86
89
|
"""
|
|
87
|
-
if getattr(
|
|
90
|
+
if getattr(field_info, "primary_key", None) is True:
|
|
88
91
|
return True
|
|
89
|
-
if getattr(
|
|
92
|
+
if getattr(field_info, "sa_column", None) is None:
|
|
90
93
|
return False
|
|
91
|
-
return bool(getattr(
|
|
94
|
+
return bool(getattr(field_info.sa_column, "primary_key", None)) # pyright: ignore[reportAttributeAccessIssue]
|
reflex/utils/misc.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
5
|
+
import inspect
|
|
5
6
|
import sys
|
|
6
7
|
import threading
|
|
7
8
|
from collections.abc import Callable
|
|
@@ -62,7 +63,7 @@ async def run_in_thread(func: Callable) -> Any:
|
|
|
62
63
|
Returns:
|
|
63
64
|
Any: The return value of the function.
|
|
64
65
|
"""
|
|
65
|
-
if
|
|
66
|
+
if inspect.iscoroutinefunction(func):
|
|
66
67
|
msg = "func must be a non-async function"
|
|
67
68
|
raise ValueError(msg)
|
|
68
69
|
return await asyncio.get_event_loop().run_in_executor(None, func)
|
reflex/utils/monitoring.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import contextlib
|
|
5
4
|
import functools
|
|
6
5
|
import inspect
|
|
@@ -154,7 +153,7 @@ def monitor_loopblocks(func: Callable) -> Callable:
|
|
|
154
153
|
|
|
155
154
|
return async_gen_wrapper
|
|
156
155
|
|
|
157
|
-
if
|
|
156
|
+
if inspect.iscoroutinefunction(func):
|
|
158
157
|
|
|
159
158
|
@functools.wraps(func)
|
|
160
159
|
async def async_wrapper(*args, **kwargs):
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -15,11 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from types import ModuleType
|
|
16
16
|
from typing import NamedTuple
|
|
17
17
|
|
|
18
|
-
from alembic.util.exc import CommandError
|
|
19
18
|
from packaging import version
|
|
20
|
-
from redis import Redis as RedisSync
|
|
21
|
-
from redis.asyncio import Redis
|
|
22
|
-
from redis.exceptions import RedisError
|
|
23
19
|
|
|
24
20
|
from reflex import constants, model
|
|
25
21
|
from reflex.config import Config, get_config
|
|
@@ -29,6 +25,9 @@ from reflex.utils.decorator import once
|
|
|
29
25
|
from reflex.utils.misc import get_module_path
|
|
30
26
|
|
|
31
27
|
if typing.TYPE_CHECKING:
|
|
28
|
+
from redis import Redis as RedisSync
|
|
29
|
+
from redis.asyncio import Redis
|
|
30
|
+
|
|
32
31
|
from reflex.app import App
|
|
33
32
|
|
|
34
33
|
|
|
@@ -370,6 +369,12 @@ def get_redis() -> Redis | None:
|
|
|
370
369
|
Returns:
|
|
371
370
|
The asynchronous redis client.
|
|
372
371
|
"""
|
|
372
|
+
try:
|
|
373
|
+
from redis.asyncio import Redis
|
|
374
|
+
from redis.exceptions import RedisError
|
|
375
|
+
except ImportError:
|
|
376
|
+
console.debug("Redis package not installed.")
|
|
377
|
+
return None
|
|
373
378
|
if (redis_url := parse_redis_url()) is not None:
|
|
374
379
|
return Redis.from_url(
|
|
375
380
|
redis_url,
|
|
@@ -384,6 +389,12 @@ def get_redis_sync() -> RedisSync | None:
|
|
|
384
389
|
Returns:
|
|
385
390
|
The synchronous redis client.
|
|
386
391
|
"""
|
|
392
|
+
try:
|
|
393
|
+
from redis import Redis as RedisSync
|
|
394
|
+
from redis.exceptions import RedisError
|
|
395
|
+
except ImportError:
|
|
396
|
+
console.debug("Redis package not installed.")
|
|
397
|
+
return None
|
|
387
398
|
if (redis_url := parse_redis_url()) is not None:
|
|
388
399
|
return RedisSync.from_url(
|
|
389
400
|
redis_url,
|
|
@@ -418,6 +429,8 @@ async def get_redis_status() -> dict[str, bool | None]:
|
|
|
418
429
|
Returns:
|
|
419
430
|
The status of the Redis connection.
|
|
420
431
|
"""
|
|
432
|
+
from redis.exceptions import RedisError
|
|
433
|
+
|
|
421
434
|
try:
|
|
422
435
|
status = True
|
|
423
436
|
redis_client = get_redis()
|
|
@@ -639,6 +652,8 @@ def check_schema_up_to_date():
|
|
|
639
652
|
if get_config().db_url is None or not environment.ALEMBIC_CONFIG.get().exists():
|
|
640
653
|
return
|
|
641
654
|
with model.Model.get_db_engine().connect() as connection:
|
|
655
|
+
from alembic.util.exc import CommandError
|
|
656
|
+
|
|
642
657
|
try:
|
|
643
658
|
if model.Model.alembic_autogenerate(
|
|
644
659
|
connection=connection,
|
reflex/utils/processes.py
CHANGED
|
@@ -16,7 +16,6 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Any, Literal, overload
|
|
17
17
|
|
|
18
18
|
import rich.markup
|
|
19
|
-
from redis.exceptions import RedisError
|
|
20
19
|
from rich.progress import Progress
|
|
21
20
|
|
|
22
21
|
from reflex import constants
|
|
@@ -45,6 +44,9 @@ def get_num_workers() -> int:
|
|
|
45
44
|
"""
|
|
46
45
|
if (redis_client := prerequisites.get_redis_sync()) is None:
|
|
47
46
|
return 1
|
|
47
|
+
|
|
48
|
+
from redis.exceptions import RedisError
|
|
49
|
+
|
|
48
50
|
try:
|
|
49
51
|
redis_client.ping()
|
|
50
52
|
except RedisError as re:
|