reflex 0.6.8a1__py3-none-any.whl → 0.7.0__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/custom_components/pyproject.toml.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +7 -7
- reflex/.templates/jinja/web/pages/utils.js.jinja2 +3 -3
- reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +1 -4
- reflex/.templates/web/utils/state.js +65 -36
- reflex/__init__.py +4 -17
- reflex/__init__.pyi +1 -2
- reflex/app.py +286 -135
- reflex/app_mixins/lifespan.py +9 -9
- reflex/app_mixins/middleware.py +6 -6
- reflex/app_module_for_backend.py +3 -7
- reflex/base.py +7 -7
- reflex/compiler/compiler.py +8 -0
- reflex/compiler/utils.py +57 -18
- reflex/components/base/app_wrap.pyi +16 -16
- reflex/components/base/bare.py +1 -1
- reflex/components/base/body.pyi +16 -16
- reflex/components/base/document.pyi +76 -76
- reflex/components/base/error_boundary.py +2 -1
- reflex/components/base/error_boundary.pyi +19 -22
- reflex/components/base/fragment.pyi +16 -16
- reflex/components/base/head.pyi +31 -31
- reflex/components/base/link.pyi +31 -31
- reflex/components/base/meta.py +2 -2
- reflex/components/base/meta.pyi +61 -61
- reflex/components/base/script.pyi +19 -19
- reflex/components/base/strict_mode.py +10 -0
- reflex/components/base/strict_mode.pyi +57 -0
- reflex/components/component.py +38 -77
- reflex/components/core/banner.py +159 -4
- reflex/components/core/banner.pyi +162 -76
- reflex/components/core/breakpoints.py +3 -1
- reflex/components/core/client_side_routing.py +1 -1
- reflex/components/core/client_side_routing.pyi +32 -32
- reflex/components/core/clipboard.pyi +17 -20
- reflex/components/core/cond.py +9 -10
- reflex/components/core/debounce.py +1 -1
- reflex/components/core/debounce.pyi +17 -17
- reflex/components/core/foreach.py +28 -3
- reflex/components/core/html.py +1 -1
- reflex/components/core/html.pyi +16 -16
- reflex/components/core/match.py +5 -5
- reflex/components/core/sticky.py +134 -0
- reflex/components/core/sticky.pyi +449 -0
- reflex/components/core/upload.py +2 -2
- reflex/components/core/upload.pyi +80 -88
- reflex/components/datadisplay/code.py +5 -14
- reflex/components/datadisplay/code.pyi +31 -31
- reflex/components/datadisplay/dataeditor.py +7 -4
- reflex/components/datadisplay/dataeditor.pyi +40 -54
- reflex/components/datadisplay/logo.py +13 -8
- reflex/components/datadisplay/shiki_code_block.py +14 -9
- reflex/components/datadisplay/shiki_code_block.pyi +46 -46
- reflex/components/dynamic.py +22 -3
- reflex/components/el/constants/reflex.py +1 -1
- reflex/components/el/element.py +1 -1
- reflex/components/el/element.pyi +16 -16
- reflex/components/el/elements/base.pyi +16 -16
- reflex/components/el/elements/forms.py +4 -4
- reflex/components/el/elements/forms.pyi +224 -258
- reflex/components/el/elements/inline.pyi +421 -421
- reflex/components/el/elements/media.pyi +376 -376
- reflex/components/el/elements/metadata.pyi +91 -91
- reflex/components/el/elements/other.pyi +106 -106
- reflex/components/el/elements/scripts.pyi +46 -46
- reflex/components/el/elements/sectioning.pyi +226 -226
- reflex/components/el/elements/tables.pyi +151 -151
- reflex/components/el/elements/typography.pyi +226 -226
- reflex/components/gridjs/datatable.pyi +31 -31
- reflex/components/lucide/icon.py +46 -8
- reflex/components/lucide/icon.pyi +85 -31
- reflex/components/markdown/markdown.py +10 -8
- reflex/components/markdown/markdown.pyi +16 -16
- reflex/components/moment/moment.py +2 -2
- reflex/components/moment/moment.pyi +17 -19
- reflex/components/next/base.pyi +16 -16
- reflex/components/next/image.py +16 -4
- reflex/components/next/image.pyi +22 -20
- reflex/components/next/link.py +1 -1
- reflex/components/next/link.pyi +16 -16
- reflex/components/next/video.pyi +16 -16
- reflex/components/plotly/__init__.py +29 -2
- reflex/components/plotly/plotly.py +240 -5
- reflex/components/plotly/plotly.pyi +799 -44
- reflex/components/props.py +3 -3
- reflex/components/radix/__init__.pyi +1 -1
- reflex/components/radix/primitives/accordion.py +9 -5
- reflex/components/radix/primitives/accordion.pyi +110 -108
- reflex/components/radix/primitives/base.pyi +31 -31
- reflex/components/radix/primitives/drawer.py +5 -2
- reflex/components/radix/primitives/drawer.pyi +179 -187
- reflex/components/radix/primitives/form.pyi +160 -172
- reflex/components/radix/primitives/progress.py +1 -1
- reflex/components/radix/primitives/progress.pyi +76 -76
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/primitives/slider.pyi +78 -82
- reflex/components/radix/themes/base.pyi +121 -121
- reflex/components/radix/themes/color_mode.py +11 -9
- reflex/components/radix/themes/color_mode.pyi +47 -49
- reflex/components/radix/themes/components/alert_dialog.py +3 -0
- reflex/components/radix/themes/components/alert_dialog.pyi +110 -112
- reflex/components/radix/themes/components/aspect_ratio.pyi +16 -16
- reflex/components/radix/themes/components/avatar.pyi +16 -16
- reflex/components/radix/themes/components/badge.pyi +16 -16
- reflex/components/radix/themes/components/button.pyi +16 -16
- reflex/components/radix/themes/components/callout.pyi +76 -76
- reflex/components/radix/themes/components/card.py +1 -1
- reflex/components/radix/themes/components/card.pyi +17 -17
- reflex/components/radix/themes/components/checkbox.pyi +49 -55
- reflex/components/radix/themes/components/checkbox_cards.pyi +31 -31
- reflex/components/radix/themes/components/checkbox_group.pyi +31 -31
- reflex/components/radix/themes/components/context_menu.py +5 -0
- reflex/components/radix/themes/components/context_menu.pyi +149 -155
- reflex/components/radix/themes/components/data_list.pyi +61 -61
- reflex/components/radix/themes/components/dialog.py +3 -0
- reflex/components/radix/themes/components/dialog.pyi +113 -117
- reflex/components/radix/themes/components/dropdown_menu.py +5 -0
- reflex/components/radix/themes/components/dropdown_menu.pyi +133 -137
- reflex/components/radix/themes/components/hover_card.py +3 -0
- reflex/components/radix/themes/components/hover_card.pyi +63 -67
- reflex/components/radix/themes/components/icon_button.py +2 -2
- reflex/components/radix/themes/components/icon_button.pyi +17 -16
- reflex/components/radix/themes/components/inset.pyi +16 -16
- reflex/components/radix/themes/components/popover.py +3 -0
- reflex/components/radix/themes/components/popover.pyi +68 -70
- reflex/components/radix/themes/components/progress.pyi +16 -16
- reflex/components/radix/themes/components/radio.pyi +16 -16
- reflex/components/radix/themes/components/radio_cards.py +2 -0
- reflex/components/radix/themes/components/radio_cards.pyi +32 -34
- reflex/components/radix/themes/components/radio_group.py +1 -1
- reflex/components/radix/themes/components/radio_group.pyi +62 -64
- reflex/components/radix/themes/components/scroll_area.pyi +16 -16
- reflex/components/radix/themes/components/segmented_control.pyi +32 -35
- reflex/components/radix/themes/components/select.py +4 -0
- reflex/components/radix/themes/components/select.pyi +145 -157
- reflex/components/radix/themes/components/separator.pyi +16 -16
- reflex/components/radix/themes/components/skeleton.py +3 -0
- reflex/components/radix/themes/components/skeleton.pyi +16 -16
- reflex/components/radix/themes/components/slider.pyi +22 -28
- reflex/components/radix/themes/components/spinner.pyi +16 -16
- reflex/components/radix/themes/components/switch.pyi +17 -19
- reflex/components/radix/themes/components/table.pyi +106 -106
- reflex/components/radix/themes/components/tabs.py +3 -0
- reflex/components/radix/themes/components/tabs.pyi +78 -82
- reflex/components/radix/themes/components/text_area.py +12 -0
- reflex/components/radix/themes/components/text_area.pyi +21 -33
- reflex/components/radix/themes/components/text_field.py +1 -1
- reflex/components/radix/themes/components/text_field.pyi +52 -80
- reflex/components/radix/themes/components/tooltip.py +6 -1
- reflex/components/radix/themes/components/tooltip.pyi +20 -21
- reflex/components/radix/themes/layout/__init__.pyi +1 -1
- reflex/components/radix/themes/layout/base.pyi +16 -16
- reflex/components/radix/themes/layout/box.pyi +16 -16
- reflex/components/radix/themes/layout/center.pyi +16 -16
- reflex/components/radix/themes/layout/container.pyi +16 -16
- reflex/components/radix/themes/layout/flex.pyi +16 -16
- reflex/components/radix/themes/layout/grid.pyi +16 -16
- reflex/components/radix/themes/layout/list.py +2 -2
- reflex/components/radix/themes/layout/list.pyi +76 -76
- reflex/components/radix/themes/layout/section.pyi +16 -16
- reflex/components/radix/themes/layout/spacer.pyi +16 -16
- reflex/components/radix/themes/layout/stack.py +2 -2
- reflex/components/radix/themes/layout/stack.pyi +46 -46
- reflex/components/radix/themes/typography/blockquote.pyi +16 -16
- reflex/components/radix/themes/typography/code.pyi +16 -16
- reflex/components/radix/themes/typography/heading.pyi +16 -16
- reflex/components/radix/themes/typography/link.py +1 -1
- reflex/components/radix/themes/typography/link.pyi +16 -16
- reflex/components/radix/themes/typography/text.py +2 -2
- reflex/components/radix/themes/typography/text.pyi +106 -106
- reflex/components/react_player/audio.pyi +33 -39
- reflex/components/react_player/react_player.py +1 -1
- reflex/components/react_player/react_player.pyi +32 -38
- reflex/components/react_player/video.pyi +33 -39
- reflex/components/recharts/__init__.py +2 -0
- reflex/components/recharts/__init__.pyi +2 -0
- reflex/components/recharts/cartesian.pyi +282 -282
- reflex/components/recharts/charts.py +15 -15
- reflex/components/recharts/charts.pyi +164 -164
- reflex/components/recharts/general.py +19 -4
- reflex/components/recharts/general.pyi +132 -81
- reflex/components/recharts/polar.py +2 -2
- reflex/components/recharts/polar.pyi +55 -55
- reflex/components/recharts/recharts.py +4 -4
- reflex/components/recharts/recharts.pyi +31 -31
- reflex/components/sonner/toast.py +15 -13
- reflex/components/sonner/toast.pyi +22 -22
- reflex/components/suneditor/editor.py +6 -4
- reflex/components/suneditor/editor.pyi +26 -40
- reflex/components/tags/iter_tag.py +3 -3
- reflex/components/tags/tag.py +25 -3
- reflex/config.py +48 -20
- reflex/constants/__init__.py +1 -0
- reflex/constants/base.py +4 -1
- reflex/constants/compiler.py +5 -2
- reflex/constants/config.py +8 -1
- reflex/constants/installer.py +9 -9
- reflex/constants/style.py +1 -1
- reflex/custom_components/custom_components.py +18 -10
- reflex/event.py +228 -233
- reflex/experimental/__init__.py +19 -11
- reflex/experimental/client_state.py +53 -28
- reflex/experimental/hooks.py +5 -5
- reflex/experimental/layout.py +8 -5
- reflex/experimental/layout.pyi +79 -83
- reflex/experimental/misc.py +3 -3
- reflex/istate/wrappers.py +1 -1
- reflex/middleware/hydrate_middleware.py +2 -2
- reflex/model.py +11 -6
- reflex/page.py +5 -5
- reflex/reflex.py +104 -26
- reflex/route.py +1 -1
- reflex/state.py +358 -401
- reflex/style.py +27 -3
- reflex/testing.py +34 -39
- reflex/utils/build.py +6 -2
- reflex/utils/codespaces.py +1 -4
- reflex/utils/compat.py +6 -5
- reflex/utils/console.py +71 -21
- reflex/utils/exceptions.py +89 -26
- reflex/utils/exec.py +69 -74
- reflex/utils/export.py +6 -1
- reflex/utils/format.py +8 -40
- reflex/utils/imports.py +5 -2
- reflex/utils/lazy_loader.py +7 -1
- reflex/utils/path_ops.py +74 -14
- reflex/utils/prerequisites.py +345 -68
- reflex/utils/processes.py +45 -32
- reflex/utils/pyi_generator.py +39 -33
- reflex/utils/registry.py +4 -4
- reflex/utils/serializers.py +1 -1
- reflex/utils/telemetry.py +5 -4
- reflex/utils/types.py +42 -18
- reflex/vars/base.py +695 -330
- reflex/vars/datetime.py +6 -7
- reflex/vars/dep_tracking.py +344 -0
- reflex/vars/function.py +11 -5
- reflex/vars/number.py +31 -43
- reflex/vars/object.py +74 -64
- reflex/vars/sequence.py +79 -67
- {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/METADATA +7 -10
- reflex-0.7.0.dist-info/RECORD +401 -0
- {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/WHEEL +1 -1
- reflex/experimental/assets.py +0 -37
- reflex/proxy.py +0 -119
- reflex-0.6.8a1.dist-info/RECORD +0 -398
- {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/LICENSE +0 -0
- {reflex-0.6.8a1.dist-info → reflex-0.7.0.dist-info}/entry_points.txt +0 -0
reflex/app.py
CHANGED
|
@@ -25,8 +25,8 @@ from typing import (
|
|
|
25
25
|
Callable,
|
|
26
26
|
Coroutine,
|
|
27
27
|
Dict,
|
|
28
|
-
Generic,
|
|
29
28
|
List,
|
|
29
|
+
MutableMapping,
|
|
30
30
|
Optional,
|
|
31
31
|
Set,
|
|
32
32
|
Type,
|
|
@@ -53,22 +53,28 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme
|
|
|
53
53
|
from reflex.components.base.app_wrap import AppWrap
|
|
54
54
|
from reflex.components.base.error_boundary import ErrorBoundary
|
|
55
55
|
from reflex.components.base.fragment import Fragment
|
|
56
|
+
from reflex.components.base.strict_mode import StrictMode
|
|
56
57
|
from reflex.components.component import (
|
|
57
58
|
Component,
|
|
58
59
|
ComponentStyle,
|
|
59
60
|
evaluate_style_namespaces,
|
|
60
61
|
)
|
|
61
|
-
from reflex.components.core.banner import
|
|
62
|
+
from reflex.components.core.banner import (
|
|
63
|
+
backend_disabled,
|
|
64
|
+
connection_pulser,
|
|
65
|
+
connection_toaster,
|
|
66
|
+
)
|
|
62
67
|
from reflex.components.core.breakpoints import set_breakpoints
|
|
63
68
|
from reflex.components.core.client_side_routing import (
|
|
64
69
|
Default404Page,
|
|
65
70
|
wait_for_client_redirect,
|
|
66
71
|
)
|
|
72
|
+
from reflex.components.core.sticky import sticky
|
|
67
73
|
from reflex.components.core.upload import Upload, get_upload_dir
|
|
68
74
|
from reflex.components.radix import themes
|
|
69
75
|
from reflex.config import environment, get_config
|
|
70
76
|
from reflex.event import (
|
|
71
|
-
|
|
77
|
+
_EVENT_FIELDS,
|
|
72
78
|
Event,
|
|
73
79
|
EventHandler,
|
|
74
80
|
EventSpec,
|
|
@@ -93,7 +99,15 @@ from reflex.state import (
|
|
|
93
99
|
_substate_key,
|
|
94
100
|
code_uses_state_contexts,
|
|
95
101
|
)
|
|
96
|
-
from reflex.utils import
|
|
102
|
+
from reflex.utils import (
|
|
103
|
+
codespaces,
|
|
104
|
+
console,
|
|
105
|
+
exceptions,
|
|
106
|
+
format,
|
|
107
|
+
path_ops,
|
|
108
|
+
prerequisites,
|
|
109
|
+
types,
|
|
110
|
+
)
|
|
97
111
|
from reflex.utils.exec import is_prod_mode, is_testing_env
|
|
98
112
|
from reflex.utils.imports import ImportVar
|
|
99
113
|
|
|
@@ -144,7 +158,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
|
|
|
144
158
|
position="top-center",
|
|
145
159
|
id="backend_error",
|
|
146
160
|
style={"width": "500px"},
|
|
147
|
-
)
|
|
161
|
+
)
|
|
148
162
|
else:
|
|
149
163
|
error_message.insert(0, "An error occurred.")
|
|
150
164
|
return window_alert("\n".join(error_message))
|
|
@@ -156,9 +170,12 @@ def default_overlay_component() -> Component:
|
|
|
156
170
|
Returns:
|
|
157
171
|
The default overlay_component, which is a connection_modal.
|
|
158
172
|
"""
|
|
173
|
+
config = get_config()
|
|
174
|
+
|
|
159
175
|
return Fragment.create(
|
|
160
176
|
connection_pulser(),
|
|
161
177
|
connection_toaster(),
|
|
178
|
+
*([backend_disabled()] if config.is_reflex_cloud else []),
|
|
162
179
|
*codespaces.codespaces_auto_redirect(),
|
|
163
180
|
)
|
|
164
181
|
|
|
@@ -185,7 +202,7 @@ class OverlayFragment(Fragment):
|
|
|
185
202
|
@dataclasses.dataclass(
|
|
186
203
|
frozen=True,
|
|
187
204
|
)
|
|
188
|
-
class UnevaluatedPage
|
|
205
|
+
class UnevaluatedPage:
|
|
189
206
|
"""An uncompiled page."""
|
|
190
207
|
|
|
191
208
|
component: Union[Component, ComponentCallable]
|
|
@@ -193,7 +210,7 @@ class UnevaluatedPage(Generic[BASE_STATE]):
|
|
|
193
210
|
title: Union[Var, str, None]
|
|
194
211
|
description: Union[Var, str, None]
|
|
195
212
|
image: str
|
|
196
|
-
on_load: Union[EventType[
|
|
213
|
+
on_load: Union[EventType[()], None]
|
|
197
214
|
meta: List[Dict[str, str]]
|
|
198
215
|
|
|
199
216
|
|
|
@@ -250,36 +267,36 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
250
267
|
# Attributes to add to the html root tag of every page.
|
|
251
268
|
html_custom_attrs: Optional[Dict[str, str]] = None
|
|
252
269
|
|
|
253
|
-
# A map from a route to an unevaluated page.
|
|
254
|
-
|
|
270
|
+
# A map from a route to an unevaluated page.
|
|
271
|
+
_unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
|
|
255
272
|
default_factory=dict
|
|
256
273
|
)
|
|
257
274
|
|
|
258
|
-
# A map from a page route to the component to render. Users should use `add_page`.
|
|
259
|
-
|
|
275
|
+
# A map from a page route to the component to render. Users should use `add_page`.
|
|
276
|
+
_pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
|
|
260
277
|
|
|
261
|
-
# The backend API object.
|
|
262
|
-
|
|
278
|
+
# The backend API object.
|
|
279
|
+
_api: FastAPI | None = None
|
|
263
280
|
|
|
264
|
-
# The state class to use for the app.
|
|
265
|
-
|
|
281
|
+
# The state class to use for the app.
|
|
282
|
+
_state: Optional[Type[BaseState]] = None
|
|
266
283
|
|
|
267
284
|
# Class to manage many client states.
|
|
268
285
|
_state_manager: Optional[StateManager] = None
|
|
269
286
|
|
|
270
|
-
# Mapping from a route to event handlers to trigger when the page loads.
|
|
271
|
-
|
|
287
|
+
# Mapping from a route to event handlers to trigger when the page loads.
|
|
288
|
+
_load_events: Dict[str, List[IndividualEventType[()]]] = dataclasses.field(
|
|
272
289
|
default_factory=dict
|
|
273
290
|
)
|
|
274
291
|
|
|
275
|
-
# Admin dashboard to view and manage the database.
|
|
292
|
+
# Admin dashboard to view and manage the database.
|
|
276
293
|
admin_dash: Optional[AdminDash] = None
|
|
277
294
|
|
|
278
|
-
# The async server name space.
|
|
279
|
-
|
|
295
|
+
# The async server name space.
|
|
296
|
+
_event_namespace: Optional[EventNamespace] = None
|
|
280
297
|
|
|
281
|
-
# Background tasks that are currently running.
|
|
282
|
-
|
|
298
|
+
# Background tasks that are currently running.
|
|
299
|
+
_background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
|
|
283
300
|
|
|
284
301
|
# Frontend Error Handler Function
|
|
285
302
|
frontend_exception_handler: Callable[[Exception], None] = (
|
|
@@ -291,6 +308,24 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
291
308
|
[Exception], Union[EventSpec, List[EventSpec], None]
|
|
292
309
|
] = default_backend_exception_handler
|
|
293
310
|
|
|
311
|
+
@property
|
|
312
|
+
def api(self) -> FastAPI | None:
|
|
313
|
+
"""Get the backend api.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
The backend api.
|
|
317
|
+
"""
|
|
318
|
+
return self._api
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def event_namespace(self) -> EventNamespace | None:
|
|
322
|
+
"""Get the event namespace.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
The event namespace.
|
|
326
|
+
"""
|
|
327
|
+
return self._event_namespace
|
|
328
|
+
|
|
294
329
|
def __post_init__(self):
|
|
295
330
|
"""Initialize the app.
|
|
296
331
|
|
|
@@ -310,7 +345,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
310
345
|
set_breakpoints(self.style.pop("breakpoints"))
|
|
311
346
|
|
|
312
347
|
# Set up the API.
|
|
313
|
-
self.
|
|
348
|
+
self._api = FastAPI(lifespan=self._run_lifespan_tasks)
|
|
314
349
|
self._add_cors()
|
|
315
350
|
self._add_default_endpoints()
|
|
316
351
|
|
|
@@ -331,16 +366,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
331
366
|
|
|
332
367
|
self.register_lifespan_task(windows_hot_reload_lifespan_hack)
|
|
333
368
|
|
|
334
|
-
# Enable proxying to frontend server.
|
|
335
|
-
if not environment.REFLEX_BACKEND_ONLY.get():
|
|
336
|
-
from reflex.proxy import proxy_middleware
|
|
337
|
-
|
|
338
|
-
self.register_lifespan_task(proxy_middleware)
|
|
339
|
-
|
|
340
369
|
def _enable_state(self) -> None:
|
|
341
370
|
"""Enable state for the app."""
|
|
342
|
-
if not self.
|
|
343
|
-
self.
|
|
371
|
+
if not self._state:
|
|
372
|
+
self._state = State
|
|
344
373
|
self._setup_state()
|
|
345
374
|
|
|
346
375
|
def _setup_state(self) -> None:
|
|
@@ -349,13 +378,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
349
378
|
Raises:
|
|
350
379
|
RuntimeError: If the socket server is invalid.
|
|
351
380
|
"""
|
|
352
|
-
if not self.
|
|
381
|
+
if not self._state:
|
|
353
382
|
return
|
|
354
383
|
|
|
355
384
|
config = get_config()
|
|
356
385
|
|
|
357
386
|
# Set up the state manager.
|
|
358
|
-
self._state_manager = StateManager.create(state=self.
|
|
387
|
+
self._state_manager = StateManager.create(state=self._state)
|
|
359
388
|
|
|
360
389
|
# Set up the Socket.IO AsyncServer.
|
|
361
390
|
if not self.sio:
|
|
@@ -386,12 +415,42 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
386
415
|
namespace = config.get_event_namespace()
|
|
387
416
|
|
|
388
417
|
# Create the event namespace and attach the main app. Not related to any paths.
|
|
389
|
-
self.
|
|
418
|
+
self._event_namespace = EventNamespace(namespace, self)
|
|
390
419
|
|
|
391
420
|
# Register the event namespace with the socket.
|
|
392
421
|
self.sio.register_namespace(self.event_namespace)
|
|
393
422
|
# Mount the socket app with the API.
|
|
394
|
-
self.api
|
|
423
|
+
if self.api:
|
|
424
|
+
|
|
425
|
+
class HeaderMiddleware:
|
|
426
|
+
def __init__(self, app: ASGIApp):
|
|
427
|
+
self.app = app
|
|
428
|
+
|
|
429
|
+
async def __call__(
|
|
430
|
+
self, scope: MutableMapping[str, Any], receive: Any, send: Callable
|
|
431
|
+
):
|
|
432
|
+
original_send = send
|
|
433
|
+
|
|
434
|
+
async def modified_send(message: dict):
|
|
435
|
+
if message["type"] == "websocket.accept":
|
|
436
|
+
if scope.get("subprotocols"):
|
|
437
|
+
# The following *does* say "subprotocol" instead of "subprotocols", intentionally.
|
|
438
|
+
message["subprotocol"] = scope["subprotocols"][0]
|
|
439
|
+
|
|
440
|
+
headers = dict(message.get("headers", []))
|
|
441
|
+
header_key = b"sec-websocket-protocol"
|
|
442
|
+
if subprotocol := headers.get(header_key):
|
|
443
|
+
message["headers"] = [
|
|
444
|
+
*message.get("headers", []),
|
|
445
|
+
(header_key, subprotocol),
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
return await original_send(message)
|
|
449
|
+
|
|
450
|
+
return await self.app(scope, receive, modified_send)
|
|
451
|
+
|
|
452
|
+
socket_app_with_headers = HeaderMiddleware(socket_app)
|
|
453
|
+
self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
|
|
395
454
|
|
|
396
455
|
# Check the exception handlers
|
|
397
456
|
self._validate_exception_handlers()
|
|
@@ -402,24 +461,35 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
402
461
|
Returns:
|
|
403
462
|
The string representation of the app.
|
|
404
463
|
"""
|
|
405
|
-
return f"<App state={self.
|
|
464
|
+
return f"<App state={self._state.__name__ if self._state else None}>"
|
|
406
465
|
|
|
407
466
|
def __call__(self) -> FastAPI:
|
|
408
467
|
"""Run the backend api instance.
|
|
409
468
|
|
|
469
|
+
Raises:
|
|
470
|
+
ValueError: If the app has not been initialized.
|
|
471
|
+
|
|
410
472
|
Returns:
|
|
411
473
|
The backend api.
|
|
412
474
|
"""
|
|
475
|
+
if not self.api:
|
|
476
|
+
raise ValueError("The app has not been initialized.")
|
|
413
477
|
return self.api
|
|
414
478
|
|
|
415
479
|
def _add_default_endpoints(self):
|
|
416
480
|
"""Add default api endpoints (ping)."""
|
|
417
481
|
# To test the server.
|
|
482
|
+
if not self.api:
|
|
483
|
+
return
|
|
484
|
+
|
|
418
485
|
self.api.get(str(constants.Endpoint.PING))(ping)
|
|
419
486
|
self.api.get(str(constants.Endpoint.HEALTH))(health)
|
|
420
487
|
|
|
421
488
|
def _add_optional_endpoints(self):
|
|
422
489
|
"""Add optional api endpoints (_upload)."""
|
|
490
|
+
if not self.api:
|
|
491
|
+
return
|
|
492
|
+
|
|
423
493
|
if Upload.is_used:
|
|
424
494
|
# To upload files.
|
|
425
495
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
|
@@ -437,6 +507,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
437
507
|
|
|
438
508
|
def _add_cors(self):
|
|
439
509
|
"""Add CORS middleware to the app."""
|
|
510
|
+
if not self.api:
|
|
511
|
+
return
|
|
440
512
|
self.api.add_middleware(
|
|
441
513
|
cors.CORSMiddleware,
|
|
442
514
|
allow_credentials=True,
|
|
@@ -468,14 +540,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
468
540
|
|
|
469
541
|
Returns:
|
|
470
542
|
The generated component.
|
|
471
|
-
|
|
472
|
-
Raises:
|
|
473
|
-
exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
|
|
474
543
|
"""
|
|
475
|
-
|
|
476
|
-
return component if isinstance(component, Component) else component()
|
|
477
|
-
except exceptions.MatchTypeError:
|
|
478
|
-
raise
|
|
544
|
+
return component if isinstance(component, Component) else component()
|
|
479
545
|
|
|
480
546
|
def add_page(
|
|
481
547
|
self,
|
|
@@ -484,7 +550,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
484
550
|
title: str | Var | None = None,
|
|
485
551
|
description: str | Var | None = None,
|
|
486
552
|
image: str = constants.DefaultPage.IMAGE,
|
|
487
|
-
on_load: EventType[
|
|
553
|
+
on_load: EventType[()] | None = None,
|
|
488
554
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
489
555
|
):
|
|
490
556
|
"""Add a page to the app.
|
|
@@ -532,13 +598,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
532
598
|
# Check if the route given is valid
|
|
533
599
|
verify_route_validity(route)
|
|
534
600
|
|
|
535
|
-
if route in self.
|
|
601
|
+
if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
|
|
536
602
|
# when the app is reloaded(typically for app harness tests), we should maintain
|
|
537
603
|
# the latest render function of a route.This applies typically to decorated pages
|
|
538
604
|
# since they are only added when app._compile is called.
|
|
539
|
-
self.
|
|
605
|
+
self._unevaluated_pages.pop(route)
|
|
540
606
|
|
|
541
|
-
if route in self.
|
|
607
|
+
if route in self._unevaluated_pages:
|
|
542
608
|
route_name = (
|
|
543
609
|
f"`{route}` or `/`"
|
|
544
610
|
if route == constants.PageNames.INDEX_ROUTE
|
|
@@ -551,15 +617,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
551
617
|
|
|
552
618
|
# Setup dynamic args for the route.
|
|
553
619
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
554
|
-
state = self.
|
|
620
|
+
state = self._state if self._state else State
|
|
555
621
|
state.setup_dynamic_args(get_route_args(route))
|
|
556
622
|
|
|
557
623
|
if on_load:
|
|
558
|
-
self.
|
|
624
|
+
self._load_events[route] = (
|
|
559
625
|
on_load if isinstance(on_load, list) else [on_load]
|
|
560
626
|
)
|
|
561
627
|
|
|
562
|
-
self.
|
|
628
|
+
self._unevaluated_pages[route] = UnevaluatedPage(
|
|
563
629
|
component=component,
|
|
564
630
|
route=route,
|
|
565
631
|
title=title,
|
|
@@ -569,14 +635,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
569
635
|
meta=meta,
|
|
570
636
|
)
|
|
571
637
|
|
|
572
|
-
def _compile_page(self, route: str):
|
|
638
|
+
def _compile_page(self, route: str, save_page: bool = True):
|
|
573
639
|
"""Compile a page.
|
|
574
640
|
|
|
575
641
|
Args:
|
|
576
642
|
route: The route of the page to compile.
|
|
643
|
+
save_page: If True, the compiled page is saved to self._pages.
|
|
577
644
|
"""
|
|
578
645
|
component, enable_state = compiler.compile_unevaluated_page(
|
|
579
|
-
route, self.
|
|
646
|
+
route, self._unevaluated_pages[route], self._state, self.style, self.theme
|
|
580
647
|
)
|
|
581
648
|
|
|
582
649
|
if enable_state:
|
|
@@ -584,9 +651,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
584
651
|
|
|
585
652
|
# Add the page.
|
|
586
653
|
self._check_routes_conflict(route)
|
|
587
|
-
|
|
654
|
+
if save_page:
|
|
655
|
+
self._pages[route] = component
|
|
588
656
|
|
|
589
|
-
def get_load_events(self, route: str) -> list[IndividualEventType[
|
|
657
|
+
def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
|
|
590
658
|
"""Get the load events for a route.
|
|
591
659
|
|
|
592
660
|
Args:
|
|
@@ -598,7 +666,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
598
666
|
route = route.lstrip("/")
|
|
599
667
|
if route == "":
|
|
600
668
|
route = constants.PageNames.INDEX_ROUTE
|
|
601
|
-
return self.
|
|
669
|
+
return self._load_events.get(route, [])
|
|
602
670
|
|
|
603
671
|
def _check_routes_conflict(self, new_route: str):
|
|
604
672
|
"""Verify if there is any conflict between the new route and any existing route.
|
|
@@ -622,10 +690,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
622
690
|
constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
|
|
623
691
|
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
|
|
624
692
|
)
|
|
625
|
-
for route in self.
|
|
693
|
+
for route in self._pages:
|
|
626
694
|
replaced_route = replace_brackets_with_keywords(route)
|
|
627
695
|
for rw, r, nr in zip(
|
|
628
|
-
replaced_route.split("/"),
|
|
696
|
+
replaced_route.split("/"),
|
|
697
|
+
route.split("/"),
|
|
698
|
+
new_route.split("/"),
|
|
699
|
+
strict=False,
|
|
629
700
|
):
|
|
630
701
|
if rw in segments and r != nr:
|
|
631
702
|
# If the slugs in the segments of both routes are not the same, then the route is invalid
|
|
@@ -645,7 +716,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
645
716
|
title: str = constants.Page404.TITLE,
|
|
646
717
|
image: str = constants.Page404.IMAGE,
|
|
647
718
|
description: str = constants.Page404.DESCRIPTION,
|
|
648
|
-
on_load: EventType[
|
|
719
|
+
on_load: EventType[()] | None = None,
|
|
649
720
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
650
721
|
):
|
|
651
722
|
"""Define a custom 404 page for any url having no match.
|
|
@@ -656,8 +727,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
656
727
|
Args:
|
|
657
728
|
component: The component to display at the page.
|
|
658
729
|
title: The title of the page.
|
|
659
|
-
description: The description of the page.
|
|
660
730
|
image: The image to display on the page.
|
|
731
|
+
description: The description of the page.
|
|
661
732
|
on_load: The event handler(s) that will be called each time the page load.
|
|
662
733
|
meta: The metadata of the page.
|
|
663
734
|
"""
|
|
@@ -680,6 +751,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
680
751
|
def _setup_admin_dash(self):
|
|
681
752
|
"""Setup the admin dash."""
|
|
682
753
|
# Get the admin dash.
|
|
754
|
+
if not self.api:
|
|
755
|
+
return
|
|
756
|
+
|
|
683
757
|
admin_dash = self.admin_dash
|
|
684
758
|
|
|
685
759
|
if admin_dash and admin_dash.models:
|
|
@@ -721,7 +795,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
721
795
|
frontend_packages = get_config().frontend_packages
|
|
722
796
|
_frontend_packages = []
|
|
723
797
|
for package in frontend_packages:
|
|
724
|
-
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
798
|
+
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
725
799
|
console.warn(
|
|
726
800
|
f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
|
|
727
801
|
)
|
|
@@ -784,10 +858,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
784
858
|
|
|
785
859
|
def _setup_overlay_component(self):
|
|
786
860
|
"""If a State is not used and no overlay_component is specified, do not render the connection modal."""
|
|
787
|
-
if self.
|
|
861
|
+
if self._state is None and self.overlay_component is default_overlay_component:
|
|
788
862
|
self.overlay_component = None
|
|
789
|
-
for k, component in self.
|
|
790
|
-
self.
|
|
863
|
+
for k, component in self._pages.items():
|
|
864
|
+
self._pages[k] = self._add_overlay_to_component(component)
|
|
791
865
|
|
|
792
866
|
def _add_error_boundary_to_component(self, component: Component) -> Component:
|
|
793
867
|
if self.error_boundary is None:
|
|
@@ -799,14 +873,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
799
873
|
|
|
800
874
|
def _setup_error_boundary(self):
|
|
801
875
|
"""If a State is not used and no error_boundary is specified, do not render the error boundary."""
|
|
802
|
-
if self.
|
|
876
|
+
if self._state is None and self.error_boundary is default_error_boundary:
|
|
803
877
|
self.error_boundary = None
|
|
804
878
|
|
|
805
|
-
for k, component in self.
|
|
879
|
+
for k, component in self._pages.items():
|
|
806
880
|
# Skip the 404 page
|
|
807
881
|
if k == constants.Page404.SLUG:
|
|
808
882
|
continue
|
|
809
|
-
self.
|
|
883
|
+
self._pages[k] = self._add_error_boundary_to_component(component)
|
|
884
|
+
|
|
885
|
+
def _setup_sticky_badge(self):
|
|
886
|
+
"""Add the sticky badge to the app."""
|
|
887
|
+
for k, component in self._pages.items():
|
|
888
|
+
# Would be nice to share single sticky_badge across all pages, but
|
|
889
|
+
# it bungles the StatefulComponent compile step.
|
|
890
|
+
sticky_badge = sticky()
|
|
891
|
+
sticky_badge._add_style_recursive({})
|
|
892
|
+
self._pages[k] = Fragment.create(sticky_badge, component)
|
|
810
893
|
|
|
811
894
|
def _apply_decorated_pages(self):
|
|
812
895
|
"""Add @rx.page decorated pages to the app.
|
|
@@ -832,21 +915,27 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
832
915
|
Raises:
|
|
833
916
|
VarDependencyError: When a computed var has an invalid dependency.
|
|
834
917
|
"""
|
|
835
|
-
if not self.
|
|
918
|
+
if not self._state:
|
|
836
919
|
return
|
|
837
920
|
|
|
838
921
|
if not state:
|
|
839
|
-
state = self.
|
|
922
|
+
state = self._state
|
|
840
923
|
|
|
841
924
|
for var in state.computed_vars.values():
|
|
842
925
|
if not var._cache:
|
|
843
926
|
continue
|
|
844
927
|
deps = var._deps(objclass=state)
|
|
845
|
-
for
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
928
|
+
for state_name, dep_set in deps.items():
|
|
929
|
+
state_cls = (
|
|
930
|
+
state.get_root_state().get_class_substate(state_name)
|
|
931
|
+
if state_name != state.get_full_name()
|
|
932
|
+
else state
|
|
933
|
+
)
|
|
934
|
+
for dep in dep_set:
|
|
935
|
+
if dep not in state_cls.vars and dep not in state_cls.backend_vars:
|
|
936
|
+
raise exceptions.VarDependencyError(
|
|
937
|
+
f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}"
|
|
938
|
+
)
|
|
850
939
|
|
|
851
940
|
for substate in state.class_subclasses:
|
|
852
941
|
self._validate_var_dependencies(substate)
|
|
@@ -862,13 +951,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
862
951
|
"""
|
|
863
952
|
from reflex.utils.exceptions import ReflexRuntimeError
|
|
864
953
|
|
|
865
|
-
self.
|
|
954
|
+
self._pages = {}
|
|
866
955
|
|
|
867
956
|
def get_compilation_time() -> str:
|
|
868
957
|
return str(datetime.now().time()).split(".")[0]
|
|
869
958
|
|
|
870
959
|
# Render a default 404 page if the user didn't supply one
|
|
871
|
-
if constants.Page404.SLUG not in self.
|
|
960
|
+
if constants.Page404.SLUG not in self._unevaluated_pages:
|
|
872
961
|
self.add_page(route=constants.Page404.SLUG)
|
|
873
962
|
|
|
874
963
|
# Fix up the style.
|
|
@@ -884,19 +973,24 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
884
973
|
# If a theme component was provided, wrap the app with it
|
|
885
974
|
app_wrappers[(20, "Theme")] = self.theme
|
|
886
975
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
self._compile_page(route)
|
|
976
|
+
# Get the env mode.
|
|
977
|
+
config = get_config()
|
|
890
978
|
|
|
891
|
-
|
|
892
|
-
|
|
979
|
+
if config.react_strict_mode:
|
|
980
|
+
app_wrappers[(200, "StrictMode")] = StrictMode.create()
|
|
893
981
|
|
|
894
|
-
|
|
895
|
-
return
|
|
982
|
+
should_compile = self._should_compile()
|
|
896
983
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
984
|
+
if not should_compile:
|
|
985
|
+
with console.timing("Evaluate Pages (Backend)"):
|
|
986
|
+
for route in self._unevaluated_pages:
|
|
987
|
+
console.debug(f"Evaluating page: {route}")
|
|
988
|
+
self._compile_page(route, save_page=should_compile)
|
|
989
|
+
|
|
990
|
+
# Add the optional endpoints (_upload)
|
|
991
|
+
self._add_optional_endpoints()
|
|
992
|
+
|
|
993
|
+
return
|
|
900
994
|
|
|
901
995
|
# Create a progress bar.
|
|
902
996
|
progress = Progress(
|
|
@@ -906,18 +1000,33 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
906
1000
|
)
|
|
907
1001
|
|
|
908
1002
|
# try to be somewhat accurate - but still not 100%
|
|
909
|
-
adhoc_steps_without_executor =
|
|
1003
|
+
adhoc_steps_without_executor = 7
|
|
910
1004
|
fixed_pages_within_executor = 5
|
|
911
1005
|
progress.start()
|
|
912
1006
|
task = progress.add_task(
|
|
913
1007
|
f"[{get_compilation_time()}] Compiling:",
|
|
914
|
-
total=len(self.
|
|
1008
|
+
total=len(self._pages)
|
|
1009
|
+
+ (len(self._unevaluated_pages) * 2)
|
|
915
1010
|
+ fixed_pages_within_executor
|
|
916
1011
|
+ adhoc_steps_without_executor,
|
|
917
1012
|
)
|
|
918
1013
|
|
|
919
|
-
|
|
920
|
-
|
|
1014
|
+
with console.timing("Evaluate Pages (Frontend)"):
|
|
1015
|
+
for route in self._unevaluated_pages:
|
|
1016
|
+
console.debug(f"Evaluating page: {route}")
|
|
1017
|
+
self._compile_page(route, save_page=should_compile)
|
|
1018
|
+
progress.advance(task)
|
|
1019
|
+
|
|
1020
|
+
# Add the optional endpoints (_upload)
|
|
1021
|
+
self._add_optional_endpoints()
|
|
1022
|
+
|
|
1023
|
+
self._validate_var_dependencies()
|
|
1024
|
+
self._setup_overlay_component()
|
|
1025
|
+
self._setup_error_boundary()
|
|
1026
|
+
if is_prod_mode() and config.show_built_with_reflex:
|
|
1027
|
+
self._setup_sticky_badge()
|
|
1028
|
+
|
|
1029
|
+
progress.advance(task)
|
|
921
1030
|
|
|
922
1031
|
# Store the compile results.
|
|
923
1032
|
compile_results = []
|
|
@@ -930,7 +1039,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
930
1039
|
|
|
931
1040
|
# This has to happen before compiling stateful components as that
|
|
932
1041
|
# prevents recursive functions from reaching all components.
|
|
933
|
-
for component in self.
|
|
1042
|
+
for component in self._pages.values():
|
|
934
1043
|
# Add component._get_all_imports() to all_imports.
|
|
935
1044
|
all_imports.update(component._get_all_imports())
|
|
936
1045
|
|
|
@@ -941,16 +1050,16 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
941
1050
|
custom_components |= component._get_all_custom_components()
|
|
942
1051
|
|
|
943
1052
|
# Perform auto-memoization of stateful components.
|
|
944
|
-
(
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1053
|
+
with console.timing("Auto-memoize StatefulComponents"):
|
|
1054
|
+
(
|
|
1055
|
+
stateful_components_path,
|
|
1056
|
+
stateful_components_code,
|
|
1057
|
+
page_components,
|
|
1058
|
+
) = compiler.compile_stateful_components(self._pages.values())
|
|
1059
|
+
progress.advance(task)
|
|
951
1060
|
|
|
952
1061
|
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
|
953
|
-
if code_uses_state_contexts(stateful_components_code) and self.
|
|
1062
|
+
if code_uses_state_contexts(stateful_components_code) and self._state is None:
|
|
954
1063
|
raise ReflexRuntimeError(
|
|
955
1064
|
"To access rx.State in frontend components, at least one "
|
|
956
1065
|
"subclass of rx.State must be defined in the app."
|
|
@@ -964,12 +1073,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
964
1073
|
compiler.compile_document_root(
|
|
965
1074
|
self.head_components,
|
|
966
1075
|
html_lang=self.html_lang,
|
|
967
|
-
html_custom_attrs=self.html_custom_attrs, #
|
|
1076
|
+
html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
|
|
968
1077
|
)
|
|
969
1078
|
)
|
|
970
1079
|
|
|
971
1080
|
progress.advance(task)
|
|
972
1081
|
|
|
1082
|
+
# Copy the assets.
|
|
1083
|
+
assets_src = Path.cwd() / constants.Dirs.APP_ASSETS
|
|
1084
|
+
if assets_src.is_dir():
|
|
1085
|
+
with console.timing("Copy assets"):
|
|
1086
|
+
path_ops.update_directory_tree(
|
|
1087
|
+
src=assets_src,
|
|
1088
|
+
dest=(
|
|
1089
|
+
Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.PUBLIC
|
|
1090
|
+
),
|
|
1091
|
+
)
|
|
1092
|
+
|
|
973
1093
|
# Use a forking process pool, if possible. Much faster, especially for large sites.
|
|
974
1094
|
# Fallback to ThreadPoolExecutor as something that will always work.
|
|
975
1095
|
executor = None
|
|
@@ -987,20 +1107,20 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
987
1107
|
max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
|
|
988
1108
|
)
|
|
989
1109
|
|
|
990
|
-
for route, component in zip(self.
|
|
1110
|
+
for route, component in zip(self._pages, page_components, strict=True):
|
|
991
1111
|
ExecutorSafeFunctions.COMPONENTS[route] = component
|
|
992
1112
|
|
|
993
|
-
ExecutorSafeFunctions.STATE = self.
|
|
1113
|
+
ExecutorSafeFunctions.STATE = self._state
|
|
994
1114
|
|
|
995
1115
|
with executor:
|
|
996
1116
|
result_futures = []
|
|
997
1117
|
|
|
998
|
-
def _submit_work(fn, *args, **kwargs):
|
|
1118
|
+
def _submit_work(fn: Callable, *args, **kwargs):
|
|
999
1119
|
f = executor.submit(fn, *args, **kwargs)
|
|
1000
1120
|
result_futures.append(f)
|
|
1001
1121
|
|
|
1002
1122
|
# Compile the pre-compiled pages.
|
|
1003
|
-
for route in self.
|
|
1123
|
+
for route in self._pages:
|
|
1004
1124
|
_submit_work(
|
|
1005
1125
|
ExecutorSafeFunctions.compile_page,
|
|
1006
1126
|
route,
|
|
@@ -1022,9 +1142,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1022
1142
|
_submit_work(compiler.remove_tailwind_from_postcss)
|
|
1023
1143
|
|
|
1024
1144
|
# Wait for all compilation tasks to complete.
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1145
|
+
with console.timing("Compile to Javascript"):
|
|
1146
|
+
for future in concurrent.futures.as_completed(result_futures):
|
|
1147
|
+
compile_results.append(future.result())
|
|
1148
|
+
progress.advance(task)
|
|
1028
1149
|
|
|
1029
1150
|
app_root = self._app_root(app_wrappers=app_wrappers)
|
|
1030
1151
|
|
|
@@ -1035,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1035
1156
|
|
|
1036
1157
|
# Compile the contexts.
|
|
1037
1158
|
compile_results.append(
|
|
1038
|
-
compiler.compile_contexts(self.
|
|
1159
|
+
compiler.compile_contexts(self._state, self.theme),
|
|
1039
1160
|
)
|
|
1040
1161
|
if self.theme is not None:
|
|
1041
1162
|
# Fix #2992 by removing the top-level appearance prop
|
|
@@ -1059,7 +1180,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1059
1180
|
progress.stop()
|
|
1060
1181
|
|
|
1061
1182
|
# Install frontend packages.
|
|
1062
|
-
|
|
1183
|
+
with console.timing("Install Frontend Packages"):
|
|
1184
|
+
self._get_frontend_packages(all_imports)
|
|
1063
1185
|
|
|
1064
1186
|
# Setup the next.config.js
|
|
1065
1187
|
transpile_packages = [
|
|
@@ -1085,8 +1207,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1085
1207
|
# Remove pages that are no longer in the app.
|
|
1086
1208
|
p.unlink()
|
|
1087
1209
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1210
|
+
with console.timing("Write to Disk"):
|
|
1211
|
+
for output_path, code in compile_results:
|
|
1212
|
+
compiler_utils.write_page(output_path, code)
|
|
1090
1213
|
|
|
1091
1214
|
@contextlib.asynccontextmanager
|
|
1092
1215
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
@@ -1157,9 +1280,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1157
1280
|
)
|
|
1158
1281
|
|
|
1159
1282
|
task = asyncio.create_task(_coro())
|
|
1160
|
-
self.
|
|
1283
|
+
self._background_tasks.add(task)
|
|
1161
1284
|
# Clean up task from background_tasks set when complete.
|
|
1162
|
-
task.add_done_callback(self.
|
|
1285
|
+
task.add_done_callback(self._background_tasks.discard)
|
|
1163
1286
|
return task
|
|
1164
1287
|
|
|
1165
1288
|
def _validate_exception_handlers(self):
|
|
@@ -1169,11 +1292,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1169
1292
|
ValueError: If the custom exception handlers are invalid.
|
|
1170
1293
|
|
|
1171
1294
|
"""
|
|
1172
|
-
|
|
1295
|
+
frontend_arg_spec = {
|
|
1173
1296
|
"exception": Exception,
|
|
1174
1297
|
}
|
|
1175
1298
|
|
|
1176
|
-
|
|
1299
|
+
backend_arg_spec = {
|
|
1177
1300
|
"exception": Exception,
|
|
1178
1301
|
}
|
|
1179
1302
|
|
|
@@ -1181,9 +1304,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1181
1304
|
["frontend", "backend"],
|
|
1182
1305
|
[self.frontend_exception_handler, self.backend_exception_handler],
|
|
1183
1306
|
[
|
|
1184
|
-
|
|
1185
|
-
|
|
1307
|
+
frontend_arg_spec,
|
|
1308
|
+
backend_arg_spec,
|
|
1186
1309
|
],
|
|
1310
|
+
strict=True,
|
|
1187
1311
|
):
|
|
1188
1312
|
if hasattr(handler_fn, "__name__"):
|
|
1189
1313
|
_fn_name = handler_fn.__name__
|
|
@@ -1224,7 +1348,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1224
1348
|
):
|
|
1225
1349
|
raise ValueError(
|
|
1226
1350
|
f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
|
|
1227
|
-
f"Expected `{required_arg}` as the {required_arg_index+1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
|
|
1351
|
+
f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`"
|
|
1228
1352
|
)
|
|
1229
1353
|
|
|
1230
1354
|
if not issubclass(arg_annotations[required_arg], Exception):
|
|
@@ -1325,15 +1449,14 @@ async def process(
|
|
|
1325
1449
|
if app._process_background(state, event) is not None:
|
|
1326
1450
|
# `final=True` allows the frontend send more events immediately.
|
|
1327
1451
|
yield StateUpdate(final=True)
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
yield update
|
|
1452
|
+
else:
|
|
1453
|
+
# Process the event synchronously.
|
|
1454
|
+
async for update in state._process(event):
|
|
1455
|
+
# Postprocess the event.
|
|
1456
|
+
update = await app._postprocess(state, event, update)
|
|
1457
|
+
|
|
1458
|
+
# Yield the update.
|
|
1459
|
+
yield update
|
|
1337
1460
|
except Exception as ex:
|
|
1338
1461
|
telemetry.send_error(ex, context="backend")
|
|
1339
1462
|
|
|
@@ -1528,16 +1651,20 @@ class EventNamespace(AsyncNamespace):
|
|
|
1528
1651
|
self.sid_to_token = {}
|
|
1529
1652
|
self.app = app
|
|
1530
1653
|
|
|
1531
|
-
def on_connect(self, sid, environ):
|
|
1654
|
+
def on_connect(self, sid: str, environ: dict):
|
|
1532
1655
|
"""Event for when the websocket is connected.
|
|
1533
1656
|
|
|
1534
1657
|
Args:
|
|
1535
1658
|
sid: The Socket.IO session id.
|
|
1536
1659
|
environ: The request information, including HTTP headers.
|
|
1537
1660
|
"""
|
|
1538
|
-
|
|
1661
|
+
subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL")
|
|
1662
|
+
if subprotocol and subprotocol != constants.Reflex.VERSION:
|
|
1663
|
+
console.warn(
|
|
1664
|
+
f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}."
|
|
1665
|
+
)
|
|
1539
1666
|
|
|
1540
|
-
def on_disconnect(self, sid):
|
|
1667
|
+
def on_disconnect(self, sid: str):
|
|
1541
1668
|
"""Event for when the websocket disconnects.
|
|
1542
1669
|
|
|
1543
1670
|
Args:
|
|
@@ -1559,7 +1686,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1559
1686
|
self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
|
|
1560
1687
|
)
|
|
1561
1688
|
|
|
1562
|
-
async def on_event(self, sid, data):
|
|
1689
|
+
async def on_event(self, sid: str, data: Any):
|
|
1563
1690
|
"""Event for receiving front-end websocket events.
|
|
1564
1691
|
|
|
1565
1692
|
Raises:
|
|
@@ -1568,12 +1695,36 @@ class EventNamespace(AsyncNamespace):
|
|
|
1568
1695
|
Args:
|
|
1569
1696
|
sid: The Socket.IO session id.
|
|
1570
1697
|
data: The event data.
|
|
1698
|
+
|
|
1699
|
+
Raises:
|
|
1700
|
+
EventDeserializationError: If the event data is not a dictionary.
|
|
1571
1701
|
"""
|
|
1572
1702
|
fields = data
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1703
|
+
|
|
1704
|
+
if isinstance(fields, str):
|
|
1705
|
+
console.warn(
|
|
1706
|
+
"Received event data as a string. This generally should not happen and may indicate a bug."
|
|
1707
|
+
f" Event data: {fields}"
|
|
1708
|
+
)
|
|
1709
|
+
try:
|
|
1710
|
+
fields = json.loads(fields)
|
|
1711
|
+
except json.JSONDecodeError as ex:
|
|
1712
|
+
raise exceptions.EventDeserializationError(
|
|
1713
|
+
f"Failed to deserialize event data: {fields}."
|
|
1714
|
+
) from ex
|
|
1715
|
+
|
|
1716
|
+
if not isinstance(fields, dict):
|
|
1717
|
+
raise exceptions.EventDeserializationError(
|
|
1718
|
+
f"Event data must be a dictionary, but received {fields} of type {type(fields)}."
|
|
1719
|
+
)
|
|
1720
|
+
|
|
1721
|
+
try:
|
|
1722
|
+
# Get the event.
|
|
1723
|
+
event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS})
|
|
1724
|
+
except (TypeError, ValueError) as ex:
|
|
1725
|
+
raise exceptions.EventDeserializationError(
|
|
1726
|
+
f"Failed to deserialize event data: {fields}."
|
|
1727
|
+
) from ex
|
|
1577
1728
|
|
|
1578
1729
|
self.token_to_sid[event.token] = sid
|
|
1579
1730
|
self.sid_to_token[sid] = event.token
|
|
@@ -1602,7 +1753,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1602
1753
|
# Emit the update from processing the event.
|
|
1603
1754
|
await self.emit_update(update=update, sid=sid)
|
|
1604
1755
|
|
|
1605
|
-
async def on_ping(self, sid):
|
|
1756
|
+
async def on_ping(self, sid: str):
|
|
1606
1757
|
"""Event for testing the API endpoint.
|
|
1607
1758
|
|
|
1608
1759
|
Args:
|