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.

Files changed (65) hide show
  1. reflex/.templates/jinja/web/pages/_app.js.jinja2 +2 -4
  2. reflex/.templates/jinja/web/pages/custom_component.js.jinja2 +3 -4
  3. reflex/.templates/jinja/web/pages/index.js.jinja2 +2 -3
  4. reflex/.templates/jinja/web/pages/macros.js.jinja2 +38 -0
  5. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -16
  6. reflex/.templates/jinja/web/utils/context.js.jinja2 +1 -1
  7. reflex/.templates/web/utils/state.js +9 -4
  8. reflex/app.py +18 -10
  9. reflex/compiler/compiler.py +2 -2
  10. reflex/compiler/templates.py +41 -0
  11. reflex/compiler/utils.py +1 -1
  12. reflex/components/base/bare.py +7 -3
  13. reflex/components/component.py +78 -116
  14. reflex/components/core/banner.py +1 -1
  15. reflex/components/core/breakpoints.py +1 -1
  16. reflex/components/datadisplay/code.py +14 -10
  17. reflex/components/datadisplay/dataeditor.py +1 -1
  18. reflex/components/datadisplay/dataeditor.pyi +1 -1
  19. reflex/components/el/elements/forms.py +7 -5
  20. reflex/components/el/elements/metadata.py +1 -1
  21. reflex/components/lucide/icon.py +142 -19
  22. reflex/components/lucide/icon.pyi +141 -18
  23. reflex/components/markdown/markdown.py +3 -2
  24. reflex/components/plotly/plotly.py +3 -3
  25. reflex/components/plotly/plotly.pyi +3 -3
  26. reflex/components/radix/primitives/slider.py +1 -1
  27. reflex/components/radix/themes/layout/center.pyi +1 -1
  28. reflex/components/radix/themes/layout/flex.py +1 -1
  29. reflex/components/radix/themes/layout/flex.pyi +1 -1
  30. reflex/components/radix/themes/layout/grid.py +1 -1
  31. reflex/components/radix/themes/layout/grid.pyi +1 -1
  32. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  33. reflex/components/radix/themes/layout/stack.pyi +3 -3
  34. reflex/components/radix/themes/typography/link.py +1 -1
  35. reflex/components/recharts/cartesian.py +2 -2
  36. reflex/components/recharts/cartesian.pyi +6 -6
  37. reflex/components/recharts/charts.py +2 -2
  38. reflex/components/recharts/polar.py +2 -2
  39. reflex/components/recharts/polar.pyi +2 -2
  40. reflex/components/sonner/toast.py +1 -1
  41. reflex/config.py +5 -0
  42. reflex/constants/base.py +1 -1
  43. reflex/constants/compiler.py +1 -0
  44. reflex/event.py +91 -1
  45. reflex/experimental/client_state.py +42 -20
  46. reflex/istate/data.py +3 -3
  47. reflex/model.py +4 -5
  48. reflex/page.py +1 -1
  49. reflex/proxy.py +119 -0
  50. reflex/reflex.py +8 -1
  51. reflex/state.py +116 -9
  52. reflex/testing.py +19 -8
  53. reflex/utils/console.py +6 -1
  54. reflex/utils/exceptions.py +4 -0
  55. reflex/utils/prerequisites.py +37 -20
  56. reflex/utils/processes.py +2 -2
  57. reflex/utils/pyi_generator.py +2 -2
  58. reflex/utils/telemetry.py +6 -4
  59. reflex/vars/base.py +14 -3
  60. reflex/vars/sequence.py +37 -0
  61. {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/METADATA +4 -2
  62. {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/RECORD +65 -63
  63. {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/LICENSE +0 -0
  64. {reflex-0.6.7a2.dist-info → reflex-0.6.8a1.dist-info}/WHEEL +0 -0
  65. {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
- """Authenicate with experimental Reflex hosting service."""
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
- inherited_vars = {
1244
- **super().__getattribute__("inherited_vars"),
1245
- **super().__getattribute__("inherited_backend_vars"),
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 name in inherited_vars or name == constants.ROUTER_DATA:
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
- inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars}
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
- return type(self)(
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
- *self._poll_for_servers().getsockname(),
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
- print(e)
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
- self.frontend_url = "http://localhost:{1}".format(
919
- *self.frontend_server.socket.getsockname()
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
- *self._poll_for_servers().getsockname(),
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.INFO
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
 
@@ -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."""
@@ -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 depreacted python versions
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 isinstance((redis_url_or_options := parse_redis_url()), str):
337
- return Redis.from_url(redis_url_or_options)
338
- elif isinstance(redis_url_or_options, dict):
339
- return Redis(**redis_url_or_options)
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 isinstance((redis_url_or_options := parse_redis_url()), str):
350
- return RedisSync.from_url(redis_url_or_options)
351
- elif isinstance(redis_url_or_options, dict):
352
- return RedisSync(**redis_url_or_options)
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 | dict | None:
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
- bool or None: The status of the Redis connection:
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 exceptions.RedisError:
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
- # Re-use the hash if one is already created, so we don't over-write it when running reflex init
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
- Re-use the hash if one is already created, therefore do not
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 hasnt been explicitly set since its not straightforward
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 doesnt use the --loglevel flag and
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:
@@ -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 approriate stub.
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
- event_data = _prepare_event(event, **kwargs)
175
- if not event_data:
176
- return False
177
- return _send_event(event_data)
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):