reactpy_django 5.0.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.
- js/node_modules/@pyscript/core/src/stdlib/pyscript/__init__.py +61 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/display.py +177 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/event_handling.py +76 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/fetch.py +87 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/ffi.py +18 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/flatted.py +148 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/magic_js.py +79 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/storage.py +60 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/util.py +33 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/web.py +1176 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/websocket.py +71 -0
- js/node_modules/@pyscript/core/src/stdlib/pyscript/workers.py +43 -0
- js/node_modules/flatted/python/flatted.py +149 -0
- js/node_modules/flatted/python/test.py +63 -0
- js/node_modules/polyscript/python/mip.py +288 -0
- reactpy_django/__init__.py +32 -0
- reactpy_django/apps.py +11 -0
- reactpy_django/checks.py +558 -0
- reactpy_django/clean.py +141 -0
- reactpy_django/components.py +284 -0
- reactpy_django/config.py +128 -0
- reactpy_django/database.py +31 -0
- reactpy_django/decorators.py +65 -0
- reactpy_django/exceptions.py +34 -0
- reactpy_django/hooks.py +469 -0
- reactpy_django/html.py +3 -0
- reactpy_django/http/__init__.py +0 -0
- reactpy_django/http/urls.py +18 -0
- reactpy_django/http/views.py +62 -0
- reactpy_django/management/__init__.py +0 -0
- reactpy_django/management/commands/__init__.py +0 -0
- reactpy_django/management/commands/clean_reactpy.py +37 -0
- reactpy_django/migrations/0001_initial.py +25 -0
- reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
- reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
- reactpy_django/migrations/0004_config.py +27 -0
- reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
- reactpy_django/migrations/0006_userdatamodel.py +28 -0
- reactpy_django/migrations/__init__.py +0 -0
- reactpy_django/models.py +48 -0
- reactpy_django/py.typed +1 -0
- reactpy_django/pyscript/__init__.py +0 -0
- reactpy_django/pyscript/component_template.py +26 -0
- reactpy_django/pyscript/layout_handler.py +139 -0
- reactpy_django/router/__init__.py +5 -0
- reactpy_django/router/converters.py +8 -0
- reactpy_django/router/resolvers.py +22 -0
- reactpy_django/static/reactpy_django/client.js +1 -0
- reactpy_django/static/reactpy_django/morphdom/morphdom-esm.js +769 -0
- reactpy_django/static/reactpy_django/morphdom/morphdom-factory.js +705 -0
- reactpy_django/static/reactpy_django/morphdom/morphdom-umd.js +777 -0
- reactpy_django/static/reactpy_django/morphdom/morphdom-umd.min.js +1 -0
- reactpy_django/static/reactpy_django/morphdom/morphdom.js +771 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/core.css +1 -0
- reactpy_django/static/reactpy_django/pyscript/core.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/core.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/storage.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/storage.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js +3 -0
- reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js +3 -0
- reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/xterm.css +7 -0
- reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js +2 -0
- reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js.map +1 -0
- reactpy_django/static/reactpy_django/pyscript-custom.css +3 -0
- reactpy_django/static/reactpy_django/pyscript-hide-debug.css +3 -0
- reactpy_django/templates/reactpy/component.html +27 -0
- reactpy_django/templates/reactpy/pyscript_component.html +2 -0
- reactpy_django/templates/reactpy/pyscript_setup.html +8 -0
- reactpy_django/templatetags/__init__.py +0 -0
- reactpy_django/templatetags/reactpy.py +250 -0
- reactpy_django/types.py +81 -0
- reactpy_django/utils.py +560 -0
- reactpy_django/websocket/__init__.py +0 -0
- reactpy_django/websocket/consumer.py +227 -0
- reactpy_django/websocket/paths.py +15 -0
- reactpy_django-5.0.0.dist-info/LICENSE.md +9 -0
- reactpy_django-5.0.0.dist-info/METADATA +152 -0
- reactpy_django-5.0.0.dist-info/RECORD +118 -0
- reactpy_django-5.0.0.dist-info/WHEEL +5 -0
- reactpy_django-5.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Callable, Sequence, Union, cast
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from django.contrib.staticfiles.finders import find
|
|
10
|
+
from django.core.cache import caches
|
|
11
|
+
from django.http import HttpRequest
|
|
12
|
+
from django.urls import reverse
|
|
13
|
+
from django.views import View
|
|
14
|
+
from reactpy import component, hooks, html, utils
|
|
15
|
+
from reactpy.types import ComponentType, Key, VdomDict
|
|
16
|
+
|
|
17
|
+
from reactpy_django.exceptions import ViewNotRegisteredError
|
|
18
|
+
from reactpy_django.html import pyscript
|
|
19
|
+
from reactpy_django.utils import (
|
|
20
|
+
generate_obj_name,
|
|
21
|
+
import_module,
|
|
22
|
+
render_pyscript_template,
|
|
23
|
+
render_view,
|
|
24
|
+
vdom_or_component_to_string,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def view_to_component(
|
|
29
|
+
view: Callable | View | str,
|
|
30
|
+
transforms: Sequence[Callable[[VdomDict], Any]] = (),
|
|
31
|
+
strict_parsing: bool = True,
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""Converts a Django view to a ReactPy component.
|
|
34
|
+
|
|
35
|
+
Keyword Args:
|
|
36
|
+
view: The view to convert, or the view's dotted path as a string.
|
|
37
|
+
transforms: A list of functions that transforms the newly generated VDOM. \
|
|
38
|
+
The functions will be called on each VDOM node.
|
|
39
|
+
strict_parsing: If True, an exception will be generated if the HTML does not \
|
|
40
|
+
perfectly adhere to HTML5.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A function that takes `request, *args, key, **kwargs` and returns a ReactPy component.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def constructor(
|
|
47
|
+
request: HttpRequest | None = None,
|
|
48
|
+
*args,
|
|
49
|
+
key: Key | None = None,
|
|
50
|
+
**kwargs,
|
|
51
|
+
):
|
|
52
|
+
return _view_to_component(
|
|
53
|
+
view=view,
|
|
54
|
+
transforms=transforms,
|
|
55
|
+
strict_parsing=strict_parsing,
|
|
56
|
+
request=request,
|
|
57
|
+
args=args,
|
|
58
|
+
kwargs=kwargs,
|
|
59
|
+
key=key,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return constructor
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def view_to_iframe(
|
|
66
|
+
view: Callable | View | str, extra_props: dict[str, Any] | None = None
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Args:
|
|
70
|
+
view: The view function or class to convert, or the dotted path to the view.
|
|
71
|
+
|
|
72
|
+
Keyword Args:
|
|
73
|
+
extra_props: Additional properties to add to the `iframe` element.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A function that takes `*args, key, **kwargs` and returns a ReactPy component.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def constructor(
|
|
80
|
+
*args,
|
|
81
|
+
key: Key | None = None,
|
|
82
|
+
**kwargs,
|
|
83
|
+
):
|
|
84
|
+
return _view_to_iframe(
|
|
85
|
+
view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return constructor
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def django_css(static_path: str, key: Key | None = None):
|
|
92
|
+
"""Fetches a CSS static file for use within ReactPy. This allows for deferred CSS loading.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
static_path: The path to the static file. This path is identical to what you would \
|
|
96
|
+
use on Django's `{% static %}` template tag
|
|
97
|
+
key: A key to uniquely identify this component which is unique amongst a component's \
|
|
98
|
+
immediate siblings
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
return _django_css(static_path=static_path, key=key)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def django_js(static_path: str, key: Key | None = None):
|
|
105
|
+
"""Fetches a JS static file for use within ReactPy. This allows for deferred JS loading.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
static_path: The path to the static file. This path is identical to what you would \
|
|
109
|
+
use on Django's `{% static %}` template tag.
|
|
110
|
+
key: A key to uniquely identify this component which is unique amongst a component's \
|
|
111
|
+
immediate siblings
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
return _django_js(static_path=static_path, key=key)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def pyscript_component(
|
|
118
|
+
*file_paths: str,
|
|
119
|
+
initial: str | VdomDict | ComponentType = "",
|
|
120
|
+
root: str = "root",
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Args:
|
|
124
|
+
file_paths: File path to your client-side component. If multiple paths are \
|
|
125
|
+
provided, the contents are automatically merged.
|
|
126
|
+
|
|
127
|
+
Kwargs:
|
|
128
|
+
initial: The initial HTML that is displayed prior to the PyScript component \
|
|
129
|
+
loads. This can either be a string containing raw HTML, a \
|
|
130
|
+
`#!python reactpy.html` snippet, or a non-interactive component.
|
|
131
|
+
root: The name of the root component function.
|
|
132
|
+
"""
|
|
133
|
+
return _pyscript_component(
|
|
134
|
+
*file_paths,
|
|
135
|
+
initial=initial,
|
|
136
|
+
root=root,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@component
|
|
141
|
+
def _view_to_component(
|
|
142
|
+
view: Callable | View | str,
|
|
143
|
+
transforms: Sequence[Callable[[VdomDict], Any]],
|
|
144
|
+
strict_parsing: bool,
|
|
145
|
+
request: HttpRequest | None,
|
|
146
|
+
args: Sequence | None,
|
|
147
|
+
kwargs: dict | None,
|
|
148
|
+
):
|
|
149
|
+
"""The actual component. Used to prevent pollution of acceptable kwargs keys."""
|
|
150
|
+
converted_view, set_converted_view = hooks.use_state(
|
|
151
|
+
cast(Union[VdomDict, None], None)
|
|
152
|
+
)
|
|
153
|
+
_args: Sequence = args or ()
|
|
154
|
+
_kwargs: dict = kwargs or {}
|
|
155
|
+
if request:
|
|
156
|
+
_request: HttpRequest = request
|
|
157
|
+
else:
|
|
158
|
+
_request = HttpRequest()
|
|
159
|
+
_request.method = "GET"
|
|
160
|
+
resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore[assignment]
|
|
161
|
+
|
|
162
|
+
# Render the view render within a hook
|
|
163
|
+
@hooks.use_effect(
|
|
164
|
+
dependencies=[
|
|
165
|
+
json.dumps(vars(_request), default=lambda x: generate_obj_name(x)),
|
|
166
|
+
json.dumps([_args, _kwargs], default=lambda x: generate_obj_name(x)),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
async def async_render():
|
|
170
|
+
"""Render the view in an async hook to avoid blocking the main thread."""
|
|
171
|
+
# Render the view
|
|
172
|
+
response = await render_view(resolved_view, _request, _args, _kwargs)
|
|
173
|
+
set_converted_view(
|
|
174
|
+
utils.html_to_vdom(
|
|
175
|
+
response.content.decode("utf-8").strip(),
|
|
176
|
+
utils.del_html_head_body_transform,
|
|
177
|
+
*transforms,
|
|
178
|
+
strict=strict_parsing,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Return the view if it's been rendered via the `async_render` hook
|
|
183
|
+
return converted_view
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@component
|
|
187
|
+
def _view_to_iframe(
|
|
188
|
+
view: Callable | View | str,
|
|
189
|
+
extra_props: dict[str, Any] | None,
|
|
190
|
+
args: Sequence,
|
|
191
|
+
kwargs: dict,
|
|
192
|
+
) -> VdomDict:
|
|
193
|
+
"""The actual component. Used to prevent pollution of acceptable kwargs keys."""
|
|
194
|
+
from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS
|
|
195
|
+
|
|
196
|
+
if hasattr(view, "view_class"):
|
|
197
|
+
view = view.view_class
|
|
198
|
+
dotted_path = view if isinstance(view, str) else generate_obj_name(view)
|
|
199
|
+
registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path)
|
|
200
|
+
|
|
201
|
+
if not registered_view:
|
|
202
|
+
raise ViewNotRegisteredError(
|
|
203
|
+
f"'{dotted_path}' has not been registered as an iframe! "
|
|
204
|
+
"Are you sure you called `register_iframe` within a Django `AppConfig.ready` method?"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
query = kwargs.copy()
|
|
208
|
+
if args:
|
|
209
|
+
query["_args"] = args
|
|
210
|
+
query_string = f"?{urlencode(query, doseq=True)}" if args or kwargs else ""
|
|
211
|
+
extra_props = extra_props or {}
|
|
212
|
+
extra_props.pop("src", None)
|
|
213
|
+
|
|
214
|
+
return html.iframe(
|
|
215
|
+
{
|
|
216
|
+
"src": reverse("reactpy:view_to_iframe", args=[dotted_path]) + query_string,
|
|
217
|
+
"style": {"border": "none"},
|
|
218
|
+
"loading": "lazy",
|
|
219
|
+
}
|
|
220
|
+
| extra_props
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@component
|
|
225
|
+
def _django_css(static_path: str):
|
|
226
|
+
return html.style(_cached_static_contents(static_path))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@component
|
|
230
|
+
def _django_js(static_path: str):
|
|
231
|
+
return html.script(_cached_static_contents(static_path))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _cached_static_contents(static_path: str) -> str:
|
|
235
|
+
from reactpy_django.config import REACTPY_CACHE
|
|
236
|
+
|
|
237
|
+
# Try to find the file within Django's static files
|
|
238
|
+
abs_path = find(static_path)
|
|
239
|
+
if not abs_path:
|
|
240
|
+
raise FileNotFoundError(
|
|
241
|
+
f"Could not find static file {static_path} within Django's static files."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Fetch the file from cache, if available
|
|
245
|
+
last_modified_time = os.stat(abs_path).st_mtime
|
|
246
|
+
cache_key = f"reactpy_django:static_contents:{static_path}"
|
|
247
|
+
file_contents: str | None = caches[REACTPY_CACHE].get(
|
|
248
|
+
cache_key, version=int(last_modified_time)
|
|
249
|
+
)
|
|
250
|
+
if file_contents is None:
|
|
251
|
+
with open(abs_path, encoding="utf-8") as static_file:
|
|
252
|
+
file_contents = static_file.read()
|
|
253
|
+
caches[REACTPY_CACHE].delete(cache_key)
|
|
254
|
+
caches[REACTPY_CACHE].set(
|
|
255
|
+
cache_key, file_contents, timeout=None, version=int(last_modified_time)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return file_contents
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@component
|
|
262
|
+
def _pyscript_component(
|
|
263
|
+
*file_paths: str,
|
|
264
|
+
initial: str | VdomDict | ComponentType = "",
|
|
265
|
+
root: str = "root",
|
|
266
|
+
):
|
|
267
|
+
rendered, set_rendered = hooks.use_state(False)
|
|
268
|
+
uuid = uuid4().hex.replace("-", "")
|
|
269
|
+
initial = vdom_or_component_to_string(initial, uuid=uuid)
|
|
270
|
+
executor = render_pyscript_template(file_paths, uuid, root)
|
|
271
|
+
|
|
272
|
+
if not rendered:
|
|
273
|
+
# FIXME: This is needed to properly re-render PyScript during a WebSocket
|
|
274
|
+
# disconnection / reconnection. There may be a better way to do this in the future.
|
|
275
|
+
set_rendered(True)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
return html._(
|
|
279
|
+
html.div(
|
|
280
|
+
{"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid},
|
|
281
|
+
initial,
|
|
282
|
+
),
|
|
283
|
+
pyscript({"async": ""}, executor),
|
|
284
|
+
)
|
reactpy_django/config.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from itertools import cycle
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.core.cache import DEFAULT_CACHE_ALIAS
|
|
8
|
+
from django.db import DEFAULT_DB_ALIAS
|
|
9
|
+
from django.views import View
|
|
10
|
+
from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE
|
|
11
|
+
from reactpy.core.types import ComponentConstructor
|
|
12
|
+
|
|
13
|
+
from reactpy_django.types import (
|
|
14
|
+
AsyncPostprocessor,
|
|
15
|
+
SyncPostprocessor,
|
|
16
|
+
)
|
|
17
|
+
from reactpy_django.utils import import_dotted_path
|
|
18
|
+
|
|
19
|
+
# Non-configurable values
|
|
20
|
+
_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
|
|
21
|
+
REACTPY_DEBUG_MODE = _REACTPY_DEBUG_MODE.current
|
|
22
|
+
REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {}
|
|
23
|
+
REACTPY_FAILED_COMPONENTS: set[str] = set()
|
|
24
|
+
REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {}
|
|
25
|
+
|
|
26
|
+
# Configurable through Django settings.py
|
|
27
|
+
REACTPY_URL_PREFIX: str = getattr(
|
|
28
|
+
settings,
|
|
29
|
+
"REACTPY_URL_PREFIX",
|
|
30
|
+
"reactpy/",
|
|
31
|
+
).strip("/")
|
|
32
|
+
REACTPY_SESSION_MAX_AGE: int = getattr(
|
|
33
|
+
settings,
|
|
34
|
+
"REACTPY_SESSION_MAX_AGE",
|
|
35
|
+
259200, # Default to 3 days
|
|
36
|
+
)
|
|
37
|
+
REACTPY_CACHE: str = getattr(
|
|
38
|
+
settings,
|
|
39
|
+
"REACTPY_CACHE",
|
|
40
|
+
DEFAULT_CACHE_ALIAS,
|
|
41
|
+
)
|
|
42
|
+
REACTPY_DATABASE: str = getattr(
|
|
43
|
+
settings,
|
|
44
|
+
"REACTPY_DATABASE",
|
|
45
|
+
DEFAULT_DB_ALIAS,
|
|
46
|
+
)
|
|
47
|
+
_default_query_postprocessor = getattr(
|
|
48
|
+
settings,
|
|
49
|
+
"REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
|
|
50
|
+
"UNSET",
|
|
51
|
+
)
|
|
52
|
+
REACTPY_DEFAULT_QUERY_POSTPROCESSOR: AsyncPostprocessor | SyncPostprocessor | None
|
|
53
|
+
if _default_query_postprocessor is None:
|
|
54
|
+
REACTPY_DEFAULT_QUERY_POSTPROCESSOR = None
|
|
55
|
+
else:
|
|
56
|
+
REACTPY_DEFAULT_QUERY_POSTPROCESSOR = import_dotted_path(
|
|
57
|
+
"reactpy_django.utils.django_query_postprocessor"
|
|
58
|
+
if (
|
|
59
|
+
_default_query_postprocessor == "UNSET"
|
|
60
|
+
or not isinstance(_default_query_postprocessor, str)
|
|
61
|
+
)
|
|
62
|
+
else _default_query_postprocessor
|
|
63
|
+
)
|
|
64
|
+
REACTPY_AUTH_BACKEND: str | None = getattr(
|
|
65
|
+
settings,
|
|
66
|
+
"REACTPY_AUTH_BACKEND",
|
|
67
|
+
None,
|
|
68
|
+
)
|
|
69
|
+
REACTPY_BACKHAUL_THREAD: bool = getattr(
|
|
70
|
+
settings,
|
|
71
|
+
"REACTPY_BACKHAUL_THREAD",
|
|
72
|
+
False,
|
|
73
|
+
)
|
|
74
|
+
_default_hosts: list[str] | None = getattr(
|
|
75
|
+
settings,
|
|
76
|
+
"REACTPY_DEFAULT_HOSTS",
|
|
77
|
+
None,
|
|
78
|
+
)
|
|
79
|
+
REACTPY_DEFAULT_HOSTS: cycle[str] | None = (
|
|
80
|
+
cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)])
|
|
81
|
+
if _default_hosts
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
REACTPY_RECONNECT_INTERVAL: int = getattr(
|
|
85
|
+
settings,
|
|
86
|
+
"REACTPY_RECONNECT_INTERVAL",
|
|
87
|
+
750, # Default to 0.75 seconds
|
|
88
|
+
)
|
|
89
|
+
REACTPY_RECONNECT_MAX_INTERVAL: int = getattr(
|
|
90
|
+
settings,
|
|
91
|
+
"REACTPY_RECONNECT_MAX_INTERVAL",
|
|
92
|
+
60000, # Default to 60 seconds
|
|
93
|
+
)
|
|
94
|
+
REACTPY_RECONNECT_MAX_RETRIES: int = getattr(
|
|
95
|
+
settings,
|
|
96
|
+
"REACTPY_RECONNECT_MAX_RETRIES",
|
|
97
|
+
150,
|
|
98
|
+
)
|
|
99
|
+
REACTPY_RECONNECT_BACKOFF_MULTIPLIER: float | int = getattr(
|
|
100
|
+
settings,
|
|
101
|
+
"REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
|
|
102
|
+
1.25, # Default to 25% backoff per connection attempt
|
|
103
|
+
)
|
|
104
|
+
REACTPY_PRERENDER: bool = getattr(
|
|
105
|
+
settings,
|
|
106
|
+
"REACTPY_PRERENDER",
|
|
107
|
+
False,
|
|
108
|
+
)
|
|
109
|
+
REACTPY_AUTO_RELOGIN: bool = getattr(
|
|
110
|
+
settings,
|
|
111
|
+
"REACTPY_AUTO_RELOGIN",
|
|
112
|
+
False,
|
|
113
|
+
)
|
|
114
|
+
REACTPY_CLEAN_INTERVAL: int | None = getattr(
|
|
115
|
+
settings,
|
|
116
|
+
"REACTPY_CLEAN_INTERVAL",
|
|
117
|
+
604800, # Default to 7 days
|
|
118
|
+
)
|
|
119
|
+
REACTPY_CLEAN_SESSIONS: bool = getattr(
|
|
120
|
+
settings,
|
|
121
|
+
"REACTPY_CLEAN_SESSIONS",
|
|
122
|
+
True,
|
|
123
|
+
)
|
|
124
|
+
REACTPY_CLEAN_USER_DATA: bool = getattr(
|
|
125
|
+
settings,
|
|
126
|
+
"REACTPY_CLEAN_USER_DATA",
|
|
127
|
+
True,
|
|
128
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from reactpy_django.config import REACTPY_DATABASE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Router:
|
|
5
|
+
"""
|
|
6
|
+
A router to control all database operations on models in the
|
|
7
|
+
auth and contenttypes applications.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
route_app_labels = {"reactpy_django"}
|
|
11
|
+
|
|
12
|
+
def db_for_read(self, model, **hints):
|
|
13
|
+
"""Attempts to read go to REACTPY_DATABASE."""
|
|
14
|
+
if model._meta.app_label in self.route_app_labels:
|
|
15
|
+
return REACTPY_DATABASE
|
|
16
|
+
|
|
17
|
+
def db_for_write(self, model, **hints):
|
|
18
|
+
"""Attempts to write go to REACTPY_DATABASE."""
|
|
19
|
+
if model._meta.app_label in self.route_app_labels:
|
|
20
|
+
return REACTPY_DATABASE
|
|
21
|
+
|
|
22
|
+
def allow_relation(self, obj1, obj2, **hints):
|
|
23
|
+
"""Returning `None` only allow relations within the same database.
|
|
24
|
+
https://docs.djangoproject.com/en/stable/topics/db/multi-db/#limitations-of-multiple-databases
|
|
25
|
+
"""
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
|
29
|
+
"""Make sure ReactPy models only appear in REACTPY_DATABASE."""
|
|
30
|
+
if app_label in self.route_app_labels:
|
|
31
|
+
return db == REACTPY_DATABASE
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
from reactpy import component
|
|
7
|
+
from reactpy.core.types import ComponentConstructor
|
|
8
|
+
|
|
9
|
+
from reactpy_django.exceptions import DecoratorParamError
|
|
10
|
+
from reactpy_django.hooks import use_user
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from django.contrib.auth.models import AbstractUser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def user_passes_test(
|
|
19
|
+
test_func: Callable[[AbstractUser], bool],
|
|
20
|
+
/,
|
|
21
|
+
fallback: Any | None = None,
|
|
22
|
+
) -> ComponentConstructor:
|
|
23
|
+
"""You can limit component access to users that pass a test function by using this decorator.
|
|
24
|
+
|
|
25
|
+
This decorator is inspired by Django's `user_passes_test` decorator, but works with ReactPy components.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
test_func: A function that accepts a `User` returns a boolean.
|
|
29
|
+
fallback: The content to be rendered if the test fails. Typically is a ReactPy component or \
|
|
30
|
+
VDOM (`reactpy.html` snippet).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def decorator(user_component):
|
|
34
|
+
@wraps(user_component)
|
|
35
|
+
def _wrapper(*args, **kwargs):
|
|
36
|
+
return _user_passes_test(
|
|
37
|
+
user_component, fallback, test_func, *args, **kwargs
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return _wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@component
|
|
46
|
+
def _user_passes_test(component_constructor, fallback, test_func, *args, **kwargs):
|
|
47
|
+
"""Dedicated component for `user_passes_test` to allow us to always have access to hooks."""
|
|
48
|
+
user = use_user()
|
|
49
|
+
|
|
50
|
+
if test_func(user):
|
|
51
|
+
# Ensure that the component is a ReactPy component.
|
|
52
|
+
user_component = component_constructor(*args, **kwargs)
|
|
53
|
+
if not getattr(user_component, "render", None):
|
|
54
|
+
raise DecoratorParamError(
|
|
55
|
+
"`user_passes_test` is not decorating a ReactPy component. "
|
|
56
|
+
"Did you forget `@user_passes_test` must be ABOVE the `@component` decorator?"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Render the component.
|
|
60
|
+
return user_component
|
|
61
|
+
|
|
62
|
+
# Render the fallback component.
|
|
63
|
+
# Returns an empty string if fallback is None, since ReactPy currently renders None as a string.
|
|
64
|
+
# TODO: Remove this fallback when ReactPy can render None properly.
|
|
65
|
+
return fallback(*args, **kwargs) if callable(fallback) else (fallback or "")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class ComponentParamError(TypeError):
|
|
2
|
+
...
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ComponentDoesNotExistError(AttributeError):
|
|
6
|
+
...
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OfflineComponentMissing(ComponentDoesNotExistError):
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvalidHostError(ValueError):
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ComponentCarrierError(Exception):
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UserNotFoundError(Exception):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ViewNotRegisteredError(AttributeError):
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ViewDoesNotExistError(AttributeError):
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DecoratorParamError(TypeError):
|
|
34
|
+
...
|