reactpy_django 3.8.1__py2.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 (44) hide show
  1. js/node_modules/flatted/python/flatted.py +149 -0
  2. js/node_modules/flatted/python/test.py +63 -0
  3. reactpy_django/__init__.py +28 -0
  4. reactpy_django/apps.py +11 -0
  5. reactpy_django/checks.py +542 -0
  6. reactpy_django/clean.py +141 -0
  7. reactpy_django/components.py +286 -0
  8. reactpy_django/config.py +135 -0
  9. reactpy_django/database.py +31 -0
  10. reactpy_django/decorators.py +101 -0
  11. reactpy_django/exceptions.py +34 -0
  12. reactpy_django/hooks.py +496 -0
  13. reactpy_django/http/__init__.py +0 -0
  14. reactpy_django/http/urls.py +18 -0
  15. reactpy_django/http/views.py +62 -0
  16. reactpy_django/management/__init__.py +0 -0
  17. reactpy_django/management/commands/__init__.py +0 -0
  18. reactpy_django/management/commands/clean_reactpy.py +37 -0
  19. reactpy_django/migrations/0001_initial.py +25 -0
  20. reactpy_django/migrations/0002_rename_created_at_componentparams_last_accessed.py +17 -0
  21. reactpy_django/migrations/0003_componentsession_delete_componentparams.py +28 -0
  22. reactpy_django/migrations/0004_config.py +27 -0
  23. reactpy_django/migrations/0005_alter_componentsession_last_accessed.py +17 -0
  24. reactpy_django/migrations/0006_userdatamodel.py +28 -0
  25. reactpy_django/migrations/__init__.py +0 -0
  26. reactpy_django/models.py +48 -0
  27. reactpy_django/py.typed +1 -0
  28. reactpy_django/router/__init__.py +5 -0
  29. reactpy_django/router/converters.py +7 -0
  30. reactpy_django/router/resolvers.py +58 -0
  31. reactpy_django/static/reactpy_django/client.js +1630 -0
  32. reactpy_django/templates/reactpy/component.html +27 -0
  33. reactpy_django/templatetags/__init__.py +0 -0
  34. reactpy_django/templatetags/reactpy.py +221 -0
  35. reactpy_django/types.py +121 -0
  36. reactpy_django/utils.py +384 -0
  37. reactpy_django/websocket/__init__.py +0 -0
  38. reactpy_django/websocket/consumer.py +218 -0
  39. reactpy_django/websocket/paths.py +17 -0
  40. reactpy_django-3.8.1.dist-info/LICENSE.md +9 -0
  41. reactpy_django-3.8.1.dist-info/METADATA +152 -0
  42. reactpy_django-3.8.1.dist-info/RECORD +44 -0
  43. reactpy_django-3.8.1.dist-info/WHEEL +6 -0
  44. reactpy_django-3.8.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,27 @@
1
+ {% load static %}
2
+
3
+ {% if reactpy_failure and reactpy_debug_mode %}
4
+ <pre>{% firstof reactpy_error "UnknownError" %}: "{% firstof reactpy_dotted_path "UnknownPath" %}"</pre>
5
+ {% endif %}
6
+
7
+ {% if not reactpy_failure %}
8
+ {% if reactpy_class %}<div id="{{reactpy_uuid}}" class="{{reactpy_class}}"></div>{% endif %}
9
+ {% if not reactpy_class %}<div id="{{reactpy_uuid}}"></div>{% endif %}
10
+ {% if reactpy_prerender_html %}<div id="{{reactpy_uuid}}-prerender">{{reactpy_prerender_html|safe}}</div>{% endif %}
11
+ {% if reactpy_offline_html %}<div id="{{reactpy_uuid}}-offline" hidden>{{reactpy_offline_html|safe}}</div>{% endif %}
12
+ <script type="module" crossorigin="anonymous">
13
+ import { mountComponent } from "{% static 'reactpy_django/client.js' %}";
14
+ const mountElement = document.getElementById("{{reactpy_uuid}}");
15
+ mountComponent(
16
+ mountElement,
17
+ "{{reactpy_host}}",
18
+ "{{reactpy_url_prefix}}",
19
+ "{{reactpy_component_path}}",
20
+ "{{reactpy_resolved_web_modules_path}}",
21
+ Number("{{reactpy_reconnect_interval}}"),
22
+ Number("{{reactpy_reconnect_max_interval}}"),
23
+ Number("{{reactpy_reconnect_max_retries}}"),
24
+ Number("{{reactpy_reconnect_backoff_multiplier}}"),
25
+ );
26
+ </script>
27
+ {% endif %}
File without changes
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ from logging import getLogger
4
+ from uuid import uuid4
5
+
6
+ import dill as pickle
7
+ from django import template
8
+ from django.http import HttpRequest
9
+ from django.urls import NoReverseMatch, reverse
10
+ from reactpy.backend.hooks import ConnectionContext
11
+ from reactpy.backend.types import Connection, Location
12
+ from reactpy.core.types import ComponentConstructor
13
+ from reactpy.utils import vdom_to_html
14
+
15
+ from reactpy_django import config, models
16
+ from reactpy_django.exceptions import (
17
+ ComponentCarrierError,
18
+ ComponentDoesNotExistError,
19
+ ComponentParamError,
20
+ InvalidHostError,
21
+ OfflineComponentMissing,
22
+ )
23
+ from reactpy_django.types import ComponentParams
24
+ from reactpy_django.utils import SyncLayout, strtobool, validate_component_args
25
+
26
+ try:
27
+ RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
28
+ except NoReverseMatch:
29
+ RESOLVED_WEB_MODULES_PATH = ""
30
+ register = template.Library()
31
+ _logger = getLogger(__name__)
32
+
33
+
34
+ @register.inclusion_tag("reactpy/component.html", takes_context=True)
35
+ def component(
36
+ context: template.RequestContext,
37
+ dotted_path: str,
38
+ *args,
39
+ host: str | None = None,
40
+ prerender: str = str(config.REACTPY_PRERENDER),
41
+ offline: str = "",
42
+ **kwargs,
43
+ ):
44
+ """This tag is used to embed an existing ReactPy component into your HTML template.
45
+
46
+ Args:
47
+ dotted_path: The dotted path to the component to render.
48
+ *args: The positional arguments to provide to the component.
49
+
50
+ Keyword Args:
51
+ class: The HTML class to apply to the top-level component div.
52
+ key: Force the component's root node to use a specific key value. Using \
53
+ key within a template tag is effectively useless.
54
+ host: The host to use for the ReactPy connections. If set to `None`, \
55
+ the host will be automatically configured. \
56
+ Example values include: `localhost:8000`, `example.com`, `example.com/subdir`
57
+ prerender: Configures whether to pre-render this component, which \
58
+ enables SEO compatibility and reduces perceived latency.
59
+ offline: The dotted path to the component to render when the client is offline.
60
+ **kwargs: The keyword arguments to provide to the component.
61
+
62
+ Example ::
63
+
64
+ {% load reactpy %}
65
+ <!DOCTYPE html>
66
+ <html>
67
+ <body>
68
+ {% component "example_project.my_app.components.hello_world" recipient="World" %}
69
+ </body>
70
+ </html>
71
+ """
72
+ request: HttpRequest | None = context.get("request")
73
+ perceived_host = (request.get_host() if request else "").strip("/")
74
+ host = (
75
+ host
76
+ or (next(config.REACTPY_DEFAULT_HOSTS) if config.REACTPY_DEFAULT_HOSTS else "")
77
+ ).strip("/")
78
+ is_local = not host or host.startswith(perceived_host)
79
+ uuid = str(uuid4())
80
+ class_ = kwargs.pop("class", "")
81
+ has_args = bool(args or kwargs)
82
+ user_component: ComponentConstructor | None = None
83
+ _prerender_html = ""
84
+ _offline_html = ""
85
+
86
+ # Validate the host
87
+ if host and config.REACTPY_DEBUG_MODE:
88
+ try:
89
+ validate_host(host)
90
+ except InvalidHostError as e:
91
+ return failure_context(dotted_path, e)
92
+
93
+ # Fetch the component
94
+ if is_local:
95
+ user_component = config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path)
96
+ if not user_component:
97
+ msg = f"Component '{dotted_path}' is not registered as a root component. "
98
+ _logger.error(msg)
99
+ return failure_context(dotted_path, ComponentDoesNotExistError(msg))
100
+
101
+ # Validate the component args & kwargs
102
+ if is_local and config.REACTPY_DEBUG_MODE:
103
+ try:
104
+ validate_component_args(user_component, *args, **kwargs)
105
+ except ComponentParamError as e:
106
+ _logger.error(str(e))
107
+ return failure_context(dotted_path, e)
108
+
109
+ # Store args & kwargs in the database (fetched by our websocket later)
110
+ if has_args:
111
+ try:
112
+ save_component_params(args, kwargs, uuid)
113
+ except Exception as e:
114
+ _logger.exception(
115
+ "An unknown error has occurred while saving component params for '%s'.",
116
+ dotted_path,
117
+ )
118
+ return failure_context(dotted_path, e)
119
+
120
+ # Pre-render the component, if requested
121
+ if strtobool(prerender):
122
+ if not is_local:
123
+ msg = "Cannot pre-render non-local components."
124
+ _logger.error(msg)
125
+ return failure_context(dotted_path, ComponentDoesNotExistError(msg))
126
+ if not user_component:
127
+ msg = "Cannot pre-render component that is not registered."
128
+ _logger.error(msg)
129
+ return failure_context(dotted_path, ComponentDoesNotExistError(msg))
130
+ if not request:
131
+ msg = (
132
+ "Cannot pre-render component without a HTTP request. Are you missing the "
133
+ "request context processor in settings.py:TEMPLATES['OPTIONS']['context_processors']?"
134
+ )
135
+ _logger.error(msg)
136
+ return failure_context(dotted_path, ComponentCarrierError(msg))
137
+ _prerender_html = prerender_component(
138
+ user_component, args, kwargs, uuid, request
139
+ )
140
+
141
+ # Fetch the offline component's HTML, if requested
142
+ if offline:
143
+ offline_component = config.REACTPY_REGISTERED_COMPONENTS.get(offline)
144
+ if not offline_component:
145
+ msg = f"Cannot render offline component '{offline}'. It is not registered as a component."
146
+ _logger.error(msg)
147
+ return failure_context(dotted_path, OfflineComponentMissing(msg))
148
+ if not request:
149
+ msg = (
150
+ "Cannot render an offline component without a HTTP request. Are you missing the "
151
+ "request context processor in settings.py:TEMPLATES['OPTIONS']['context_processors']?"
152
+ )
153
+ _logger.error(msg)
154
+ return failure_context(dotted_path, ComponentCarrierError(msg))
155
+ _offline_html = prerender_component(offline_component, [], {}, uuid, request)
156
+
157
+ # Return the template rendering context
158
+ return {
159
+ "reactpy_class": class_,
160
+ "reactpy_uuid": uuid,
161
+ "reactpy_host": host or perceived_host,
162
+ "reactpy_url_prefix": config.REACTPY_URL_PREFIX,
163
+ "reactpy_component_path": f"{dotted_path}/{uuid}/{int(has_args)}/",
164
+ "reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
165
+ "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL,
166
+ "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL,
167
+ "reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER,
168
+ "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES,
169
+ "reactpy_prerender_html": _prerender_html,
170
+ "reactpy_offline_html": _offline_html,
171
+ }
172
+
173
+
174
+ def failure_context(dotted_path: str, error: Exception):
175
+ return {
176
+ "reactpy_failure": True,
177
+ "reactpy_debug_mode": config.REACTPY_DEBUG_MODE,
178
+ "reactpy_dotted_path": dotted_path,
179
+ "reactpy_error": type(error).__name__,
180
+ }
181
+
182
+
183
+ def save_component_params(args, kwargs, uuid):
184
+ params = ComponentParams(args, kwargs)
185
+ model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
186
+ model.full_clean()
187
+ model.save()
188
+
189
+
190
+ def validate_host(host: str):
191
+ if "://" in host:
192
+ protocol = host.split("://")[0]
193
+ msg = (
194
+ f"Invalid host provided to component. Contains a protocol '{protocol}://'."
195
+ )
196
+ _logger.error(msg)
197
+ raise InvalidHostError(msg)
198
+
199
+
200
+ def prerender_component(
201
+ user_component: ComponentConstructor, args, kwargs, uuid, request: HttpRequest
202
+ ):
203
+ search = request.GET.urlencode()
204
+ scope = getattr(request, "scope", {})
205
+ scope["reactpy"] = {"id": str(uuid)}
206
+
207
+ with SyncLayout(
208
+ ConnectionContext(
209
+ user_component(*args, **kwargs),
210
+ value=Connection(
211
+ scope=scope,
212
+ location=Location(
213
+ pathname=request.path, search=f"?{search}" if search else ""
214
+ ),
215
+ carrier=request,
216
+ ),
217
+ )
218
+ ) as layout:
219
+ vdom_tree = layout.render()["model"]
220
+
221
+ return vdom_to_html(vdom_tree)
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Callable,
8
+ Generic,
9
+ MutableMapping,
10
+ NamedTuple,
11
+ Protocol,
12
+ Sequence,
13
+ TypeVar,
14
+ Union,
15
+ )
16
+
17
+ from django.http import HttpRequest
18
+ from reactpy.types import Connection
19
+ from typing_extensions import ParamSpec
20
+
21
+ if TYPE_CHECKING:
22
+ from reactpy_django.websocket.consumer import ReactpyAsyncWebsocketConsumer
23
+
24
+
25
+ FuncParams = ParamSpec("FuncParams")
26
+ Inferred = TypeVar("Inferred")
27
+ ConnectionType = Connection[Union["ReactpyAsyncWebsocketConsumer", HttpRequest]]
28
+
29
+
30
+ @dataclass
31
+ class Query(Generic[Inferred]):
32
+ """Queries generated by the `use_query` hook."""
33
+
34
+ data: Inferred
35
+ loading: bool
36
+ error: Exception | None
37
+ refetch: Callable[[], None]
38
+
39
+
40
+ @dataclass
41
+ class Mutation(Generic[FuncParams]):
42
+ """Mutations generated by the `use_mutation` hook."""
43
+
44
+ execute: Callable[FuncParams, None]
45
+ loading: bool
46
+ error: Exception | None
47
+ reset: Callable[[], None]
48
+
49
+ def __call__(self, *args: FuncParams.args, **kwargs: FuncParams.kwargs) -> None:
50
+ """Execute the mutation."""
51
+ self.execute(*args, **kwargs)
52
+
53
+
54
+ class AsyncPostprocessor(Protocol):
55
+ async def __call__(self, data: Any) -> Any:
56
+ ...
57
+
58
+
59
+ class SyncPostprocessor(Protocol):
60
+ def __call__(self, data: Any) -> Any:
61
+ ...
62
+
63
+
64
+ @dataclass
65
+ class QueryOptions:
66
+ """Configuration options that can be provided to `use_query`."""
67
+
68
+ from reactpy_django.config import REACTPY_DEFAULT_QUERY_POSTPROCESSOR
69
+
70
+ postprocessor: AsyncPostprocessor | SyncPostprocessor | None = (
71
+ REACTPY_DEFAULT_QUERY_POSTPROCESSOR
72
+ )
73
+ """A callable that can modify the query `data` after the query has been executed.
74
+
75
+ The first argument of postprocessor must be the query `data`. All proceeding arguments
76
+ are optional `postprocessor_kwargs` (see below). This postprocessor function must return
77
+ the modified `data`.
78
+
79
+ If unset, REACTPY_DEFAULT_QUERY_POSTPROCESSOR is used.
80
+
81
+ ReactPy's default django_query_postprocessor prevents Django's lazy query execution, and
82
+ additionally can be configured via `postprocessor_kwargs` to recursively fetch
83
+ `many_to_many` and `many_to_one` fields."""
84
+
85
+ postprocessor_kwargs: MutableMapping[str, Any] = field(default_factory=lambda: {})
86
+ """Keyworded arguments directly passed into the `postprocessor` for configuration."""
87
+
88
+ thread_sensitive: bool = True
89
+ """Whether to run the query in thread-sensitive mode. This setting only applies to sync query functions."""
90
+
91
+
92
+ @dataclass
93
+ class MutationOptions:
94
+ """Configuration options that can be provided to `use_mutation`."""
95
+
96
+ thread_sensitive: bool = True
97
+ """Whether to run the mutation in thread-sensitive mode. This setting only applies to sync mutation functions."""
98
+
99
+
100
+ @dataclass
101
+ class ComponentParams:
102
+ """Container used for serializing component parameters.
103
+ This dataclass is pickled & stored in the database, then unpickled when needed."""
104
+
105
+ args: Sequence
106
+ kwargs: MutableMapping[str, Any]
107
+
108
+
109
+ class UserData(NamedTuple):
110
+ query: Query[dict | None]
111
+ mutation: Mutation[dict]
112
+
113
+
114
+ class AsyncMessageReceiver(Protocol):
115
+ async def __call__(self, message: dict) -> None:
116
+ ...
117
+
118
+
119
+ class AsyncMessageSender(Protocol):
120
+ async def __call__(self, message: dict) -> None:
121
+ ...