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,560 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import inspect
5
+ import json
6
+ import logging
7
+ import os
8
+ import re
9
+ import textwrap
10
+ from asyncio import iscoroutinefunction
11
+ from copy import deepcopy
12
+ from fnmatch import fnmatch
13
+ from importlib import import_module
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Mapping, Sequence
16
+ from uuid import UUID, uuid4
17
+
18
+ import dill
19
+ import jsonpointer
20
+ import orjson
21
+ import reactpy
22
+ from asgiref.sync import async_to_sync
23
+ from channels.db import database_sync_to_async
24
+ from django.db.models import ManyToManyField, ManyToOneRel, prefetch_related_objects
25
+ from django.db.models.base import Model
26
+ from django.db.models.query import QuerySet
27
+ from django.http import HttpRequest, HttpResponse
28
+ from django.template import engines
29
+ from django.templatetags.static import static
30
+ from django.utils.encoding import smart_str
31
+ from django.views import View
32
+ from reactpy import vdom_to_html
33
+ from reactpy.backend.hooks import ConnectionContext
34
+ from reactpy.backend.types import Connection, Location
35
+ from reactpy.core.layout import Layout
36
+ from reactpy.types import ComponentConstructor
37
+
38
+ from reactpy_django.exceptions import (
39
+ ComponentDoesNotExistError,
40
+ ComponentParamError,
41
+ InvalidHostError,
42
+ ViewDoesNotExistError,
43
+ )
44
+
45
+ _logger = logging.getLogger(__name__)
46
+ _TAG_PATTERN = r"(?P<tag>component)"
47
+ _PATH_PATTERN = r"""(?P<path>"[^"'\s]+"|'[^"'\s]+')"""
48
+ _OFFLINE_KWARG_PATTERN = (
49
+ rf"""(\s*offline\s*=\s*{_PATH_PATTERN.replace(r"<path>", r"<offline_path>")})"""
50
+ )
51
+ _GENERIC_KWARG_PATTERN = r"""(\s*.*?)"""
52
+ COMMENT_REGEX = re.compile(r"<!--[\s\S]*?-->")
53
+ COMPONENT_REGEX = re.compile(
54
+ r"{%\s*"
55
+ + _TAG_PATTERN
56
+ + r"\s*"
57
+ + _PATH_PATTERN
58
+ + rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?"
59
+ + r"\s*%}"
60
+ )
61
+ PYSCRIPT_COMPONENT_TEMPLATE = (
62
+ Path(__file__).parent / "pyscript" / "component_template.py"
63
+ ).read_text(encoding="utf-8")
64
+ PYSCRIPT_LAYOUT_HANDLER = (
65
+ Path(__file__).parent / "pyscript" / "layout_handler.py"
66
+ ).read_text(encoding="utf-8")
67
+ PYSCRIPT_DEFAULT_CONFIG: dict[str, Any] = {}
68
+
69
+
70
+ async def render_view(
71
+ view: Callable | View,
72
+ request: HttpRequest,
73
+ args: Sequence,
74
+ kwargs: dict,
75
+ ) -> HttpResponse:
76
+ """Ingests a Django view (class or function) and returns an HTTP response object."""
77
+ # Convert class-based view to function-based view
78
+ if getattr(view, "as_view", None):
79
+ view = view.as_view() # type: ignore[union-attr]
80
+
81
+ # Async function view
82
+ if iscoroutinefunction(view):
83
+ response = await view(request, *args, **kwargs)
84
+
85
+ # Sync function view
86
+ else:
87
+ response = await database_sync_to_async(view)(request, *args, **kwargs)
88
+
89
+ # TemplateView
90
+ if getattr(response, "render", None):
91
+ response = await database_sync_to_async(response.render)()
92
+
93
+ return response
94
+
95
+
96
+ def register_component(component: ComponentConstructor | str):
97
+ """Adds a component to the list of known registered components.
98
+
99
+ Args:
100
+ component: The component to register. Can be a component function or dotted path to a component.
101
+
102
+ """
103
+ from reactpy_django.config import (
104
+ REACTPY_FAILED_COMPONENTS,
105
+ REACTPY_REGISTERED_COMPONENTS,
106
+ )
107
+
108
+ dotted_path = (
109
+ component if isinstance(component, str) else generate_obj_name(component)
110
+ )
111
+ try:
112
+ REACTPY_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path)
113
+ except AttributeError as e:
114
+ REACTPY_FAILED_COMPONENTS.add(dotted_path)
115
+ raise ComponentDoesNotExistError(
116
+ f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}."
117
+ ) from e
118
+
119
+
120
+ def register_iframe(view: Callable | View | str):
121
+ """Registers a view to be used as an iframe component.
122
+
123
+ Args:
124
+ view: The view to register. Can be a function or class based view, or a dotted path to a view.
125
+ """
126
+ from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS
127
+
128
+ if hasattr(view, "view_class"):
129
+ view = view.view_class
130
+ dotted_path = view if isinstance(view, str) else generate_obj_name(view)
131
+ try:
132
+ REACTPY_REGISTERED_IFRAME_VIEWS[dotted_path] = import_dotted_path(dotted_path)
133
+ except AttributeError as e:
134
+ raise ViewDoesNotExistError(
135
+ f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}."
136
+ ) from e
137
+
138
+
139
+ def import_dotted_path(dotted_path: str) -> Callable:
140
+ """Imports a dotted path and returns the callable."""
141
+ module_name, component_name = dotted_path.rsplit(".", 1)
142
+
143
+ try:
144
+ module = import_module(module_name)
145
+ except ImportError as error:
146
+ raise RuntimeError(
147
+ f"Failed to import {module_name!r} while loading {component_name!r}"
148
+ ) from error
149
+
150
+ return getattr(module, component_name)
151
+
152
+
153
+ class RootComponentFinder:
154
+ """Searches Django templates to find and register all root components.
155
+ This should only be `run` once on startup to maintain synchronization during mulitprocessing.
156
+ """
157
+
158
+ def run(self):
159
+ """Registers all ReactPy components found within Django templates."""
160
+ # Get all template folder paths
161
+ paths = self.get_paths()
162
+ # Get all HTML template files
163
+ templates = self.get_templates(paths)
164
+ # Get all components
165
+ components = self.get_components(templates)
166
+ # Register all components
167
+ self.register_components(components)
168
+
169
+ def get_loaders(self):
170
+ """Obtains currently configured template loaders."""
171
+ template_source_loaders = []
172
+ for e in engines.all():
173
+ if hasattr(e, "engine"):
174
+ template_source_loaders.extend(
175
+ e.engine.get_template_loaders(e.engine.loaders)
176
+ )
177
+ loaders = []
178
+ for loader in template_source_loaders:
179
+ if hasattr(loader, "loaders"):
180
+ loaders.extend(loader.loaders)
181
+ else:
182
+ loaders.append(loader)
183
+ return loaders
184
+
185
+ def get_paths(self) -> set[str]:
186
+ """Obtains a set of all template directories."""
187
+ paths: set[str] = set()
188
+ for loader in self.get_loaders():
189
+ with contextlib.suppress(ImportError, AttributeError, TypeError):
190
+ module = import_module(loader.__module__)
191
+ get_template_sources = getattr(module, "get_template_sources", None)
192
+ if get_template_sources is None:
193
+ get_template_sources = loader.get_template_sources
194
+ paths.update(smart_str(origin) for origin in get_template_sources(""))
195
+ return paths
196
+
197
+ def get_templates(self, paths: set[str]) -> set[str]:
198
+ """Obtains a set of all HTML template paths."""
199
+ extensions = [".html"]
200
+ templates: set[str] = set()
201
+ for path in paths:
202
+ for root, _, files in os.walk(path, followlinks=False):
203
+ templates.update(
204
+ os.path.join(root, name)
205
+ for name in files
206
+ if not name.startswith(".")
207
+ and any(fnmatch(name, f"*{glob}") for glob in extensions)
208
+ )
209
+
210
+ return templates
211
+
212
+ def get_components(self, templates: set[str]) -> set[str]:
213
+ """Obtains a set of all ReactPy components by parsing HTML templates."""
214
+ components: set[str] = set()
215
+ for template in templates:
216
+ with contextlib.suppress(Exception):
217
+ with open(template, "r", encoding="utf-8") as template_file:
218
+ clean_template = COMMENT_REGEX.sub("", template_file.read())
219
+ regex_iterable = COMPONENT_REGEX.finditer(clean_template)
220
+ new_components: list[str] = []
221
+ for match in regex_iterable:
222
+ new_components.append(
223
+ match.group("path").replace('"', "").replace("'", "")
224
+ )
225
+ offline_path = match.group("offline_path")
226
+ if offline_path:
227
+ new_components.append(
228
+ offline_path.replace('"', "").replace("'", "")
229
+ )
230
+ components.update(new_components)
231
+ if not components:
232
+ _logger.warning(
233
+ "\033[93m"
234
+ "ReactPy did not find any components! "
235
+ "You are either not using any ReactPy components, "
236
+ "using the template tag incorrectly, "
237
+ "or your HTML templates are not registered with Django."
238
+ "\033[0m"
239
+ )
240
+ return components
241
+
242
+ def register_components(self, components: set[str]) -> None:
243
+ """Registers all ReactPy components in an iterable."""
244
+ if components:
245
+ _logger.debug("Auto-detected ReactPy root components:")
246
+ for component in components:
247
+ try:
248
+ _logger.debug("\t+ %s", component)
249
+ register_component(component)
250
+ except Exception:
251
+ _logger.exception(
252
+ "\033[91m"
253
+ "ReactPy failed to register component '%s'!\n"
254
+ "This component path may not be valid, "
255
+ "or an exception may have occurred while importing.\n"
256
+ "See the traceback below for more information."
257
+ "\033[0m",
258
+ component,
259
+ )
260
+
261
+
262
+ def generate_obj_name(obj: Any) -> str:
263
+ """Makes a best effort to create a name for an object.
264
+ Useful for JSON serialization of Python objects."""
265
+
266
+ # First attempt: Dunder methods
267
+ if hasattr(obj, "__module__"):
268
+ if hasattr(obj, "__name__"):
269
+ return f"{obj.__module__}.{obj.__name__}"
270
+ if hasattr(obj, "__class__") and hasattr(obj.__class__, "__name__"):
271
+ return f"{obj.__module__}.{obj.__class__.__name__}"
272
+
273
+ # Second attempt: String representation
274
+ with contextlib.suppress(Exception):
275
+ return str(obj)
276
+
277
+ # Fallback: Empty string
278
+ return ""
279
+
280
+
281
+ def django_query_postprocessor(
282
+ data: QuerySet | Model, many_to_many: bool = True, many_to_one: bool = True
283
+ ) -> QuerySet | Model:
284
+ """Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily.
285
+
286
+ Behavior can be modified through `postprocessor_kwargs` within your `use_query` hook.
287
+
288
+ Args:
289
+ data: The `Model` or `QuerySet` to recursively fetch fields from.
290
+
291
+ Keyword Args:
292
+ many_to_many: Whether or not to recursively fetch `ManyToManyField` relationships.
293
+ many_to_one: Whether or not to recursively fetch `ForeignKey` relationships.
294
+
295
+ Returns:
296
+ The `Model` or `QuerySet` with all fields fetched.
297
+ """
298
+
299
+ # `QuerySet`, which is an iterable containing `Model`/`QuerySet` objects.
300
+ if isinstance(data, QuerySet):
301
+ for model in data:
302
+ django_query_postprocessor(
303
+ model,
304
+ many_to_many=many_to_many,
305
+ many_to_one=many_to_one,
306
+ )
307
+
308
+ # `Model` instances
309
+ elif isinstance(data, Model):
310
+ prefetch_fields: list[str] = []
311
+ for field in data._meta.get_fields():
312
+ # Force the query to execute
313
+ getattr(data, field.name, None)
314
+
315
+ if many_to_one and type(field) == ManyToOneRel: # noqa: E721
316
+ prefetch_fields.append(field.related_name or f"{field.name}_set")
317
+
318
+ elif many_to_many and isinstance(field, ManyToManyField):
319
+ prefetch_fields.append(field.name)
320
+
321
+ if prefetch_fields:
322
+ prefetch_related_objects([data], *prefetch_fields)
323
+ for field_str in prefetch_fields:
324
+ django_query_postprocessor(
325
+ getattr(data, field_str).get_queryset(),
326
+ many_to_many=many_to_many,
327
+ many_to_one=many_to_one,
328
+ )
329
+
330
+ # Unrecognized type
331
+ else:
332
+ raise TypeError(
333
+ f"Django query postprocessor expected a Model or QuerySet, got {data!r}.\n"
334
+ "One of the following may have occurred:\n"
335
+ " - You are using a non-Django ORM.\n"
336
+ " - You are attempting to use `use_query` to fetch non-ORM data.\n\n"
337
+ "If these situations apply, you may want to disable the postprocessor."
338
+ )
339
+
340
+ return data
341
+
342
+
343
+ def validate_component_args(func, *args, **kwargs):
344
+ """
345
+ Validate whether a set of args/kwargs would work on the given component.
346
+
347
+ Raises `ComponentParamError` if the args/kwargs are invalid.
348
+ """
349
+ signature = inspect.signature(func)
350
+
351
+ try:
352
+ signature.bind(*args, **kwargs)
353
+ except TypeError as e:
354
+ name = generate_obj_name(func)
355
+ raise ComponentParamError(
356
+ f"Invalid args for '{name}'. {str(e).capitalize()}."
357
+ ) from e
358
+
359
+
360
+ def create_cache_key(*args):
361
+ """Creates a cache key string that starts with `reactpy_django` contains
362
+ all *args separated by `:`."""
363
+
364
+ if not args:
365
+ raise ValueError("At least one argument is required to create a cache key.")
366
+
367
+ return f"reactpy_django:{':'.join(str(arg) for arg in args)}"
368
+
369
+
370
+ class SyncLayout(Layout):
371
+ """Sync adapter for ReactPy's `Layout`. Allows it to be used in Django template tags.
372
+ This can be removed when Django supports async template tags.
373
+ """
374
+
375
+ def __enter__(self):
376
+ async_to_sync(self.__aenter__)()
377
+ return self
378
+
379
+ def __exit__(self, *_):
380
+ async_to_sync(self.__aexit__)(*_)
381
+
382
+ def render(self):
383
+ return async_to_sync(super().render)()
384
+
385
+
386
+ def get_pk(model):
387
+ """Returns the value of the primary key for a Django model."""
388
+ return getattr(model, model._meta.pk.name)
389
+
390
+
391
+ def strtobool(val):
392
+ """Convert a string representation of truth to true (1) or false (0).
393
+
394
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
395
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
396
+ 'val' is anything else.
397
+ """
398
+ val = val.lower()
399
+ if val in ("y", "yes", "t", "true", "on", "1"):
400
+ return 1
401
+ elif val in ("n", "no", "f", "false", "off", "0"):
402
+ return 0
403
+ else:
404
+ raise ValueError(f"invalid truth value {val}")
405
+
406
+
407
+ def prerender_component(
408
+ user_component: ComponentConstructor,
409
+ args: Sequence,
410
+ kwargs: Mapping,
411
+ uuid: str | UUID,
412
+ request: HttpRequest,
413
+ ) -> str:
414
+ """Prerenders a ReactPy component and returns the HTML string."""
415
+ search = request.GET.urlencode()
416
+ scope = getattr(request, "scope", {})
417
+ scope["reactpy"] = {"id": str(uuid)}
418
+
419
+ with SyncLayout(
420
+ ConnectionContext(
421
+ user_component(*args, **kwargs),
422
+ value=Connection(
423
+ scope=scope,
424
+ location=Location(
425
+ pathname=request.path, search=f"?{search}" if search else ""
426
+ ),
427
+ carrier=request,
428
+ ),
429
+ )
430
+ ) as layout:
431
+ vdom_tree = layout.render()["model"]
432
+
433
+ return vdom_to_html(vdom_tree)
434
+
435
+
436
+ def vdom_or_component_to_string(
437
+ vdom_or_component: Any, request: HttpRequest | None = None, uuid: str | None = None
438
+ ) -> str:
439
+ """Converts a VdomDict or component to an HTML string. If a string is provided instead, it will be
440
+ automatically returned."""
441
+ if isinstance(vdom_or_component, dict):
442
+ return vdom_to_html(vdom_or_component) # type: ignore
443
+
444
+ if hasattr(vdom_or_component, "render"):
445
+ if not request:
446
+ request = HttpRequest()
447
+ request.method = "GET"
448
+ if not uuid:
449
+ uuid = uuid4().hex
450
+ return prerender_component(vdom_or_component, [], {}, uuid, request)
451
+
452
+ if isinstance(vdom_or_component, str):
453
+ return vdom_or_component
454
+
455
+ raise ValueError(
456
+ f"Invalid type for vdom_or_component: {type(vdom_or_component)}. "
457
+ "Expected a VdomDict, component, or string."
458
+ )
459
+
460
+
461
+ def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str):
462
+ """Inserts the user's code into the PyScript template using pattern matching."""
463
+ from django.core.cache import caches
464
+
465
+ from reactpy_django.config import REACTPY_CACHE
466
+
467
+ # Create a valid PyScript executor by replacing the template values
468
+ executor = PYSCRIPT_COMPONENT_TEMPLATE.replace("UUID", uuid)
469
+ executor = executor.replace("return root()", f"return {root}()")
470
+
471
+ # Fetch the user's PyScript code
472
+ all_file_contents: list[str] = []
473
+ for file_path in file_paths:
474
+ # Try to get user code from cache
475
+ cache_key = create_cache_key("pyscript", file_path)
476
+ last_modified_time = os.stat(file_path).st_mtime
477
+ file_contents: str = caches[REACTPY_CACHE].get(
478
+ cache_key, version=int(last_modified_time)
479
+ )
480
+ if file_contents:
481
+ all_file_contents.append(file_contents)
482
+
483
+ # If not cached, read from file system
484
+ else:
485
+ file_contents = Path(file_path).read_text(encoding="utf-8").strip()
486
+ all_file_contents.append(file_contents)
487
+ caches[REACTPY_CACHE].set(
488
+ cache_key, file_contents, version=int(last_modified_time)
489
+ )
490
+
491
+ # Prepare the PyScript code block
492
+ user_code = "\n".join(all_file_contents) # Combine all user code
493
+ user_code = user_code.replace("\t", " ") # Normalize the text
494
+ user_code = textwrap.indent(user_code, " ") # Add indentation to match template
495
+
496
+ # Insert the user code into the PyScript template
497
+ return executor.replace(" def root(): ...", user_code)
498
+
499
+
500
+ def extend_pyscript_config(
501
+ extra_py: Sequence, extra_js: dict | str, config: dict | str
502
+ ) -> str:
503
+ """Extends ReactPy's default PyScript config with user provided values."""
504
+ # Lazily set up the initial config in to wait for Django's static file system
505
+ if not PYSCRIPT_DEFAULT_CONFIG:
506
+ PYSCRIPT_DEFAULT_CONFIG.update(
507
+ {
508
+ "packages": [
509
+ f"reactpy=={reactpy.__version__}",
510
+ f"jsonpointer=={jsonpointer.__version__}",
511
+ "ssl",
512
+ ],
513
+ "js_modules": {
514
+ "main": {
515
+ static("reactpy_django/morphdom/morphdom-esm.js"): "morphdom"
516
+ }
517
+ },
518
+ }
519
+ )
520
+
521
+ # Extend the Python dependency list
522
+ pyscript_config = deepcopy(PYSCRIPT_DEFAULT_CONFIG)
523
+ pyscript_config["packages"].extend(extra_py)
524
+
525
+ # Extend the JavaScript dependency list
526
+ if extra_js and isinstance(extra_js, str):
527
+ pyscript_config["js_modules"]["main"].update(json.loads(extra_js))
528
+ elif extra_js and isinstance(extra_js, dict):
529
+ pyscript_config["js_modules"]["main"].update(extra_py)
530
+
531
+ # Update the config
532
+ if config and isinstance(config, str):
533
+ pyscript_config.update(json.loads(config))
534
+ elif config and isinstance(config, dict):
535
+ pyscript_config.update(config)
536
+ return orjson.dumps(pyscript_config).decode("utf-8")
537
+
538
+
539
+ def save_component_params(args, kwargs, uuid) -> None:
540
+ """Saves the component parameters to the database.
541
+ This is used within our template tag in order to propogate
542
+ the parameters between the HTTP and WebSocket stack."""
543
+ from reactpy_django import models
544
+ from reactpy_django.types import ComponentParams
545
+
546
+ params = ComponentParams(args, kwargs)
547
+ model = models.ComponentSession(uuid=uuid, params=dill.dumps(params))
548
+ model.full_clean()
549
+ model.save()
550
+
551
+
552
+ def validate_host(host: str) -> None:
553
+ """Validates the host string to ensure it does not contain a protocol."""
554
+ if "://" in host:
555
+ protocol = host.split("://")[0]
556
+ msg = (
557
+ f"Invalid host provided to component. Contains a protocol '{protocol}://'."
558
+ )
559
+ _logger.error(msg)
560
+ raise InvalidHostError(msg)
File without changes