reflex 0.6.8a2__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 -129
- 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 -15
- 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 +221 -231
- 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 +29 -23
- reflex/utils/build.py +6 -2
- reflex/utils/codespaces.py +1 -4
- reflex/utils/compat.py +6 -5
- reflex/utils/console.py +71 -16
- 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.8a2.dist-info → reflex-0.7.0.dist-info}/METADATA +7 -8
- reflex-0.7.0.dist-info/RECORD +401 -0
- {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/WHEEL +1 -1
- reflex/experimental/assets.py +0 -37
- reflex-0.6.8a2.dist-info/RECORD +0 -397
- {reflex-0.6.8a2.dist-info → reflex-0.7.0.dist-info}/LICENSE +0 -0
- {reflex-0.6.8a2.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
|
|
|
@@ -333,8 +368,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
333
368
|
|
|
334
369
|
def _enable_state(self) -> None:
|
|
335
370
|
"""Enable state for the app."""
|
|
336
|
-
if not self.
|
|
337
|
-
self.
|
|
371
|
+
if not self._state:
|
|
372
|
+
self._state = State
|
|
338
373
|
self._setup_state()
|
|
339
374
|
|
|
340
375
|
def _setup_state(self) -> None:
|
|
@@ -343,13 +378,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
343
378
|
Raises:
|
|
344
379
|
RuntimeError: If the socket server is invalid.
|
|
345
380
|
"""
|
|
346
|
-
if not self.
|
|
381
|
+
if not self._state:
|
|
347
382
|
return
|
|
348
383
|
|
|
349
384
|
config = get_config()
|
|
350
385
|
|
|
351
386
|
# Set up the state manager.
|
|
352
|
-
self._state_manager = StateManager.create(state=self.
|
|
387
|
+
self._state_manager = StateManager.create(state=self._state)
|
|
353
388
|
|
|
354
389
|
# Set up the Socket.IO AsyncServer.
|
|
355
390
|
if not self.sio:
|
|
@@ -380,12 +415,42 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
380
415
|
namespace = config.get_event_namespace()
|
|
381
416
|
|
|
382
417
|
# Create the event namespace and attach the main app. Not related to any paths.
|
|
383
|
-
self.
|
|
418
|
+
self._event_namespace = EventNamespace(namespace, self)
|
|
384
419
|
|
|
385
420
|
# Register the event namespace with the socket.
|
|
386
421
|
self.sio.register_namespace(self.event_namespace)
|
|
387
422
|
# Mount the socket app with the API.
|
|
388
|
-
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)
|
|
389
454
|
|
|
390
455
|
# Check the exception handlers
|
|
391
456
|
self._validate_exception_handlers()
|
|
@@ -396,24 +461,35 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
396
461
|
Returns:
|
|
397
462
|
The string representation of the app.
|
|
398
463
|
"""
|
|
399
|
-
return f"<App state={self.
|
|
464
|
+
return f"<App state={self._state.__name__ if self._state else None}>"
|
|
400
465
|
|
|
401
466
|
def __call__(self) -> FastAPI:
|
|
402
467
|
"""Run the backend api instance.
|
|
403
468
|
|
|
469
|
+
Raises:
|
|
470
|
+
ValueError: If the app has not been initialized.
|
|
471
|
+
|
|
404
472
|
Returns:
|
|
405
473
|
The backend api.
|
|
406
474
|
"""
|
|
475
|
+
if not self.api:
|
|
476
|
+
raise ValueError("The app has not been initialized.")
|
|
407
477
|
return self.api
|
|
408
478
|
|
|
409
479
|
def _add_default_endpoints(self):
|
|
410
480
|
"""Add default api endpoints (ping)."""
|
|
411
481
|
# To test the server.
|
|
482
|
+
if not self.api:
|
|
483
|
+
return
|
|
484
|
+
|
|
412
485
|
self.api.get(str(constants.Endpoint.PING))(ping)
|
|
413
486
|
self.api.get(str(constants.Endpoint.HEALTH))(health)
|
|
414
487
|
|
|
415
488
|
def _add_optional_endpoints(self):
|
|
416
489
|
"""Add optional api endpoints (_upload)."""
|
|
490
|
+
if not self.api:
|
|
491
|
+
return
|
|
492
|
+
|
|
417
493
|
if Upload.is_used:
|
|
418
494
|
# To upload files.
|
|
419
495
|
self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
|
|
@@ -431,6 +507,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
431
507
|
|
|
432
508
|
def _add_cors(self):
|
|
433
509
|
"""Add CORS middleware to the app."""
|
|
510
|
+
if not self.api:
|
|
511
|
+
return
|
|
434
512
|
self.api.add_middleware(
|
|
435
513
|
cors.CORSMiddleware,
|
|
436
514
|
allow_credentials=True,
|
|
@@ -462,14 +540,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
462
540
|
|
|
463
541
|
Returns:
|
|
464
542
|
The generated component.
|
|
465
|
-
|
|
466
|
-
Raises:
|
|
467
|
-
exceptions.MatchTypeError: If the return types of match cases in rx.match are different.
|
|
468
543
|
"""
|
|
469
|
-
|
|
470
|
-
return component if isinstance(component, Component) else component()
|
|
471
|
-
except exceptions.MatchTypeError:
|
|
472
|
-
raise
|
|
544
|
+
return component if isinstance(component, Component) else component()
|
|
473
545
|
|
|
474
546
|
def add_page(
|
|
475
547
|
self,
|
|
@@ -478,7 +550,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
478
550
|
title: str | Var | None = None,
|
|
479
551
|
description: str | Var | None = None,
|
|
480
552
|
image: str = constants.DefaultPage.IMAGE,
|
|
481
|
-
on_load: EventType[
|
|
553
|
+
on_load: EventType[()] | None = None,
|
|
482
554
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
483
555
|
):
|
|
484
556
|
"""Add a page to the app.
|
|
@@ -526,13 +598,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
526
598
|
# Check if the route given is valid
|
|
527
599
|
verify_route_validity(route)
|
|
528
600
|
|
|
529
|
-
if route in self.
|
|
601
|
+
if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
|
|
530
602
|
# when the app is reloaded(typically for app harness tests), we should maintain
|
|
531
603
|
# the latest render function of a route.This applies typically to decorated pages
|
|
532
604
|
# since they are only added when app._compile is called.
|
|
533
|
-
self.
|
|
605
|
+
self._unevaluated_pages.pop(route)
|
|
534
606
|
|
|
535
|
-
if route in self.
|
|
607
|
+
if route in self._unevaluated_pages:
|
|
536
608
|
route_name = (
|
|
537
609
|
f"`{route}` or `/`"
|
|
538
610
|
if route == constants.PageNames.INDEX_ROUTE
|
|
@@ -545,15 +617,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
545
617
|
|
|
546
618
|
# Setup dynamic args for the route.
|
|
547
619
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
548
|
-
state = self.
|
|
620
|
+
state = self._state if self._state else State
|
|
549
621
|
state.setup_dynamic_args(get_route_args(route))
|
|
550
622
|
|
|
551
623
|
if on_load:
|
|
552
|
-
self.
|
|
624
|
+
self._load_events[route] = (
|
|
553
625
|
on_load if isinstance(on_load, list) else [on_load]
|
|
554
626
|
)
|
|
555
627
|
|
|
556
|
-
self.
|
|
628
|
+
self._unevaluated_pages[route] = UnevaluatedPage(
|
|
557
629
|
component=component,
|
|
558
630
|
route=route,
|
|
559
631
|
title=title,
|
|
@@ -563,14 +635,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
563
635
|
meta=meta,
|
|
564
636
|
)
|
|
565
637
|
|
|
566
|
-
def _compile_page(self, route: str):
|
|
638
|
+
def _compile_page(self, route: str, save_page: bool = True):
|
|
567
639
|
"""Compile a page.
|
|
568
640
|
|
|
569
641
|
Args:
|
|
570
642
|
route: The route of the page to compile.
|
|
643
|
+
save_page: If True, the compiled page is saved to self._pages.
|
|
571
644
|
"""
|
|
572
645
|
component, enable_state = compiler.compile_unevaluated_page(
|
|
573
|
-
route, self.
|
|
646
|
+
route, self._unevaluated_pages[route], self._state, self.style, self.theme
|
|
574
647
|
)
|
|
575
648
|
|
|
576
649
|
if enable_state:
|
|
@@ -578,9 +651,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
578
651
|
|
|
579
652
|
# Add the page.
|
|
580
653
|
self._check_routes_conflict(route)
|
|
581
|
-
|
|
654
|
+
if save_page:
|
|
655
|
+
self._pages[route] = component
|
|
582
656
|
|
|
583
|
-
def get_load_events(self, route: str) -> list[IndividualEventType[
|
|
657
|
+
def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
|
|
584
658
|
"""Get the load events for a route.
|
|
585
659
|
|
|
586
660
|
Args:
|
|
@@ -592,7 +666,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
592
666
|
route = route.lstrip("/")
|
|
593
667
|
if route == "":
|
|
594
668
|
route = constants.PageNames.INDEX_ROUTE
|
|
595
|
-
return self.
|
|
669
|
+
return self._load_events.get(route, [])
|
|
596
670
|
|
|
597
671
|
def _check_routes_conflict(self, new_route: str):
|
|
598
672
|
"""Verify if there is any conflict between the new route and any existing route.
|
|
@@ -616,10 +690,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
616
690
|
constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
|
|
617
691
|
constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
|
|
618
692
|
)
|
|
619
|
-
for route in self.
|
|
693
|
+
for route in self._pages:
|
|
620
694
|
replaced_route = replace_brackets_with_keywords(route)
|
|
621
695
|
for rw, r, nr in zip(
|
|
622
|
-
replaced_route.split("/"),
|
|
696
|
+
replaced_route.split("/"),
|
|
697
|
+
route.split("/"),
|
|
698
|
+
new_route.split("/"),
|
|
699
|
+
strict=False,
|
|
623
700
|
):
|
|
624
701
|
if rw in segments and r != nr:
|
|
625
702
|
# If the slugs in the segments of both routes are not the same, then the route is invalid
|
|
@@ -639,7 +716,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
639
716
|
title: str = constants.Page404.TITLE,
|
|
640
717
|
image: str = constants.Page404.IMAGE,
|
|
641
718
|
description: str = constants.Page404.DESCRIPTION,
|
|
642
|
-
on_load: EventType[
|
|
719
|
+
on_load: EventType[()] | None = None,
|
|
643
720
|
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
|
|
644
721
|
):
|
|
645
722
|
"""Define a custom 404 page for any url having no match.
|
|
@@ -650,8 +727,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
650
727
|
Args:
|
|
651
728
|
component: The component to display at the page.
|
|
652
729
|
title: The title of the page.
|
|
653
|
-
description: The description of the page.
|
|
654
730
|
image: The image to display on the page.
|
|
731
|
+
description: The description of the page.
|
|
655
732
|
on_load: The event handler(s) that will be called each time the page load.
|
|
656
733
|
meta: The metadata of the page.
|
|
657
734
|
"""
|
|
@@ -674,6 +751,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
674
751
|
def _setup_admin_dash(self):
|
|
675
752
|
"""Setup the admin dash."""
|
|
676
753
|
# Get the admin dash.
|
|
754
|
+
if not self.api:
|
|
755
|
+
return
|
|
756
|
+
|
|
677
757
|
admin_dash = self.admin_dash
|
|
678
758
|
|
|
679
759
|
if admin_dash and admin_dash.models:
|
|
@@ -715,7 +795,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
715
795
|
frontend_packages = get_config().frontend_packages
|
|
716
796
|
_frontend_packages = []
|
|
717
797
|
for package in frontend_packages:
|
|
718
|
-
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
798
|
+
if package in (get_config().tailwind or {}).get("plugins", []):
|
|
719
799
|
console.warn(
|
|
720
800
|
f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`"
|
|
721
801
|
)
|
|
@@ -778,10 +858,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
778
858
|
|
|
779
859
|
def _setup_overlay_component(self):
|
|
780
860
|
"""If a State is not used and no overlay_component is specified, do not render the connection modal."""
|
|
781
|
-
if self.
|
|
861
|
+
if self._state is None and self.overlay_component is default_overlay_component:
|
|
782
862
|
self.overlay_component = None
|
|
783
|
-
for k, component in self.
|
|
784
|
-
self.
|
|
863
|
+
for k, component in self._pages.items():
|
|
864
|
+
self._pages[k] = self._add_overlay_to_component(component)
|
|
785
865
|
|
|
786
866
|
def _add_error_boundary_to_component(self, component: Component) -> Component:
|
|
787
867
|
if self.error_boundary is None:
|
|
@@ -793,14 +873,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
793
873
|
|
|
794
874
|
def _setup_error_boundary(self):
|
|
795
875
|
"""If a State is not used and no error_boundary is specified, do not render the error boundary."""
|
|
796
|
-
if self.
|
|
876
|
+
if self._state is None and self.error_boundary is default_error_boundary:
|
|
797
877
|
self.error_boundary = None
|
|
798
878
|
|
|
799
|
-
for k, component in self.
|
|
879
|
+
for k, component in self._pages.items():
|
|
800
880
|
# Skip the 404 page
|
|
801
881
|
if k == constants.Page404.SLUG:
|
|
802
882
|
continue
|
|
803
|
-
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)
|
|
804
893
|
|
|
805
894
|
def _apply_decorated_pages(self):
|
|
806
895
|
"""Add @rx.page decorated pages to the app.
|
|
@@ -826,21 +915,27 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
826
915
|
Raises:
|
|
827
916
|
VarDependencyError: When a computed var has an invalid dependency.
|
|
828
917
|
"""
|
|
829
|
-
if not self.
|
|
918
|
+
if not self._state:
|
|
830
919
|
return
|
|
831
920
|
|
|
832
921
|
if not state:
|
|
833
|
-
state = self.
|
|
922
|
+
state = self._state
|
|
834
923
|
|
|
835
924
|
for var in state.computed_vars.values():
|
|
836
925
|
if not var._cache:
|
|
837
926
|
continue
|
|
838
927
|
deps = var._deps(objclass=state)
|
|
839
|
-
for
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
+
)
|
|
844
939
|
|
|
845
940
|
for substate in state.class_subclasses:
|
|
846
941
|
self._validate_var_dependencies(substate)
|
|
@@ -856,13 +951,13 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
856
951
|
"""
|
|
857
952
|
from reflex.utils.exceptions import ReflexRuntimeError
|
|
858
953
|
|
|
859
|
-
self.
|
|
954
|
+
self._pages = {}
|
|
860
955
|
|
|
861
956
|
def get_compilation_time() -> str:
|
|
862
957
|
return str(datetime.now().time()).split(".")[0]
|
|
863
958
|
|
|
864
959
|
# Render a default 404 page if the user didn't supply one
|
|
865
|
-
if constants.Page404.SLUG not in self.
|
|
960
|
+
if constants.Page404.SLUG not in self._unevaluated_pages:
|
|
866
961
|
self.add_page(route=constants.Page404.SLUG)
|
|
867
962
|
|
|
868
963
|
# Fix up the style.
|
|
@@ -878,19 +973,24 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
878
973
|
# If a theme component was provided, wrap the app with it
|
|
879
974
|
app_wrappers[(20, "Theme")] = self.theme
|
|
880
975
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
self._compile_page(route)
|
|
976
|
+
# Get the env mode.
|
|
977
|
+
config = get_config()
|
|
884
978
|
|
|
885
|
-
|
|
886
|
-
|
|
979
|
+
if config.react_strict_mode:
|
|
980
|
+
app_wrappers[(200, "StrictMode")] = StrictMode.create()
|
|
887
981
|
|
|
888
|
-
|
|
889
|
-
return
|
|
982
|
+
should_compile = self._should_compile()
|
|
890
983
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
894
994
|
|
|
895
995
|
# Create a progress bar.
|
|
896
996
|
progress = Progress(
|
|
@@ -900,18 +1000,33 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
900
1000
|
)
|
|
901
1001
|
|
|
902
1002
|
# try to be somewhat accurate - but still not 100%
|
|
903
|
-
adhoc_steps_without_executor =
|
|
1003
|
+
adhoc_steps_without_executor = 7
|
|
904
1004
|
fixed_pages_within_executor = 5
|
|
905
1005
|
progress.start()
|
|
906
1006
|
task = progress.add_task(
|
|
907
1007
|
f"[{get_compilation_time()}] Compiling:",
|
|
908
|
-
total=len(self.
|
|
1008
|
+
total=len(self._pages)
|
|
1009
|
+
+ (len(self._unevaluated_pages) * 2)
|
|
909
1010
|
+ fixed_pages_within_executor
|
|
910
1011
|
+ adhoc_steps_without_executor,
|
|
911
1012
|
)
|
|
912
1013
|
|
|
913
|
-
|
|
914
|
-
|
|
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)
|
|
915
1030
|
|
|
916
1031
|
# Store the compile results.
|
|
917
1032
|
compile_results = []
|
|
@@ -924,7 +1039,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
924
1039
|
|
|
925
1040
|
# This has to happen before compiling stateful components as that
|
|
926
1041
|
# prevents recursive functions from reaching all components.
|
|
927
|
-
for component in self.
|
|
1042
|
+
for component in self._pages.values():
|
|
928
1043
|
# Add component._get_all_imports() to all_imports.
|
|
929
1044
|
all_imports.update(component._get_all_imports())
|
|
930
1045
|
|
|
@@ -935,16 +1050,16 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
935
1050
|
custom_components |= component._get_all_custom_components()
|
|
936
1051
|
|
|
937
1052
|
# Perform auto-memoization of stateful components.
|
|
938
|
-
(
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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)
|
|
945
1060
|
|
|
946
1061
|
# Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
|
|
947
|
-
if code_uses_state_contexts(stateful_components_code) and self.
|
|
1062
|
+
if code_uses_state_contexts(stateful_components_code) and self._state is None:
|
|
948
1063
|
raise ReflexRuntimeError(
|
|
949
1064
|
"To access rx.State in frontend components, at least one "
|
|
950
1065
|
"subclass of rx.State must be defined in the app."
|
|
@@ -958,12 +1073,23 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
958
1073
|
compiler.compile_document_root(
|
|
959
1074
|
self.head_components,
|
|
960
1075
|
html_lang=self.html_lang,
|
|
961
|
-
html_custom_attrs=self.html_custom_attrs, #
|
|
1076
|
+
html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType]
|
|
962
1077
|
)
|
|
963
1078
|
)
|
|
964
1079
|
|
|
965
1080
|
progress.advance(task)
|
|
966
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
|
+
|
|
967
1093
|
# Use a forking process pool, if possible. Much faster, especially for large sites.
|
|
968
1094
|
# Fallback to ThreadPoolExecutor as something that will always work.
|
|
969
1095
|
executor = None
|
|
@@ -981,20 +1107,20 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
981
1107
|
max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
|
|
982
1108
|
)
|
|
983
1109
|
|
|
984
|
-
for route, component in zip(self.
|
|
1110
|
+
for route, component in zip(self._pages, page_components, strict=True):
|
|
985
1111
|
ExecutorSafeFunctions.COMPONENTS[route] = component
|
|
986
1112
|
|
|
987
|
-
ExecutorSafeFunctions.STATE = self.
|
|
1113
|
+
ExecutorSafeFunctions.STATE = self._state
|
|
988
1114
|
|
|
989
1115
|
with executor:
|
|
990
1116
|
result_futures = []
|
|
991
1117
|
|
|
992
|
-
def _submit_work(fn, *args, **kwargs):
|
|
1118
|
+
def _submit_work(fn: Callable, *args, **kwargs):
|
|
993
1119
|
f = executor.submit(fn, *args, **kwargs)
|
|
994
1120
|
result_futures.append(f)
|
|
995
1121
|
|
|
996
1122
|
# Compile the pre-compiled pages.
|
|
997
|
-
for route in self.
|
|
1123
|
+
for route in self._pages:
|
|
998
1124
|
_submit_work(
|
|
999
1125
|
ExecutorSafeFunctions.compile_page,
|
|
1000
1126
|
route,
|
|
@@ -1016,9 +1142,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1016
1142
|
_submit_work(compiler.remove_tailwind_from_postcss)
|
|
1017
1143
|
|
|
1018
1144
|
# Wait for all compilation tasks to complete.
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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)
|
|
1022
1149
|
|
|
1023
1150
|
app_root = self._app_root(app_wrappers=app_wrappers)
|
|
1024
1151
|
|
|
@@ -1029,7 +1156,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1029
1156
|
|
|
1030
1157
|
# Compile the contexts.
|
|
1031
1158
|
compile_results.append(
|
|
1032
|
-
compiler.compile_contexts(self.
|
|
1159
|
+
compiler.compile_contexts(self._state, self.theme),
|
|
1033
1160
|
)
|
|
1034
1161
|
if self.theme is not None:
|
|
1035
1162
|
# Fix #2992 by removing the top-level appearance prop
|
|
@@ -1053,7 +1180,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1053
1180
|
progress.stop()
|
|
1054
1181
|
|
|
1055
1182
|
# Install frontend packages.
|
|
1056
|
-
|
|
1183
|
+
with console.timing("Install Frontend Packages"):
|
|
1184
|
+
self._get_frontend_packages(all_imports)
|
|
1057
1185
|
|
|
1058
1186
|
# Setup the next.config.js
|
|
1059
1187
|
transpile_packages = [
|
|
@@ -1079,8 +1207,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1079
1207
|
# Remove pages that are no longer in the app.
|
|
1080
1208
|
p.unlink()
|
|
1081
1209
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1210
|
+
with console.timing("Write to Disk"):
|
|
1211
|
+
for output_path, code in compile_results:
|
|
1212
|
+
compiler_utils.write_page(output_path, code)
|
|
1084
1213
|
|
|
1085
1214
|
@contextlib.asynccontextmanager
|
|
1086
1215
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
@@ -1151,9 +1280,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1151
1280
|
)
|
|
1152
1281
|
|
|
1153
1282
|
task = asyncio.create_task(_coro())
|
|
1154
|
-
self.
|
|
1283
|
+
self._background_tasks.add(task)
|
|
1155
1284
|
# Clean up task from background_tasks set when complete.
|
|
1156
|
-
task.add_done_callback(self.
|
|
1285
|
+
task.add_done_callback(self._background_tasks.discard)
|
|
1157
1286
|
return task
|
|
1158
1287
|
|
|
1159
1288
|
def _validate_exception_handlers(self):
|
|
@@ -1163,11 +1292,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1163
1292
|
ValueError: If the custom exception handlers are invalid.
|
|
1164
1293
|
|
|
1165
1294
|
"""
|
|
1166
|
-
|
|
1295
|
+
frontend_arg_spec = {
|
|
1167
1296
|
"exception": Exception,
|
|
1168
1297
|
}
|
|
1169
1298
|
|
|
1170
|
-
|
|
1299
|
+
backend_arg_spec = {
|
|
1171
1300
|
"exception": Exception,
|
|
1172
1301
|
}
|
|
1173
1302
|
|
|
@@ -1175,9 +1304,10 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1175
1304
|
["frontend", "backend"],
|
|
1176
1305
|
[self.frontend_exception_handler, self.backend_exception_handler],
|
|
1177
1306
|
[
|
|
1178
|
-
|
|
1179
|
-
|
|
1307
|
+
frontend_arg_spec,
|
|
1308
|
+
backend_arg_spec,
|
|
1180
1309
|
],
|
|
1310
|
+
strict=True,
|
|
1181
1311
|
):
|
|
1182
1312
|
if hasattr(handler_fn, "__name__"):
|
|
1183
1313
|
_fn_name = handler_fn.__name__
|
|
@@ -1218,7 +1348,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1218
1348
|
):
|
|
1219
1349
|
raise ValueError(
|
|
1220
1350
|
f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order."
|
|
1221
|
-
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]}`"
|
|
1222
1352
|
)
|
|
1223
1353
|
|
|
1224
1354
|
if not issubclass(arg_annotations[required_arg], Exception):
|
|
@@ -1319,15 +1449,14 @@ async def process(
|
|
|
1319
1449
|
if app._process_background(state, event) is not None:
|
|
1320
1450
|
# `final=True` allows the frontend send more events immediately.
|
|
1321
1451
|
yield StateUpdate(final=True)
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
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
|
|
1331
1460
|
except Exception as ex:
|
|
1332
1461
|
telemetry.send_error(ex, context="backend")
|
|
1333
1462
|
|
|
@@ -1522,16 +1651,20 @@ class EventNamespace(AsyncNamespace):
|
|
|
1522
1651
|
self.sid_to_token = {}
|
|
1523
1652
|
self.app = app
|
|
1524
1653
|
|
|
1525
|
-
def on_connect(self, sid, environ):
|
|
1654
|
+
def on_connect(self, sid: str, environ: dict):
|
|
1526
1655
|
"""Event for when the websocket is connected.
|
|
1527
1656
|
|
|
1528
1657
|
Args:
|
|
1529
1658
|
sid: The Socket.IO session id.
|
|
1530
1659
|
environ: The request information, including HTTP headers.
|
|
1531
1660
|
"""
|
|
1532
|
-
|
|
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
|
+
)
|
|
1533
1666
|
|
|
1534
|
-
def on_disconnect(self, sid):
|
|
1667
|
+
def on_disconnect(self, sid: str):
|
|
1535
1668
|
"""Event for when the websocket disconnects.
|
|
1536
1669
|
|
|
1537
1670
|
Args:
|
|
@@ -1553,7 +1686,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1553
1686
|
self.emit(str(constants.SocketEvent.EVENT), update, to=sid)
|
|
1554
1687
|
)
|
|
1555
1688
|
|
|
1556
|
-
async def on_event(self, sid, data):
|
|
1689
|
+
async def on_event(self, sid: str, data: Any):
|
|
1557
1690
|
"""Event for receiving front-end websocket events.
|
|
1558
1691
|
|
|
1559
1692
|
Raises:
|
|
@@ -1562,12 +1695,36 @@ class EventNamespace(AsyncNamespace):
|
|
|
1562
1695
|
Args:
|
|
1563
1696
|
sid: The Socket.IO session id.
|
|
1564
1697
|
data: The event data.
|
|
1698
|
+
|
|
1699
|
+
Raises:
|
|
1700
|
+
EventDeserializationError: If the event data is not a dictionary.
|
|
1565
1701
|
"""
|
|
1566
1702
|
fields = data
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
|
1571
1728
|
|
|
1572
1729
|
self.token_to_sid[event.token] = sid
|
|
1573
1730
|
self.sid_to_token[sid] = event.token
|
|
@@ -1596,7 +1753,7 @@ class EventNamespace(AsyncNamespace):
|
|
|
1596
1753
|
# Emit the update from processing the event.
|
|
1597
1754
|
await self.emit_update(update=update, sid=sid)
|
|
1598
1755
|
|
|
1599
|
-
async def on_ping(self, sid):
|
|
1756
|
+
async def on_ping(self, sid: str):
|
|
1600
1757
|
"""Event for testing the API endpoint.
|
|
1601
1758
|
|
|
1602
1759
|
Args:
|