reflex 0.6.7a2__py3-none-any.whl → 0.6.8a1__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 -4
- reflex/.templates/jinja/web/pages/custom_component.js.jinja2 +3 -4
- reflex/.templates/jinja/web/pages/index.js.jinja2 +2 -3
- reflex/.templates/jinja/web/pages/macros.js.jinja2 +38 -0
- reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -16
- reflex/.templates/jinja/web/utils/context.js.jinja2 +1 -1
- reflex/.templates/web/utils/state.js +9 -4
- reflex/app.py +18 -10
- reflex/compiler/compiler.py +2 -2
- reflex/compiler/templates.py +41 -0
- reflex/compiler/utils.py +1 -1
- reflex/components/base/bare.py +7 -3
- reflex/components/component.py +78 -116
- reflex/components/core/banner.py +1 -1
- reflex/components/core/breakpoints.py +1 -1
- reflex/components/datadisplay/code.py +14 -10
- reflex/components/datadisplay/dataeditor.py +1 -1
- reflex/components/datadisplay/dataeditor.pyi +1 -1
- reflex/components/el/elements/forms.py +7 -5
- reflex/components/el/elements/metadata.py +1 -1
- reflex/components/lucide/icon.py +142 -19
- reflex/components/lucide/icon.pyi +141 -18
- reflex/components/markdown/markdown.py +3 -2
- reflex/components/plotly/plotly.py +3 -3
- reflex/components/plotly/plotly.pyi +3 -3
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/themes/layout/center.pyi +1 -1
- reflex/components/radix/themes/layout/flex.py +1 -1
- reflex/components/radix/themes/layout/flex.pyi +1 -1
- reflex/components/radix/themes/layout/grid.py +1 -1
- reflex/components/radix/themes/layout/grid.pyi +1 -1
- reflex/components/radix/themes/layout/spacer.pyi +1 -1
- reflex/components/radix/themes/layout/stack.pyi +3 -3
- reflex/components/radix/themes/typography/link.py +1 -1
- reflex/components/recharts/cartesian.py +2 -2
- reflex/components/recharts/cartesian.pyi +6 -6
- reflex/components/recharts/charts.py +2 -2
- reflex/components/recharts/polar.py +2 -2
- reflex/components/recharts/polar.pyi +2 -2
- reflex/components/sonner/toast.py +1 -1
- reflex/config.py +5 -0
- reflex/constants/base.py +1 -1
- reflex/constants/compiler.py +1 -0
- reflex/event.py +91 -1
- reflex/experimental/client_state.py +42 -20
- reflex/istate/data.py +3 -3
- reflex/model.py +4 -5
- reflex/page.py +1 -1
- reflex/proxy.py +119 -0
- reflex/reflex.py +8 -1
- reflex/state.py +116 -9
- reflex/testing.py +19 -8
- reflex/utils/console.py +6 -1
- reflex/utils/exceptions.py +4 -0
- reflex/utils/prerequisites.py +37 -20
- reflex/utils/processes.py +2 -2
- reflex/utils/pyi_generator.py +2 -2
- reflex/utils/telemetry.py +6 -4
- reflex/vars/base.py +14 -3
- reflex/vars/sequence.py +37 -0
- {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/METADATA +4 -2
- {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/RECORD +65 -63
- {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/LICENSE +0 -0
- {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/WHEEL +0 -0
- {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/entry_points.txt +0 -0
reflex/proxy.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Handle proxying frontend requests from the backend server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import Any, AsyncGenerator
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
12
|
+
|
|
13
|
+
from .config import get_config
|
|
14
|
+
from .utils import console
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import aiohttp
|
|
18
|
+
from asgiproxy.config import BaseURLProxyConfigMixin, ProxyConfig
|
|
19
|
+
from asgiproxy.context import ProxyContext
|
|
20
|
+
from asgiproxy.proxies.http import proxy_http
|
|
21
|
+
from asgiproxy.simple_proxy import make_simple_proxy_app
|
|
22
|
+
except ImportError:
|
|
23
|
+
|
|
24
|
+
@asynccontextmanager
|
|
25
|
+
async def proxy_middleware(*args, **kwargs) -> AsyncGenerator[None, None]:
|
|
26
|
+
"""A no-op proxy middleware for when asgiproxy is not installed.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
*args: The positional arguments.
|
|
30
|
+
**kwargs: The keyword arguments.
|
|
31
|
+
|
|
32
|
+
Yields:
|
|
33
|
+
None
|
|
34
|
+
"""
|
|
35
|
+
yield
|
|
36
|
+
else:
|
|
37
|
+
MAX_PROXY_RETRY = 25
|
|
38
|
+
|
|
39
|
+
async def proxy_http_with_retry(
|
|
40
|
+
*,
|
|
41
|
+
context: ProxyContext,
|
|
42
|
+
scope: Scope,
|
|
43
|
+
receive: Receive,
|
|
44
|
+
send: Send,
|
|
45
|
+
) -> Any:
|
|
46
|
+
"""Proxy an HTTP request with retries.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
context: The proxy context.
|
|
50
|
+
scope: The request scope.
|
|
51
|
+
receive: The receive channel.
|
|
52
|
+
send: The send channel.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The response from `proxy_http`.
|
|
56
|
+
"""
|
|
57
|
+
for _attempt in range(MAX_PROXY_RETRY):
|
|
58
|
+
try:
|
|
59
|
+
return await proxy_http(
|
|
60
|
+
context=context,
|
|
61
|
+
scope=scope,
|
|
62
|
+
receive=receive,
|
|
63
|
+
send=send,
|
|
64
|
+
)
|
|
65
|
+
except aiohttp.ClientError as err: # noqa: PERF203
|
|
66
|
+
console.debug(
|
|
67
|
+
f"Retrying request {scope['path']} due to client error {err!r}."
|
|
68
|
+
)
|
|
69
|
+
await asyncio.sleep(0.3)
|
|
70
|
+
except Exception as ex:
|
|
71
|
+
console.debug(
|
|
72
|
+
f"Retrying request {scope['path']} due to unhandled exception {ex!r}."
|
|
73
|
+
)
|
|
74
|
+
await asyncio.sleep(0.3)
|
|
75
|
+
|
|
76
|
+
def _get_proxy_app_with_context(frontend_host: str) -> tuple[ProxyContext, ASGIApp]:
|
|
77
|
+
"""Get the proxy app with the given frontend host.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
frontend_host: The frontend host to proxy requests to.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The proxy context and app.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
class LocalProxyConfig(BaseURLProxyConfigMixin, ProxyConfig):
|
|
87
|
+
upstream_base_url = frontend_host
|
|
88
|
+
rewrite_host_header = urlparse(upstream_base_url).netloc
|
|
89
|
+
|
|
90
|
+
proxy_context = ProxyContext(LocalProxyConfig())
|
|
91
|
+
proxy_app = make_simple_proxy_app(
|
|
92
|
+
proxy_context, proxy_http_handler=proxy_http_with_retry
|
|
93
|
+
)
|
|
94
|
+
return proxy_context, proxy_app
|
|
95
|
+
|
|
96
|
+
@asynccontextmanager
|
|
97
|
+
async def proxy_middleware( # pyright: ignore[reportGeneralTypeIssues]
|
|
98
|
+
app: FastAPI,
|
|
99
|
+
) -> AsyncGenerator[None, None]:
|
|
100
|
+
"""A middleware to proxy requests to the separate frontend server.
|
|
101
|
+
|
|
102
|
+
The proxy is installed on the / endpoint of the FastAPI instance.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
app: The FastAPI instance.
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
None
|
|
109
|
+
"""
|
|
110
|
+
config = get_config()
|
|
111
|
+
backend_port = config.backend_port
|
|
112
|
+
frontend_host = f"http://localhost:{config.frontend_port}"
|
|
113
|
+
proxy_context, proxy_app = _get_proxy_app_with_context(frontend_host)
|
|
114
|
+
app.mount("/", proxy_app)
|
|
115
|
+
console.debug(
|
|
116
|
+
f"Proxying '/' requests on port {backend_port} to {frontend_host}"
|
|
117
|
+
)
|
|
118
|
+
async with proxy_context:
|
|
119
|
+
yield
|
reflex/reflex.py
CHANGED
|
@@ -329,13 +329,14 @@ def export(
|
|
|
329
329
|
|
|
330
330
|
@cli.command()
|
|
331
331
|
def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)):
|
|
332
|
-
"""
|
|
332
|
+
"""Authenticate with experimental Reflex hosting service."""
|
|
333
333
|
from reflex_cli.v2 import cli as hosting_cli
|
|
334
334
|
|
|
335
335
|
check_version()
|
|
336
336
|
|
|
337
337
|
validated_info = hosting_cli.login()
|
|
338
338
|
if validated_info is not None:
|
|
339
|
+
_skip_compile() # Allow running outside of an app dir
|
|
339
340
|
telemetry.send("login", user_uuid=validated_info.get("user_id"))
|
|
340
341
|
|
|
341
342
|
|
|
@@ -484,6 +485,11 @@ def deploy(
|
|
|
484
485
|
"--token",
|
|
485
486
|
help="token to use for auth",
|
|
486
487
|
),
|
|
488
|
+
config_path: Optional[str] = typer.Option(
|
|
489
|
+
None,
|
|
490
|
+
"--config",
|
|
491
|
+
help="path to the config file",
|
|
492
|
+
),
|
|
487
493
|
):
|
|
488
494
|
"""Deploy the app to the Reflex hosting service."""
|
|
489
495
|
from reflex_cli.utils import dependency
|
|
@@ -539,6 +545,7 @@ def deploy(
|
|
|
539
545
|
loglevel=type(loglevel).INFO, # type: ignore
|
|
540
546
|
token=token,
|
|
541
547
|
project=project,
|
|
548
|
+
config_path=config_path,
|
|
542
549
|
)
|
|
543
550
|
|
|
544
551
|
|
reflex/state.py
CHANGED
|
@@ -107,6 +107,7 @@ from reflex.utils.exceptions import (
|
|
|
107
107
|
StateSchemaMismatchError,
|
|
108
108
|
StateSerializationError,
|
|
109
109
|
StateTooLargeError,
|
|
110
|
+
UnretrievableVarValueError,
|
|
110
111
|
)
|
|
111
112
|
from reflex.utils.exec import is_testing_env
|
|
112
113
|
from reflex.utils.serializers import serializer
|
|
@@ -143,6 +144,9 @@ HANDLED_PICKLE_ERRORS = (
|
|
|
143
144
|
ValueError,
|
|
144
145
|
)
|
|
145
146
|
|
|
147
|
+
# For BaseState.get_var_value
|
|
148
|
+
VAR_TYPE = TypeVar("VAR_TYPE")
|
|
149
|
+
|
|
146
150
|
|
|
147
151
|
def _no_chain_background_task(
|
|
148
152
|
state_cls: Type["BaseState"], name: str, fn: Callable
|
|
@@ -1193,6 +1197,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1193
1197
|
continue
|
|
1194
1198
|
dynamic_vars[param] = DynamicRouteVar(
|
|
1195
1199
|
fget=func,
|
|
1200
|
+
auto_deps=False,
|
|
1201
|
+
deps=["router"],
|
|
1196
1202
|
cache=True,
|
|
1197
1203
|
_js_expr=param,
|
|
1198
1204
|
_var_data=VarData.from_state(cls),
|
|
@@ -1240,13 +1246,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1240
1246
|
if not super().__getattribute__("__dict__"):
|
|
1241
1247
|
return super().__getattribute__(name)
|
|
1242
1248
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
}
|
|
1249
|
+
# Fast path for dunder
|
|
1250
|
+
if name.startswith("__"):
|
|
1251
|
+
return super().__getattribute__(name)
|
|
1247
1252
|
|
|
1248
1253
|
# For now, handle router_data updates as a special case.
|
|
1249
|
-
if
|
|
1254
|
+
if (
|
|
1255
|
+
name == constants.ROUTER_DATA
|
|
1256
|
+
or name in super().__getattribute__("inherited_vars")
|
|
1257
|
+
or name in super().__getattribute__("inherited_backend_vars")
|
|
1258
|
+
):
|
|
1250
1259
|
parent_state = super().__getattribute__("parent_state")
|
|
1251
1260
|
if parent_state is not None:
|
|
1252
1261
|
return getattr(parent_state, name)
|
|
@@ -1301,8 +1310,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1301
1310
|
value = value.__wrapped__
|
|
1302
1311
|
|
|
1303
1312
|
# Set the var on the parent state.
|
|
1304
|
-
|
|
1305
|
-
if name in inherited_vars:
|
|
1313
|
+
if name in self.inherited_vars or name in self.inherited_backend_vars:
|
|
1306
1314
|
setattr(self.parent_state, name, value)
|
|
1307
1315
|
return
|
|
1308
1316
|
|
|
@@ -1596,6 +1604,42 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
|
|
|
1596
1604
|
# Slow case - fetch missing parent states from redis.
|
|
1597
1605
|
return await self._get_state_from_redis(state_cls)
|
|
1598
1606
|
|
|
1607
|
+
async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE:
|
|
1608
|
+
"""Get the value of an rx.Var from another state.
|
|
1609
|
+
|
|
1610
|
+
Args:
|
|
1611
|
+
var: The var to get the value for.
|
|
1612
|
+
|
|
1613
|
+
Returns:
|
|
1614
|
+
The value of the var.
|
|
1615
|
+
|
|
1616
|
+
Raises:
|
|
1617
|
+
UnretrievableVarValueError: If the var does not have a literal value
|
|
1618
|
+
or associated state.
|
|
1619
|
+
"""
|
|
1620
|
+
# Oopsie case: you didn't give me a Var... so get what you give.
|
|
1621
|
+
if not isinstance(var, Var):
|
|
1622
|
+
return var # type: ignore
|
|
1623
|
+
|
|
1624
|
+
# Fast case: this is a literal var and the value is known.
|
|
1625
|
+
if hasattr(var, "_var_value"):
|
|
1626
|
+
return var._var_value
|
|
1627
|
+
|
|
1628
|
+
var_data = var._get_all_var_data()
|
|
1629
|
+
if var_data is None or not var_data.state:
|
|
1630
|
+
raise UnretrievableVarValueError(
|
|
1631
|
+
f"Unable to retrieve value for {var._js_expr}: not associated with any state."
|
|
1632
|
+
)
|
|
1633
|
+
# Fastish case: this var belongs to this state
|
|
1634
|
+
if var_data.state == self.get_full_name():
|
|
1635
|
+
return getattr(self, var_data.field_name)
|
|
1636
|
+
|
|
1637
|
+
# Slow case: this var belongs to another state
|
|
1638
|
+
other_state = await self.get_state(
|
|
1639
|
+
self._get_root_state().get_class_substate(var_data.state)
|
|
1640
|
+
)
|
|
1641
|
+
return getattr(other_state, var_data.field_name)
|
|
1642
|
+
|
|
1599
1643
|
def _get_event_handler(
|
|
1600
1644
|
self, event: Event
|
|
1601
1645
|
) -> tuple[BaseState | StateProxy, EventHandler]:
|
|
@@ -3645,6 +3689,9 @@ def get_state_manager() -> StateManager:
|
|
|
3645
3689
|
class MutableProxy(wrapt.ObjectProxy):
|
|
3646
3690
|
"""A proxy for a mutable object that tracks changes."""
|
|
3647
3691
|
|
|
3692
|
+
# Hint for finding the base class of the proxy.
|
|
3693
|
+
__base_proxy__ = "MutableProxy"
|
|
3694
|
+
|
|
3648
3695
|
# Methods on wrapped objects which should mark the state as dirty.
|
|
3649
3696
|
__mark_dirty_attrs__ = {
|
|
3650
3697
|
"add",
|
|
@@ -3687,6 +3734,39 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
3687
3734
|
BaseModelV1,
|
|
3688
3735
|
)
|
|
3689
3736
|
|
|
3737
|
+
# Dynamically generated classes for tracking dataclass mutations.
|
|
3738
|
+
__dataclass_proxies__: Dict[type, type] = {}
|
|
3739
|
+
|
|
3740
|
+
def __new__(cls, wrapped: Any, *args, **kwargs) -> MutableProxy:
|
|
3741
|
+
"""Create a proxy instance for a mutable object that tracks changes.
|
|
3742
|
+
|
|
3743
|
+
Args:
|
|
3744
|
+
wrapped: The object to proxy.
|
|
3745
|
+
*args: Other args passed to MutableProxy (ignored).
|
|
3746
|
+
**kwargs: Other kwargs passed to MutableProxy (ignored).
|
|
3747
|
+
|
|
3748
|
+
Returns:
|
|
3749
|
+
The proxy instance.
|
|
3750
|
+
"""
|
|
3751
|
+
if dataclasses.is_dataclass(wrapped):
|
|
3752
|
+
wrapped_cls = type(wrapped)
|
|
3753
|
+
wrapper_cls_name = wrapped_cls.__name__ + cls.__name__
|
|
3754
|
+
# Find the associated class
|
|
3755
|
+
if wrapper_cls_name not in cls.__dataclass_proxies__:
|
|
3756
|
+
# Create a new class that has the __dataclass_fields__ defined
|
|
3757
|
+
cls.__dataclass_proxies__[wrapper_cls_name] = type(
|
|
3758
|
+
wrapper_cls_name,
|
|
3759
|
+
(cls,),
|
|
3760
|
+
{
|
|
3761
|
+
dataclasses._FIELDS: getattr( # pyright: ignore [reportGeneralTypeIssues]
|
|
3762
|
+
wrapped_cls,
|
|
3763
|
+
dataclasses._FIELDS, # pyright: ignore [reportGeneralTypeIssues]
|
|
3764
|
+
),
|
|
3765
|
+
},
|
|
3766
|
+
)
|
|
3767
|
+
cls = cls.__dataclass_proxies__[wrapper_cls_name]
|
|
3768
|
+
return super().__new__(cls)
|
|
3769
|
+
|
|
3690
3770
|
def __init__(self, wrapped: Any, state: BaseState, field_name: str):
|
|
3691
3771
|
"""Create a proxy for a mutable object that tracks changes.
|
|
3692
3772
|
|
|
@@ -3743,7 +3823,27 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
3743
3823
|
Returns:
|
|
3744
3824
|
Whether the value is of a mutable type.
|
|
3745
3825
|
"""
|
|
3746
|
-
return isinstance(value, cls.__mutable_types__)
|
|
3826
|
+
return isinstance(value, cls.__mutable_types__) or (
|
|
3827
|
+
dataclasses.is_dataclass(value) and not isinstance(value, Var)
|
|
3828
|
+
)
|
|
3829
|
+
|
|
3830
|
+
@staticmethod
|
|
3831
|
+
def _is_called_from_dataclasses_internal() -> bool:
|
|
3832
|
+
"""Check if the current function is called from dataclasses helper.
|
|
3833
|
+
|
|
3834
|
+
Returns:
|
|
3835
|
+
Whether the current function is called from dataclasses internal code.
|
|
3836
|
+
"""
|
|
3837
|
+
# Walk up the stack a bit to see if we are called from dataclasses
|
|
3838
|
+
# internal code, for example `asdict` or `astuple`.
|
|
3839
|
+
frame = inspect.currentframe()
|
|
3840
|
+
for _ in range(5):
|
|
3841
|
+
# Why not `inspect.stack()` -- this is much faster!
|
|
3842
|
+
if not (frame := frame and frame.f_back):
|
|
3843
|
+
break
|
|
3844
|
+
if inspect.getfile(frame) == dataclasses.__file__:
|
|
3845
|
+
return True
|
|
3846
|
+
return False
|
|
3747
3847
|
|
|
3748
3848
|
def _wrap_recursive(self, value: Any) -> Any:
|
|
3749
3849
|
"""Wrap a value recursively if it is mutable.
|
|
@@ -3754,9 +3854,13 @@ class MutableProxy(wrapt.ObjectProxy):
|
|
|
3754
3854
|
Returns:
|
|
3755
3855
|
The wrapped value.
|
|
3756
3856
|
"""
|
|
3857
|
+
# When called from dataclasses internal code, return the unwrapped value
|
|
3858
|
+
if self._is_called_from_dataclasses_internal():
|
|
3859
|
+
return value
|
|
3757
3860
|
# Recursively wrap mutable types, but do not re-wrap MutableProxy instances.
|
|
3758
3861
|
if self._is_mutable_type(value) and not isinstance(value, MutableProxy):
|
|
3759
|
-
|
|
3862
|
+
base_cls = globals()[self.__base_proxy__]
|
|
3863
|
+
return base_cls(
|
|
3760
3864
|
wrapped=value,
|
|
3761
3865
|
state=self._self_state,
|
|
3762
3866
|
field_name=self._self_field_name,
|
|
@@ -3964,6 +4068,9 @@ class ImmutableMutableProxy(MutableProxy):
|
|
|
3964
4068
|
to modify the wrapped object when the StateProxy is immutable.
|
|
3965
4069
|
"""
|
|
3966
4070
|
|
|
4071
|
+
# Ensure that recursively wrapped proxies use ImmutableMutableProxy as base.
|
|
4072
|
+
__base_proxy__ = "ImmutableMutableProxy"
|
|
4073
|
+
|
|
3967
4074
|
def _mark_dirty(
|
|
3968
4075
|
self,
|
|
3969
4076
|
wrapped=None,
|
reflex/testing.py
CHANGED
|
@@ -44,6 +44,7 @@ import reflex.utils.format
|
|
|
44
44
|
import reflex.utils.prerequisites
|
|
45
45
|
import reflex.utils.processes
|
|
46
46
|
from reflex.config import environment
|
|
47
|
+
from reflex.proxy import proxy_middleware
|
|
47
48
|
from reflex.state import (
|
|
48
49
|
BaseState,
|
|
49
50
|
StateManager,
|
|
@@ -52,6 +53,7 @@ from reflex.state import (
|
|
|
52
53
|
StateManagerRedis,
|
|
53
54
|
reload_state_module,
|
|
54
55
|
)
|
|
56
|
+
from reflex.utils import console
|
|
55
57
|
|
|
56
58
|
try:
|
|
57
59
|
from selenium import webdriver # pyright: ignore [reportMissingImports]
|
|
@@ -297,6 +299,9 @@ class AppHarness:
|
|
|
297
299
|
self.state_manager = StateManagerRedis.create(self.app_instance.state)
|
|
298
300
|
else:
|
|
299
301
|
self.state_manager = self.app_instance._state_manager
|
|
302
|
+
# Disable proxy for app harness tests.
|
|
303
|
+
if proxy_middleware in self.app_instance.lifespan_tasks:
|
|
304
|
+
self.app_instance.lifespan_tasks.remove(proxy_middleware)
|
|
300
305
|
|
|
301
306
|
def _reload_state_module(self):
|
|
302
307
|
"""Reload the rx.State module to avoid conflict when reloading."""
|
|
@@ -364,9 +369,12 @@ class AppHarness:
|
|
|
364
369
|
def _start_frontend(self):
|
|
365
370
|
# Set up the frontend.
|
|
366
371
|
with chdir(self.app_path):
|
|
372
|
+
backend_host, backend_port = self._poll_for_servers().getsockname()
|
|
367
373
|
config = reflex.config.get_config()
|
|
374
|
+
config.backend_port = backend_port
|
|
368
375
|
config.api_url = "http://{0}:{1}".format(
|
|
369
|
-
|
|
376
|
+
backend_host,
|
|
377
|
+
backend_port,
|
|
370
378
|
)
|
|
371
379
|
reflex.utils.build.setup_frontend(self.app_path)
|
|
372
380
|
|
|
@@ -385,12 +393,13 @@ class AppHarness:
|
|
|
385
393
|
)
|
|
386
394
|
if not line:
|
|
387
395
|
break
|
|
388
|
-
print(line) # for pytest diagnosis
|
|
396
|
+
print(line) # for pytest diagnosis #noqa: T201
|
|
389
397
|
m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line)
|
|
390
398
|
if m is not None:
|
|
391
399
|
self.frontend_url = m.group(1)
|
|
392
400
|
config = reflex.config.get_config()
|
|
393
401
|
config.deploy_url = self.frontend_url
|
|
402
|
+
config.frontend_port = int(self.frontend_url.rpartition(":")[2])
|
|
394
403
|
break
|
|
395
404
|
if self.frontend_url is None:
|
|
396
405
|
raise RuntimeError("Frontend did not start")
|
|
@@ -403,11 +412,10 @@ class AppHarness:
|
|
|
403
412
|
)
|
|
404
413
|
# catch I/O operation on closed file.
|
|
405
414
|
except ValueError as e:
|
|
406
|
-
|
|
415
|
+
console.error(str(e))
|
|
407
416
|
break
|
|
408
417
|
if not line:
|
|
409
418
|
break
|
|
410
|
-
print(line)
|
|
411
419
|
|
|
412
420
|
self.frontend_output_thread = threading.Thread(target=consume_frontend_output)
|
|
413
421
|
self.frontend_output_thread.start()
|
|
@@ -915,17 +923,20 @@ class AppHarnessProd(AppHarness):
|
|
|
915
923
|
root=web_root,
|
|
916
924
|
error_page_map=error_page_map,
|
|
917
925
|
) as self.frontend_server:
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
926
|
+
config = reflex.config.get_config()
|
|
927
|
+
config.frontend_port = self.frontend_server.server_address[1]
|
|
928
|
+
self.frontend_url = f"http://localhost:{config.frontend_port}"
|
|
921
929
|
self.frontend_server.serve_forever()
|
|
922
930
|
|
|
923
931
|
def _start_frontend(self):
|
|
924
932
|
# Set up the frontend.
|
|
925
933
|
with chdir(self.app_path):
|
|
934
|
+
backend_host, backend_port = self._poll_for_servers().getsockname()
|
|
926
935
|
config = reflex.config.get_config()
|
|
936
|
+
config.backend_port = backend_port
|
|
927
937
|
config.api_url = "http://{0}:{1}".format(
|
|
928
|
-
|
|
938
|
+
backend_host,
|
|
939
|
+
backend_port,
|
|
929
940
|
)
|
|
930
941
|
reflex.reflex.export(
|
|
931
942
|
zipping=False,
|
reflex/utils/console.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
|
|
5
7
|
from rich.console import Console
|
|
6
8
|
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
|
|
7
9
|
from rich.prompt import Prompt
|
|
@@ -12,7 +14,7 @@ from reflex.constants import LogLevel
|
|
|
12
14
|
_console = Console()
|
|
13
15
|
|
|
14
16
|
# The current log level.
|
|
15
|
-
_LOG_LEVEL = LogLevel.
|
|
17
|
+
_LOG_LEVEL = LogLevel.DEFAULT
|
|
16
18
|
|
|
17
19
|
# Deprecated features who's warning has been printed.
|
|
18
20
|
_EMITTED_DEPRECATION_WARNINGS = set()
|
|
@@ -61,6 +63,9 @@ def set_log_level(log_level: LogLevel):
|
|
|
61
63
|
raise ValueError(f"Invalid log level: {log_level}") from ae
|
|
62
64
|
|
|
63
65
|
global _LOG_LEVEL
|
|
66
|
+
if log_level != _LOG_LEVEL:
|
|
67
|
+
# Set the loglevel persistently for subprocesses
|
|
68
|
+
os.environ["LOGLEVEL"] = log_level.value
|
|
64
69
|
_LOG_LEVEL = log_level
|
|
65
70
|
|
|
66
71
|
|
reflex/utils/exceptions.py
CHANGED
|
@@ -187,3 +187,7 @@ def raise_system_package_missing_error(package: str) -> NoReturn:
|
|
|
187
187
|
|
|
188
188
|
class InvalidLockWarningThresholdError(ReflexError):
|
|
189
189
|
"""Raised when an invalid lock warning threshold is provided."""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class UnretrievableVarValueError(ReflexError):
|
|
193
|
+
"""Raised when the value of a var is not retrievable."""
|
reflex/utils/prerequisites.py
CHANGED
|
@@ -28,8 +28,8 @@ import typer
|
|
|
28
28
|
from alembic.util.exc import CommandError
|
|
29
29
|
from packaging import version
|
|
30
30
|
from redis import Redis as RedisSync
|
|
31
|
-
from redis import exceptions
|
|
32
31
|
from redis.asyncio import Redis
|
|
32
|
+
from redis.exceptions import RedisError
|
|
33
33
|
|
|
34
34
|
from reflex import constants, model
|
|
35
35
|
from reflex.compiler import templates
|
|
@@ -109,7 +109,7 @@ def check_latest_package_version(package_name: str):
|
|
|
109
109
|
console.warn(
|
|
110
110
|
f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
|
|
111
111
|
)
|
|
112
|
-
# Check for
|
|
112
|
+
# Check for deprecated python versions
|
|
113
113
|
_python_version_check()
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
@@ -333,10 +333,11 @@ def get_redis() -> Redis | None:
|
|
|
333
333
|
Returns:
|
|
334
334
|
The asynchronous redis client.
|
|
335
335
|
"""
|
|
336
|
-
if
|
|
337
|
-
return Redis.from_url(
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
if (redis_url := parse_redis_url()) is not None:
|
|
337
|
+
return Redis.from_url(
|
|
338
|
+
redis_url,
|
|
339
|
+
retry_on_error=[RedisError],
|
|
340
|
+
)
|
|
340
341
|
return None
|
|
341
342
|
|
|
342
343
|
|
|
@@ -346,14 +347,15 @@ def get_redis_sync() -> RedisSync | None:
|
|
|
346
347
|
Returns:
|
|
347
348
|
The synchronous redis client.
|
|
348
349
|
"""
|
|
349
|
-
if
|
|
350
|
-
return RedisSync.from_url(
|
|
351
|
-
|
|
352
|
-
|
|
350
|
+
if (redis_url := parse_redis_url()) is not None:
|
|
351
|
+
return RedisSync.from_url(
|
|
352
|
+
redis_url,
|
|
353
|
+
retry_on_error=[RedisError],
|
|
354
|
+
)
|
|
353
355
|
return None
|
|
354
356
|
|
|
355
357
|
|
|
356
|
-
def parse_redis_url() -> str |
|
|
358
|
+
def parse_redis_url() -> str | None:
|
|
357
359
|
"""Parse the REDIS_URL in config if applicable.
|
|
358
360
|
|
|
359
361
|
Returns:
|
|
@@ -372,16 +374,13 @@ def parse_redis_url() -> str | dict | None:
|
|
|
372
374
|
return config.redis_url
|
|
373
375
|
|
|
374
376
|
|
|
375
|
-
async def get_redis_status() -> bool | None:
|
|
377
|
+
async def get_redis_status() -> dict[str, bool | None]:
|
|
376
378
|
"""Checks the status of the Redis connection.
|
|
377
379
|
|
|
378
380
|
Attempts to connect to Redis and send a ping command to verify connectivity.
|
|
379
381
|
|
|
380
382
|
Returns:
|
|
381
|
-
|
|
382
|
-
- True: Redis is accessible and responding.
|
|
383
|
-
- False: Redis is not accessible due to a connection error.
|
|
384
|
-
- None: Redis not used i.e redis_url is not set in rxconfig.
|
|
383
|
+
The status of the Redis connection.
|
|
385
384
|
"""
|
|
386
385
|
try:
|
|
387
386
|
status = True
|
|
@@ -390,10 +389,10 @@ async def get_redis_status() -> bool | None:
|
|
|
390
389
|
redis_client.ping()
|
|
391
390
|
else:
|
|
392
391
|
status = None
|
|
393
|
-
except
|
|
392
|
+
except RedisError:
|
|
394
393
|
status = False
|
|
395
394
|
|
|
396
|
-
return status
|
|
395
|
+
return {"redis": status}
|
|
397
396
|
|
|
398
397
|
|
|
399
398
|
def validate_app_name(app_name: str | None = None) -> str:
|
|
@@ -594,7 +593,7 @@ def initialize_web_directory():
|
|
|
594
593
|
"""Initialize the web directory on reflex init."""
|
|
595
594
|
console.log("Initializing the web directory.")
|
|
596
595
|
|
|
597
|
-
#
|
|
596
|
+
# Reuse the hash if one is already created, so we don't over-write it when running reflex init
|
|
598
597
|
project_hash = get_project_hash()
|
|
599
598
|
|
|
600
599
|
path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir()))
|
|
@@ -647,7 +646,7 @@ def initialize_bun_config():
|
|
|
647
646
|
def init_reflex_json(project_hash: int | None):
|
|
648
647
|
"""Write the hash of the Reflex project to a REFLEX_JSON.
|
|
649
648
|
|
|
650
|
-
|
|
649
|
+
Reuse the hash if one is already created, therefore do not
|
|
651
650
|
overwrite it every time we run the reflex init command
|
|
652
651
|
.
|
|
653
652
|
|
|
@@ -1177,6 +1176,24 @@ def initialize_frontend_dependencies():
|
|
|
1177
1176
|
initialize_web_directory()
|
|
1178
1177
|
|
|
1179
1178
|
|
|
1179
|
+
def check_db_used() -> bool:
|
|
1180
|
+
"""Check if the database is used.
|
|
1181
|
+
|
|
1182
|
+
Returns:
|
|
1183
|
+
True if the database is used.
|
|
1184
|
+
"""
|
|
1185
|
+
return bool(get_config().db_url)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def check_redis_used() -> bool:
|
|
1189
|
+
"""Check if Redis is used.
|
|
1190
|
+
|
|
1191
|
+
Returns:
|
|
1192
|
+
True if Redis is used.
|
|
1193
|
+
"""
|
|
1194
|
+
return bool(get_config().redis_url)
|
|
1195
|
+
|
|
1196
|
+
|
|
1180
1197
|
def check_db_initialized() -> bool:
|
|
1181
1198
|
"""Check if the database migrations are initialized.
|
|
1182
1199
|
|
reflex/utils/processes.py
CHANGED
|
@@ -118,7 +118,7 @@ def handle_port(service_name: str, port: str, default_port: str) -> str:
|
|
|
118
118
|
"""Change port if the specified port is in use and is not explicitly specified as a CLI arg or config arg.
|
|
119
119
|
otherwise tell the user the port is in use and exit the app.
|
|
120
120
|
|
|
121
|
-
We make an assumption that when port is the default port,then it
|
|
121
|
+
We make an assumption that when port is the default port,then it hasn't been explicitly set since its not straightforward
|
|
122
122
|
to know whether a port was explicitly provided by the user unless its any other than the default.
|
|
123
123
|
|
|
124
124
|
Args:
|
|
@@ -351,7 +351,7 @@ def atexit_handler():
|
|
|
351
351
|
|
|
352
352
|
def get_command_with_loglevel(command: list[str]) -> list[str]:
|
|
353
353
|
"""Add the right loglevel flag to the designated command.
|
|
354
|
-
npm uses --loglevel <level>, Bun
|
|
354
|
+
npm uses --loglevel <level>, Bun doesn't use the --loglevel flag and
|
|
355
355
|
runs in debug mode by default.
|
|
356
356
|
|
|
357
357
|
Args:
|
reflex/utils/pyi_generator.py
CHANGED
|
@@ -1023,7 +1023,7 @@ class InitStubGenerator(StubGenerator):
|
|
|
1023
1023
|
|
|
1024
1024
|
class PyiGenerator:
|
|
1025
1025
|
"""A .pyi file generator that will scan all defined Component in Reflex and
|
|
1026
|
-
generate the
|
|
1026
|
+
generate the appropriate stub.
|
|
1027
1027
|
"""
|
|
1028
1028
|
|
|
1029
1029
|
modules: list = []
|
|
@@ -1202,4 +1202,4 @@ class PyiGenerator:
|
|
|
1202
1202
|
or "Var[Template]" in line
|
|
1203
1203
|
):
|
|
1204
1204
|
line = line.rstrip() + " # type: ignore\n"
|
|
1205
|
-
print(line, end="")
|
|
1205
|
+
print(line, end="") # noqa: T201
|
reflex/utils/telemetry.py
CHANGED
|
@@ -7,6 +7,7 @@ import dataclasses
|
|
|
7
7
|
import multiprocessing
|
|
8
8
|
import platform
|
|
9
9
|
import warnings
|
|
10
|
+
from contextlib import suppress
|
|
10
11
|
|
|
11
12
|
from reflex.config import environment
|
|
12
13
|
|
|
@@ -171,10 +172,11 @@ def _send(event, telemetry_enabled, **kwargs):
|
|
|
171
172
|
if not telemetry_enabled:
|
|
172
173
|
return False
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
with suppress(Exception):
|
|
176
|
+
event_data = _prepare_event(event, **kwargs)
|
|
177
|
+
if not event_data:
|
|
178
|
+
return False
|
|
179
|
+
return _send_event(event_data)
|
|
178
180
|
|
|
179
181
|
|
|
180
182
|
def send(event: str, telemetry_enabled: bool | None = None, **kwargs):
|