reflex 0.7.8a1__py3-none-any.whl → 0.7.9a2__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 +191 -87
- reflex/app_mixins/lifespan.py +2 -2
- reflex/compiler/compiler.py +31 -4
- reflex/components/base/body.pyi +3 -197
- reflex/components/base/link.pyi +4 -392
- reflex/components/base/meta.pyi +28 -608
- reflex/components/component.py +39 -57
- reflex/components/core/upload.py +8 -0
- reflex/components/dynamic.py +9 -1
- reflex/components/el/elements/metadata.pyi +0 -1
- reflex/components/markdown/markdown.py +0 -21
- reflex/components/markdown/markdown.pyi +2 -2
- 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/radix/themes/layout/list.pyi +2 -391
- 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/page.py +8 -0
- reflex/reflex.py +277 -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.8a1.dist-info → reflex-0.7.9a2.dist-info}/METADATA +6 -9
- {reflex-0.7.8a1.dist-info → reflex-0.7.9a2.dist-info}/RECORD +44 -44
- {reflex-0.7.8a1.dist-info → reflex-0.7.9a2.dist-info}/WHEEL +0 -0
- {reflex-0.7.8a1.dist-info → reflex-0.7.9a2.dist-info}/entry_points.txt +0 -0
- {reflex-0.7.8a1.dist-info → reflex-0.7.9a2.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 current = ref?.current;
|
|
247
|
+
if (current === undefined || current?.focus === undefined) {
|
|
248
|
+
console.error(
|
|
249
|
+
`No element found for ref ${event.payload.ref} in _set_focus`,
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
current.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,8 +488,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
439
488
|
set_breakpoints(self.style.pop("breakpoints"))
|
|
440
489
|
|
|
441
490
|
# Set up the API.
|
|
442
|
-
self._api =
|
|
443
|
-
|
|
491
|
+
self._api = Starlette(lifespan=self._run_lifespan_tasks)
|
|
492
|
+
App._add_cors(self._api)
|
|
444
493
|
self._add_default_endpoints()
|
|
445
494
|
|
|
446
495
|
for clz in App.__mro__:
|
|
@@ -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,9 +613,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
566
613
|
Returns:
|
|
567
614
|
The backend api.
|
|
568
615
|
"""
|
|
569
|
-
if not self.api:
|
|
570
|
-
raise ValueError("The app has not been initialized.")
|
|
571
|
-
|
|
572
616
|
# For py3.9 compatibility when redis is used, we MUST add any decorator pages
|
|
573
617
|
# before compiling the app in a thread to avoid event loop error (REF-2172).
|
|
574
618
|
self._apply_decorated_pages()
|
|
@@ -580,34 +624,71 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
580
624
|
# Force background compile errors to print eagerly
|
|
581
625
|
lambda f: f.result()
|
|
582
626
|
)
|
|
583
|
-
# Wait for the compile to finish
|
|
584
|
-
|
|
585
|
-
compile_future.result()
|
|
627
|
+
# Wait for the compile to finish to ensure all optional endpoints are mounted.
|
|
628
|
+
compile_future.result()
|
|
586
629
|
|
|
587
|
-
|
|
630
|
+
if not self._api:
|
|
631
|
+
raise ValueError("The app has not been initialized.")
|
|
632
|
+
if self._cached_fastapi_app is not None:
|
|
633
|
+
asgi_app = self._cached_fastapi_app
|
|
634
|
+
asgi_app.mount("", self._api)
|
|
635
|
+
App._add_cors(asgi_app)
|
|
636
|
+
else:
|
|
637
|
+
asgi_app = self._api
|
|
638
|
+
|
|
639
|
+
if self.api_transformer is not None:
|
|
640
|
+
api_transformers: Sequence[Starlette | Callable[[ASGIApp], ASGIApp]] = (
|
|
641
|
+
[self.api_transformer]
|
|
642
|
+
if not isinstance(self.api_transformer, Sequence)
|
|
643
|
+
else self.api_transformer
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
for api_transformer in api_transformers:
|
|
647
|
+
if isinstance(api_transformer, Starlette):
|
|
648
|
+
# Mount the api to the fastapi app.
|
|
649
|
+
App._add_cors(api_transformer)
|
|
650
|
+
api_transformer.mount("", asgi_app)
|
|
651
|
+
asgi_app = api_transformer
|
|
652
|
+
else:
|
|
653
|
+
# Transform the asgi app.
|
|
654
|
+
asgi_app = api_transformer(asgi_app)
|
|
655
|
+
|
|
656
|
+
return asgi_app
|
|
588
657
|
|
|
589
658
|
def _add_default_endpoints(self):
|
|
590
659
|
"""Add default api endpoints (ping)."""
|
|
591
660
|
# To test the server.
|
|
592
|
-
if not self.
|
|
661
|
+
if not self._api:
|
|
593
662
|
return
|
|
594
663
|
|
|
595
|
-
self.
|
|
596
|
-
|
|
664
|
+
self._api.add_route(
|
|
665
|
+
str(constants.Endpoint.PING),
|
|
666
|
+
ping,
|
|
667
|
+
methods=["GET"],
|
|
668
|
+
)
|
|
669
|
+
self._api.add_route(
|
|
670
|
+
str(constants.Endpoint.HEALTH),
|
|
671
|
+
health,
|
|
672
|
+
methods=["GET"],
|
|
673
|
+
)
|
|
597
674
|
|
|
598
675
|
def _add_optional_endpoints(self):
|
|
599
676
|
"""Add optional api endpoints (_upload)."""
|
|
600
|
-
if not self.
|
|
677
|
+
if not self._api:
|
|
601
678
|
return
|
|
602
679
|
upload_is_used_marker = (
|
|
603
680
|
prerequisites.get_backend_dir() / constants.Dirs.UPLOAD_IS_USED
|
|
604
681
|
)
|
|
605
682
|
if Upload.is_used or upload_is_used_marker.exists():
|
|
606
683
|
# To upload files.
|
|
607
|
-
self.
|
|
684
|
+
self._api.add_route(
|
|
685
|
+
str(constants.Endpoint.UPLOAD),
|
|
686
|
+
upload(self),
|
|
687
|
+
methods=["POST"],
|
|
688
|
+
)
|
|
608
689
|
|
|
609
690
|
# To access uploaded files.
|
|
610
|
-
self.
|
|
691
|
+
self._api.mount(
|
|
611
692
|
str(constants.Endpoint.UPLOAD),
|
|
612
693
|
StaticFiles(directory=get_upload_dir()),
|
|
613
694
|
name="uploaded_files",
|
|
@@ -616,17 +697,22 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
616
697
|
upload_is_used_marker.parent.mkdir(parents=True, exist_ok=True)
|
|
617
698
|
upload_is_used_marker.touch()
|
|
618
699
|
if codespaces.is_running_in_codespaces():
|
|
619
|
-
self.
|
|
620
|
-
|
|
700
|
+
self._api.add_route(
|
|
701
|
+
str(constants.Endpoint.AUTH_CODESPACE),
|
|
702
|
+
codespaces.auth_codespace,
|
|
703
|
+
methods=["GET"],
|
|
621
704
|
)
|
|
622
705
|
if environment.REFLEX_ADD_ALL_ROUTES_ENDPOINT.get():
|
|
623
706
|
self.add_all_routes_endpoint()
|
|
624
707
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
708
|
+
@staticmethod
|
|
709
|
+
def _add_cors(api: Starlette):
|
|
710
|
+
"""Add CORS middleware to the app.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
api: The Starlette app to add CORS middleware to.
|
|
714
|
+
"""
|
|
715
|
+
api.add_middleware(
|
|
630
716
|
cors.CORSMiddleware,
|
|
631
717
|
allow_credentials=True,
|
|
632
718
|
allow_methods=["*"],
|
|
@@ -719,22 +805,37 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
719
805
|
# Check if the route given is valid
|
|
720
806
|
verify_route_validity(route)
|
|
721
807
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
808
|
+
unevaluated_page = UnevaluatedPage(
|
|
809
|
+
component=component,
|
|
810
|
+
route=route,
|
|
811
|
+
title=title,
|
|
812
|
+
description=description,
|
|
813
|
+
image=image,
|
|
814
|
+
on_load=on_load,
|
|
815
|
+
meta=meta,
|
|
816
|
+
context=context,
|
|
817
|
+
)
|
|
727
818
|
|
|
728
819
|
if route in self._unevaluated_pages:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
820
|
+
if self._unevaluated_pages[route].component is component:
|
|
821
|
+
unevaluated_page = unevaluated_page.merged_with(
|
|
822
|
+
self._unevaluated_pages[route]
|
|
823
|
+
)
|
|
824
|
+
console.warn(
|
|
825
|
+
f"Page {route} is being redefined with the same component."
|
|
826
|
+
)
|
|
827
|
+
else:
|
|
828
|
+
route_name = (
|
|
829
|
+
f"`{route}` or `/`"
|
|
830
|
+
if route == constants.PageNames.INDEX_ROUTE
|
|
831
|
+
else f"`{route}`"
|
|
832
|
+
)
|
|
833
|
+
existing_component = self._unevaluated_pages[route].component
|
|
834
|
+
raise exceptions.RouteValueError(
|
|
835
|
+
f"Tried to add page {readable_name_from_component(component)} with route {route_name} but "
|
|
836
|
+
f"page {readable_name_from_component(existing_component)} with the same route already exists. "
|
|
837
|
+
"Make sure you do not have two pages with the same route."
|
|
838
|
+
)
|
|
738
839
|
|
|
739
840
|
# Setup dynamic args for the route.
|
|
740
841
|
# this state assignment is only required for tests using the deprecated state kwarg for App
|
|
@@ -746,16 +847,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
746
847
|
on_load if isinstance(on_load, list) else [on_load]
|
|
747
848
|
)
|
|
748
849
|
|
|
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
|
-
)
|
|
850
|
+
self._unevaluated_pages[route] = unevaluated_page
|
|
759
851
|
|
|
760
852
|
def _compile_page(self, route: str, save_page: bool = True):
|
|
761
853
|
"""Compile a page.
|
|
@@ -885,7 +977,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
885
977
|
return
|
|
886
978
|
|
|
887
979
|
# Get the admin dash.
|
|
888
|
-
if not self.
|
|
980
|
+
if not self._api:
|
|
889
981
|
return
|
|
890
982
|
|
|
891
983
|
admin_dash = self.admin_dash
|
|
@@ -906,7 +998,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
906
998
|
view = admin_dash.view_overrides.get(model, ModelView)
|
|
907
999
|
admin.add_view(view(model))
|
|
908
1000
|
|
|
909
|
-
admin.mount_to(self.
|
|
1001
|
+
admin.mount_to(self._api)
|
|
910
1002
|
|
|
911
1003
|
def _get_frontend_packages(self, imports: dict[str, set[ImportVar]]):
|
|
912
1004
|
"""Gets the frontend packages to be installed and filters out the unnecessary ones.
|
|
@@ -1021,8 +1113,9 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1021
1113
|
|
|
1022
1114
|
This can move back into `compile_` when py39 support is dropped.
|
|
1023
1115
|
"""
|
|
1116
|
+
app_name = get_config().app_name
|
|
1024
1117
|
# Add the @rx.page decorated pages to collect on_load events.
|
|
1025
|
-
for render, kwargs in DECORATED_PAGES[
|
|
1118
|
+
for render, kwargs in DECORATED_PAGES[app_name]:
|
|
1026
1119
|
self.add_page(render, **kwargs)
|
|
1027
1120
|
|
|
1028
1121
|
def _validate_var_dependencies(self, state: type[BaseState] | None = None) -> None:
|
|
@@ -1191,9 +1284,8 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1191
1284
|
|
|
1192
1285
|
progress.advance(task)
|
|
1193
1286
|
|
|
1194
|
-
# Track imports
|
|
1287
|
+
# Track imports found.
|
|
1195
1288
|
all_imports = {}
|
|
1196
|
-
custom_components = set()
|
|
1197
1289
|
|
|
1198
1290
|
# This has to happen before compiling stateful components as that
|
|
1199
1291
|
# prevents recursive functions from reaching all components.
|
|
@@ -1204,9 +1296,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1204
1296
|
# Add the app wrappers from this component.
|
|
1205
1297
|
app_wrappers.update(component._get_all_app_wrap_components())
|
|
1206
1298
|
|
|
1207
|
-
# Add the custom components from the page to the set.
|
|
1208
|
-
custom_components |= component._get_all_custom_components()
|
|
1209
|
-
|
|
1210
1299
|
if (toaster := self.toaster) is not None:
|
|
1211
1300
|
from reflex.components.component import memo
|
|
1212
1301
|
|
|
@@ -1224,9 +1313,6 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1224
1313
|
if component is not None:
|
|
1225
1314
|
app_wrappers[key] = component
|
|
1226
1315
|
|
|
1227
|
-
for component in app_wrappers.values():
|
|
1228
|
-
custom_components |= component._get_all_custom_components()
|
|
1229
|
-
|
|
1230
1316
|
if self.error_boundary:
|
|
1231
1317
|
from reflex.compiler.compiler import into_component
|
|
1232
1318
|
|
|
@@ -1351,7 +1437,7 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1351
1437
|
custom_components_output,
|
|
1352
1438
|
custom_components_result,
|
|
1353
1439
|
custom_components_imports,
|
|
1354
|
-
) = compiler.compile_components(
|
|
1440
|
+
) = compiler.compile_components(set(CUSTOM_COMPONENTS.values()))
|
|
1355
1441
|
compile_results.append((custom_components_output, custom_components_result))
|
|
1356
1442
|
all_imports.update(custom_components_imports)
|
|
1357
1443
|
|
|
@@ -1402,12 +1488,15 @@ class App(MiddlewareMixin, LifespanMixin):
|
|
|
1402
1488
|
|
|
1403
1489
|
def add_all_routes_endpoint(self):
|
|
1404
1490
|
"""Add an endpoint to the app that returns all the routes."""
|
|
1405
|
-
if not self.
|
|
1491
|
+
if not self._api:
|
|
1406
1492
|
return
|
|
1407
1493
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1494
|
+
async def all_routes(_request: Request) -> Response:
|
|
1495
|
+
return JSONResponse(list(self._unevaluated_pages.keys()))
|
|
1496
|
+
|
|
1497
|
+
self._api.add_route(
|
|
1498
|
+
str(constants.Endpoint.ALL_ROUTES), all_routes, methods=["GET"]
|
|
1499
|
+
)
|
|
1411
1500
|
|
|
1412
1501
|
@contextlib.asynccontextmanager
|
|
1413
1502
|
async def modify_state(self, token: str) -> AsyncIterator[BaseState]:
|
|
@@ -1662,18 +1751,24 @@ async def process(
|
|
|
1662
1751
|
raise
|
|
1663
1752
|
|
|
1664
1753
|
|
|
1665
|
-
async def ping() ->
|
|
1754
|
+
async def ping(_request: Request) -> Response:
|
|
1666
1755
|
"""Test API endpoint.
|
|
1667
1756
|
|
|
1757
|
+
Args:
|
|
1758
|
+
_request: The Starlette request object.
|
|
1759
|
+
|
|
1668
1760
|
Returns:
|
|
1669
1761
|
The response.
|
|
1670
1762
|
"""
|
|
1671
|
-
return "pong"
|
|
1763
|
+
return JSONResponse("pong")
|
|
1672
1764
|
|
|
1673
1765
|
|
|
1674
|
-
async def health() -> JSONResponse:
|
|
1766
|
+
async def health(_request: Request) -> JSONResponse:
|
|
1675
1767
|
"""Health check endpoint to assess the status of the database and Redis services.
|
|
1676
1768
|
|
|
1769
|
+
Args:
|
|
1770
|
+
_request: The Starlette request object.
|
|
1771
|
+
|
|
1677
1772
|
Returns:
|
|
1678
1773
|
JSONResponse: A JSON object with the health status:
|
|
1679
1774
|
- "status" (bool): Overall health, True if all checks pass.
|
|
@@ -1715,12 +1810,11 @@ def upload(app: App):
|
|
|
1715
1810
|
The upload function.
|
|
1716
1811
|
"""
|
|
1717
1812
|
|
|
1718
|
-
async def upload_file(request: Request
|
|
1813
|
+
async def upload_file(request: Request):
|
|
1719
1814
|
"""Upload a file.
|
|
1720
1815
|
|
|
1721
1816
|
Args:
|
|
1722
|
-
request: The
|
|
1723
|
-
files: The file(s) to upload.
|
|
1817
|
+
request: The Starlette request object.
|
|
1724
1818
|
|
|
1725
1819
|
Returns:
|
|
1726
1820
|
StreamingResponse yielding newline-delimited JSON of StateUpdate
|
|
@@ -1733,6 +1827,12 @@ def upload(app: App):
|
|
|
1733
1827
|
"""
|
|
1734
1828
|
from reflex.utils.exceptions import UploadTypeError, UploadValueError
|
|
1735
1829
|
|
|
1830
|
+
# Get the files from the request.
|
|
1831
|
+
files = await request.form()
|
|
1832
|
+
files = files.getlist("files")
|
|
1833
|
+
if not files:
|
|
1834
|
+
raise UploadValueError("No files were uploaded.")
|
|
1835
|
+
|
|
1736
1836
|
token = request.headers.get("reflex-client-token")
|
|
1737
1837
|
handler = request.headers.get("reflex-event-handler")
|
|
1738
1838
|
|
|
@@ -1785,6 +1885,10 @@ def upload(app: App):
|
|
|
1785
1885
|
# event is handled.
|
|
1786
1886
|
file_copies = []
|
|
1787
1887
|
for file in files:
|
|
1888
|
+
if not isinstance(file, StarletteUploadFile):
|
|
1889
|
+
raise UploadValueError(
|
|
1890
|
+
"Uploaded file is not an UploadFile." + str(file)
|
|
1891
|
+
)
|
|
1788
1892
|
content_copy = io.BytesIO()
|
|
1789
1893
|
content_copy.write(await file.read())
|
|
1790
1894
|
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:
|