reflex 0.7.14a6__py3-none-any.whl → 0.8.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/app/rxconfig.py.jinja2 +4 -1
- reflex/.templates/jinja/web/package.json.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/_app.js.jinja2 +21 -11
- reflex/.templates/jinja/web/pages/_document.js.jinja2 +1 -1
- reflex/.templates/jinja/web/pages/base_page.js.jinja2 +0 -1
- reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
- reflex/.templates/jinja/web/styles/styles.css.jinja2 +1 -0
- reflex/.templates/jinja/web/utils/context.js.jinja2 +25 -8
- reflex/.templates/web/app/entry.client.js +8 -0
- reflex/.templates/web/app/routes.js +10 -0
- reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +12 -37
- reflex/.templates/web/postcss.config.js +1 -1
- reflex/.templates/web/react-router.config.js +6 -0
- reflex/.templates/web/styles/__reflex_style_reset.css +399 -0
- reflex/.templates/web/utils/client_side_routing.js +21 -19
- reflex/.templates/web/utils/react-theme.js +92 -0
- reflex/.templates/web/utils/state.js +251 -100
- reflex/.templates/web/vite-plugin-safari-cachebust.js +160 -0
- reflex/.templates/web/vite.config.js +39 -0
- reflex/__init__.py +1 -6
- reflex/__init__.pyi +327 -192
- reflex/app.py +86 -135
- reflex/base.py +1 -87
- reflex/compiler/compiler.py +70 -19
- reflex/compiler/templates.py +3 -3
- reflex/compiler/utils.py +91 -33
- reflex/components/__init__.py +0 -2
- reflex/components/__init__.pyi +34 -18
- reflex/components/base/__init__.py +1 -5
- reflex/components/base/__init__.pyi +30 -21
- reflex/components/base/app_wrap.pyi +7 -7
- reflex/components/base/body.pyi +7 -7
- reflex/components/base/document.py +18 -14
- reflex/components/base/document.pyi +88 -38
- reflex/components/base/error_boundary.pyi +7 -7
- reflex/components/base/fragment.pyi +7 -7
- reflex/components/base/link.pyi +12 -12
- reflex/components/base/meta.py +4 -15
- reflex/components/base/meta.pyi +31 -31
- reflex/components/base/script.py +60 -58
- reflex/components/base/script.pyi +248 -34
- reflex/components/base/strict_mode.pyi +7 -7
- reflex/components/component.py +146 -217
- reflex/components/core/__init__.py +1 -0
- reflex/components/core/__init__.pyi +77 -37
- reflex/components/core/auto_scroll.pyi +7 -7
- reflex/components/core/banner.pyi +33 -33
- reflex/components/core/client_side_routing.py +7 -6
- reflex/components/core/client_side_routing.pyi +8 -59
- reflex/components/core/clipboard.pyi +7 -7
- reflex/components/core/debounce.py +1 -0
- reflex/components/core/debounce.pyi +7 -7
- reflex/components/core/foreach.py +5 -4
- reflex/components/core/helmet.py +14 -0
- reflex/components/{next/base.pyi → core/helmet.pyi} +12 -10
- reflex/components/core/html.pyi +7 -7
- reflex/components/core/match.py +3 -3
- reflex/components/core/sticky.pyi +21 -20
- reflex/components/core/upload.py +4 -2
- reflex/components/core/upload.pyi +26 -25
- reflex/components/datadisplay/__init__.pyi +13 -7
- reflex/components/datadisplay/code.py +14 -79
- reflex/components/datadisplay/code.pyi +11 -13
- reflex/components/datadisplay/dataeditor.pyi +38 -15
- reflex/components/datadisplay/shiki_code_block.py +5 -3
- reflex/components/datadisplay/shiki_code_block.pyi +16 -15
- reflex/components/dynamic.py +5 -5
- reflex/components/el/__init__.pyi +506 -246
- reflex/components/el/element.pyi +7 -7
- reflex/components/el/elements/__init__.pyi +504 -245
- reflex/components/el/elements/base.pyi +7 -7
- reflex/components/el/elements/forms.pyi +146 -101
- reflex/components/el/elements/inline.pyi +142 -142
- reflex/components/el/elements/media.pyi +131 -130
- reflex/components/el/elements/metadata.pyi +32 -32
- reflex/components/el/elements/other.pyi +37 -37
- reflex/components/el/elements/scripts.pyi +17 -17
- reflex/components/el/elements/sectioning.pyi +77 -77
- reflex/components/el/elements/tables.pyi +52 -52
- reflex/components/el/elements/typography.pyi +77 -77
- reflex/components/field.py +175 -0
- reflex/components/gridjs/datatable.py +2 -2
- reflex/components/gridjs/datatable.pyi +14 -14
- reflex/components/lucide/icon.py +6 -2
- reflex/components/lucide/icon.pyi +19 -17
- reflex/components/markdown/markdown.py +5 -3
- reflex/components/markdown/markdown.pyi +7 -7
- reflex/components/moment/moment.py +1 -1
- reflex/components/moment/moment.pyi +7 -7
- reflex/components/plotly/plotly.py +12 -6
- reflex/components/plotly/plotly.pyi +50 -49
- reflex/components/props.py +376 -27
- reflex/components/radix/__init__.pyi +123 -65
- reflex/components/radix/primitives/__init__.pyi +6 -4
- reflex/components/radix/primitives/accordion.py +8 -1
- reflex/components/radix/primitives/accordion.pyi +37 -37
- reflex/components/radix/primitives/base.pyi +12 -12
- reflex/components/radix/primitives/drawer.pyi +56 -55
- reflex/components/radix/primitives/form.pyi +63 -53
- reflex/components/radix/primitives/progress.pyi +26 -25
- reflex/components/radix/primitives/slider.pyi +27 -27
- reflex/components/radix/themes/__init__.pyi +5 -6
- reflex/components/radix/themes/base.py +3 -3
- reflex/components/radix/themes/base.pyi +42 -42
- reflex/components/radix/themes/color_mode.py +5 -6
- reflex/components/radix/themes/color_mode.pyi +17 -17
- reflex/components/radix/themes/components/__init__.pyi +75 -38
- reflex/components/radix/themes/components/alert_dialog.pyi +37 -37
- reflex/components/radix/themes/components/aspect_ratio.pyi +7 -7
- reflex/components/radix/themes/components/avatar.pyi +7 -7
- reflex/components/radix/themes/components/badge.pyi +7 -7
- reflex/components/radix/themes/components/button.pyi +7 -7
- reflex/components/radix/themes/components/callout.pyi +26 -25
- reflex/components/radix/themes/components/card.pyi +7 -7
- reflex/components/radix/themes/components/checkbox.pyi +16 -15
- reflex/components/radix/themes/components/checkbox_cards.pyi +12 -12
- reflex/components/radix/themes/components/checkbox_group.pyi +12 -12
- reflex/components/radix/themes/components/context_menu.pyi +67 -67
- reflex/components/radix/themes/components/data_list.pyi +22 -22
- reflex/components/radix/themes/components/dialog.pyi +36 -35
- reflex/components/radix/themes/components/dropdown_menu.pyi +42 -42
- reflex/components/radix/themes/components/hover_card.pyi +21 -20
- reflex/components/radix/themes/components/icon_button.pyi +7 -7
- reflex/components/radix/themes/components/inset.pyi +7 -7
- reflex/components/radix/themes/components/popover.pyi +22 -22
- reflex/components/radix/themes/components/progress.pyi +7 -7
- reflex/components/radix/themes/components/radio.pyi +7 -7
- reflex/components/radix/themes/components/radio_cards.pyi +12 -12
- reflex/components/radix/themes/components/radio_group.pyi +21 -20
- reflex/components/radix/themes/components/scroll_area.pyi +7 -7
- reflex/components/radix/themes/components/segmented_control.pyi +12 -12
- reflex/components/radix/themes/components/select.pyi +46 -45
- reflex/components/radix/themes/components/separator.pyi +7 -7
- reflex/components/radix/themes/components/skeleton.pyi +7 -7
- reflex/components/radix/themes/components/slider.pyi +17 -9
- reflex/components/radix/themes/components/spinner.pyi +7 -7
- reflex/components/radix/themes/components/switch.pyi +7 -7
- reflex/components/radix/themes/components/table.pyi +37 -37
- reflex/components/radix/themes/components/tabs.pyi +26 -25
- reflex/components/radix/themes/components/text_area.pyi +15 -9
- reflex/components/radix/themes/components/text_field.pyi +32 -19
- reflex/components/radix/themes/components/tooltip.pyi +7 -7
- reflex/components/radix/themes/layout/__init__.pyi +27 -14
- reflex/components/radix/themes/layout/base.pyi +7 -7
- reflex/components/radix/themes/layout/box.pyi +7 -7
- reflex/components/radix/themes/layout/center.pyi +7 -7
- reflex/components/radix/themes/layout/container.pyi +7 -7
- reflex/components/radix/themes/layout/flex.pyi +7 -7
- reflex/components/radix/themes/layout/grid.pyi +7 -7
- reflex/components/radix/themes/layout/list.pyi +26 -25
- reflex/components/radix/themes/layout/section.pyi +7 -7
- reflex/components/radix/themes/layout/spacer.pyi +7 -7
- reflex/components/radix/themes/layout/stack.pyi +17 -17
- reflex/components/radix/themes/typography/__init__.pyi +7 -5
- reflex/components/radix/themes/typography/blockquote.pyi +7 -7
- reflex/components/radix/themes/typography/code.pyi +7 -7
- reflex/components/radix/themes/typography/heading.pyi +7 -7
- reflex/components/radix/themes/typography/link.py +46 -11
- reflex/components/radix/themes/typography/link.pyi +312 -9
- reflex/components/radix/themes/typography/text.pyi +36 -35
- reflex/components/react_player/audio.pyi +10 -8
- reflex/components/react_player/react_player.pyi +7 -7
- reflex/components/react_player/video.pyi +10 -8
- reflex/components/recharts/__init__.pyi +208 -100
- reflex/components/recharts/cartesian.py +10 -8
- reflex/components/recharts/cartesian.pyi +90 -94
- reflex/components/recharts/charts.py +4 -2
- reflex/components/recharts/charts.pyi +49 -49
- reflex/components/recharts/general.pyi +31 -31
- reflex/components/recharts/polar.py +8 -4
- reflex/components/recharts/polar.pyi +23 -23
- reflex/components/recharts/recharts.py +2 -2
- reflex/components/recharts/recharts.pyi +12 -12
- reflex/components/sonner/toast.py +3 -3
- reflex/components/sonner/toast.pyi +9 -9
- reflex/config.py +10 -113
- reflex/constants/__init__.py +2 -2
- reflex/constants/base.py +28 -11
- reflex/constants/compiler.py +12 -3
- reflex/constants/event.py +1 -0
- reflex/constants/installer.py +26 -20
- reflex/constants/route.py +27 -8
- reflex/constants/state.py +2 -0
- reflex/custom_components/custom_components.py +0 -14
- reflex/environment.py +77 -5
- reflex/event.py +178 -81
- reflex/experimental/__init__.py +0 -30
- reflex/istate/__init__.py +69 -0
- reflex/istate/manager.py +1 -0
- reflex/istate/proxy.py +5 -3
- reflex/page.py +0 -27
- reflex/plugins/__init__.py +3 -2
- reflex/plugins/base.py +5 -1
- reflex/plugins/shared_tailwind.py +215 -0
- reflex/plugins/sitemap.py +206 -0
- reflex/plugins/tailwind_v3.py +15 -108
- reflex/plugins/tailwind_v4.py +18 -110
- reflex/reflex.py +1 -0
- reflex/route.py +157 -75
- reflex/state.py +171 -155
- reflex/testing.py +86 -16
- reflex/utils/build.py +38 -82
- reflex/utils/exec.py +83 -175
- reflex/utils/export.py +2 -2
- reflex/utils/format.py +1 -5
- reflex/utils/imports.py +5 -16
- reflex/utils/misc.py +67 -0
- reflex/utils/prerequisites.py +66 -68
- reflex/utils/processes.py +24 -47
- reflex/utils/pyi_generator.py +44 -49
- reflex/utils/serializers.py +14 -1
- reflex/utils/telemetry.py +0 -15
- reflex/utils/types.py +197 -62
- reflex/vars/__init__.py +2 -0
- reflex/vars/base.py +367 -134
- {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/METADATA +15 -8
- reflex-0.8.0.dist-info/RECORD +403 -0
- reflex/.templates/web/next.config.js +0 -7
- reflex/components/base/head.py +0 -20
- reflex/components/base/head.pyi +0 -116
- reflex/components/next/__init__.py +0 -10
- reflex/components/next/base.py +0 -7
- reflex/components/next/image.py +0 -117
- reflex/components/next/image.pyi +0 -94
- reflex/components/next/link.py +0 -20
- reflex/components/next/link.pyi +0 -67
- reflex/components/next/video.py +0 -38
- reflex/components/next/video.pyi +0 -68
- reflex/components/suneditor/__init__.py +0 -5
- reflex/components/suneditor/editor.py +0 -269
- reflex/components/suneditor/editor.pyi +0 -199
- reflex/experimental/layout.py +0 -254
- reflex/experimental/layout.pyi +0 -814
- reflex-0.7.14a6.dist-info/RECORD +0 -408
- {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/WHEEL +0 -0
- {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/entry_points.txt +0 -0
- {reflex-0.7.14a6.dist-info → reflex-0.8.0.dist-info}/licenses/LICENSE +0 -0
reflex/istate/__init__.py
CHANGED
|
@@ -1 +1,70 @@
|
|
|
1
1
|
"""This module will provide interfaces for the state."""
|
|
2
|
+
|
|
3
|
+
import pickle
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
# Errors caught during pickling of state
|
|
9
|
+
HANDLED_PICKLE_ERRORS = (
|
|
10
|
+
pickle.PicklingError,
|
|
11
|
+
AttributeError,
|
|
12
|
+
IndexError,
|
|
13
|
+
TypeError,
|
|
14
|
+
ValueError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_picklable(obj: Any, dumps: Callable[[object], bytes]) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
dumps(obj)
|
|
21
|
+
except Exception:
|
|
22
|
+
return False
|
|
23
|
+
else:
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def debug_failed_pickles(obj: object, dumps: Callable[[object], bytes]):
|
|
28
|
+
"""Recursively check the picklability of an object and its contents.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
obj: The object to check.
|
|
32
|
+
dumps: The pickle dump function to use.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
HANDLED_PICKLE_ERRORS: If the object or any of its contents are not picklable.
|
|
36
|
+
"""
|
|
37
|
+
if _is_picklable(obj, dumps):
|
|
38
|
+
return
|
|
39
|
+
if sys.version_info < (3, 11):
|
|
40
|
+
return
|
|
41
|
+
if isinstance(obj, dict):
|
|
42
|
+
for k, v in obj.items():
|
|
43
|
+
try:
|
|
44
|
+
debug_failed_pickles(v, dumps)
|
|
45
|
+
except HANDLED_PICKLE_ERRORS as e:
|
|
46
|
+
e.add_note(f"While pickling dict value for key {k!r}")
|
|
47
|
+
raise
|
|
48
|
+
try:
|
|
49
|
+
debug_failed_pickles(k, dumps)
|
|
50
|
+
except HANDLED_PICKLE_ERRORS as e:
|
|
51
|
+
e.add_note(f"While pickling dict key {k!r}")
|
|
52
|
+
raise
|
|
53
|
+
return
|
|
54
|
+
if isinstance(obj, (list, tuple)):
|
|
55
|
+
for i, v in enumerate(obj):
|
|
56
|
+
try:
|
|
57
|
+
debug_failed_pickles(v, dumps)
|
|
58
|
+
except HANDLED_PICKLE_ERRORS as e: # noqa: PERF203
|
|
59
|
+
e.add_note(f"While pickling index {i} of {type(obj).__name__}")
|
|
60
|
+
raise
|
|
61
|
+
return
|
|
62
|
+
picklable_thing = obj.__getstate__()
|
|
63
|
+
if picklable_thing is not None:
|
|
64
|
+
debug_failed_pickles(picklable_thing, dumps)
|
|
65
|
+
else:
|
|
66
|
+
try:
|
|
67
|
+
dumps(obj)
|
|
68
|
+
except HANDLED_PICKLE_ERRORS as e:
|
|
69
|
+
e.add_note(f"While pickling object of type {type(obj).__name__}")
|
|
70
|
+
raise
|
reflex/istate/manager.py
CHANGED
|
@@ -179,6 +179,7 @@ def _default_token_expiration() -> int:
|
|
|
179
179
|
|
|
180
180
|
def reset_disk_state_manager():
|
|
181
181
|
"""Reset the disk state manager."""
|
|
182
|
+
console.debug("Resetting disk state manager.")
|
|
182
183
|
states_directory = prerequisites.get_states_dir()
|
|
183
184
|
if states_directory.exists():
|
|
184
185
|
for path in states_directory.iterdir():
|
reflex/istate/proxy.py
CHANGED
|
@@ -10,7 +10,7 @@ import inspect
|
|
|
10
10
|
import json
|
|
11
11
|
from collections.abc import Callable, Sequence
|
|
12
12
|
from types import MethodType
|
|
13
|
-
from typing import TYPE_CHECKING, Any, SupportsIndex
|
|
13
|
+
from typing import TYPE_CHECKING, Any, SupportsIndex, TypeVar
|
|
14
14
|
|
|
15
15
|
import pydantic
|
|
16
16
|
import wrapt
|
|
@@ -27,6 +27,8 @@ from reflex.vars.base import Var
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from reflex.state import BaseState, StateUpdate
|
|
29
29
|
|
|
30
|
+
T_STATE = TypeVar("T_STATE", bound="BaseState")
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
class StateProxy(wrapt.ObjectProxy):
|
|
32
34
|
"""Proxy of a state instance to control mutability of vars for a background task.
|
|
@@ -269,7 +271,7 @@ class StateProxy(wrapt.ObjectProxy):
|
|
|
269
271
|
raise ImmutableStateError(msg)
|
|
270
272
|
return self.__wrapped__.get_substate(path)
|
|
271
273
|
|
|
272
|
-
async def get_state(self, state_cls: type[
|
|
274
|
+
async def get_state(self, state_cls: type[T_STATE]) -> T_STATE:
|
|
273
275
|
"""Get an instance of the state associated with this token.
|
|
274
276
|
|
|
275
277
|
Args:
|
|
@@ -289,7 +291,7 @@ class StateProxy(wrapt.ObjectProxy):
|
|
|
289
291
|
raise ImmutableStateError(msg)
|
|
290
292
|
return type(self)(
|
|
291
293
|
await self.__wrapped__.get_state(state_cls), parent_state_proxy=self
|
|
292
|
-
)
|
|
294
|
+
) # pyright: ignore [reportReturnType]
|
|
293
295
|
|
|
294
296
|
async def _as_state_update(self, *args, **kwargs) -> StateUpdate:
|
|
295
297
|
"""Temporarily allow mutability to access parent_state.
|
reflex/page.py
CHANGED
|
@@ -8,7 +8,6 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from reflex.config import get_config
|
|
10
10
|
from reflex.event import EventType
|
|
11
|
-
from reflex.utils import console
|
|
12
11
|
|
|
13
12
|
DECORATED_PAGES: dict[str, list] = defaultdict(list)
|
|
14
13
|
|
|
@@ -66,29 +65,3 @@ def page(
|
|
|
66
65
|
return render_fn
|
|
67
66
|
|
|
68
67
|
return decorator
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def get_decorated_pages(omit_implicit_routes: bool = True) -> list[dict[str, Any]]:
|
|
72
|
-
"""Get the decorated pages.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
omit_implicit_routes: Whether to omit pages where the route will be implicitly guessed later.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
The decorated pages.
|
|
79
|
-
"""
|
|
80
|
-
console.deprecate(
|
|
81
|
-
"get_decorated_pages",
|
|
82
|
-
reason="This function is deprecated and will be removed in a future version.",
|
|
83
|
-
deprecation_version="0.7.9",
|
|
84
|
-
removal_version="0.8.0",
|
|
85
|
-
dedupe=True,
|
|
86
|
-
)
|
|
87
|
-
return sorted(
|
|
88
|
-
[
|
|
89
|
-
page_data
|
|
90
|
-
for _, page_data in DECORATED_PAGES[get_config().app_name]
|
|
91
|
-
if not omit_implicit_routes or "route" in page_data
|
|
92
|
-
],
|
|
93
|
-
key=lambda x: x.get("route", ""),
|
|
94
|
-
)
|
reflex/plugins/__init__.py
CHANGED
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
from .base import CommonContext as CommonContext
|
|
4
4
|
from .base import Plugin as Plugin
|
|
5
5
|
from .base import PreCompileContext as PreCompileContext
|
|
6
|
-
from .
|
|
7
|
-
from .
|
|
6
|
+
from .sitemap import Plugin as SitemapPlugin
|
|
7
|
+
from .tailwind_v3 import TailwindV3Plugin as TailwindV3Plugin
|
|
8
|
+
from .tailwind_v4 import TailwindV4Plugin as TailwindV4Plugin
|
reflex/plugins/base.py
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Sequence
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import ParamSpec, Protocol, TypedDict
|
|
5
|
+
from typing import TYPE_CHECKING, ParamSpec, Protocol, TypedDict
|
|
6
6
|
|
|
7
7
|
from typing_extensions import Unpack
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from reflex.app import UnevaluatedPage
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
class CommonContext(TypedDict):
|
|
11
14
|
"""Common context for all plugins."""
|
|
@@ -38,6 +41,7 @@ class PreCompileContext(CommonContext):
|
|
|
38
41
|
|
|
39
42
|
add_save_task: AddTaskProtcol
|
|
40
43
|
add_modify_task: Callable[[str, Callable[[str], str]], None]
|
|
44
|
+
unevaluated_pages: Sequence["UnevaluatedPage"]
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
class Plugin:
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Tailwind CSS configuration types for Reflex plugins."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
from typing_extensions import NotRequired
|
|
8
|
+
|
|
9
|
+
from reflex.utils.decorator import once
|
|
10
|
+
|
|
11
|
+
from .base import Plugin as PluginBase
|
|
12
|
+
|
|
13
|
+
TailwindPluginImport = TypedDict(
|
|
14
|
+
"TailwindPluginImport",
|
|
15
|
+
{
|
|
16
|
+
"name": str,
|
|
17
|
+
"from": str,
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
TailwindPluginWithCallConfig = TypedDict(
|
|
22
|
+
"TailwindPluginWithCallConfig",
|
|
23
|
+
{
|
|
24
|
+
"name": str,
|
|
25
|
+
"import": NotRequired[TailwindPluginImport],
|
|
26
|
+
"call": str,
|
|
27
|
+
"args": NotRequired[dict[str, Any]],
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
TailwindPluginWithoutCallConfig = TypedDict(
|
|
32
|
+
"TailwindPluginWithoutCallConfig",
|
|
33
|
+
{
|
|
34
|
+
"name": str,
|
|
35
|
+
"import": NotRequired[TailwindPluginImport],
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
TailwindPluginConfig = (
|
|
40
|
+
TailwindPluginWithCallConfig | TailwindPluginWithoutCallConfig | str
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def remove_version_from_plugin(plugin: TailwindPluginConfig) -> TailwindPluginConfig:
|
|
45
|
+
"""Remove the version from a plugin name.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
plugin: The plugin to remove the version from.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
The plugin without the version.
|
|
52
|
+
"""
|
|
53
|
+
from reflex.utils.format import format_library_name
|
|
54
|
+
|
|
55
|
+
if isinstance(plugin, str):
|
|
56
|
+
return format_library_name(plugin)
|
|
57
|
+
|
|
58
|
+
if plugin_import := plugin.get("import"):
|
|
59
|
+
plugin_import["from"] = format_library_name(plugin_import["from"])
|
|
60
|
+
|
|
61
|
+
plugin["name"] = format_library_name(plugin["name"])
|
|
62
|
+
|
|
63
|
+
return plugin
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TailwindConfig(TypedDict):
|
|
67
|
+
"""Tailwind CSS configuration options.
|
|
68
|
+
|
|
69
|
+
See: https://tailwindcss.com/docs/configuration
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
content: NotRequired[list[str]]
|
|
73
|
+
important: NotRequired[str | bool]
|
|
74
|
+
prefix: NotRequired[str]
|
|
75
|
+
separator: NotRequired[str]
|
|
76
|
+
presets: NotRequired[list[str]]
|
|
77
|
+
darkMode: NotRequired[Literal["media", "class", "selector"]]
|
|
78
|
+
theme: NotRequired[dict[str, Any]]
|
|
79
|
+
corePlugins: NotRequired[list[str] | dict[str, bool]]
|
|
80
|
+
plugins: NotRequired[list[TailwindPluginConfig]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@once
|
|
84
|
+
def tailwind_config_js_template():
|
|
85
|
+
"""Get the Tailwind config template.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
The Tailwind config template.
|
|
89
|
+
"""
|
|
90
|
+
from reflex.compiler.templates import from_string
|
|
91
|
+
|
|
92
|
+
source = r"""
|
|
93
|
+
{# Extract destructured imports from plugin dicts only #}
|
|
94
|
+
{%- set imports = [] %}
|
|
95
|
+
|
|
96
|
+
{%- for plugin in plugins if plugin is mapping and plugin.import is defined %}
|
|
97
|
+
{%- set _ = imports.append(plugin.import) %}
|
|
98
|
+
{%- endfor %}
|
|
99
|
+
|
|
100
|
+
{%- for imp in imports %}
|
|
101
|
+
import { {{ imp.name }} } from {{ imp.from | tojson }};
|
|
102
|
+
{%- endfor %}
|
|
103
|
+
|
|
104
|
+
{%- for plugin in plugins %}
|
|
105
|
+
{% if plugin is mapping and plugin.call is not defined %}
|
|
106
|
+
import plugin{{ loop.index }} from {{ plugin.name | tojson }};
|
|
107
|
+
{%- elif plugin is not mapping %}
|
|
108
|
+
import plugin{{ loop.index }} from {{ plugin | tojson }};
|
|
109
|
+
{%- endif %}
|
|
110
|
+
{%- endfor %}
|
|
111
|
+
|
|
112
|
+
{%- for preset in presets %}
|
|
113
|
+
import preset{{ loop.index }} from {{ preset | tojson }};
|
|
114
|
+
{%- endfor %}
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
content: {{ (content if content is defined else DEFAULT_CONTENT) | tojson }},
|
|
118
|
+
{% if theme is defined %}theme: {{ theme | tojson }},{% else %}theme: {},{% endif %}
|
|
119
|
+
{% if darkMode is defined %}darkMode: {{ darkMode | tojson }},{% endif %}
|
|
120
|
+
{% if corePlugins is defined %}corePlugins: {{ corePlugins | tojson }},{% endif %}
|
|
121
|
+
{% if important is defined %}important: {{ important | tojson }},{% endif %}
|
|
122
|
+
{% if prefix is defined %}prefix: {{ prefix | tojson }},{% endif %}
|
|
123
|
+
{% if separator is defined %}separator: {{ separator | tojson }},{% endif %}
|
|
124
|
+
{% if presets is defined %}
|
|
125
|
+
presets: [
|
|
126
|
+
{% for preset in presets %}
|
|
127
|
+
preset{{ loop.index }},
|
|
128
|
+
{% endfor %}
|
|
129
|
+
],
|
|
130
|
+
{% endif %}
|
|
131
|
+
plugins: [
|
|
132
|
+
{% for plugin in plugins %}
|
|
133
|
+
{% if plugin is mapping and plugin.call is defined %}
|
|
134
|
+
{{ plugin.call }}(
|
|
135
|
+
{%- if plugin.args is defined -%}
|
|
136
|
+
{{ plugin.args | tojson }}
|
|
137
|
+
{%- endif -%}
|
|
138
|
+
),
|
|
139
|
+
{% else %}
|
|
140
|
+
plugin{{ loop.index }},
|
|
141
|
+
{% endif %}
|
|
142
|
+
{% endfor %}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
return from_string(source)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclasses.dataclass
|
|
151
|
+
class TailwindPlugin(PluginBase):
|
|
152
|
+
"""Plugin for Tailwind CSS."""
|
|
153
|
+
|
|
154
|
+
config: TailwindConfig = dataclasses.field(
|
|
155
|
+
default_factory=lambda: TailwindConfig(
|
|
156
|
+
plugins=[
|
|
157
|
+
"@tailwindcss/typography@0.5.16",
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def get_frontend_development_dependencies(self, **context) -> list[str]:
|
|
163
|
+
"""Get the packages required by the plugin.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
**context: The context for the plugin.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A list of packages required by the plugin.
|
|
170
|
+
"""
|
|
171
|
+
config = self.get_config()
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
plugin if isinstance(plugin, str) else plugin.get("name")
|
|
175
|
+
for plugin in config.get("plugins", [])
|
|
176
|
+
] + config.get("presets", [])
|
|
177
|
+
|
|
178
|
+
def get_config(self) -> TailwindConfig:
|
|
179
|
+
"""Get the Tailwind CSS configuration.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The Tailwind CSS configuration.
|
|
183
|
+
"""
|
|
184
|
+
from reflex.config import get_config
|
|
185
|
+
|
|
186
|
+
rxconfig_config = getattr(get_config(), "tailwind", None)
|
|
187
|
+
|
|
188
|
+
if rxconfig_config is not None and rxconfig_config != self.config:
|
|
189
|
+
from reflex.utils import console
|
|
190
|
+
|
|
191
|
+
console.warn(
|
|
192
|
+
"It seems you have provided a tailwind configuration in your call to `rx.Config`."
|
|
193
|
+
f" You should provide the configuration as an argument to `rx.plugins.{self.__class__.__name__}()` instead."
|
|
194
|
+
)
|
|
195
|
+
return rxconfig_config
|
|
196
|
+
|
|
197
|
+
return self.config
|
|
198
|
+
|
|
199
|
+
def get_unversioned_config(self) -> TailwindConfig:
|
|
200
|
+
"""Get the Tailwind CSS configuration without version-specific adjustments.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The Tailwind CSS configuration without version-specific adjustments.
|
|
204
|
+
"""
|
|
205
|
+
from reflex.utils.format import format_library_name
|
|
206
|
+
|
|
207
|
+
config = deepcopy(self.get_config())
|
|
208
|
+
if presets := config.get("presets"):
|
|
209
|
+
# Somehow, having an empty list of presets breaks Tailwind.
|
|
210
|
+
# So we only set the presets if there are any.
|
|
211
|
+
config["presets"] = [format_library_name(preset) for preset in presets]
|
|
212
|
+
config["plugins"] = [
|
|
213
|
+
remove_version_from_plugin(plugin) for plugin in config.get("plugins", [])
|
|
214
|
+
]
|
|
215
|
+
return config
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Sitemap plugin for Reflex."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, TypedDict
|
|
8
|
+
from xml.dom import minidom
|
|
9
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
10
|
+
|
|
11
|
+
from typing_extensions import NotRequired
|
|
12
|
+
|
|
13
|
+
from reflex import constants
|
|
14
|
+
|
|
15
|
+
from .base import Plugin as PluginBase
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from reflex.app import UnevaluatedPage
|
|
19
|
+
|
|
20
|
+
Location = str
|
|
21
|
+
LastModified = datetime.datetime
|
|
22
|
+
ChangeFrequency = Literal[
|
|
23
|
+
"always", "hourly", "daily", "weekly", "monthly", "yearly", "never"
|
|
24
|
+
]
|
|
25
|
+
Priority = float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SitemapLink(TypedDict):
|
|
29
|
+
"""A link in the sitemap."""
|
|
30
|
+
|
|
31
|
+
loc: Location
|
|
32
|
+
lastmod: NotRequired[LastModified]
|
|
33
|
+
changefreq: NotRequired[ChangeFrequency]
|
|
34
|
+
priority: NotRequired[Priority]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SitemapLinkConfiguration(TypedDict):
|
|
38
|
+
"""Configuration for a sitemap link."""
|
|
39
|
+
|
|
40
|
+
loc: NotRequired[Location]
|
|
41
|
+
lastmod: NotRequired[LastModified]
|
|
42
|
+
changefreq: NotRequired[ChangeFrequency]
|
|
43
|
+
priority: NotRequired[Priority]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Constants(SimpleNamespace):
|
|
47
|
+
"""Sitemap constants."""
|
|
48
|
+
|
|
49
|
+
FILE_PATH: Path = Path(constants.Dirs.PUBLIC) / "sitemap.xml"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def configuration_with_loc(
|
|
53
|
+
*, config: SitemapLinkConfiguration, deploy_url: str | None, loc: Location
|
|
54
|
+
) -> SitemapLink:
|
|
55
|
+
"""Set the 'loc' field of the configuration.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
config: The configuration dictionary.
|
|
59
|
+
deploy_url: The deployment URL, if any.
|
|
60
|
+
loc: The location to set.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A SitemapLink dictionary with the 'loc' field set.
|
|
64
|
+
"""
|
|
65
|
+
if deploy_url and not loc.startswith("http://") and not loc.startswith("https://"):
|
|
66
|
+
loc = f"{deploy_url.rstrip('/')}/{loc.lstrip('/')}"
|
|
67
|
+
link: SitemapLink = {"loc": loc}
|
|
68
|
+
if (lastmod := config.get("lastmod")) is not None:
|
|
69
|
+
link["lastmod"] = lastmod
|
|
70
|
+
if (changefreq := config.get("changefreq")) is not None:
|
|
71
|
+
link["changefreq"] = changefreq
|
|
72
|
+
if (priority := config.get("priority")) is not None:
|
|
73
|
+
link["priority"] = min(1.0, max(0.0, priority))
|
|
74
|
+
return link
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_xml(links: Sequence[SitemapLink]) -> str:
|
|
78
|
+
"""Generate an XML sitemap from a list of links.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
links: A sequence of SitemapLink dictionaries.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A pretty-printed XML string representing the sitemap.
|
|
85
|
+
"""
|
|
86
|
+
urlset = Element("urlset", xmlns="https://www.sitemaps.org/schemas/sitemap/0.9")
|
|
87
|
+
|
|
88
|
+
for link in links:
|
|
89
|
+
url = SubElement(urlset, "url")
|
|
90
|
+
|
|
91
|
+
loc_element = SubElement(url, "loc")
|
|
92
|
+
loc_element.text = link["loc"]
|
|
93
|
+
|
|
94
|
+
if (changefreq := link.get("changefreq")) is not None:
|
|
95
|
+
changefreq_element = SubElement(url, "changefreq")
|
|
96
|
+
changefreq_element.text = changefreq
|
|
97
|
+
|
|
98
|
+
if (lastmod := link.get("lastmod")) is not None:
|
|
99
|
+
lastmod_element = SubElement(url, "lastmod")
|
|
100
|
+
if isinstance(lastmod, datetime.datetime):
|
|
101
|
+
lastmod = lastmod.isoformat()
|
|
102
|
+
lastmod_element.text = lastmod
|
|
103
|
+
|
|
104
|
+
if (priority := link.get("priority")) is not None:
|
|
105
|
+
priority_element = SubElement(url, "priority")
|
|
106
|
+
priority_element.text = str(priority)
|
|
107
|
+
|
|
108
|
+
rough_string = tostring(urlset, "utf-8")
|
|
109
|
+
reparsed = minidom.parseString(rough_string)
|
|
110
|
+
return reparsed.toprettyxml(indent=" ")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_route_dynamic(route: str) -> bool:
|
|
114
|
+
"""Check if a route is dynamic.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
route: The route to check.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if the route is dynamic, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
return "[" in route and "]" in route
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def generate_links_for_sitemap(
|
|
126
|
+
unevaluated_pages: Sequence["UnevaluatedPage"],
|
|
127
|
+
) -> list[SitemapLink]:
|
|
128
|
+
"""Generate sitemap links from unevaluated pages.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
unevaluated_pages: Sequence of unevaluated pages.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
A list of SitemapLink dictionaries.
|
|
135
|
+
"""
|
|
136
|
+
from reflex.config import get_config
|
|
137
|
+
from reflex.utils import console
|
|
138
|
+
|
|
139
|
+
deploy_url = get_config().deploy_url
|
|
140
|
+
|
|
141
|
+
links: list[SitemapLink] = []
|
|
142
|
+
|
|
143
|
+
for page in unevaluated_pages:
|
|
144
|
+
sitemap_config: SitemapLinkConfiguration = page.context.get("sitemap", {})
|
|
145
|
+
|
|
146
|
+
if is_route_dynamic(page.route) or page.route == "404":
|
|
147
|
+
if not sitemap_config:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if (loc := sitemap_config.get("loc")) is None:
|
|
151
|
+
route_message = (
|
|
152
|
+
"Dynamic route" if is_route_dynamic(page.route) else "Route 404"
|
|
153
|
+
)
|
|
154
|
+
console.warn(
|
|
155
|
+
route_message
|
|
156
|
+
+ f" '{page.route}' does not have a 'loc' in sitemap configuration. Skipping."
|
|
157
|
+
)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
sitemap_link = configuration_with_loc(
|
|
161
|
+
config=sitemap_config, deploy_url=deploy_url, loc=loc
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
elif (loc := sitemap_config.get("loc")) is not None:
|
|
165
|
+
sitemap_link = configuration_with_loc(
|
|
166
|
+
config=sitemap_config, deploy_url=deploy_url, loc=loc
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
loc = page.route if page.route != "index" else "/"
|
|
171
|
+
if not loc.startswith("/"):
|
|
172
|
+
loc = "/" + loc
|
|
173
|
+
sitemap_link = configuration_with_loc(
|
|
174
|
+
config=sitemap_config, deploy_url=deploy_url, loc=loc
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
links.append(sitemap_link)
|
|
178
|
+
return links
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def sitemap_task(unevaluated_pages: Sequence["UnevaluatedPage"]) -> tuple[str, str]:
|
|
182
|
+
"""Task to generate the sitemap XML file.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
unevaluated_pages: Sequence of unevaluated pages.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
A tuple containing the file path and the generated XML content.
|
|
189
|
+
"""
|
|
190
|
+
return (
|
|
191
|
+
str(Constants.FILE_PATH),
|
|
192
|
+
generate_xml(generate_links_for_sitemap(unevaluated_pages)),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Plugin(PluginBase):
|
|
197
|
+
"""Sitemap plugin for Reflex."""
|
|
198
|
+
|
|
199
|
+
def pre_compile(self, **context):
|
|
200
|
+
"""Generate the sitemap XML file before compilation.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
context: The context for the plugin.
|
|
204
|
+
"""
|
|
205
|
+
unevaluated_pages = context.get("unevaluated_pages", [])
|
|
206
|
+
context["add_save_task"](sitemap_task, unevaluated_pages)
|