reflex 0.8.0a6__py3-none-any.whl → 0.8.1a1__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 +18 -1
- reflex/.templates/web/vite-plugin-safari-cachebust.js +160 -0
- reflex/.templates/web/vite.config.js +28 -6
- reflex/app.py +1 -1
- reflex/components/__init__.py +1 -0
- reflex/components/component.py +53 -1
- reflex/components/core/upload.py +5 -5
- reflex/components/el/__init__.py +7 -1
- reflex/components/radix/themes/typography/link.py +1 -30
- reflex/components/react_router/__init__.py +5 -0
- reflex/components/react_router/dom.py +69 -0
- reflex/components/recharts/recharts.py +2 -2
- reflex/config.py +7 -36
- reflex/constants/installer.py +2 -2
- reflex/environment.py +116 -0
- reflex/event.py +18 -1
- reflex/istate/data.py +142 -69
- reflex/plugins/tailwind_v4.py +2 -2
- reflex/state.py +3 -3
- reflex/utils/exec.py +35 -8
- reflex/utils/lazy_loader.py +7 -1
- reflex/utils/misc.py +1 -2
- reflex/utils/processes.py +11 -2
- reflex/utils/pyi_generator.py +17 -2
- {reflex-0.8.0a6.dist-info → reflex-0.8.1a1.dist-info}/METADATA +3 -3
- {reflex-0.8.0a6.dist-info → reflex-0.8.1a1.dist-info}/RECORD +29 -26
- {reflex-0.8.0a6.dist-info → reflex-0.8.1a1.dist-info}/WHEEL +0 -0
- {reflex-0.8.0a6.dist-info → reflex-0.8.1a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.0a6.dist-info → reflex-0.8.1a1.dist-info}/licenses/LICENSE +0 -0
reflex/environment.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import concurrent.futures
|
|
6
6
|
import dataclasses
|
|
7
7
|
import enum
|
|
8
|
+
import importlib
|
|
8
9
|
import inspect
|
|
9
10
|
import multiprocessing
|
|
10
11
|
import os
|
|
@@ -24,6 +25,7 @@ from typing import (
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
from reflex import constants
|
|
28
|
+
from reflex.plugins import Plugin
|
|
27
29
|
from reflex.utils.exceptions import EnvironmentVarValueError
|
|
28
30
|
from reflex.utils.types import GenericType, is_union, value_inside_optional
|
|
29
31
|
|
|
@@ -126,6 +128,48 @@ def interpret_path_env(value: str, field_name: str) -> Path:
|
|
|
126
128
|
return Path(value)
|
|
127
129
|
|
|
128
130
|
|
|
131
|
+
def interpret_plugin_env(value: str, field_name: str) -> Plugin:
|
|
132
|
+
"""Interpret a plugin environment variable value.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: The environment variable value.
|
|
136
|
+
field_name: The field name.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The interpreted value.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
EnvironmentVarValueError: If the value is invalid.
|
|
143
|
+
"""
|
|
144
|
+
if "." not in value:
|
|
145
|
+
msg = f"Invalid plugin value: {value!r} for {field_name}. Plugin name must be in the format 'package.module.PluginName'."
|
|
146
|
+
raise EnvironmentVarValueError(msg)
|
|
147
|
+
|
|
148
|
+
import_path, plugin_name = value.rsplit(".", 1)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
module = importlib.import_module(import_path)
|
|
152
|
+
except ImportError as e:
|
|
153
|
+
msg = f"Failed to import module {import_path!r} for {field_name}: {e}"
|
|
154
|
+
raise EnvironmentVarValueError(msg) from e
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
plugin_class = getattr(module, plugin_name, None)
|
|
158
|
+
except Exception as e:
|
|
159
|
+
msg = f"Failed to get plugin class {plugin_name!r} from module {import_path!r} for {field_name}: {e}"
|
|
160
|
+
raise EnvironmentVarValueError(msg) from e
|
|
161
|
+
|
|
162
|
+
if not inspect.isclass(plugin_class) or not issubclass(plugin_class, Plugin):
|
|
163
|
+
msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin."
|
|
164
|
+
raise EnvironmentVarValueError(msg)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
return plugin_class()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
msg = f"Failed to instantiate plugin {plugin_name!r} for {field_name}: {e}"
|
|
170
|
+
raise EnvironmentVarValueError(msg) from e
|
|
171
|
+
|
|
172
|
+
|
|
129
173
|
def interpret_enum_env(value: str, field_type: GenericType, field_name: str) -> Any:
|
|
130
174
|
"""Interpret an enum environment variable value.
|
|
131
175
|
|
|
@@ -181,6 +225,8 @@ def interpret_env_var_value(
|
|
|
181
225
|
return interpret_path_env(value, field_name)
|
|
182
226
|
if field_type is ExistingPath:
|
|
183
227
|
return interpret_existing_path_env(value, field_name)
|
|
228
|
+
if field_type is Plugin:
|
|
229
|
+
return interpret_plugin_env(value, field_name)
|
|
184
230
|
if get_origin(field_type) is list:
|
|
185
231
|
return [
|
|
186
232
|
interpret_env_var_value(
|
|
@@ -606,3 +652,73 @@ class EnvironmentVariables:
|
|
|
606
652
|
|
|
607
653
|
|
|
608
654
|
environment = EnvironmentVariables()
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
from dotenv import load_dotenv
|
|
658
|
+
except ImportError:
|
|
659
|
+
load_dotenv = None
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _paths_from_env_files(env_files: str) -> list[Path]:
|
|
663
|
+
"""Convert a string of paths separated by os.pathsep into a list of Path objects.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
env_files: The string of paths.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
A list of Path objects.
|
|
670
|
+
"""
|
|
671
|
+
# load env files in reverse order
|
|
672
|
+
return list(
|
|
673
|
+
reversed(
|
|
674
|
+
[
|
|
675
|
+
Path(path)
|
|
676
|
+
for path_element in env_files.split(os.pathsep)
|
|
677
|
+
if (path := path_element.strip())
|
|
678
|
+
]
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _load_dotenv_from_files(files: list[Path]):
|
|
684
|
+
"""Load environment variables from a list of files.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
files: A list of Path objects representing the environment variable files.
|
|
688
|
+
"""
|
|
689
|
+
from reflex.utils import console
|
|
690
|
+
|
|
691
|
+
if not files:
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
if load_dotenv is None:
|
|
695
|
+
console.error(
|
|
696
|
+
"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
|
|
697
|
+
)
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
for env_file in files:
|
|
701
|
+
if env_file.exists():
|
|
702
|
+
load_dotenv(env_file, override=True)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _paths_from_environment() -> list[Path]:
|
|
706
|
+
"""Get the paths from the REFLEX_ENV_FILE environment variable.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
A list of Path objects.
|
|
710
|
+
"""
|
|
711
|
+
env_files = os.environ.get("REFLEX_ENV_FILE")
|
|
712
|
+
if not env_files:
|
|
713
|
+
return []
|
|
714
|
+
|
|
715
|
+
return _paths_from_env_files(env_files)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _load_dotenv_from_env():
|
|
719
|
+
"""Load environment variables from paths specified in REFLEX_ENV_FILE."""
|
|
720
|
+
_load_dotenv_from_files(_paths_from_environment())
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# Load the env files at import time if they are set in the ENV_FILE environment variable.
|
|
724
|
+
_load_dotenv_from_env()
|
reflex/event.py
CHANGED
|
@@ -570,7 +570,7 @@ class JavascriptHTMLInputElement:
|
|
|
570
570
|
class JavascriptInputEvent:
|
|
571
571
|
"""Interface for a Javascript InputEvent https://developer.mozilla.org/en-US/docs/Web/API/InputEvent."""
|
|
572
572
|
|
|
573
|
-
target: JavascriptHTMLInputElement = JavascriptHTMLInputElement()
|
|
573
|
+
target: JavascriptHTMLInputElement = JavascriptHTMLInputElement()
|
|
574
574
|
|
|
575
575
|
|
|
576
576
|
@dataclasses.dataclass(
|
|
@@ -1025,6 +1025,22 @@ def set_focus(ref: str) -> EventSpec:
|
|
|
1025
1025
|
)
|
|
1026
1026
|
|
|
1027
1027
|
|
|
1028
|
+
def blur_focus(ref: str) -> EventSpec:
|
|
1029
|
+
"""Blur focus of specified ref.
|
|
1030
|
+
|
|
1031
|
+
Args:
|
|
1032
|
+
ref: The ref.
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
An event to blur focus on the ref
|
|
1036
|
+
"""
|
|
1037
|
+
return server_side(
|
|
1038
|
+
"_blur_focus",
|
|
1039
|
+
get_fn_signature(blur_focus),
|
|
1040
|
+
ref=LiteralVar.create(format.format_ref(ref)),
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
|
|
1028
1044
|
def scroll_to(elem_id: str, align_to_top: bool | Var[bool] = True) -> EventSpec:
|
|
1029
1045
|
"""Select the id of a html element for scrolling into view.
|
|
1030
1046
|
|
|
@@ -2293,6 +2309,7 @@ class EventNamespace:
|
|
|
2293
2309
|
back = staticmethod(back)
|
|
2294
2310
|
window_alert = staticmethod(window_alert)
|
|
2295
2311
|
set_focus = staticmethod(set_focus)
|
|
2312
|
+
blur_focus = staticmethod(blur_focus)
|
|
2296
2313
|
scroll_to = staticmethod(scroll_to)
|
|
2297
2314
|
set_value = staticmethod(set_value)
|
|
2298
2315
|
remove_cookie = staticmethod(remove_cookie)
|
reflex/istate/data.py
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
from collections.abc import Mapping
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from urllib.parse import _NetlocResultMixinStr, urlsplit
|
|
5
7
|
|
|
6
8
|
from reflex import constants
|
|
7
|
-
from reflex.utils import format
|
|
9
|
+
from reflex.utils import console, format
|
|
8
10
|
from reflex.utils.serializers import serializer
|
|
9
11
|
|
|
10
12
|
|
|
@@ -45,36 +47,40 @@ class _HeaderData:
|
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
_HEADER_DATA_FIELDS = frozenset(
|
|
51
|
+
[field.name for field in dataclasses.fields(_HeaderData)]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclasses.dataclass(frozen=True)
|
|
49
56
|
class HeaderData(_HeaderData):
|
|
50
57
|
"""An object containing headers data."""
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_router_data(cls, router_data: dict) -> "HeaderData":
|
|
61
|
+
"""Create a HeaderData object from the given router_data.
|
|
54
62
|
|
|
55
63
|
Args:
|
|
56
64
|
router_data: the router_data dict.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A HeaderData object initialized with the provided router_data.
|
|
57
68
|
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
).items()
|
|
74
|
-
if v
|
|
75
|
-
}
|
|
76
|
-
),
|
|
77
|
-
)
|
|
69
|
+
return cls(
|
|
70
|
+
**{
|
|
71
|
+
snake_case_key: v
|
|
72
|
+
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
|
|
73
|
+
if v
|
|
74
|
+
and (snake_case_key := format.to_snake_case(k)) in _HEADER_DATA_FIELDS
|
|
75
|
+
},
|
|
76
|
+
raw_headers=_FrozenDictStrStr(
|
|
77
|
+
**{
|
|
78
|
+
k: v
|
|
79
|
+
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
|
|
80
|
+
if v
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
|
|
80
86
|
@serializer(to=dict)
|
|
@@ -90,6 +96,35 @@ def serialize_frozen_dict_str_str(obj: _FrozenDictStrStr) -> dict:
|
|
|
90
96
|
return dict(obj._data)
|
|
91
97
|
|
|
92
98
|
|
|
99
|
+
class ReflexURL(str, _NetlocResultMixinStr):
|
|
100
|
+
"""A class representing a URL split result."""
|
|
101
|
+
|
|
102
|
+
if TYPE_CHECKING:
|
|
103
|
+
scheme: str
|
|
104
|
+
netloc: str
|
|
105
|
+
path: str
|
|
106
|
+
query: str
|
|
107
|
+
fragment: str
|
|
108
|
+
|
|
109
|
+
def __new__(cls, url: str):
|
|
110
|
+
"""Create a new ReflexURL instance.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
url: the URL to split.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A new ReflexURL instance.
|
|
117
|
+
"""
|
|
118
|
+
(scheme, netloc, path, query, fragment) = urlsplit(url)
|
|
119
|
+
obj = super().__new__(cls, url)
|
|
120
|
+
object.__setattr__(obj, "scheme", scheme)
|
|
121
|
+
object.__setattr__(obj, "netloc", netloc)
|
|
122
|
+
object.__setattr__(obj, "path", path)
|
|
123
|
+
object.__setattr__(obj, "query", query)
|
|
124
|
+
object.__setattr__(obj, "fragment", fragment)
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
|
|
93
128
|
@dataclasses.dataclass(frozen=True)
|
|
94
129
|
class PageData:
|
|
95
130
|
"""An object containing page data."""
|
|
@@ -101,39 +136,30 @@ class PageData:
|
|
|
101
136
|
full_raw_path: str = ""
|
|
102
137
|
params: dict = dataclasses.field(default_factory=dict)
|
|
103
138
|
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_router_data(cls, router_data: dict) -> "PageData":
|
|
141
|
+
"""Create a PageData object from the given router_data.
|
|
106
142
|
|
|
107
143
|
Args:
|
|
108
144
|
router_data: the router_data dict.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A PageData object initialized with the provided router_data.
|
|
109
148
|
"""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
object.__setattr__(self, "full_path", f"{self.host}{self.path}")
|
|
123
|
-
object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
|
|
124
|
-
object.__setattr__(
|
|
125
|
-
self, "params", router_data.get(constants.RouteVar.QUERY, {})
|
|
126
|
-
)
|
|
127
|
-
else:
|
|
128
|
-
object.__setattr__(self, "host", "")
|
|
129
|
-
object.__setattr__(self, "path", "")
|
|
130
|
-
object.__setattr__(self, "raw_path", "")
|
|
131
|
-
object.__setattr__(self, "full_path", "")
|
|
132
|
-
object.__setattr__(self, "full_raw_path", "")
|
|
133
|
-
object.__setattr__(self, "params", {})
|
|
149
|
+
host = router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
|
|
150
|
+
path = router_data.get(constants.RouteVar.PATH, "")
|
|
151
|
+
raw_path = router_data.get(constants.RouteVar.ORIGIN, "")
|
|
152
|
+
return cls(
|
|
153
|
+
host=host,
|
|
154
|
+
path=path,
|
|
155
|
+
raw_path=raw_path,
|
|
156
|
+
full_path=f"{host}{path}",
|
|
157
|
+
full_raw_path=f"{host}{raw_path}",
|
|
158
|
+
params=router_data.get(constants.RouteVar.QUERY, {}),
|
|
159
|
+
)
|
|
134
160
|
|
|
135
161
|
|
|
136
|
-
@dataclasses.dataclass(frozen=True
|
|
162
|
+
@dataclasses.dataclass(frozen=True)
|
|
137
163
|
class SessionData:
|
|
138
164
|
"""An object containing session data."""
|
|
139
165
|
|
|
@@ -141,37 +167,84 @@ class SessionData:
|
|
|
141
167
|
client_ip: str = ""
|
|
142
168
|
session_id: str = ""
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
|
|
170
|
+
@classmethod
|
|
171
|
+
def from_router_data(cls, router_data: dict) -> "SessionData":
|
|
172
|
+
"""Create a SessionData object from the given router_data.
|
|
146
173
|
|
|
147
174
|
Args:
|
|
148
175
|
router_data: the router_data dict.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A SessionData object initialized with the provided router_data.
|
|
149
179
|
"""
|
|
150
|
-
|
|
151
|
-
client_token
|
|
152
|
-
client_ip
|
|
153
|
-
session_id
|
|
154
|
-
|
|
155
|
-
client_token = client_ip = session_id = ""
|
|
156
|
-
object.__setattr__(self, "client_token", client_token)
|
|
157
|
-
object.__setattr__(self, "client_ip", client_ip)
|
|
158
|
-
object.__setattr__(self, "session_id", session_id)
|
|
180
|
+
return cls(
|
|
181
|
+
client_token=router_data.get(constants.RouteVar.CLIENT_TOKEN, ""),
|
|
182
|
+
client_ip=router_data.get(constants.RouteVar.CLIENT_IP, ""),
|
|
183
|
+
session_id=router_data.get(constants.RouteVar.SESSION_ID, ""),
|
|
184
|
+
)
|
|
159
185
|
|
|
160
186
|
|
|
161
|
-
@dataclasses.dataclass(frozen=True
|
|
187
|
+
@dataclasses.dataclass(frozen=True)
|
|
162
188
|
class RouterData:
|
|
163
189
|
"""An object containing RouterData."""
|
|
164
190
|
|
|
165
191
|
session: SessionData = dataclasses.field(default_factory=SessionData)
|
|
166
192
|
headers: HeaderData = dataclasses.field(default_factory=HeaderData)
|
|
167
|
-
|
|
193
|
+
_page: PageData = dataclasses.field(default_factory=PageData)
|
|
194
|
+
url: ReflexURL = dataclasses.field(default=ReflexURL(""))
|
|
195
|
+
route_id: str = ""
|
|
168
196
|
|
|
169
|
-
|
|
170
|
-
|
|
197
|
+
@property
|
|
198
|
+
def page(self) -> PageData:
|
|
199
|
+
"""Get the page data.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The PageData object.
|
|
203
|
+
"""
|
|
204
|
+
console.deprecate(
|
|
205
|
+
"RouterData.page",
|
|
206
|
+
"Use RouterData.url instead",
|
|
207
|
+
deprecation_version="0.8.1",
|
|
208
|
+
removal_version="0.9.0",
|
|
209
|
+
)
|
|
210
|
+
return self._page
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_router_data(cls, router_data: dict) -> "RouterData":
|
|
214
|
+
"""Create a RouterData object from the given router_data.
|
|
171
215
|
|
|
172
216
|
Args:
|
|
173
217
|
router_data: the router_data dict.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A RouterData object initialized with the provided router_data.
|
|
174
221
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
222
|
+
return cls(
|
|
223
|
+
session=SessionData.from_router_data(router_data),
|
|
224
|
+
headers=HeaderData.from_router_data(router_data),
|
|
225
|
+
_page=PageData.from_router_data(router_data),
|
|
226
|
+
url=ReflexURL(
|
|
227
|
+
router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
|
|
228
|
+
+ router_data.get(constants.RouteVar.ORIGIN, "")
|
|
229
|
+
),
|
|
230
|
+
route_id=router_data.get(constants.RouteVar.PATH, ""),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@serializer(to=dict)
|
|
235
|
+
def serialize_router_data(obj: RouterData) -> dict:
|
|
236
|
+
"""Serialize a RouterData object to a dict.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
obj: the RouterData object.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
A dict representation of the RouterData object.
|
|
243
|
+
"""
|
|
244
|
+
return {
|
|
245
|
+
"session": obj.session,
|
|
246
|
+
"headers": obj.headers,
|
|
247
|
+
"page": obj._page,
|
|
248
|
+
"url": obj.url,
|
|
249
|
+
"route_id": obj.route_id,
|
|
250
|
+
}
|
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.11"
|
|
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.11",
|
|
160
160
|
]
|
|
161
161
|
|
|
162
162
|
def pre_compile(self, **context):
|
reflex/state.py
CHANGED
|
@@ -1204,7 +1204,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1204
1204
|
|
|
1205
1205
|
def argsingle_factory(param: str):
|
|
1206
1206
|
def inner_func(self: BaseState) -> str:
|
|
1207
|
-
return self.router.
|
|
1207
|
+
return self.router._page.params.get(param, "")
|
|
1208
1208
|
|
|
1209
1209
|
inner_func.__name__ = param
|
|
1210
1210
|
|
|
@@ -1212,7 +1212,7 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1212
1212
|
|
|
1213
1213
|
def arglist_factory(param: str):
|
|
1214
1214
|
def inner_func(self: BaseState) -> list[str]:
|
|
1215
|
-
return self.router.
|
|
1215
|
+
return self.router._page.params.get(param, [])
|
|
1216
1216
|
|
|
1217
1217
|
inner_func.__name__ = param
|
|
1218
1218
|
|
|
@@ -2466,7 +2466,7 @@ class OnLoadInternalState(State):
|
|
|
2466
2466
|
"""
|
|
2467
2467
|
# Do not app._compile()! It should be already compiled by now.
|
|
2468
2468
|
load_events = prerequisites.get_and_validate_app().app.get_load_events(
|
|
2469
|
-
self.router.
|
|
2469
|
+
self.router._page.path
|
|
2470
2470
|
)
|
|
2471
2471
|
if not load_events:
|
|
2472
2472
|
self.is_hydrated = True
|
reflex/utils/exec.py
CHANGED
|
@@ -519,9 +519,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
|
|
|
519
519
|
|
|
520
520
|
from granian.constants import Interfaces
|
|
521
521
|
from granian.log import LogLevels
|
|
522
|
-
from granian.server import
|
|
522
|
+
from granian.server import Server as Granian
|
|
523
523
|
|
|
524
|
-
|
|
524
|
+
from reflex.environment import _paths_from_environment
|
|
525
|
+
|
|
526
|
+
granian_app = Granian(
|
|
525
527
|
target=get_app_instance_from_file(),
|
|
526
528
|
factory=True,
|
|
527
529
|
address=host,
|
|
@@ -533,8 +535,11 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel):
|
|
|
533
535
|
reload_ignore_worker_failure=True,
|
|
534
536
|
reload_ignore_patterns=HOTRELOAD_IGNORE_PATTERNS,
|
|
535
537
|
reload_tick=100,
|
|
538
|
+
env_files=_paths_from_environment() or None,
|
|
536
539
|
workers_kill_timeout=2,
|
|
537
|
-
)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
granian_app.serve()
|
|
538
543
|
|
|
539
544
|
|
|
540
545
|
def run_backend_prod(
|
|
@@ -568,15 +573,37 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
|
|
|
568
573
|
port: The app port
|
|
569
574
|
loglevel: The log level.
|
|
570
575
|
"""
|
|
576
|
+
import os
|
|
577
|
+
import shlex
|
|
578
|
+
|
|
571
579
|
from reflex.utils import processes
|
|
572
580
|
|
|
573
581
|
app_module = get_app_instance()
|
|
574
582
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
583
|
+
if constants.IS_WINDOWS:
|
|
584
|
+
command = [
|
|
585
|
+
"uvicorn",
|
|
586
|
+
*("--host", host),
|
|
587
|
+
*("--port", str(port)),
|
|
588
|
+
"--factory",
|
|
589
|
+
app_module,
|
|
590
|
+
]
|
|
591
|
+
else:
|
|
592
|
+
# Parse GUNICORN_CMD_ARGS for user overrides
|
|
593
|
+
env_args = []
|
|
594
|
+
if gunicorn_cmd_args := os.environ.get("GUNICORN_CMD_ARGS", ""):
|
|
595
|
+
env_args = shlex.split(gunicorn_cmd_args)
|
|
596
|
+
|
|
597
|
+
# Our default args, then env args (env args win on conflicts)
|
|
598
|
+
command = [
|
|
599
|
+
"gunicorn",
|
|
600
|
+
"--preload",
|
|
601
|
+
"--worker-class",
|
|
602
|
+
"uvicorn.workers.UvicornH11Worker",
|
|
603
|
+
*("--bind", f"{host}:{port}"),
|
|
604
|
+
*env_args,
|
|
605
|
+
f"{app_module}()",
|
|
606
|
+
]
|
|
580
607
|
|
|
581
608
|
command += [
|
|
582
609
|
*("--log-level", loglevel.value),
|
reflex/utils/lazy_loader.py
CHANGED
|
@@ -27,6 +27,7 @@ def attach(
|
|
|
27
27
|
package_name: str,
|
|
28
28
|
submodules: set[str] | None = None,
|
|
29
29
|
submod_attrs: dict[str, list[str]] | None = None,
|
|
30
|
+
**extra_mappings,
|
|
30
31
|
):
|
|
31
32
|
"""Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach.
|
|
32
33
|
The lazy loader __getattr__ doesn't support tuples as list values. We needed to add
|
|
@@ -39,6 +40,7 @@ def attach(
|
|
|
39
40
|
submodules : List of submodules to attach.
|
|
40
41
|
submod_attrs : Dictionary of submodule -> list of attributes / functions.
|
|
41
42
|
These attributes are imported as they are used.
|
|
43
|
+
extra_mappings: Additional mappings to resolve lazily.
|
|
42
44
|
|
|
43
45
|
Returns:
|
|
44
46
|
__getattr__, __dir__, __all__
|
|
@@ -60,9 +62,13 @@ def attach(
|
|
|
60
62
|
attr: mod for mod, attrs in submod_attrs.items() for attr in attrs
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
__all__ = sorted(submodules | attr_to_modules.keys())
|
|
65
|
+
__all__ = sorted([*(submodules | attr_to_modules.keys()), *(extra_mappings or [])])
|
|
64
66
|
|
|
65
67
|
def __getattr__(name: str): # noqa: N807
|
|
68
|
+
if name in extra_mappings:
|
|
69
|
+
submod_path, attr = extra_mappings[name].rsplit(".", 1)
|
|
70
|
+
submod = importlib.import_module(submod_path)
|
|
71
|
+
return getattr(submod, attr)
|
|
66
72
|
if name in submodules:
|
|
67
73
|
return importlib.import_module(f"{package_name}.{name}")
|
|
68
74
|
if name in attr_to_modules:
|
reflex/utils/misc.py
CHANGED
|
@@ -31,14 +31,13 @@ def get_module_path(module_name: str) -> Path | None:
|
|
|
31
31
|
for i, part in enumerate(parts):
|
|
32
32
|
potential_file = current_path / (part + ".py")
|
|
33
33
|
potential_dir = current_path / part
|
|
34
|
-
potential_init = current_path / part / "__init__.py"
|
|
35
34
|
|
|
36
35
|
if potential_file.is_file():
|
|
37
36
|
# We encountered a file, but we can't continue deeper
|
|
38
37
|
if i == len(parts) - 1:
|
|
39
38
|
return potential_file
|
|
40
39
|
return None # Can't continue deeper
|
|
41
|
-
if potential_dir.is_dir()
|
|
40
|
+
if potential_dir.is_dir():
|
|
42
41
|
# It's a package, so we can continue deeper
|
|
43
42
|
current_path = potential_dir
|
|
44
43
|
else:
|
reflex/utils/processes.py
CHANGED
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
from typing import Any, Literal, overload
|
|
16
16
|
|
|
17
17
|
import click
|
|
18
|
+
import rich.markup
|
|
18
19
|
from redis.exceptions import RedisError
|
|
19
20
|
from rich.progress import Progress
|
|
20
21
|
|
|
@@ -61,8 +62,16 @@ def is_process_on_port(port: int) -> bool:
|
|
|
61
62
|
Returns:
|
|
62
63
|
Whether a process is running on the given port.
|
|
63
64
|
"""
|
|
65
|
+
# Test IPv4 localhost (127.0.0.1)
|
|
64
66
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
|
65
|
-
|
|
67
|
+
ipv4_result = sock.connect_ex(("127.0.0.1", port)) == 0
|
|
68
|
+
|
|
69
|
+
# Test IPv6 localhost (::1)
|
|
70
|
+
with closing(socket.socket(socket.AF_INET6, socket.SOCK_STREAM)) as sock:
|
|
71
|
+
ipv6_result = sock.connect_ex(("::1", port)) == 0
|
|
72
|
+
|
|
73
|
+
# Port is in use if either IPv4 or IPv6 is listening
|
|
74
|
+
return ipv4_result or ipv6_result
|
|
66
75
|
|
|
67
76
|
|
|
68
77
|
def change_port(port: int, _type: str) -> int:
|
|
@@ -273,7 +282,7 @@ def stream_logs(
|
|
|
273
282
|
return
|
|
274
283
|
try:
|
|
275
284
|
for line in process.stdout:
|
|
276
|
-
console.debug(line, end="", progress=progress)
|
|
285
|
+
console.debug(rich.markup.escape(line), end="", progress=progress)
|
|
277
286
|
logs.append(line)
|
|
278
287
|
yield line
|
|
279
288
|
except ValueError:
|