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.
Files changed (118) hide show
  1. js/node_modules/@pyscript/core/src/stdlib/pyscript/__init__.py +61 -0
  2. js/node_modules/@pyscript/core/src/stdlib/pyscript/display.py +177 -0
  3. js/node_modules/@pyscript/core/src/stdlib/pyscript/event_handling.py +76 -0
  4. js/node_modules/@pyscript/core/src/stdlib/pyscript/fetch.py +87 -0
  5. js/node_modules/@pyscript/core/src/stdlib/pyscript/ffi.py +18 -0
  6. js/node_modules/@pyscript/core/src/stdlib/pyscript/flatted.py +148 -0
  7. js/node_modules/@pyscript/core/src/stdlib/pyscript/magic_js.py +79 -0
  8. js/node_modules/@pyscript/core/src/stdlib/pyscript/storage.py +60 -0
  9. js/node_modules/@pyscript/core/src/stdlib/pyscript/util.py +33 -0
  10. js/node_modules/@pyscript/core/src/stdlib/pyscript/web.py +1176 -0
  11. js/node_modules/@pyscript/core/src/stdlib/pyscript/websocket.py +71 -0
  12. js/node_modules/@pyscript/core/src/stdlib/pyscript/workers.py +43 -0
  13. js/node_modules/flatted/python/flatted.py +149 -0
  14. js/node_modules/flatted/python/test.py +63 -0
  15. js/node_modules/polyscript/python/mip.py +288 -0
  16. reactpy_django/__init__.py +32 -0
  17. reactpy_django/apps.py +11 -0
  18. reactpy_django/checks.py +558 -0
  19. reactpy_django/clean.py +141 -0
  20. reactpy_django/components.py +284 -0
  21. reactpy_django/config.py +128 -0
  22. reactpy_django/database.py +31 -0
  23. reactpy_django/decorators.py +65 -0
  24. reactpy_django/exceptions.py +34 -0
  25. reactpy_django/hooks.py +469 -0
  26. reactpy_django/html.py +3 -0
  27. reactpy_django/http/__init__.py +0 -0
  28. reactpy_django/http/urls.py +18 -0
  29. reactpy_django/http/views.py +62 -0
  30. reactpy_django/management/__init__.py +0 -0
  31. reactpy_django/management/commands/__init__.py +0 -0
  32. reactpy_django/management/commands/clean_reactpy.py +37 -0
  33. reactpy_django/migrations/0001_initial.py +25 -0
  34. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  35. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  36. reactpy_django/migrations/0004_config.py +27 -0
  37. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  38. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  39. reactpy_django/migrations/__init__.py +0 -0
  40. reactpy_django/models.py +48 -0
  41. reactpy_django/py.typed +1 -0
  42. reactpy_django/pyscript/__init__.py +0 -0
  43. reactpy_django/pyscript/component_template.py +26 -0
  44. reactpy_django/pyscript/layout_handler.py +139 -0
  45. reactpy_django/router/__init__.py +5 -0
  46. reactpy_django/router/converters.py +8 -0
  47. reactpy_django/router/resolvers.py +22 -0
  48. reactpy_django/static/reactpy_django/client.js +1 -0
  49. reactpy_django/static/reactpy_django/morphdom/morphdom-esm.js +769 -0
  50. reactpy_django/static/reactpy_django/morphdom/morphdom-factory.js +705 -0
  51. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.js +777 -0
  52. reactpy_django/static/reactpy_django/morphdom/morphdom-umd.min.js +1 -0
  53. reactpy_django/static/reactpy_django/morphdom/morphdom.js +771 -0
  54. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js +2 -0
  55. reactpy_django/static/reactpy_django/pyscript/codemirror-BEtcgaoQ.js.map +1 -0
  56. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js +2 -0
  57. reactpy_django/static/reactpy_django/pyscript/codemirror_commands-DDxffOmd.js.map +1 -0
  58. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js +2 -0
  59. reactpy_django/static/reactpy_django/pyscript/codemirror_lang-python-CnWnFqxD.js.map +1 -0
  60. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js +2 -0
  61. reactpy_django/static/reactpy_django/pyscript/codemirror_language-CjmvX4ix.js.map +1 -0
  62. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js +2 -0
  63. reactpy_django/static/reactpy_django/pyscript/codemirror_state-D1qTXrff.js.map +1 -0
  64. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js +2 -0
  65. reactpy_django/static/reactpy_django/pyscript/codemirror_view-DVb8uYMr.js.map +1 -0
  66. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js +2 -0
  67. reactpy_django/static/reactpy_django/pyscript/core-CjO3FOKB.js.map +1 -0
  68. reactpy_django/static/reactpy_django/pyscript/core.css +1 -0
  69. reactpy_django/static/reactpy_django/pyscript/core.js +2 -0
  70. reactpy_django/static/reactpy_django/pyscript/core.js.map +1 -0
  71. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js +2 -0
  72. reactpy_django/static/reactpy_django/pyscript/deprecations-manager-pFtn19mE.js.map +1 -0
  73. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js +2 -0
  74. reactpy_django/static/reactpy_django/pyscript/error-tq-z48YI.js.map +1 -0
  75. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js +2 -0
  76. reactpy_django/static/reactpy_django/pyscript/index-S1Do43bx.js.map +1 -0
  77. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js +2 -0
  78. reactpy_django/static/reactpy_django/pyscript/mpy-DovD7Qjy.js.map +1 -0
  79. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js +2 -0
  80. reactpy_django/static/reactpy_django/pyscript/py-BUsUWVJg.js.map +1 -0
  81. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js +2 -0
  82. reactpy_django/static/reactpy_django/pyscript/py-editor-CeySmmer.js.map +1 -0
  83. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js +2 -0
  84. reactpy_django/static/reactpy_django/pyscript/py-terminal-CH_wV7wQ.js.map +1 -0
  85. reactpy_django/static/reactpy_django/pyscript/storage.js +2 -0
  86. reactpy_django/static/reactpy_django/pyscript/storage.js.map +1 -0
  87. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js +3 -0
  88. reactpy_django/static/reactpy_django/pyscript/toml-CvAfdf9_.js.map +1 -0
  89. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js +3 -0
  90. reactpy_django/static/reactpy_django/pyscript/toml-DiUM0_qs.js.map +1 -0
  91. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js +2 -0
  92. reactpy_django/static/reactpy_django/pyscript/xterm-BY7uk_OU.js.map +1 -0
  93. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js +2 -0
  94. reactpy_django/static/reactpy_django/pyscript/xterm-readline-CZfBw7ic.js.map +1 -0
  95. reactpy_django/static/reactpy_django/pyscript/xterm.css +7 -0
  96. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js +2 -0
  97. reactpy_django/static/reactpy_django/pyscript/xterm_addon-fit--gyF3PcZ.js.map +1 -0
  98. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js +2 -0
  99. reactpy_django/static/reactpy_django/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +1 -0
  100. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js +2 -0
  101. reactpy_django/static/reactpy_django/pyscript/zip-DrwYHuF9.js.map +1 -0
  102. reactpy_django/static/reactpy_django/pyscript-custom.css +3 -0
  103. reactpy_django/static/reactpy_django/pyscript-hide-debug.css +3 -0
  104. reactpy_django/templates/reactpy/component.html +27 -0
  105. reactpy_django/templates/reactpy/pyscript_component.html +2 -0
  106. reactpy_django/templates/reactpy/pyscript_setup.html +8 -0
  107. reactpy_django/templatetags/__init__.py +0 -0
  108. reactpy_django/templatetags/reactpy.py +250 -0
  109. reactpy_django/types.py +81 -0
  110. reactpy_django/utils.py +560 -0
  111. reactpy_django/websocket/__init__.py +0 -0
  112. reactpy_django/websocket/consumer.py +227 -0
  113. reactpy_django/websocket/paths.py +15 -0
  114. reactpy_django-5.0.0.dist-info/LICENSE.md +9 -0
  115. reactpy_django-5.0.0.dist-info/METADATA +152 -0
  116. reactpy_django-5.0.0.dist-info/RECORD +118 -0
  117. reactpy_django-5.0.0.dist-info/WHEEL +5 -0
  118. reactpy_django-5.0.0.dist-info/top_level.txt +2 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ from reactpy.core.vdom import make_vdom_constructor
2
+
3
+ pyscript = make_vdom_constructor("py-script")
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
+ ]