reflex 0.7.8__py3-none-any.whl → 0.7.9a1__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/web/tailwind.config.js.jinja2 +65 -31
- reflex/.templates/web/utils/state.js +11 -1
- reflex/app.py +185 -79
- reflex/app_mixins/lifespan.py +2 -2
- reflex/compiler/compiler.py +31 -4
- reflex/components/component.py +39 -57
- reflex/components/core/upload.py +8 -0
- reflex/components/dynamic.py +9 -1
- reflex/components/markdown/markdown.py +0 -21
- reflex/components/radix/primitives/accordion.py +1 -1
- reflex/components/radix/primitives/form.py +1 -1
- reflex/components/radix/primitives/progress.py +1 -1
- reflex/components/radix/primitives/slider.py +1 -1
- reflex/components/radix/themes/color_mode.py +1 -1
- reflex/components/radix/themes/color_mode.pyi +1 -1
- reflex/components/recharts/recharts.py +2 -2
- reflex/components/sonner/toast.py +1 -1
- reflex/config.py +4 -7
- reflex/constants/base.py +21 -0
- reflex/constants/installer.py +6 -6
- reflex/custom_components/custom_components.py +67 -64
- reflex/event.py +2 -0
- reflex/reflex.py +276 -265
- reflex/testing.py +30 -24
- reflex/utils/codespaces.py +6 -2
- reflex/utils/console.py +4 -3
- reflex/utils/exec.py +60 -24
- reflex/utils/format.py +17 -2
- reflex/utils/prerequisites.py +43 -30
- reflex/utils/processes.py +6 -6
- reflex/utils/types.py +11 -6
- reflex/vars/base.py +19 -1
- {reflex-0.7.8.dist-info → reflex-0.7.9a1.dist-info}/METADATA +6 -9
- {reflex-0.7.8.dist-info → reflex-0.7.9a1.dist-info}/RECORD +37 -37
- {reflex-0.7.8.dist-info → reflex-0.7.9a1.dist-info}/WHEEL +0 -0
- {reflex-0.7.8.dist-info → reflex-0.7.9a1.dist-info}/entry_points.txt +0 -0
- {reflex-0.7.8.dist-info → reflex-0.7.9a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,32 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
{# Helper macro to render JS objects and arrays #}
|
|
2
|
+
{% macro render_js(val, indent=2, level=0) -%}
|
|
3
|
+
{%- set space = ' ' * (indent * level) -%}
|
|
4
|
+
{%- set next_space = ' ' * (indent * (level + 1)) -%}
|
|
5
|
+
|
|
6
|
+
{%- if val is mapping -%}
|
|
7
|
+
{
|
|
8
|
+
{%- for k, v in val.items() %}
|
|
9
|
+
{{ next_space }}{{ k if k is string and k.isidentifier() else k|tojson }}: {{ render_js(v, indent, level + 1) }}{{ "," if not loop.last }}
|
|
10
|
+
{%- endfor %}
|
|
11
|
+
{{ space }}}
|
|
12
|
+
{%- elif val is iterable and val is not string -%}
|
|
13
|
+
[
|
|
14
|
+
{%- for item in val %}
|
|
15
|
+
{{ next_space }}{{ render_js(item, indent, level + 1) }}{{ "," if not loop.last }}
|
|
16
|
+
{%- endfor %}
|
|
17
|
+
{{ space }}]
|
|
18
|
+
{%- else -%}
|
|
19
|
+
{{ val | tojson }}
|
|
20
|
+
{%- endif -%}
|
|
21
|
+
{%- endmacro %}
|
|
22
|
+
|
|
23
|
+
{# Extract destructured imports from plugin dicts only #}
|
|
24
|
+
{%- set imports = [] %}
|
|
25
|
+
{%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
|
|
26
|
+
{%- set _ = imports.append(plugin.import) %}
|
|
27
|
+
{%- endfor %}
|
|
28
|
+
|
|
29
|
+
/** @type {import('tailwindcss').Config} */
|
|
30
|
+
{%- for imp in imports %}
|
|
31
|
+
const { {{ imp.name }} } = require({{ imp.from | tojson }});
|
|
32
|
+
{%- endfor %}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
content: {{ render_js(content) }},
|
|
36
|
+
theme: {{ render_js(theme) }},
|
|
37
|
+
{% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
|
|
38
|
+
{% if corePlugins is defined %}corePlugins: {{ render_js(corePlugins) }},{% endif %}
|
|
39
|
+
{% if important is defined %}important: {{ important | tojson }},{% endif %}
|
|
40
|
+
{% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
|
|
41
|
+
{% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
|
|
42
|
+
{% if presets is defined %}
|
|
43
|
+
presets: [
|
|
44
|
+
{% for preset in presets %}
|
|
45
|
+
require({{ preset | tojson }}){{ "," if not loop.last }}
|
|
46
|
+
{% endfor %}
|
|
47
|
+
],
|
|
48
|
+
{% endif %}
|
|
49
|
+
plugins: [
|
|
50
|
+
{% for plugin in plugins %}
|
|
51
|
+
{% if plugin is mapping %}
|
|
52
|
+
{% if plugin.call is defined %}
|
|
53
|
+
{{ plugin.call }}(
|
|
54
|
+
{%- if plugin.args is defined -%}
|
|
55
|
+
{{ render_js(plugin.args) }}
|
|
56
|
+
{%- endif -%}
|
|
57
|
+
){{ "," if not loop.last }}
|
|
58
|
+
{% else %}
|
|
59
|
+
require({{ plugin.name | tojson }}){{ "," if not loop.last }}
|
|
60
|
+
{% endif %}
|
|
61
|
+
{% else %}
|
|
62
|
+
require({{ plugin | tojson }}){{ "," if not loop.last }}
|
|
63
|
+
{% endif %}
|
|
64
|
+
{% endfor %}
|
|
65
|
+
]
|
|
32
66
|
};
|
|
@@ -178,6 +178,9 @@ export const queueEventIfSocketExists = async (events, socket) => {
|
|
|
178
178
|
export const applyEvent = async (event, socket) => {
|
|
179
179
|
// Handle special events
|
|
180
180
|
if (event.name == "_redirect") {
|
|
181
|
+
if ((event.payload.path ?? undefined) === undefined) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
181
184
|
if (event.payload.external) {
|
|
182
185
|
window.open(event.payload.path, "_blank", "noopener");
|
|
183
186
|
} else if (event.payload.replace) {
|
|
@@ -240,7 +243,14 @@ export const applyEvent = async (event, socket) => {
|
|
|
240
243
|
if (event.name == "_set_focus") {
|
|
241
244
|
const ref =
|
|
242
245
|
event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
|
|
243
|
-
ref
|
|
246
|
+
const focus = ref?.current?.focus;
|
|
247
|
+
if (focus === undefined) {
|
|
248
|
+
console.error(
|
|
249
|
+
`No element found for ref ${event.payload.ref} in _set_focus`,
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
focus();
|
|
253
|
+
}
|
|
244
254
|
return false;
|
|
245
255
|
}
|
|
246
256
|
|
reflex/app.py
CHANGED
|
@@ -13,34 +13,43 @@ import io
|
|
|
13
13
|
import json
|
|
14
14
|
import sys
|
|
15
15
|
import traceback
|
|
16
|
-
from collections.abc import AsyncIterator, Callable, Coroutine,
|
|
16
|
+
from collections.abc import AsyncIterator, Callable, Coroutine, Sequence
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from timeit import default_timer as timer
|
|
20
20
|
from types import SimpleNamespace
|
|
21
21
|
from typing import TYPE_CHECKING, Any, BinaryIO, get_args, get_type_hints
|
|
22
22
|
|
|
23
|
-
from fastapi import FastAPI
|
|
24
|
-
from fastapi import UploadFile as FastAPIUploadFile
|
|
25
|
-
from fastapi.middleware import cors
|
|
26
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
27
|
-
from fastapi.staticfiles import StaticFiles
|
|
23
|
+
from fastapi import FastAPI
|
|
28
24
|
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
|
|
29
|
-
from socketio import ASGIApp
|
|
25
|
+
from socketio import ASGIApp as EngineIOApp
|
|
26
|
+
from socketio import AsyncNamespace, AsyncServer
|
|
27
|
+
from starlette.applications import Starlette
|
|
30
28
|
from starlette.datastructures import Headers
|
|
31
29
|
from starlette.datastructures import UploadFile as StarletteUploadFile
|
|
30
|
+
from starlette.exceptions import HTTPException
|
|
31
|
+
from starlette.middleware import cors
|
|
32
|
+
from starlette.requests import Request
|
|
33
|
+
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
34
|
+
from starlette.staticfiles import StaticFiles
|
|
35
|
+
from typing_extensions import deprecated
|
|
32
36
|
|
|
33
37
|
from reflex import constants
|
|
34
38
|
from reflex.admin import AdminDash
|
|
35
39
|
from reflex.app_mixins import AppMixin, LifespanMixin, MiddlewareMixin
|
|
36
40
|
from reflex.compiler import compiler
|
|
37
41
|
from reflex.compiler import utils as compiler_utils
|
|
38
|
-
from reflex.compiler.compiler import
|
|
42
|
+
from reflex.compiler.compiler import (
|
|
43
|
+
ExecutorSafeFunctions,
|
|
44
|
+
compile_theme,
|
|
45
|
+
readable_name_from_component,
|
|
46
|
+
)
|
|
39
47
|
from reflex.components.base.app_wrap import AppWrap
|
|
40
48
|
from reflex.components.base.error_boundary import ErrorBoundary
|
|
41
49
|
from reflex.components.base.fragment import Fragment
|
|
42
50
|
from reflex.components.base.strict_mode import StrictMode
|
|
43
51
|
from reflex.components.component import (
|
|
52
|
+
CUSTOM_COMPONENTS,
|
|
44
53
|
Component,
|
|
45
54
|
ComponentStyle,
|
|
46
55
|
evaluate_style_namespaces,
|
|
@@ -97,6 +106,7 @@ from reflex.utils import (
|
|
|
97
106
|
)
|
|
98
107
|
from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
|
|
99
108
|
from reflex.utils.imports import ImportVar
|
|
109
|
+
from reflex.utils.types import ASGIApp, Message, Receive, Scope, Send
|
|
100
110
|
|
|
101
111
|
if TYPE_CHECKING:
|
|
102
112
|
from reflex.vars import Var
|
|
@@ -284,6 +294,25 @@ class UnevaluatedPage:
|
|
|
284
294
|
meta: list[dict[str, str]]
|
|
285
295
|
context: dict[str, Any] | None
|
|
286
296
|
|
|
297
|
+
def merged_with(self, other: UnevaluatedPage) -> UnevaluatedPage:
|
|
298
|
+
"""Merge the other page into this one.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
other: The other page to merge with.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The merged page.
|
|
305
|
+
"""
|
|
306
|
+
return dataclasses.replace(
|
|
307
|
+
self,
|
|
308
|
+
title=self.title if self.title is not None else other.title,
|
|
309
|
+
description=self.description
|
|
310
|
+
if self.description is not None
|
|
311
|
+
else other.description,
|
|
312
|
+
on_load=self.on_load if self.on_load is not None else other.on_load,
|
|
313
|
+
context=self.context if self.context is not None else other.context,
|
|
314
|
+
)
|
|
315
|
+
|
|
287
316
|
|
|
288
317
|
@dataclasses.dataclass()
|
|
289
318
|
class App(MiddlewareMixin, LifespanMixin):
|
|
@@ -365,7 +394,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
365
394
|
_stateful_pages: dict[str, None] = dataclasses.field(default_factory=dict)
|
|
366
395
|
|
|
367
396
|
# The backend API object.
|
|
368
|
-
_api:
|
|
397
|
+
_api: Starlette | None = None
|
|
369
398
|
|
|
370
399
|
# The state class to use for the app.
|
|
371
400
|
_state: type[BaseState] | None = None
|
|
@@ -400,14 +429,34 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
400
429
|
# Put the toast provider in the app wrap.
|
|
401
430
|
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
|
|
402
431
|
|
|
432
|
+
# Transform the ASGI app before running it.
|
|
433
|
+
api_transformer: (
|
|
434
|
+
Sequence[Callable[[ASGIApp], ASGIApp] | Starlette]
|
|
435
|
+
| Callable[[ASGIApp], ASGIApp]
|
|
436
|
+
| Starlette
|
|
437
|
+
| None
|
|
438
|
+
) = None
|
|
439
|
+
|
|
440
|
+
# FastAPI app for compatibility with FastAPI.
|
|
441
|
+
_cached_fastapi_app: FastAPI | None = None
|
|
442
|
+
|
|
403
443
|
@property
|
|
404
|
-
|
|
444
|
+
@deprecated("Use `api_transformer=your_fastapi_app` instead.")
|
|
445
|
+
def api(self) -> FastAPI:
|
|
405
446
|
"""Get the backend api.
|
|
406
447
|
|
|
407
448
|
Returns:
|
|
408
449
|
The backend api.
|
|
409
450
|
"""
|
|
410
|
-
|
|
451
|
+
if self._cached_fastapi_app is None:
|
|
452
|
+
self._cached_fastapi_app = FastAPI()
|
|
453
|
+
console.deprecate(
|
|
454
|
+
feature_name="App.api",
|
|
455
|
+
reason="Set `api_transformer=your_fastapi_app` instead.",
|
|
456
|
+
deprecation_version="0.7.9",
|
|
457
|
+
removal_version="0.8.0",
|
|
458
|
+
)
|
|
459
|
+
return self._cached_fastapi_app
|
|
411
460
|
|
|
412
461
|
@property
|
|
413
462
|
def event_namespace(self) -> EventNamespace | None:
|
|
@@ -439,7 +488,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
439
488
|
set_breakpoints(self.style.pop("breakpoints"))
|
|
440
489
|
|
|
441
490
|
# Set up the API.
|
|
442
|
-
self._api =
|
|
491
|
+
self._api = Starlette(lifespan=self._run_lifespan_tasks)
|
|
443
492
|
self._add_cors()
|
|
444
493
|
self._add_default_endpoints()
|
|
445
494
|
|
|
@@ -505,7 +554,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
505
554
|
)
|
|
506
555
|
|
|
507
556
|
# Create the socket app. Note event endpoint constant replaces the default 'socket.io' path.
|
|
508
|
-
socket_app =
|
|
557
|
+
socket_app = EngineIOApp(self.sio, socketio_path="")
|
|
509
558
|
namespace = config.get_event_namespace()
|
|
510
559
|
|
|
511
560
|
# Create the event namespace and attach the main app. Not related to any paths.
|
|
@@ -514,18 +563,16 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
514
563
|
# Register the event namespace with the socket.
|
|
515
564
|
self.sio.register_namespace(self.event_namespace)
|
|
516
565
|
# Mount the socket app with the API.
|
|
517
|
-
if self.
|
|
566
|
+
if self._api:
|
|
518
567
|
|
|
519
568
|
class HeaderMiddleware:
|
|
520
569
|
def __init__(self, app: ASGIApp):
|
|
521
570
|
self.app = app
|
|
522
571
|
|
|
523
|
-
async def __call__(
|
|
524
|
-
self, scope: MutableMapping[str, Any], receive: Any, send: Callable
|
|
525
|
-
):
|
|
572
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
526
573
|
original_send = send
|
|
527
574
|
|
|
528
|
-
async def modified_send(message:
|
|
575
|
+
async def modified_send(message: Message):
|
|
529
576
|
if message["type"] == "websocket.accept":
|
|
530
577
|
if scope.get("subprotocols"):
|
|
531
578
|
# The following *does* say "subprotocol" instead of "subprotocols", intentionally.
|
|
@@ -544,7 +591,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
544
591
|
return await self.app(scope, receive, modified_send)
|
|
545
592
|
|
|
546
593
|
socket_app_with_headers = HeaderMiddleware(socket_app)
|
|
547
|
-
self.
|
|
594
|
+
self._api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers)
|
|
548
595
|
|
|
549
596
|
# Check the exception handlers
|
|
550
597
|
self._validate_exception_handlers()
|
|
@@ -557,7 +604,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
557
604
|
"""
|
|
558
605
|
return f"<App state={self._state.__name__ if self._state else None}>"
|
|
559
606
|
|
|
560
|
-
def __call__(self) ->
|
|
607
|
+
def __call__(self) -> ASGIApp:
|
|
561
608
|
"""Run the backend api instance.
|
|
562
609
|
|
|
563
610
|
Raises:
|
|
@@ -566,8 +613,18 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
566
613
|
Returns:
|
|
567
614
|
The backend api.
|
|
568
615
|
"""
|
|
569
|
-
if not
|
|
570
|
-
|
|
616
|
+
if self._cached_fastapi_app is not None:
|
|
617
|
+
asgi_app = self._cached_fastapi_app
|
|
618
|
+
|
|
619
|
+
if not asgi_app or not self._api:
|
|
620
|
+
raise ValueError("The app has not been initialized.")
|
|
621
|
+
|
|
622
|
+
asgi_app.mount("", self._api)
|
|
623
|
+
else:
|
|
624
|
+
asgi_app = self._api
|
|
625
|
+
|
|
626
|
+
if not asgi_app:
|
|
627
|
+
raise ValueError("The app has not been initialized.")
|
|
571
628
|
|
|
572
629
|
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
|
573
630
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
|
@@ -584,30 +641,58 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
584
641
|
if is_prod_mode():
|
|
585
642
|
compile_future.result()
|
|
586
643
|
|
|
587
|
-
|
|
644
|
+
if self.api_transformer is not None:
|
|
645
|
+
api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
|
|
646
|
+
[self.api_transformer]
|
|
647
|
+
if not isinstance(self.api_transformer, Sequence)
|
|
648
|
+
else self.api_transformer
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
for api_transformer in api_transformers:
|
|
652
|
+
if isinstance(api_transformer, Starlette):
|
|
653
|
+
# Mount the api to the fastapi app.
|
|
654
|
+
api_transformer.mount("", asgi_app)
|
|
655
|
+
asgi_app = api_transformer
|
|
656
|
+
else:
|
|
657
|
+
# Transform the asgi app.
|
|
658
|
+
asgi_app = api_transformer(asgi_app)
|
|
659
|
+
|
|
660
|
+
return asgi_app
|
|
588
661
|
|
|
589
662
|
def _add_default_endpoints(self):
|
|
590
663
|
"""Add default api endpoints (ping)."""
|
|
591
664
|
# To test the server.
|
|
592
|
-
if not self.
|
|
665
|
+
if not self._api:
|
|
593
666
|
return
|
|
594
667
|
|
|
595
|
-
self.
|
|
596
|
-
|
|
668
|
+
self._api.add_route(
|
|
669
|
+
str(constants.Endpoint.PING),
|
|
670
|
+
ping,
|
|
671
|
+
methods=["GET"],
|
|
672
|
+
)
|
|
673
|
+
self._api.add_route(
|
|
674
|
+
str(constants.Endpoint.HEALTH),
|
|
675
|
+
health,
|
|
676
|
+
methods=["GET"],
|
|
677
|
+
)
|
|
597
678
|
|
|
598
679
|
def _add_optional_endpoints(self):
|
|
599
680
|
"""Add optional api endpoints (_upload)."""
|
|
600
|
-
if not self.
|
|
681
|
+
if not self._api:
|
|
601
682
|
return
|
|
602
683
|
upload_is_used_marker = (
|
|
603
684
|
prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
|
|
604
685
|
)
|
|
605
686
|
if Upload.is_used or upload_is_used_marker.exists():
|
|
606
687
|
# To upload files.
|
|
607
|
-
self.
|
|
688
|
+
self._api.add_route(
|
|
689
|
+
str(constants.Endpoint.UPLOAD),
|
|
690
|
+
upload(self),
|
|
691
|
+
methods=["POST"],
|
|
692
|
+
)
|
|
608
693
|
|
|
609
694
|
# To access uploaded files.
|
|
610
|
-
self.
|
|
695
|
+
self._api.mount(
|
|
611
696
|
str(constants.Endpoint.UPLOAD),
|
|
612
697
|
StaticFiles(directory=get_upload_dir()),
|
|
613
698
|
name="uploaded_files",
|
|
@@ -616,17 +701,19 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
616
701
|
upload_is_used_marker.parent.mkdir(parents=True, exist_ok=True)
|
|
617
702
|
upload_is_used_marker.touch()
|
|
618
703
|
if codespaces.is_running_in_codespaces():
|
|
619
|
-
self.
|
|
620
|
-
|
|
704
|
+
self._api.add_route(
|
|
705
|
+
str(constants.Endpoint.AUTH_CODESPACE),
|
|
706
|
+
codespaces.auth_codespace,
|
|
707
|
+
methods=["GET"],
|
|
621
708
|
)
|
|
622
709
|
if environment.REFLEX_ADD_ALL_ROUTES_ENDPOINT.get():
|
|
623
710
|
self.add_all_routes_endpoint()
|
|
624
711
|
|
|
625
712
|
def _add_cors(self):
|
|
626
713
|
"""Add CORS middleware to the app."""
|
|
627
|
-
if not self.
|
|
714
|
+
if not self._api:
|
|
628
715
|
return
|
|
629
|
-
self.
|
|
716
|
+
self._api.add_middleware(
|
|
630
717
|
cors.CORSMiddleware,
|
|
631
718
|
allow_credentials=True,
|
|
632
719
|
allow_methods=["*"],
|
|
@@ -719,22 +806,37 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
719
806
|
# Check if the route given is valid
|
|
720
807
|
verify_route_validity(route)
|
|
721
808
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
809
|
+
unevaluated_page = UnevaluatedPage(
|
|
810
|
+
component=component,
|
|
811
|
+
route=route,
|
|
812
|
+
title=title,
|
|
813
|
+
description=description,
|
|
814
|
+
image=image,
|
|
815
|
+
on_load=on_load,
|
|
816
|
+
meta=meta,
|
|
817
|
+
context=context,
|
|
818
|
+
)
|
|
727
819
|
|
|
728
820
|
if route in self._unevaluated_pages:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
821
|
+
if self._unevaluated_pages[route].component is component:
|
|
822
|
+
unevaluated_page = unevaluated_page.merged_with(
|
|
823
|
+
self._unevaluated_pages[route]
|
|
824
|
+
)
|
|
825
|
+
console.warn(
|
|
826
|
+
f"Page {route} is being redefined with the same component."
|
|
827
|
+
)
|
|
828
|
+
else:
|
|
829
|
+
route_name = (
|
|
830
|
+
f"`{route}` or `/`"
|
|
831
|
+
if route == constants.PageNames.INDEX_ROUTE
|
|
832
|
+
else f"`{route}`"
|
|
833
|
+
)
|
|
834
|
+
existing_component = self._unevaluated_pages[route].component
|
|
835
|
+
raise exceptions.RouteValueError(
|
|
836
|
+
f"Tried to add page {readable_name_from_component(component)} with route {route_name} but "
|
|
837
|
+
f"page {readable_name_from_component(existing_component)} with the same route already exists. "
|
|
838
|
+
"Make sure you do not have two pages with the same route."
|
|
839
|
+
)
|
|
738
840
|
|
|
739
841
|
# Setup dynamic args for the route.
|
|
740
842
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
@@ -746,16 +848,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
746
848
|
on_load if isinstance(on_load, list) else [on_load]
|
|
747
849
|
)
|
|
748
850
|
|
|
749
|
-
self._unevaluated_pages[route] =
|
|
750
|
-
component=component,
|
|
751
|
-
route=route,
|
|
752
|
-
title=title,
|
|
753
|
-
description=description,
|
|
754
|
-
image=image,
|
|
755
|
-
on_load=on_load,
|
|
756
|
-
meta=meta,
|
|
757
|
-
context=context,
|
|
758
|
-
)
|
|
851
|
+
self._unevaluated_pages[route] = unevaluated_page
|
|
759
852
|
|
|
760
853
|
def _compile_page(self, route: str, save_page: bool = True):
|
|
761
854
|
"""Compile a page.
|
|
@@ -885,7 +978,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
885
978
|
return
|
|
886
979
|
|
|
887
980
|
# Get the admin dash.
|
|
888
|
-
if not self.
|
|
981
|
+
if not self._api:
|
|
889
982
|
return
|
|
890
983
|
|
|
891
984
|
admin_dash = self.admin_dash
|
|
@@ -906,7 +999,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
906
999
|
view = admin_dash.view_overrides.get(model, ModelView)
|
|
907
1000
|
admin.add_view(view(model))
|
|
908
1001
|
|
|
909
|
-
admin.mount_to(self.
|
|
1002
|
+
admin.mount_to(self._api)
|
|
910
1003
|
|
|
911
1004
|
def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]):
|
|
912
1005
|
"""Gets the frontend packages to be installed and filters out the unnecessary ones.
|
|
@@ -1021,9 +1114,11 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1021
1114
|
|
|
1022
1115
|
This can move back into `compile_` when py39 support is dropped.
|
|
1023
1116
|
"""
|
|
1117
|
+
app_name = get_config().app_name
|
|
1024
1118
|
# Add the @rx.page decorated pages to collect on_load events.
|
|
1025
|
-
for render, kwargs in DECORATED_PAGES[
|
|
1119
|
+
for render, kwargs in DECORATED_PAGES[app_name]:
|
|
1026
1120
|
self.add_page(render, **kwargs)
|
|
1121
|
+
DECORATED_PAGES[app_name].clear()
|
|
1027
1122
|
|
|
1028
1123
|
def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> None:
|
|
1029
1124
|
"""Validate the dependencies of the vars in the app.
|
|
@@ -1191,9 +1286,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1191
1286
|
|
|
1192
1287
|
progress.advance(task)
|
|
1193
1288
|
|
|
1194
|
-
# Track imports
|
|
1289
|
+
# Track imports found.
|
|
1195
1290
|
all_imports = {}
|
|
1196
|
-
custom_components = set()
|
|
1197
1291
|
|
|
1198
1292
|
# This has to happen before compiling stateful components as that
|
|
1199
1293
|
# prevents recursive functions from reaching all components.
|
|
@@ -1204,9 +1298,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1204
1298
|
# Add the app wrappers from this component.
|
|
1205
1299
|
app_wrappers.update(component._get_all_app_wrap_components())
|
|
1206
1300
|
|
|
1207
|
-
# Add the custom components from the page to the set.
|
|
1208
|
-
custom_components |= component._get_all_custom_components()
|
|
1209
|
-
|
|
1210
1301
|
if (toaster := self.toaster) is not None:
|
|
1211
1302
|
from reflex.components.component import memo
|
|
1212
1303
|
|
|
@@ -1224,9 +1315,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1224
1315
|
if component is not None:
|
|
1225
1316
|
app_wrappers[key] = component
|
|
1226
1317
|
|
|
1227
|
-
for component in app_wrappers.values():
|
|
1228
|
-
custom_components |= component._get_all_custom_components()
|
|
1229
|
-
|
|
1230
1318
|
if self.error_boundary:
|
|
1231
1319
|
from reflex.compiler.compiler import into_component
|
|
1232
1320
|
|
|
@@ -1351,7 +1439,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1351
1439
|
custom_components_output,
|
|
1352
1440
|
custom_components_result,
|
|
1353
1441
|
custom_components_imports,
|
|
1354
|
-
) = compiler.compile_components(
|
|
1442
|
+
) = compiler.compile_components(set(CUSTOM_COMPONENTS.values()))
|
|
1355
1443
|
compile_results.append((custom_components_output, custom_components_result))
|
|
1356
1444
|
all_imports.update(custom_components_imports)
|
|
1357
1445
|
|
|
@@ -1402,12 +1490,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1402
1490
|
|
|
1403
1491
|
def add_all_routes_endpoint(self):
|
|
1404
1492
|
"""Add an endpoint to the app that returns all the routes."""
|
|
1405
|
-
if not self.
|
|
1493
|
+
if not self._api:
|
|
1406
1494
|
return
|
|
1407
1495
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1496
|
+
async def all_routes(_request: Request) -> Response:
|
|
1497
|
+
return JSONResponse(list(self._unevaluated_pages.keys()))
|
|
1498
|
+
|
|
1499
|
+
self._api.add_route(
|
|
1500
|
+
str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
|
|
1501
|
+
)
|
|
1411
1502
|
|
|
1412
1503
|
@contextlib.asynccontextmanager
|
|
1413
1504
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
@@ -1662,18 +1753,24 @@ async def process(
|
|
|
1662
1753
|
raise
|
|
1663
1754
|
|
|
1664
1755
|
|
|
1665
|
-
async def ping() ->
|
|
1756
|
+
async def ping(_request: Request) -> Response:
|
|
1666
1757
|
"""Test API endpoint.
|
|
1667
1758
|
|
|
1759
|
+
Args:
|
|
1760
|
+
_request: The Starlette request object.
|
|
1761
|
+
|
|
1668
1762
|
Returns:
|
|
1669
1763
|
The response.
|
|
1670
1764
|
"""
|
|
1671
|
-
return "pong"
|
|
1765
|
+
return JSONResponse("pong")
|
|
1672
1766
|
|
|
1673
1767
|
|
|
1674
|
-
async def health() -> JSONResponse:
|
|
1768
|
+
async def health(_request: Request) -> JSONResponse:
|
|
1675
1769
|
"""Health check endpoint to assess the status of the database and Redis services.
|
|
1676
1770
|
|
|
1771
|
+
Args:
|
|
1772
|
+
_request: The Starlette request object.
|
|
1773
|
+
|
|
1677
1774
|
Returns:
|
|
1678
1775
|
JSONResponse: A JSON object with the health status:
|
|
1679
1776
|
- "status" (bool): Overall health, True if all checks pass.
|
|
@@ -1715,12 +1812,11 @@ def upload(app: App):
|
|
|
1715
1812
|
The upload function.
|
|
1716
1813
|
"""
|
|
1717
1814
|
|
|
1718
|
-
async def upload_file(request: Request
|
|
1815
|
+
async def upload_file(request: Request):
|
|
1719
1816
|
"""Upload a file.
|
|
1720
1817
|
|
|
1721
1818
|
Args:
|
|
1722
|
-
request: The
|
|
1723
|
-
files: The file(s) to upload.
|
|
1819
|
+
request: The Starlette request object.
|
|
1724
1820
|
|
|
1725
1821
|
Returns:
|
|
1726
1822
|
StreamingResponse yielding newline-delimited JSON of StateUpdate
|
|
@@ -1733,6 +1829,12 @@ def upload(app: App):
|
|
|
1733
1829
|
"""
|
|
1734
1830
|
from reflex.utils.exceptions import UploadTypeError, UploadValueError
|
|
1735
1831
|
|
|
1832
|
+
# Get the files from the request.
|
|
1833
|
+
files = await request.form()
|
|
1834
|
+
files = files.getlist("files")
|
|
1835
|
+
if not files:
|
|
1836
|
+
raise UploadValueError("No files were uploaded.")
|
|
1837
|
+
|
|
1736
1838
|
token = request.headers.get("reflex-client-token")
|
|
1737
1839
|
handler = request.headers.get("reflex-event-handler")
|
|
1738
1840
|
|
|
@@ -1785,6 +1887,10 @@ def upload(app: App):
|
|
|
1785
1887
|
# event is handled.
|
|
1786
1888
|
file_copies = []
|
|
1787
1889
|
for file in files:
|
|
1890
|
+
if not isinstance(file, StarletteUploadFile):
|
|
1891
|
+
raise UploadValueError(
|
|
1892
|
+
"Uploaded file is not an UploadFile." + str(file)
|
|
1893
|
+
)
|
|
1788
1894
|
content_copy = io.BytesIO()
|
|
1789
1895
|
content_copy.write(await file.read())
|
|
1790
1896
|
content_copy.seek(0)
|
reflex/app_mixins/lifespan.py
CHANGED
|
@@ -9,7 +9,7 @@ import functools
|
|
|
9
9
|
import inspect
|
|
10
10
|
from collections.abc import Callable, Coroutine
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from starlette.applications import Starlette
|
|
13
13
|
|
|
14
14
|
from reflex.utils import console
|
|
15
15
|
from reflex.utils.exceptions import InvalidLifespanTaskTypeError
|
|
@@ -27,7 +27,7 @@ class LifespanMixin(AppMixin):
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
@contextlib.asynccontextmanager
|
|
30
|
-
async def _run_lifespan_tasks(self, app:
|
|
30
|
+
async def _run_lifespan_tasks(self, app: Starlette):
|
|
31
31
|
running_tasks = []
|
|
32
32
|
try:
|
|
33
33
|
async with contextlib.AsyncExitStack() as stack:
|