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
reactpy_django/hooks.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import (
|
|
6
|
+
TYPE_CHECKING,
|
|
7
|
+
Any,
|
|
8
|
+
Awaitable,
|
|
9
|
+
Callable,
|
|
10
|
+
DefaultDict,
|
|
11
|
+
Sequence,
|
|
12
|
+
Union,
|
|
13
|
+
cast,
|
|
14
|
+
)
|
|
15
|
+
from uuid import uuid4
|
|
16
|
+
|
|
17
|
+
import orjson
|
|
18
|
+
from channels import DEFAULT_CHANNEL_LAYER
|
|
19
|
+
from channels.db import database_sync_to_async
|
|
20
|
+
from channels.layers import InMemoryChannelLayer, get_channel_layer
|
|
21
|
+
from reactpy import use_callback, use_effect, use_memo, use_ref, use_state
|
|
22
|
+
from reactpy import use_connection as _use_connection
|
|
23
|
+
from reactpy import use_location as _use_location
|
|
24
|
+
from reactpy import use_scope as _use_scope
|
|
25
|
+
from reactpy.backend.types import Location
|
|
26
|
+
|
|
27
|
+
from reactpy_django.exceptions import UserNotFoundError
|
|
28
|
+
from reactpy_django.types import (
|
|
29
|
+
AsyncMessageReceiver,
|
|
30
|
+
AsyncMessageSender,
|
|
31
|
+
AsyncPostprocessor,
|
|
32
|
+
ConnectionType,
|
|
33
|
+
FuncParams,
|
|
34
|
+
Inferred,
|
|
35
|
+
Mutation,
|
|
36
|
+
Query,
|
|
37
|
+
SyncPostprocessor,
|
|
38
|
+
UserData,
|
|
39
|
+
)
|
|
40
|
+
from reactpy_django.utils import django_query_postprocessor, generate_obj_name, get_pk
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from channels_redis.core import RedisChannelLayer
|
|
44
|
+
from django.contrib.auth.models import AbstractUser
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_logger = logging.getLogger(__name__)
|
|
48
|
+
_REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = (
|
|
49
|
+
DefaultDict(set)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def use_location() -> Location:
|
|
54
|
+
"""Get the current route as a `Location` object"""
|
|
55
|
+
return _use_location()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def use_origin() -> str | None:
|
|
59
|
+
"""Get the current origin as a string. If the browser did not send an origin header,
|
|
60
|
+
this will be None."""
|
|
61
|
+
scope = _use_scope()
|
|
62
|
+
try:
|
|
63
|
+
if scope["type"] == "websocket":
|
|
64
|
+
return next(
|
|
65
|
+
(
|
|
66
|
+
header[1].decode("utf-8")
|
|
67
|
+
for header in scope["headers"]
|
|
68
|
+
if header[0] == b"origin"
|
|
69
|
+
),
|
|
70
|
+
None,
|
|
71
|
+
)
|
|
72
|
+
if scope["type"] == "http":
|
|
73
|
+
host = next(
|
|
74
|
+
(
|
|
75
|
+
header[1].decode("utf-8")
|
|
76
|
+
for header in scope["headers"]
|
|
77
|
+
if header[0] == b"host"
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return f"{scope['scheme']}://{host}" if host else None
|
|
81
|
+
except Exception:
|
|
82
|
+
_logger.info("Failed to get origin")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def use_scope() -> dict[str, Any]:
|
|
87
|
+
"""Get the current ASGI scope dictionary"""
|
|
88
|
+
scope = _use_scope()
|
|
89
|
+
|
|
90
|
+
if isinstance(scope, dict):
|
|
91
|
+
return scope
|
|
92
|
+
|
|
93
|
+
raise TypeError(f"Expected scope to be a dict, got {type(scope)}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def use_connection() -> ConnectionType:
|
|
97
|
+
"""Get the current `Connection` object"""
|
|
98
|
+
return _use_connection()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def use_query(
|
|
102
|
+
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
|
|
103
|
+
kwargs: dict[str, Any] | None = None,
|
|
104
|
+
*,
|
|
105
|
+
thread_sensitive: bool = True,
|
|
106
|
+
postprocessor: (
|
|
107
|
+
AsyncPostprocessor | SyncPostprocessor | None
|
|
108
|
+
) = django_query_postprocessor,
|
|
109
|
+
postprocessor_kwargs: dict[str, Any] | None = None,
|
|
110
|
+
) -> Query[Inferred]:
|
|
111
|
+
"""This hook is used to execute functions in the background and return the result, \
|
|
112
|
+
typically to read data the Django ORM.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
query: A function that executes a query and returns some data.
|
|
116
|
+
|
|
117
|
+
Kwargs:
|
|
118
|
+
kwargs: Keyword arguments to passed into the `query` function.
|
|
119
|
+
thread_sensitive: Whether to run the query in thread sensitive mode. \
|
|
120
|
+
This mode only applies to sync query functions, and is turned on by default \
|
|
121
|
+
due to Django ORM limitations.
|
|
122
|
+
postprocessor: A callable that processes the query `data` before it is returned. \
|
|
123
|
+
The first argument of postprocessor function must be the query `data`. All \
|
|
124
|
+
proceeding arguments are optional `postprocessor_kwargs`. This postprocessor \
|
|
125
|
+
function must return the modified `data`. \
|
|
126
|
+
\
|
|
127
|
+
If unset, `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` is used. By default, this \
|
|
128
|
+
is used to prevent Django's lazy query execution and supports `many_to_many` \
|
|
129
|
+
and `many_to_one` as `postprocessor_kwargs`.
|
|
130
|
+
postprocessor_kwargs: Keyworded arguments passed into the `postprocessor` function.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
An object containing `loading`/`#!python error` states, your `data` (if the query \
|
|
134
|
+
has successfully executed), and a `refetch` callable that can be used to re-run the query.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
should_execute, set_should_execute = use_state(True)
|
|
138
|
+
data, set_data = use_state(cast(Inferred, None))
|
|
139
|
+
loading, set_loading = use_state(True)
|
|
140
|
+
error, set_error = use_state(cast(Union[Exception, None], None))
|
|
141
|
+
query_ref = use_ref(query)
|
|
142
|
+
kwargs = kwargs or {}
|
|
143
|
+
postprocessor_kwargs = postprocessor_kwargs or {}
|
|
144
|
+
|
|
145
|
+
if query_ref.current is not query:
|
|
146
|
+
raise ValueError(f"Query function changed from {query_ref.current} to {query}.")
|
|
147
|
+
|
|
148
|
+
async def execute_query() -> None:
|
|
149
|
+
"""The main running function for `use_query`"""
|
|
150
|
+
try:
|
|
151
|
+
# Run the query
|
|
152
|
+
if asyncio.iscoroutinefunction(query):
|
|
153
|
+
new_data = await query(**kwargs)
|
|
154
|
+
else:
|
|
155
|
+
new_data = await database_sync_to_async(
|
|
156
|
+
query, thread_sensitive=thread_sensitive
|
|
157
|
+
)(**kwargs)
|
|
158
|
+
|
|
159
|
+
# Run the postprocessor
|
|
160
|
+
if postprocessor:
|
|
161
|
+
if asyncio.iscoroutinefunction(postprocessor):
|
|
162
|
+
new_data = await postprocessor(new_data, **postprocessor_kwargs)
|
|
163
|
+
else:
|
|
164
|
+
new_data = await database_sync_to_async(
|
|
165
|
+
postprocessor, thread_sensitive=thread_sensitive
|
|
166
|
+
)(new_data, **postprocessor_kwargs)
|
|
167
|
+
|
|
168
|
+
# Log any errors and set the error state
|
|
169
|
+
except Exception as e:
|
|
170
|
+
set_data(cast(Inferred, None))
|
|
171
|
+
set_loading(False)
|
|
172
|
+
set_error(e)
|
|
173
|
+
_logger.exception("Failed to execute query: %s", generate_obj_name(query))
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Query was successful
|
|
177
|
+
else:
|
|
178
|
+
set_data(new_data)
|
|
179
|
+
set_loading(False)
|
|
180
|
+
set_error(None)
|
|
181
|
+
|
|
182
|
+
@use_effect(dependencies=None)
|
|
183
|
+
def schedule_query() -> None:
|
|
184
|
+
"""Schedule the query to be run when needed"""
|
|
185
|
+
# Make sure we don't re-execute the query unless we're told to
|
|
186
|
+
if not should_execute:
|
|
187
|
+
return
|
|
188
|
+
set_should_execute(False)
|
|
189
|
+
|
|
190
|
+
# Execute the query in the background
|
|
191
|
+
asyncio.create_task(execute_query())
|
|
192
|
+
|
|
193
|
+
@use_callback
|
|
194
|
+
def refetch() -> None:
|
|
195
|
+
"""Callable provided to the user, used to re-execute the query"""
|
|
196
|
+
set_should_execute(True)
|
|
197
|
+
set_loading(True)
|
|
198
|
+
set_error(None)
|
|
199
|
+
|
|
200
|
+
@use_effect(dependencies=[])
|
|
201
|
+
def register_refetch_callback() -> Callable[[], None]:
|
|
202
|
+
"""Track the refetch callback so we can re-execute the query"""
|
|
203
|
+
# By tracking callbacks globally, any usage of the query function will be re-run
|
|
204
|
+
# if the user has told a mutation to refetch it.
|
|
205
|
+
_REFETCH_CALLBACKS[query].add(refetch)
|
|
206
|
+
return lambda: _REFETCH_CALLBACKS[query].remove(refetch)
|
|
207
|
+
|
|
208
|
+
# Return Query user API
|
|
209
|
+
return Query(data, loading, error, refetch)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def use_mutation(
|
|
213
|
+
mutation: (
|
|
214
|
+
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
|
|
215
|
+
),
|
|
216
|
+
*,
|
|
217
|
+
thread_sensitive: bool = True,
|
|
218
|
+
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
|
|
219
|
+
) -> Mutation[FuncParams]:
|
|
220
|
+
"""This hook is used to modify data in the background, typically to create/update/delete \
|
|
221
|
+
data from the Django ORM.
|
|
222
|
+
|
|
223
|
+
Mutation functions can `return False` to prevent executing your `refetch` function. All \
|
|
224
|
+
other returns are ignored. Mutation functions can be sync or async.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
mutation: A callable that performs Django ORM create, update, or delete \
|
|
228
|
+
functionality. If this function returns `False`, then your `refetch` \
|
|
229
|
+
function will not be used.
|
|
230
|
+
|
|
231
|
+
Kwargs:
|
|
232
|
+
thread_sensitive: Whether to run the mutation in thread sensitive mode. \
|
|
233
|
+
This mode only applies to sync mutation functions, and is turned on by default \
|
|
234
|
+
due to Django ORM limitations.
|
|
235
|
+
refetch: A query function (the function you provide to your `use_query` \
|
|
236
|
+
hook) or a sequence of query functions that need a `refetch` if the \
|
|
237
|
+
mutation succeeds. This is useful for refreshing data after a mutation \
|
|
238
|
+
has been performed.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
An object containing `#!python loading`/`#!python error` states, and a \
|
|
242
|
+
`#!python reset` callable that will set `#!python loading`/`#!python error` \
|
|
243
|
+
states to defaults. This object can be called to run the query.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
loading, set_loading = use_state(False)
|
|
247
|
+
error, set_error = use_state(cast(Union[Exception, None], None))
|
|
248
|
+
|
|
249
|
+
# The main "running" function for `use_mutation`
|
|
250
|
+
async def execute_mutation(exec_args, exec_kwargs) -> None:
|
|
251
|
+
# Run the mutation
|
|
252
|
+
try:
|
|
253
|
+
if asyncio.iscoroutinefunction(mutation):
|
|
254
|
+
should_refetch = await mutation(*exec_args, **exec_kwargs)
|
|
255
|
+
else:
|
|
256
|
+
should_refetch = await database_sync_to_async(
|
|
257
|
+
mutation, thread_sensitive=thread_sensitive
|
|
258
|
+
)(*exec_args, **exec_kwargs)
|
|
259
|
+
|
|
260
|
+
# Log any errors and set the error state
|
|
261
|
+
except Exception as e:
|
|
262
|
+
set_loading(False)
|
|
263
|
+
set_error(e)
|
|
264
|
+
_logger.exception(
|
|
265
|
+
"Failed to execute mutation: %s", generate_obj_name(mutation)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Mutation was successful
|
|
269
|
+
else:
|
|
270
|
+
set_loading(False)
|
|
271
|
+
set_error(None)
|
|
272
|
+
|
|
273
|
+
# `refetch` will execute unless explicitly told not to
|
|
274
|
+
# or if `refetch` was not defined.
|
|
275
|
+
if should_refetch is not False and refetch:
|
|
276
|
+
for query in (refetch,) if callable(refetch) else refetch:
|
|
277
|
+
for callback in _REFETCH_CALLBACKS.get(query) or ():
|
|
278
|
+
callback()
|
|
279
|
+
|
|
280
|
+
# Schedule the mutation to be run when needed
|
|
281
|
+
@use_callback
|
|
282
|
+
def schedule_mutation(
|
|
283
|
+
*exec_args: FuncParams.args, **exec_kwargs: FuncParams.kwargs
|
|
284
|
+
) -> None:
|
|
285
|
+
# Set the loading state.
|
|
286
|
+
# It's okay to re-execute the mutation if we're told to. The user
|
|
287
|
+
# can use the `loading` state to prevent this.
|
|
288
|
+
set_loading(True)
|
|
289
|
+
|
|
290
|
+
# Execute the mutation in the background
|
|
291
|
+
asyncio.ensure_future(
|
|
292
|
+
execute_mutation(exec_args=exec_args, exec_kwargs=exec_kwargs)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Used when the user has told us to reset this mutation
|
|
296
|
+
@use_callback
|
|
297
|
+
def reset() -> None:
|
|
298
|
+
set_loading(False)
|
|
299
|
+
set_error(None)
|
|
300
|
+
|
|
301
|
+
# Return mutation user API
|
|
302
|
+
return Mutation(schedule_mutation, loading, error, reset)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def use_user() -> AbstractUser:
|
|
306
|
+
"""Get the current `User` object from either the WebSocket or HTTP request."""
|
|
307
|
+
connection = use_connection()
|
|
308
|
+
user = connection.scope.get("user") or getattr(connection.carrier, "user", None)
|
|
309
|
+
if user is None:
|
|
310
|
+
raise UserNotFoundError("No user is available in the current environment.")
|
|
311
|
+
return user
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def use_user_data(
|
|
315
|
+
default_data: (
|
|
316
|
+
None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any]
|
|
317
|
+
) = None,
|
|
318
|
+
save_default_data: bool = False,
|
|
319
|
+
) -> UserData:
|
|
320
|
+
"""Get or set user data stored within the REACTPY_DATABASE.
|
|
321
|
+
|
|
322
|
+
Kwargs:
|
|
323
|
+
default_data: A dictionary containing `{key: default_value}` pairs. \
|
|
324
|
+
For computationally intensive defaults, your `default_value` \
|
|
325
|
+
can be sync or async functions that return the value to set.
|
|
326
|
+
save_default_data: If True, `default_data` values will automatically be stored \
|
|
327
|
+
within the database if they do not exist.
|
|
328
|
+
"""
|
|
329
|
+
from reactpy_django.models import UserDataModel
|
|
330
|
+
|
|
331
|
+
user = use_user()
|
|
332
|
+
|
|
333
|
+
async def _set_user_data(data: dict):
|
|
334
|
+
if not isinstance(data, dict):
|
|
335
|
+
raise TypeError(f"Expected dict while setting user data, got {type(data)}")
|
|
336
|
+
if user.is_anonymous:
|
|
337
|
+
raise ValueError("AnonymousUser cannot have user data.")
|
|
338
|
+
|
|
339
|
+
pk = get_pk(user)
|
|
340
|
+
model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
|
|
341
|
+
model.data = orjson.dumps(data)
|
|
342
|
+
await model.asave()
|
|
343
|
+
|
|
344
|
+
query: Query[dict | None] = use_query(
|
|
345
|
+
_get_user_data,
|
|
346
|
+
kwargs={
|
|
347
|
+
"user": user,
|
|
348
|
+
"default_data": default_data,
|
|
349
|
+
"save_default_data": save_default_data,
|
|
350
|
+
},
|
|
351
|
+
postprocessor=None,
|
|
352
|
+
)
|
|
353
|
+
mutation = use_mutation(_set_user_data, refetch=_get_user_data)
|
|
354
|
+
|
|
355
|
+
return UserData(query, mutation)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def use_channel_layer(
|
|
359
|
+
name: str | None = None,
|
|
360
|
+
*,
|
|
361
|
+
group_name: str | None = None,
|
|
362
|
+
group_add: bool = True,
|
|
363
|
+
group_discard: bool = True,
|
|
364
|
+
receiver: AsyncMessageReceiver | None = None,
|
|
365
|
+
layer: str = DEFAULT_CHANNEL_LAYER,
|
|
366
|
+
) -> AsyncMessageSender:
|
|
367
|
+
"""
|
|
368
|
+
Subscribe to a Django Channels layer to send/receive messages.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
name: The name of the channel to subscribe to. If you define a `group_name`, you \
|
|
372
|
+
can keep `name` undefined to auto-generate a unique name.
|
|
373
|
+
group_name: If configured, any messages sent within this hook will be broadcasted \
|
|
374
|
+
to all channels in this group.
|
|
375
|
+
group_add: If `True`, the channel will automatically be added to the group \
|
|
376
|
+
when the component mounts.
|
|
377
|
+
group_discard: If `True`, the channel will automatically be removed from the \
|
|
378
|
+
group when the component dismounts.
|
|
379
|
+
receiver: An async function that receives a `message: dict` from a channel. \
|
|
380
|
+
If more than one receiver waits on the same channel name, a random receiver \
|
|
381
|
+
will get the result.
|
|
382
|
+
layer: The channel layer to use. This layer must be defined in \
|
|
383
|
+
`settings.py:CHANNEL_LAYERS`.
|
|
384
|
+
"""
|
|
385
|
+
channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer)
|
|
386
|
+
channel_name = use_memo(lambda: str(name or uuid4()))
|
|
387
|
+
|
|
388
|
+
if not name and not group_name:
|
|
389
|
+
raise ValueError("You must define a `name` or `group_name` for the channel.")
|
|
390
|
+
|
|
391
|
+
if not channel_layer:
|
|
392
|
+
raise ValueError(
|
|
393
|
+
f"Channel layer '{layer}' is not available. Are you sure you"
|
|
394
|
+
" configured settings.py:CHANNEL_LAYERS properly?"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Add/remove a group's channel during component mount/dismount respectively.
|
|
398
|
+
@use_effect(dependencies=[])
|
|
399
|
+
async def group_manager():
|
|
400
|
+
if group_name and group_add:
|
|
401
|
+
await channel_layer.group_add(group_name, channel_name)
|
|
402
|
+
|
|
403
|
+
if group_name and group_discard:
|
|
404
|
+
return lambda: asyncio.run(
|
|
405
|
+
channel_layer.group_discard(group_name, channel_name)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Listen for messages on the channel using the provided `receiver` function.
|
|
409
|
+
@use_effect
|
|
410
|
+
async def message_receiver():
|
|
411
|
+
if not receiver:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
while True:
|
|
415
|
+
message = await channel_layer.receive(channel_name)
|
|
416
|
+
await receiver(message)
|
|
417
|
+
|
|
418
|
+
# User interface for sending messages to the channel
|
|
419
|
+
async def message_sender(message: dict):
|
|
420
|
+
if group_name:
|
|
421
|
+
await channel_layer.group_send(group_name, message)
|
|
422
|
+
else:
|
|
423
|
+
await channel_layer.send(channel_name, message)
|
|
424
|
+
|
|
425
|
+
return message_sender
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def use_root_id() -> str:
|
|
429
|
+
"""Get the root element's ID. This value is guaranteed to be unique. Current versions of \
|
|
430
|
+
ReactPy-Django return a `uuid4` string."""
|
|
431
|
+
scope = use_scope()
|
|
432
|
+
|
|
433
|
+
return scope["reactpy"]["id"]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def _get_user_data(
|
|
437
|
+
user: AbstractUser, default_data: None | dict, save_default_data: bool
|
|
438
|
+
) -> dict | None:
|
|
439
|
+
"""The mutation function for `use_user_data`"""
|
|
440
|
+
from reactpy_django.models import UserDataModel
|
|
441
|
+
|
|
442
|
+
if not user or user.is_anonymous:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
pk = get_pk(user)
|
|
446
|
+
model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk)
|
|
447
|
+
data = orjson.loads(model.data) if model.data else {}
|
|
448
|
+
|
|
449
|
+
if not isinstance(data, dict):
|
|
450
|
+
raise TypeError(f"Expected dict while loading user data, got {type(data)}")
|
|
451
|
+
|
|
452
|
+
# Set default values, if needed
|
|
453
|
+
if default_data:
|
|
454
|
+
changed = False
|
|
455
|
+
for key, value in default_data.items():
|
|
456
|
+
if key not in data:
|
|
457
|
+
new_value: Any = value
|
|
458
|
+
if asyncio.iscoroutinefunction(value):
|
|
459
|
+
new_value = await value()
|
|
460
|
+
elif callable(value):
|
|
461
|
+
new_value = value()
|
|
462
|
+
data[key] = new_value
|
|
463
|
+
changed = True
|
|
464
|
+
if changed:
|
|
465
|
+
model.data = orjson.dumps(data)
|
|
466
|
+
if save_default_data:
|
|
467
|
+
await model.asave()
|
|
468
|
+
|
|
469
|
+
return data
|
reactpy_django/html.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
app_name = "reactpy"
|
|
6
|
+
|
|
7
|
+
urlpatterns = [
|
|
8
|
+
path(
|
|
9
|
+
"web_module/<path:file>",
|
|
10
|
+
views.web_modules_file,
|
|
11
|
+
name="web_modules",
|
|
12
|
+
),
|
|
13
|
+
path(
|
|
14
|
+
"iframe/<str:dotted_path>",
|
|
15
|
+
views.view_to_iframe,
|
|
16
|
+
name="view_to_iframe",
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from urllib.parse import parse_qs
|
|
4
|
+
|
|
5
|
+
from django.core.cache import caches
|
|
6
|
+
from django.core.exceptions import SuspiciousOperation
|
|
7
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseNotFound
|
|
8
|
+
from reactpy.config import REACTPY_WEB_MODULES_DIR
|
|
9
|
+
|
|
10
|
+
from reactpy_django.utils import create_cache_key, render_view
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse:
|
|
14
|
+
"""Gets JavaScript required for ReactPy modules at runtime. These modules are
|
|
15
|
+
returned from cache if available."""
|
|
16
|
+
from reactpy_django.config import REACTPY_CACHE
|
|
17
|
+
|
|
18
|
+
web_modules_dir = REACTPY_WEB_MODULES_DIR.current
|
|
19
|
+
path = os.path.abspath(web_modules_dir.joinpath(file))
|
|
20
|
+
|
|
21
|
+
# Prevent attempts to walk outside of the web modules dir
|
|
22
|
+
if str(web_modules_dir) != os.path.commonpath((path, web_modules_dir)):
|
|
23
|
+
raise SuspiciousOperation(
|
|
24
|
+
"Attempt to access a directory outside of REACTPY_WEB_MODULES_DIR."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Fetch the file from cache, if available
|
|
28
|
+
last_modified_time = os.stat(path).st_mtime
|
|
29
|
+
cache_key = create_cache_key("web_modules", path)
|
|
30
|
+
file_contents = await caches[REACTPY_CACHE].aget(
|
|
31
|
+
cache_key, version=int(last_modified_time)
|
|
32
|
+
)
|
|
33
|
+
if file_contents is None:
|
|
34
|
+
with open(path, "r", encoding="utf-8") as fp:
|
|
35
|
+
file_contents = await asyncio.to_thread(fp.read)
|
|
36
|
+
await caches[REACTPY_CACHE].adelete(cache_key)
|
|
37
|
+
await caches[REACTPY_CACHE].aset(
|
|
38
|
+
cache_key, file_contents, timeout=604800, version=int(last_modified_time)
|
|
39
|
+
)
|
|
40
|
+
return HttpResponse(file_contents, content_type="text/javascript")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def view_to_iframe(request: HttpRequest, dotted_path: str) -> HttpResponse:
|
|
44
|
+
"""Returns a view that was registered by reactpy_django.components.view_to_iframe."""
|
|
45
|
+
from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS
|
|
46
|
+
|
|
47
|
+
# Get the view
|
|
48
|
+
registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path)
|
|
49
|
+
if not registered_view:
|
|
50
|
+
return HttpResponseNotFound()
|
|
51
|
+
|
|
52
|
+
# Get args and kwargs from the request
|
|
53
|
+
query = request.META.get("QUERY_STRING", "")
|
|
54
|
+
kwargs = {k: v if len(v) > 1 else v[0] for k, v in parse_qs(query).items()}
|
|
55
|
+
args = kwargs.pop("_args", [])
|
|
56
|
+
|
|
57
|
+
# Render the view
|
|
58
|
+
response = await render_view(registered_view, request, args, kwargs)
|
|
59
|
+
|
|
60
|
+
# Ensure page can be rendered as an iframe
|
|
61
|
+
response["X-Frame-Options"] = "SAMEORIGIN"
|
|
62
|
+
return response
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from django.core.management.base import BaseCommand
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command(BaseCommand):
|
|
7
|
+
help = "Manually clean ReactPy data. When using this command without args, it will perform all cleaning operations."
|
|
8
|
+
|
|
9
|
+
def handle(self, **options):
|
|
10
|
+
from reactpy_django.clean import clean
|
|
11
|
+
|
|
12
|
+
verbosity = options.get("verbosity", 1)
|
|
13
|
+
|
|
14
|
+
cleaning_args: set[Literal["all", "sessions", "user_data"]] = set()
|
|
15
|
+
if options.get("sessions"):
|
|
16
|
+
cleaning_args.add("sessions")
|
|
17
|
+
if options.get("user_data"):
|
|
18
|
+
cleaning_args.add("user_data")
|
|
19
|
+
if not cleaning_args:
|
|
20
|
+
cleaning_args = {"all"}
|
|
21
|
+
|
|
22
|
+
clean(*cleaning_args, immediate=True, verbosity=verbosity)
|
|
23
|
+
|
|
24
|
+
if verbosity >= 1:
|
|
25
|
+
print("ReactPy data has been cleaned!")
|
|
26
|
+
|
|
27
|
+
def add_arguments(self, parser):
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--sessions",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="Clean session data. This value can be combined with other cleaning options.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--user-data",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Clean user data. This value can be combined with other cleaning options.",
|
|
37
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by Django 4.1.3 on 2023-01-10 00:54
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
initial = True
|
|
8
|
+
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.CreateModel(
|
|
13
|
+
name="ComponentParams",
|
|
14
|
+
fields=[
|
|
15
|
+
(
|
|
16
|
+
"uuid",
|
|
17
|
+
models.UUIDField(
|
|
18
|
+
editable=False, primary_key=True, serialize=False, unique=True
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
("data", models.BinaryField()),
|
|
22
|
+
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
23
|
+
],
|
|
24
|
+
),
|
|
25
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 4.1.5 on 2023-01-11 01:55
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("reactpy_django", "0001_initial"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RenameField(
|
|
13
|
+
model_name="componentparams",
|
|
14
|
+
old_name="created_at",
|
|
15
|
+
new_name="last_accessed",
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 4.1.6 on 2023-02-21 10:59
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("reactpy_django", "0002_rename_created_at_componentparams_last_accessed"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.CreateModel(
|
|
13
|
+
name="ComponentSession",
|
|
14
|
+
fields=[
|
|
15
|
+
(
|
|
16
|
+
"uuid",
|
|
17
|
+
models.UUIDField(
|
|
18
|
+
editable=False, primary_key=True, serialize=False, unique=True
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
("params", models.BinaryField()),
|
|
22
|
+
("last_accessed", models.DateTimeField(auto_now_add=True)),
|
|
23
|
+
],
|
|
24
|
+
),
|
|
25
|
+
migrations.DeleteModel(
|
|
26
|
+
name="ComponentParams",
|
|
27
|
+
),
|
|
28
|
+
]
|