reflex 0.8.2__py3-none-any.whl → 0.8.3__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 +7 -2
- reflex/__init__.py +1 -0
- reflex/__init__.pyi +2 -0
- reflex/app.py +8 -11
- reflex/compiler/compiler.py +10 -38
- reflex/components/base/error_boundary.py +6 -5
- reflex/components/base/meta.pyi +4 -256
- reflex/components/core/__init__.py +1 -0
- reflex/components/core/__init__.pyi +3 -0
- reflex/components/core/window_events.py +104 -0
- reflex/components/core/window_events.pyi +84 -0
- reflex/components/el/__init__.pyi +4 -0
- reflex/components/el/elements/__init__.py +1 -0
- reflex/components/el/elements/__init__.pyi +5 -0
- reflex/components/el/elements/forms.py +4 -2
- reflex/components/el/elements/metadata.pyi +2 -0
- reflex/components/el/elements/typography.py +7 -0
- reflex/components/el/elements/typography.pyi +246 -0
- reflex/components/lucide/icon.py +303 -292
- reflex/components/lucide/icon.pyi +303 -292
- reflex/components/radix/themes/components/separator.py +4 -4
- reflex/components/radix/themes/components/separator.pyi +3 -3
- reflex/components/recharts/recharts.py +2 -2
- reflex/components/sonner/toast.py +1 -1
- reflex/constants/installer.py +1 -1
- reflex/event.py +71 -10
- reflex/model.py +55 -0
- reflex/plugins/base.py +2 -2
- reflex/reflex.py +33 -0
- reflex/route.py +4 -4
- reflex/state.py +9 -4
- reflex/utils/console.py +3 -3
- reflex/utils/exec.py +22 -6
- reflex/utils/format.py +1 -1
- reflex/utils/processes.py +28 -38
- reflex/utils/types.py +1 -1
- {reflex-0.8.2.dist-info → reflex-0.8.3.dist-info}/METADATA +1 -1
- {reflex-0.8.2.dist-info → reflex-0.8.3.dist-info}/RECORD +41 -39
- {reflex-0.8.2.dist-info → reflex-0.8.3.dist-info}/WHEEL +0 -0
- {reflex-0.8.2.dist-info → reflex-0.8.3.dist-info}/entry_points.txt +0 -0
- {reflex-0.8.2.dist-info → reflex-0.8.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,7 +6,7 @@ from reflex.components.core.breakpoints import Responsive
|
|
|
6
6
|
from reflex.components.radix.themes.base import LiteralAccentColor, RadixThemesComponent
|
|
7
7
|
from reflex.vars.base import LiteralVar, Var
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
LiteralSeparatorSize = Literal["1", "2", "3", "4"]
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Separator(RadixThemesComponent):
|
|
@@ -14,10 +14,10 @@ class Separator(RadixThemesComponent):
|
|
|
14
14
|
|
|
15
15
|
tag = "Separator"
|
|
16
16
|
|
|
17
|
-
# The size of the
|
|
18
|
-
size: Var[Responsive[
|
|
17
|
+
# The size of the separator: "1" | "2" | "3" | "4"
|
|
18
|
+
size: Var[Responsive[LiteralSeparatorSize]] = LiteralVar.create("4")
|
|
19
19
|
|
|
20
|
-
# The color of the
|
|
20
|
+
# The color of the separator
|
|
21
21
|
color_scheme: Var[LiteralAccentColor]
|
|
22
22
|
|
|
23
23
|
# The orientation of the separator.
|
|
@@ -11,7 +11,7 @@ from reflex.components.radix.themes.base import RadixThemesComponent
|
|
|
11
11
|
from reflex.event import EventType, PointerEventInfo
|
|
12
12
|
from reflex.vars.base import Var
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
LiteralSeparatorSize = Literal["1", "2", "3", "4"]
|
|
15
15
|
|
|
16
16
|
class Separator(RadixThemesComponent):
|
|
17
17
|
@classmethod
|
|
@@ -127,8 +127,8 @@ class Separator(RadixThemesComponent):
|
|
|
127
127
|
|
|
128
128
|
Args:
|
|
129
129
|
*children: Child components.
|
|
130
|
-
size: The size of the
|
|
131
|
-
color_scheme: The color of the
|
|
130
|
+
size: The size of the separator: "1" | "2" | "3" | "4"
|
|
131
|
+
color_scheme: The color of the separator
|
|
132
132
|
orientation: The orientation of the separator.
|
|
133
133
|
decorative: When true, signifies that it is purely visual, carries no semantic meaning, and ensures it is not present in the accessibility tree.
|
|
134
134
|
style: The style of the component.
|
|
@@ -8,7 +8,7 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone
|
|
|
8
8
|
class Recharts(Component):
|
|
9
9
|
"""A component that wraps a recharts lib."""
|
|
10
10
|
|
|
11
|
-
library = "recharts@3.0
|
|
11
|
+
library = "recharts@3.1.0"
|
|
12
12
|
|
|
13
13
|
def _get_style(self) -> dict:
|
|
14
14
|
return {"wrapperStyle": self.style}
|
|
@@ -17,7 +17,7 @@ class Recharts(Component):
|
|
|
17
17
|
class RechartsCharts(NoSSRComponent, MemoizationLeaf):
|
|
18
18
|
"""A component that wraps a recharts lib."""
|
|
19
19
|
|
|
20
|
-
library = "recharts@3.0
|
|
20
|
+
library = "recharts@3.1.0"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]
|
reflex/constants/installer.py
CHANGED
|
@@ -143,7 +143,7 @@ class PackageJson(SimpleNamespace):
|
|
|
143
143
|
"postcss-import": "16.1.1",
|
|
144
144
|
"@react-router/dev": _react_router_version,
|
|
145
145
|
"@react-router/fs-routes": _react_router_version,
|
|
146
|
-
"rolldown-vite": "7.0.
|
|
146
|
+
"rolldown-vite": "7.0.9",
|
|
147
147
|
}
|
|
148
148
|
OVERRIDES = {
|
|
149
149
|
# This should always match the `react` version in DEPENDENCIES for recharts compatibility.
|
reflex/event.py
CHANGED
|
@@ -109,7 +109,7 @@ class EventActionsMixin:
|
|
|
109
109
|
"""
|
|
110
110
|
return dataclasses.replace(
|
|
111
111
|
self,
|
|
112
|
-
event_actions={"stopPropagation": True
|
|
112
|
+
event_actions={**self.event_actions, "stopPropagation": True},
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
@property
|
|
@@ -121,7 +121,7 @@ class EventActionsMixin:
|
|
|
121
121
|
"""
|
|
122
122
|
return dataclasses.replace(
|
|
123
123
|
self,
|
|
124
|
-
event_actions={"preventDefault": True
|
|
124
|
+
event_actions={**self.event_actions, "preventDefault": True},
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
def throttle(self, limit_ms: int) -> Self:
|
|
@@ -135,7 +135,7 @@ class EventActionsMixin:
|
|
|
135
135
|
"""
|
|
136
136
|
return dataclasses.replace(
|
|
137
137
|
self,
|
|
138
|
-
event_actions={"throttle": limit_ms
|
|
138
|
+
event_actions={**self.event_actions, "throttle": limit_ms},
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
def debounce(self, delay_ms: int) -> Self:
|
|
@@ -149,7 +149,7 @@ class EventActionsMixin:
|
|
|
149
149
|
"""
|
|
150
150
|
return dataclasses.replace(
|
|
151
151
|
self,
|
|
152
|
-
event_actions={"debounce": delay_ms
|
|
152
|
+
event_actions={**self.event_actions, "debounce": delay_ms},
|
|
153
153
|
)
|
|
154
154
|
|
|
155
155
|
@property
|
|
@@ -161,7 +161,7 @@ class EventActionsMixin:
|
|
|
161
161
|
"""
|
|
162
162
|
return dataclasses.replace(
|
|
163
163
|
self,
|
|
164
|
-
event_actions={"temporal": True
|
|
164
|
+
event_actions={**self.event_actions, "temporal": True},
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
@@ -577,7 +577,7 @@ class JavascriptInputEvent:
|
|
|
577
577
|
init=True,
|
|
578
578
|
frozen=True,
|
|
579
579
|
)
|
|
580
|
-
class
|
|
580
|
+
class JavascriptKeyboardEvent:
|
|
581
581
|
"""Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent."""
|
|
582
582
|
|
|
583
583
|
key: str = ""
|
|
@@ -645,7 +645,7 @@ class KeyInputInfo(TypedDict):
|
|
|
645
645
|
|
|
646
646
|
|
|
647
647
|
def key_event(
|
|
648
|
-
e: ObjectVar[
|
|
648
|
+
e: ObjectVar[JavascriptKeyboardEvent],
|
|
649
649
|
) -> tuple[Var[str], Var[KeyInputInfo]]:
|
|
650
650
|
"""Get the key from a keyboard event.
|
|
651
651
|
|
|
@@ -1269,7 +1269,7 @@ def call_script(
|
|
|
1269
1269
|
Returns:
|
|
1270
1270
|
EventSpec: An event that will execute the client side javascript.
|
|
1271
1271
|
"""
|
|
1272
|
-
callback_kwargs = {}
|
|
1272
|
+
callback_kwargs = {"callback": None}
|
|
1273
1273
|
if callback is not None:
|
|
1274
1274
|
callback_kwargs = {
|
|
1275
1275
|
"callback": str(
|
|
@@ -2211,7 +2211,15 @@ class EventNamespace:
|
|
|
2211
2211
|
|
|
2212
2212
|
@overload
|
|
2213
2213
|
def __new__(
|
|
2214
|
-
cls,
|
|
2214
|
+
cls,
|
|
2215
|
+
func: None = None,
|
|
2216
|
+
*,
|
|
2217
|
+
background: bool | None = None,
|
|
2218
|
+
stop_propagation: bool | None = None,
|
|
2219
|
+
prevent_default: bool | None = None,
|
|
2220
|
+
throttle: int | None = None,
|
|
2221
|
+
debounce: int | None = None,
|
|
2222
|
+
temporal: bool | None = None,
|
|
2215
2223
|
) -> Callable[
|
|
2216
2224
|
[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]] # pyright: ignore [reportInvalidTypeVarUse]
|
|
2217
2225
|
]: ...
|
|
@@ -2222,6 +2230,11 @@ class EventNamespace:
|
|
|
2222
2230
|
func: Callable[[BASE_STATE, Unpack[P]], Any],
|
|
2223
2231
|
*,
|
|
2224
2232
|
background: bool | None = None,
|
|
2233
|
+
stop_propagation: bool | None = None,
|
|
2234
|
+
prevent_default: bool | None = None,
|
|
2235
|
+
throttle: int | None = None,
|
|
2236
|
+
debounce: int | None = None,
|
|
2237
|
+
temporal: bool | None = None,
|
|
2225
2238
|
) -> EventCallback[Unpack[P]]: ...
|
|
2226
2239
|
|
|
2227
2240
|
def __new__(
|
|
@@ -2229,6 +2242,11 @@ class EventNamespace:
|
|
|
2229
2242
|
func: Callable[[BASE_STATE, Unpack[P]], Any] | None = None,
|
|
2230
2243
|
*,
|
|
2231
2244
|
background: bool | None = None,
|
|
2245
|
+
stop_propagation: bool | None = None,
|
|
2246
|
+
prevent_default: bool | None = None,
|
|
2247
|
+
throttle: int | None = None,
|
|
2248
|
+
debounce: int | None = None,
|
|
2249
|
+
temporal: bool | None = None,
|
|
2232
2250
|
) -> (
|
|
2233
2251
|
EventCallback[Unpack[P]]
|
|
2234
2252
|
| Callable[[Callable[[BASE_STATE, Unpack[P]], Any]], EventCallback[Unpack[P]]]
|
|
@@ -2238,6 +2256,11 @@ class EventNamespace:
|
|
|
2238
2256
|
Args:
|
|
2239
2257
|
func: The function to wrap.
|
|
2240
2258
|
background: Whether the event should be run in the background. Defaults to False.
|
|
2259
|
+
stop_propagation: Whether to stop the event from bubbling up the DOM tree.
|
|
2260
|
+
prevent_default: Whether to prevent the default behavior of the event.
|
|
2261
|
+
throttle: Throttle the event handler to limit calls (in milliseconds).
|
|
2262
|
+
debounce: Debounce the event handler to delay calls (in milliseconds).
|
|
2263
|
+
temporal: Whether the event should be dropped when the backend is down.
|
|
2241
2264
|
|
|
2242
2265
|
Raises:
|
|
2243
2266
|
TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402
|
|
@@ -2246,6 +2269,30 @@ class EventNamespace:
|
|
|
2246
2269
|
The wrapped function.
|
|
2247
2270
|
"""
|
|
2248
2271
|
|
|
2272
|
+
def _build_event_actions():
|
|
2273
|
+
"""Build event_actions dict from decorator parameters.
|
|
2274
|
+
|
|
2275
|
+
Returns:
|
|
2276
|
+
Dict of event actions to apply, or empty dict if none specified.
|
|
2277
|
+
"""
|
|
2278
|
+
if not any(
|
|
2279
|
+
[stop_propagation, prevent_default, throttle, debounce, temporal]
|
|
2280
|
+
):
|
|
2281
|
+
return {}
|
|
2282
|
+
|
|
2283
|
+
event_actions = {}
|
|
2284
|
+
if stop_propagation is not None:
|
|
2285
|
+
event_actions["stopPropagation"] = stop_propagation
|
|
2286
|
+
if prevent_default is not None:
|
|
2287
|
+
event_actions["preventDefault"] = prevent_default
|
|
2288
|
+
if throttle is not None:
|
|
2289
|
+
event_actions["throttle"] = throttle
|
|
2290
|
+
if debounce is not None:
|
|
2291
|
+
event_actions["debounce"] = debounce
|
|
2292
|
+
if temporal is not None:
|
|
2293
|
+
event_actions["temporal"] = temporal
|
|
2294
|
+
return event_actions
|
|
2295
|
+
|
|
2249
2296
|
def wrapper(
|
|
2250
2297
|
func: Callable[[BASE_STATE, Unpack[P]], T],
|
|
2251
2298
|
) -> EventCallback[Unpack[P]]:
|
|
@@ -2281,8 +2328,22 @@ class EventNamespace:
|
|
|
2281
2328
|
object.__setattr__(func, "__name__", name)
|
|
2282
2329
|
object.__setattr__(func, "__qualname__", name)
|
|
2283
2330
|
state_cls._add_event_handler(name, func)
|
|
2284
|
-
|
|
2331
|
+
event_callback = getattr(state_cls, name)
|
|
2332
|
+
|
|
2333
|
+
# Apply decorator event actions
|
|
2334
|
+
event_actions = _build_event_actions()
|
|
2335
|
+
if event_actions:
|
|
2336
|
+
# Create new EventCallback with updated event_actions
|
|
2337
|
+
event_callback = dataclasses.replace(
|
|
2338
|
+
event_callback, event_actions=event_actions
|
|
2339
|
+
)
|
|
2340
|
+
|
|
2341
|
+
return event_callback
|
|
2285
2342
|
|
|
2343
|
+
# Store decorator event actions on the function for later processing
|
|
2344
|
+
event_actions = _build_event_actions()
|
|
2345
|
+
if event_actions:
|
|
2346
|
+
func._rx_event_actions = event_actions # pyright: ignore [reportFunctionMemberAccess]
|
|
2286
2347
|
return func # pyright: ignore [reportReturnType]
|
|
2287
2348
|
|
|
2288
2349
|
if func is not None:
|
reflex/model.py
CHANGED
|
@@ -19,6 +19,7 @@ import sqlalchemy.exc
|
|
|
19
19
|
import sqlalchemy.ext.asyncio
|
|
20
20
|
import sqlalchemy.orm
|
|
21
21
|
from alembic.runtime.migration import MigrationContext
|
|
22
|
+
from alembic.script.base import Script
|
|
22
23
|
|
|
23
24
|
from reflex.base import Base
|
|
24
25
|
from reflex.config import get_config
|
|
@@ -34,6 +35,41 @@ _AsyncSessionLocal: dict[str | None, sqlalchemy.ext.asyncio.async_sessionmaker]
|
|
|
34
35
|
from sqlmodel.ext.asyncio.session import AsyncSession # noqa: E402
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def format_revision(
|
|
39
|
+
rev: Script,
|
|
40
|
+
current_rev: str | None,
|
|
41
|
+
current_reached_ref: list[bool],
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Format a single revision for display.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
rev: The alembic script object
|
|
47
|
+
current_rev: The currently applied revision ID
|
|
48
|
+
current_reached_ref: Mutable reference to track if we've reached current revision
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Formatted string for display
|
|
52
|
+
"""
|
|
53
|
+
current = rev.revision
|
|
54
|
+
message = rev.doc
|
|
55
|
+
|
|
56
|
+
# Determine if this migration is applied
|
|
57
|
+
if current_rev is None:
|
|
58
|
+
is_applied = False
|
|
59
|
+
elif current == current_rev:
|
|
60
|
+
is_applied = True
|
|
61
|
+
current_reached_ref[0] = True
|
|
62
|
+
else:
|
|
63
|
+
is_applied = not current_reached_ref[0]
|
|
64
|
+
|
|
65
|
+
# Show checkmark or X with colors
|
|
66
|
+
status_icon = "[green]✓[/green]" if is_applied else "[red]✗[/red]"
|
|
67
|
+
head_marker = " (head)" if rev.is_head else ""
|
|
68
|
+
|
|
69
|
+
# Format output with message
|
|
70
|
+
return f" [{status_icon}] {current}{head_marker}, {message}"
|
|
71
|
+
|
|
72
|
+
|
|
37
73
|
def _safe_db_url_for_logging(url: str) -> str:
|
|
38
74
|
"""Remove username and password from the database URL for logging.
|
|
39
75
|
|
|
@@ -361,6 +397,25 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue
|
|
|
361
397
|
directory=str(environment.ALEMBIC_CONFIG.get().parent / "alembic"),
|
|
362
398
|
)
|
|
363
399
|
|
|
400
|
+
@classmethod
|
|
401
|
+
def get_migration_history(cls):
|
|
402
|
+
"""Get migration history with current database state.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
tuple: (current_revision, revisions_list) where revisions_list is in chronological order
|
|
406
|
+
"""
|
|
407
|
+
# Get current revision from database
|
|
408
|
+
with cls.get_db_engine().connect() as connection:
|
|
409
|
+
context = MigrationContext.configure(connection)
|
|
410
|
+
current_rev = context.get_current_revision()
|
|
411
|
+
|
|
412
|
+
# Get all revisions from base to head
|
|
413
|
+
_, script_dir = cls._alembic_config()
|
|
414
|
+
revisions = list(script_dir.walk_revisions())
|
|
415
|
+
revisions.reverse() # Reverse to get chronological order (base first)
|
|
416
|
+
|
|
417
|
+
return current_rev, revisions
|
|
418
|
+
|
|
364
419
|
@classmethod
|
|
365
420
|
def alembic_autogenerate(
|
|
366
421
|
cls,
|
reflex/plugins/base.py
CHANGED
|
@@ -17,7 +17,7 @@ class CommonContext(TypedDict):
|
|
|
17
17
|
P = ParamSpec("P")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class
|
|
20
|
+
class AddTaskProtocol(Protocol):
|
|
21
21
|
"""Protocol for adding a task to the pre-compile context."""
|
|
22
22
|
|
|
23
23
|
def __call__(
|
|
@@ -39,7 +39,7 @@ class AddTaskProtcol(Protocol):
|
|
|
39
39
|
class PreCompileContext(CommonContext):
|
|
40
40
|
"""Context for pre-compile hooks."""
|
|
41
41
|
|
|
42
|
-
add_save_task:
|
|
42
|
+
add_save_task: AddTaskProtocol
|
|
43
43
|
add_modify_task: Callable[[str, Callable[[str], str]], None]
|
|
44
44
|
unevaluated_pages: Sequence["UnevaluatedPage"]
|
|
45
45
|
|
reflex/reflex.py
CHANGED
|
@@ -540,6 +540,39 @@ def migrate():
|
|
|
540
540
|
prerequisites.check_schema_up_to_date()
|
|
541
541
|
|
|
542
542
|
|
|
543
|
+
@db_cli.command()
|
|
544
|
+
def status():
|
|
545
|
+
"""Check the status of the database schema."""
|
|
546
|
+
from reflex.model import Model, format_revision
|
|
547
|
+
from reflex.utils import prerequisites
|
|
548
|
+
|
|
549
|
+
prerequisites.get_app()
|
|
550
|
+
if not prerequisites.check_db_initialized():
|
|
551
|
+
console.info(
|
|
552
|
+
"Database is not initialized. Run [bold]reflex db init[/bold] to initialize."
|
|
553
|
+
)
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
# Run alembic check command and display output
|
|
557
|
+
import reflex.config
|
|
558
|
+
|
|
559
|
+
config = reflex.config.get_config()
|
|
560
|
+
console.print(f"[bold]\\[{config.db_url}][/bold]")
|
|
561
|
+
|
|
562
|
+
# Get migration history using Model method
|
|
563
|
+
current_rev, revisions = Model.get_migration_history()
|
|
564
|
+
if current_rev is None and not revisions:
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
current_reached_ref = [current_rev is None]
|
|
568
|
+
|
|
569
|
+
# Show migration history in chronological order
|
|
570
|
+
console.print("<base>")
|
|
571
|
+
for rev in revisions:
|
|
572
|
+
# Format and print the revision
|
|
573
|
+
console.print(format_revision(rev, current_rev, current_reached_ref))
|
|
574
|
+
|
|
575
|
+
|
|
543
576
|
@db_cli.command()
|
|
544
577
|
@click.option(
|
|
545
578
|
"--message",
|
reflex/route.py
CHANGED
|
@@ -131,7 +131,7 @@ def replace_brackets_with_keywords(input_string: str) -> str:
|
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def
|
|
134
|
+
def route_specificity(keyworded_route: str) -> tuple[int, int, int]:
|
|
135
135
|
"""Get the specificity of a route with keywords.
|
|
136
136
|
|
|
137
137
|
The smaller the number, the more specific the route is.
|
|
@@ -193,13 +193,13 @@ def get_router(routes: list[str]) -> Callable[[str], str | None]:
|
|
|
193
193
|
keyworded_routes = {
|
|
194
194
|
replace_brackets_with_keywords(route): route for route in routes
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
sorted_routes_by_specificity = sorted(
|
|
197
197
|
keyworded_routes.items(),
|
|
198
|
-
key=lambda item:
|
|
198
|
+
key=lambda item: route_specificity(item[0]),
|
|
199
199
|
)
|
|
200
200
|
regexed_routes = [
|
|
201
201
|
(get_route_regex(keyworded_route), original_route)
|
|
202
|
-
for keyworded_route, original_route in
|
|
202
|
+
for keyworded_route, original_route in sorted_routes_by_specificity
|
|
203
203
|
]
|
|
204
204
|
|
|
205
205
|
def get_route(path: str) -> str | None:
|
reflex/state.py
CHANGED
|
@@ -1100,7 +1100,12 @@ class BaseState(EvenMoreBasicBaseState):
|
|
|
1100
1100
|
Returns:
|
|
1101
1101
|
The event handler.
|
|
1102
1102
|
"""
|
|
1103
|
-
|
|
1103
|
+
# Check if function has stored event_actions from decorator
|
|
1104
|
+
event_actions = getattr(fn, "_rx_event_actions", {})
|
|
1105
|
+
|
|
1106
|
+
return EventHandler(
|
|
1107
|
+
fn=fn, state_full_name=cls.get_full_name(), event_actions=event_actions
|
|
1108
|
+
)
|
|
1104
1109
|
|
|
1105
1110
|
@classmethod
|
|
1106
1111
|
def _create_setvar(cls):
|
|
@@ -2412,19 +2417,19 @@ class FrontendEventExceptionState(State):
|
|
|
2412
2417
|
"""Substate for handling frontend exceptions."""
|
|
2413
2418
|
|
|
2414
2419
|
@event
|
|
2415
|
-
def handle_frontend_exception(self,
|
|
2420
|
+
def handle_frontend_exception(self, info: str, component_stack: str) -> None:
|
|
2416
2421
|
"""Handle frontend exceptions.
|
|
2417
2422
|
|
|
2418
2423
|
If a frontend exception handler is provided, it will be called.
|
|
2419
2424
|
Otherwise, the default frontend exception handler will be called.
|
|
2420
2425
|
|
|
2421
2426
|
Args:
|
|
2422
|
-
|
|
2427
|
+
info: The exception information.
|
|
2423
2428
|
component_stack: The stack trace of the component where the exception occurred.
|
|
2424
2429
|
|
|
2425
2430
|
"""
|
|
2426
2431
|
prerequisites.get_and_validate_app().app.frontend_exception_handler(
|
|
2427
|
-
Exception(
|
|
2432
|
+
Exception(info)
|
|
2428
2433
|
)
|
|
2429
2434
|
|
|
2430
2435
|
|
reflex/utils/console.py
CHANGED
|
@@ -31,7 +31,7 @@ _EMITTED_DEPRECATION_WARNINGS = set()
|
|
|
31
31
|
_EMITTED_INFO = set()
|
|
32
32
|
|
|
33
33
|
# Warnings which have been printed.
|
|
34
|
-
|
|
34
|
+
_EMITTED_WARNINGS = set()
|
|
35
35
|
|
|
36
36
|
# Errors which have been printed.
|
|
37
37
|
_EMITTED_ERRORS = set()
|
|
@@ -235,9 +235,9 @@ def warn(msg: str, *, dedupe: bool = False, **kwargs):
|
|
|
235
235
|
"""
|
|
236
236
|
if _LOG_LEVEL <= LogLevel.WARNING:
|
|
237
237
|
if dedupe:
|
|
238
|
-
if msg in
|
|
238
|
+
if msg in _EMITTED_WARNINGS:
|
|
239
239
|
return
|
|
240
|
-
|
|
240
|
+
_EMITTED_WARNINGS.add(msg)
|
|
241
241
|
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
|
|
242
242
|
if should_use_log_file_console():
|
|
243
243
|
print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
|
reflex/utils/exec.py
CHANGED
|
@@ -405,7 +405,10 @@ def get_reload_paths() -> Sequence[Path]:
|
|
|
405
405
|
module_path = module_path.parent
|
|
406
406
|
|
|
407
407
|
while module_path.parent.name and _has_child_file(module_path, "__init__.py"):
|
|
408
|
-
if
|
|
408
|
+
if (
|
|
409
|
+
_has_child_file(module_path, "rxconfig.py")
|
|
410
|
+
and module_path == Path.cwd()
|
|
411
|
+
):
|
|
409
412
|
init_file = module_path / "__init__.py"
|
|
410
413
|
init_file_content = init_file.read_text()
|
|
411
414
|
if init_file_content.strip():
|
|
@@ -565,6 +568,12 @@ def run_backend_prod(
|
|
|
565
568
|
run_uvicorn_backend_prod(host, port, loglevel)
|
|
566
569
|
|
|
567
570
|
|
|
571
|
+
def _get_backend_workers():
|
|
572
|
+
from reflex.utils import processes
|
|
573
|
+
|
|
574
|
+
return processes.get_num_workers()
|
|
575
|
+
|
|
576
|
+
|
|
568
577
|
def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
|
|
569
578
|
"""Run the backend in production mode using Uvicorn.
|
|
570
579
|
|
|
@@ -585,6 +594,7 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
|
|
|
585
594
|
"uvicorn",
|
|
586
595
|
*("--host", host),
|
|
587
596
|
*("--port", str(port)),
|
|
597
|
+
*("--workers", str(_get_backend_workers())),
|
|
588
598
|
"--factory",
|
|
589
599
|
app_module,
|
|
590
600
|
]
|
|
@@ -598,8 +608,8 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel):
|
|
|
598
608
|
command = [
|
|
599
609
|
"gunicorn",
|
|
600
610
|
"--preload",
|
|
601
|
-
"--worker-class",
|
|
602
|
-
"
|
|
611
|
+
*("--worker-class", "uvicorn.workers.UvicornH11Worker"),
|
|
612
|
+
*("--threads", str(_get_backend_workers())),
|
|
603
613
|
*("--bind", f"{host}:{port}"),
|
|
604
614
|
*env_args,
|
|
605
615
|
f"{app_module}()",
|
|
@@ -639,13 +649,19 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel):
|
|
|
639
649
|
*("--interface", str(Interfaces.ASGI)),
|
|
640
650
|
*("--factory", get_app_instance_from_file()),
|
|
641
651
|
]
|
|
652
|
+
|
|
653
|
+
extra_env = {
|
|
654
|
+
environment.REFLEX_SKIP_COMPILE.name: "true", # skip compile for prod backend
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if "GRANIAN_WORKERS" not in os.environ:
|
|
658
|
+
extra_env["GRANIAN_WORKERS"] = str(_get_backend_workers())
|
|
659
|
+
|
|
642
660
|
processes.new_process(
|
|
643
661
|
command,
|
|
644
662
|
run=True,
|
|
645
663
|
show_logs=True,
|
|
646
|
-
env=
|
|
647
|
-
environment.REFLEX_SKIP_COMPILE.name: "true"
|
|
648
|
-
}, # skip compile for prod backend
|
|
664
|
+
env=extra_env,
|
|
649
665
|
)
|
|
650
666
|
|
|
651
667
|
|
reflex/utils/format.py
CHANGED
|
@@ -251,7 +251,7 @@ def _escape_js_string(string: str) -> str:
|
|
|
251
251
|
The escaped string.
|
|
252
252
|
"""
|
|
253
253
|
|
|
254
|
-
# TODO: we may need to re-
|
|
254
|
+
# TODO: we may need to re-visit this logic after new Var API is implemented.
|
|
255
255
|
def escape_outside_segments(segment: str):
|
|
256
256
|
"""Escape backticks in segments outside of `${}`.
|
|
257
257
|
|
reflex/utils/processes.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import signal
|
|
9
9
|
import socket
|
|
10
10
|
import subprocess
|
|
11
|
+
import sys
|
|
11
12
|
from collections.abc import Callable, Generator, Sequence
|
|
12
13
|
from concurrent import futures
|
|
13
14
|
from contextlib import closing
|
|
@@ -68,12 +69,11 @@ def _can_bind_at_port(
|
|
|
68
69
|
"""
|
|
69
70
|
try:
|
|
70
71
|
with closing(socket.socket(address_family, socket.SOCK_STREAM)) as sock:
|
|
72
|
+
if sys.platform != "win32":
|
|
73
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
71
74
|
sock.bind((address, port))
|
|
72
|
-
except OverflowError:
|
|
73
|
-
|
|
74
|
-
except PermissionError:
|
|
75
|
-
return False
|
|
76
|
-
except OSError:
|
|
75
|
+
except (OverflowError, PermissionError, OSError) as e:
|
|
76
|
+
console.warn(f"Unable to bind to {address}:{port} due to: {e}.")
|
|
77
77
|
return False
|
|
78
78
|
return True
|
|
79
79
|
|
|
@@ -87,38 +87,13 @@ def is_process_on_port(port: int) -> bool:
|
|
|
87
87
|
Returns:
|
|
88
88
|
Whether a process is running on the given port.
|
|
89
89
|
"""
|
|
90
|
-
return
|
|
91
|
-
socket.AF_INET, "
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
) # Test IPv6 localhost (::1)
|
|
90
|
+
return (
|
|
91
|
+
not _can_bind_at_port(socket.AF_INET, "", port) # Test IPv4 local network
|
|
92
|
+
or not _can_bind_at_port(socket.AF_INET6, "", port) # Test IPv6 local network
|
|
93
|
+
)
|
|
95
94
|
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
"""Change the port.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
port: The port.
|
|
102
|
-
_type: The type of the port.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
The new port.
|
|
106
|
-
|
|
107
|
-
Raises:
|
|
108
|
-
Exit: If the port is invalid or if the new port is occupied.
|
|
109
|
-
"""
|
|
110
|
-
new_port = port + 1
|
|
111
|
-
if new_port < 0 or new_port > 65535:
|
|
112
|
-
console.error(
|
|
113
|
-
f"The {_type} port: {port} is invalid. It must be between 0 and 65535."
|
|
114
|
-
)
|
|
115
|
-
raise click.exceptions.Exit(1)
|
|
116
|
-
if is_process_on_port(new_port):
|
|
117
|
-
return change_port(new_port, _type)
|
|
118
|
-
console.info(
|
|
119
|
-
f"The {_type} will run on port [bold underline]{new_port}[/bold underline]."
|
|
120
|
-
)
|
|
121
|
-
return new_port
|
|
96
|
+
MAXIMUM_PORT = 2**16 - 1
|
|
122
97
|
|
|
123
98
|
|
|
124
99
|
def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
|
|
@@ -137,13 +112,28 @@ def handle_port(service_name: str, port: int, auto_increment: bool) -> int:
|
|
|
137
112
|
Exit:when the port is in use.
|
|
138
113
|
"""
|
|
139
114
|
console.debug(f"Checking if {service_name.capitalize()} port: {port} is in use.")
|
|
115
|
+
|
|
140
116
|
if not is_process_on_port(port):
|
|
141
117
|
console.debug(f"{service_name.capitalize()} port: {port} is not in use.")
|
|
142
118
|
return port
|
|
119
|
+
|
|
143
120
|
if auto_increment:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
121
|
+
for new_port in range(port + 1, MAXIMUM_PORT + 1):
|
|
122
|
+
if not is_process_on_port(new_port):
|
|
123
|
+
console.info(
|
|
124
|
+
f"The {service_name} will run on port [bold underline]{new_port}[/bold underline]."
|
|
125
|
+
)
|
|
126
|
+
return new_port
|
|
127
|
+
console.debug(
|
|
128
|
+
f"{service_name.capitalize()} port: {new_port} is already in use."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# If we reach here, it means we couldn't find an available port.
|
|
132
|
+
console.error(f"Unable to find an available port for {service_name}")
|
|
133
|
+
else:
|
|
134
|
+
console.error(f"{service_name.capitalize()} port: {port} is already in use.")
|
|
135
|
+
|
|
136
|
+
raise click.exceptions.Exit(1)
|
|
147
137
|
|
|
148
138
|
|
|
149
139
|
@overload
|
reflex/utils/types.py
CHANGED
|
@@ -478,7 +478,7 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None
|
|
|
478
478
|
if type_ in PrimitiveToAnnotation:
|
|
479
479
|
type_ = PrimitiveToAnnotation[type_]
|
|
480
480
|
type_ = type_[item_type] # pyright: ignore [reportIndexIssue]
|
|
481
|
-
if column.nullable:
|
|
481
|
+
if hasattr(column, "nullable") and column.nullable:
|
|
482
482
|
type_ = type_ | None
|
|
483
483
|
return type_
|
|
484
484
|
if name in insp.all_orm_descriptors:
|