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.
- js/node_modules/flatted/python/flatted.py +149 -0
- js/node_modules/flatted/python/test.py +63 -0
- reactpy_django/__init__.py +28 -0
- reactpy_django/apps.py +11 -0
- reactpy_django/checks.py +542 -0
- reactpy_django/clean.py +141 -0
- reactpy_django/components.py +286 -0
- reactpy_django/config.py +135 -0
- reactpy_django/database.py +31 -0
- reactpy_django/decorators.py +101 -0
- reactpy_django/exceptions.py +34 -0
- reactpy_django/hooks.py +496 -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/router/__init__.py +5 -0
- reactpy_django/router/converters.py +7 -0
- reactpy_django/router/resolvers.py +58 -0
- reactpy_django/static/reactpy_django/client.js +1630 -0
- reactpy_django/templates/reactpy/component.html +27 -0
- reactpy_django/templatetags/__init__.py +0 -0
- reactpy_django/templatetags/reactpy.py +221 -0
- reactpy_django/types.py +121 -0
- reactpy_django/utils.py +384 -0
- reactpy_django/websocket/__init__.py +0 -0
- reactpy_django/websocket/consumer.py +218 -0
- reactpy_django/websocket/paths.py +17 -0
- reactpy_django-3.8.1.dist-info/LICENSE.md +9 -0
- reactpy_django-3.8.1.dist-info/METADATA +152 -0
- reactpy_django-3.8.1.dist-info/RECORD +44 -0
- reactpy_django-3.8.1.dist-info/WHEEL +6 -0
- 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)
|
reactpy_django/types.py
ADDED
|
@@ -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
|
+
...
|