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,286 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, Callable, Sequence, Union, cast, overload
6
+ from urllib.parse import urlencode
7
+ from warnings import warn
8
+
9
+ from django.contrib.staticfiles.finders import find
10
+ from django.core.cache import caches
11
+ from django.http import HttpRequest
12
+ from django.urls import reverse
13
+ from django.views import View
14
+ from reactpy import component, hooks, html, utils
15
+ from reactpy.types import Key, VdomDict
16
+
17
+ from reactpy_django.exceptions import ViewNotRegisteredError
18
+ from reactpy_django.utils import generate_obj_name, import_module, render_view
19
+
20
+
21
+ # Type hints for:
22
+ # 1. example = view_to_component(my_view, ...)
23
+ # 2. @view_to_component
24
+ @overload
25
+ def view_to_component(
26
+ view: Callable | View | str,
27
+ compatibility: bool = False,
28
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
29
+ strict_parsing: bool = True,
30
+ ) -> Any:
31
+ ...
32
+
33
+
34
+ # Type hints for:
35
+ # 1. @view_to_component(...)
36
+ @overload
37
+ def view_to_component(
38
+ view: None = ...,
39
+ compatibility: bool = False,
40
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
41
+ strict_parsing: bool = True,
42
+ ) -> Callable[[Callable], Any]:
43
+ ...
44
+
45
+
46
+ def view_to_component(
47
+ view: Callable | View | str | None = None,
48
+ compatibility: bool = False,
49
+ transforms: Sequence[Callable[[VdomDict], Any]] = (),
50
+ strict_parsing: bool = True,
51
+ ) -> Any | Callable[[Callable], Any]:
52
+ """Converts a Django view to a ReactPy component.
53
+
54
+ Keyword Args:
55
+ view: The view to convert, or the view's dotted path as a string.
56
+ compatibility: **DEPRECATED.** Use `view_to_iframe` instead.
57
+ transforms: A list of functions that transforms the newly generated VDOM. \
58
+ The functions will be called on each VDOM node.
59
+ strict_parsing: If True, an exception will be generated if the HTML does not \
60
+ perfectly adhere to HTML5.
61
+
62
+ Returns:
63
+ A function that takes `request, *args, key, **kwargs` and returns a ReactPy component.
64
+ """
65
+
66
+ def decorator(view: Callable | View | str):
67
+ if not view:
68
+ raise ValueError("A view must be provided to `view_to_component`")
69
+
70
+ def constructor(
71
+ request: HttpRequest | None = None,
72
+ *args,
73
+ key: Key | None = None,
74
+ **kwargs,
75
+ ):
76
+ return _view_to_component(
77
+ view=view,
78
+ compatibility=compatibility,
79
+ transforms=transforms,
80
+ strict_parsing=strict_parsing,
81
+ request=request,
82
+ args=args,
83
+ kwargs=kwargs,
84
+ key=key,
85
+ )
86
+
87
+ return constructor
88
+
89
+ if not view:
90
+ warn(
91
+ "Using `view_to_component` as a decorator is deprecated. "
92
+ "This functionality will be removed in a future version.",
93
+ DeprecationWarning,
94
+ )
95
+
96
+ return decorator(view) if view else decorator
97
+
98
+
99
+ def view_to_iframe(
100
+ view: Callable | View | str, extra_props: dict[str, Any] | None = None
101
+ ):
102
+ """
103
+ Args:
104
+ view: The view function or class to convert, or the dotted path to the view.
105
+
106
+ Keyword Args:
107
+ extra_props: Additional properties to add to the `iframe` element.
108
+
109
+ Returns:
110
+ A function that takes `*args, key, **kwargs` and returns a ReactPy component.
111
+ """
112
+
113
+ def constructor(
114
+ *args,
115
+ key: Key | None = None,
116
+ **kwargs,
117
+ ):
118
+ return _view_to_iframe(
119
+ view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key
120
+ )
121
+
122
+ return constructor
123
+
124
+
125
+ def django_css(static_path: str, key: Key | None = None):
126
+ """Fetches a CSS static file for use within ReactPy. This allows for deferred CSS loading.
127
+
128
+ Args:
129
+ static_path: The path to the static file. This path is identical to what you would \
130
+ use on Django's `{% static %}` template tag
131
+ key: A key to uniquely identify this component which is unique amongst a component's \
132
+ immediate siblings
133
+ """
134
+
135
+ return _django_css(static_path=static_path, key=key)
136
+
137
+
138
+ def django_js(static_path: str, key: Key | None = None):
139
+ """Fetches a JS static file for use within ReactPy. This allows for deferred JS loading.
140
+
141
+ Args:
142
+ static_path: The path to the static file. This path is identical to what you would \
143
+ use on Django's `{% static %}` template tag.
144
+ key: A key to uniquely identify this component which is unique amongst a component's \
145
+ immediate siblings
146
+ """
147
+
148
+ return _django_js(static_path=static_path, key=key)
149
+
150
+
151
+ @component
152
+ def _view_to_component(
153
+ view: Callable | View | str,
154
+ compatibility: bool,
155
+ transforms: Sequence[Callable[[VdomDict], Any]],
156
+ strict_parsing: bool,
157
+ request: HttpRequest | None,
158
+ args: Sequence | None,
159
+ kwargs: dict | None,
160
+ ):
161
+ """The actual component. Used to prevent pollution of acceptable kwargs keys."""
162
+ converted_view, set_converted_view = hooks.use_state(
163
+ cast(Union[VdomDict, None], None)
164
+ )
165
+ _args: Sequence = args or ()
166
+ _kwargs: dict = kwargs or {}
167
+ if request:
168
+ _request: HttpRequest = request
169
+ else:
170
+ _request = HttpRequest()
171
+ _request.method = "GET"
172
+ resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore[assignment]
173
+
174
+ # Render the view render within a hook
175
+ @hooks.use_effect(
176
+ dependencies=[
177
+ json.dumps(vars(_request), default=lambda x: generate_obj_name(x)),
178
+ json.dumps([_args, _kwargs], default=lambda x: generate_obj_name(x)),
179
+ ]
180
+ )
181
+ async def async_render():
182
+ """Render the view in an async hook to avoid blocking the main thread."""
183
+ # Compatibility mode doesn't require a traditional render
184
+ if compatibility:
185
+ return
186
+
187
+ # Render the view
188
+ response = await render_view(resolved_view, _request, _args, _kwargs)
189
+ set_converted_view(
190
+ utils.html_to_vdom(
191
+ response.content.decode("utf-8").strip(),
192
+ utils.del_html_head_body_transform,
193
+ *transforms,
194
+ strict=strict_parsing,
195
+ )
196
+ )
197
+
198
+ # Render in compatibility mode, if needed
199
+ if compatibility:
200
+ # Warn the user that compatibility mode is deprecated
201
+ warn(
202
+ "view_to_component(compatibility=True) is deprecated and will be removed in a future version. "
203
+ "Please use `view_to_iframe` instead.",
204
+ DeprecationWarning,
205
+ )
206
+
207
+ return view_to_iframe(resolved_view)(*_args, **_kwargs)
208
+
209
+ # Return the view if it's been rendered via the `async_render` hook
210
+ return converted_view
211
+
212
+
213
+ @component
214
+ def _view_to_iframe(
215
+ view: Callable | View | str,
216
+ extra_props: dict[str, Any] | None,
217
+ args: Sequence,
218
+ kwargs: dict,
219
+ ) -> VdomDict:
220
+ """The actual component. Used to prevent pollution of acceptable kwargs keys."""
221
+ from reactpy_django.config import REACTPY_REGISTERED_IFRAME_VIEWS
222
+
223
+ if hasattr(view, "view_class"):
224
+ view = view.view_class
225
+ dotted_path = view if isinstance(view, str) else generate_obj_name(view)
226
+ registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path)
227
+
228
+ if not registered_view:
229
+ raise ViewNotRegisteredError(
230
+ f"'{dotted_path}' has not been registered as an iframe! "
231
+ "Are you sure you called `register_iframe` within a Django `AppConfig.ready` method?"
232
+ )
233
+
234
+ query = kwargs.copy()
235
+ if args:
236
+ query["_args"] = args
237
+ query_string = f"?{urlencode(query, doseq=True)}" if args or kwargs else ""
238
+ extra_props = extra_props or {}
239
+ extra_props.pop("src", None)
240
+
241
+ return html.iframe(
242
+ {
243
+ "src": reverse("reactpy:view_to_iframe", args=[dotted_path]) + query_string,
244
+ "style": {"border": "none"},
245
+ "onload": 'javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";}(this));',
246
+ "loading": "lazy",
247
+ }
248
+ | extra_props
249
+ )
250
+
251
+
252
+ @component
253
+ def _django_css(static_path: str):
254
+ return html.style(_cached_static_contents(static_path))
255
+
256
+
257
+ @component
258
+ def _django_js(static_path: str):
259
+ return html.script(_cached_static_contents(static_path))
260
+
261
+
262
+ def _cached_static_contents(static_path: str) -> str:
263
+ from reactpy_django.config import REACTPY_CACHE
264
+
265
+ # Try to find the file within Django's static files
266
+ abs_path = find(static_path)
267
+ if not abs_path:
268
+ raise FileNotFoundError(
269
+ f"Could not find static file {static_path} within Django's static files."
270
+ )
271
+
272
+ # Fetch the file from cache, if available
273
+ last_modified_time = os.stat(abs_path).st_mtime
274
+ cache_key = f"reactpy_django:static_contents:{static_path}"
275
+ file_contents: str | None = caches[REACTPY_CACHE].get(
276
+ cache_key, version=int(last_modified_time)
277
+ )
278
+ if file_contents is None:
279
+ with open(abs_path, encoding="utf-8") as static_file:
280
+ file_contents = static_file.read()
281
+ caches[REACTPY_CACHE].delete(cache_key)
282
+ caches[REACTPY_CACHE].set(
283
+ cache_key, file_contents, timeout=None, version=int(last_modified_time)
284
+ )
285
+
286
+ return file_contents
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ from itertools import cycle
4
+ from typing import Callable
5
+
6
+ from django.conf import settings
7
+ from django.core.cache import DEFAULT_CACHE_ALIAS
8
+ from django.db import DEFAULT_DB_ALIAS
9
+ from django.views import View
10
+ from reactpy.config import REACTPY_DEBUG_MODE
11
+ from reactpy.core.types import ComponentConstructor
12
+
13
+ from reactpy_django.types import (
14
+ AsyncPostprocessor,
15
+ SyncPostprocessor,
16
+ )
17
+ from reactpy_django.utils import import_dotted_path
18
+
19
+ # Non-configurable values
20
+ REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
21
+ REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {}
22
+ REACTPY_FAILED_COMPONENTS: set[str] = set()
23
+ REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {}
24
+
25
+
26
+ # Remove in a future release
27
+ REACTPY_WEBSOCKET_URL = getattr(
28
+ settings,
29
+ "REACTPY_WEBSOCKET_URL",
30
+ "reactpy/",
31
+ )
32
+
33
+ # Configurable through Django settings.py
34
+ REACTPY_URL_PREFIX: str = getattr(
35
+ settings,
36
+ "REACTPY_URL_PREFIX",
37
+ REACTPY_WEBSOCKET_URL,
38
+ ).strip("/")
39
+ REACTPY_SESSION_MAX_AGE: int = getattr(
40
+ settings,
41
+ "REACTPY_SESSION_MAX_AGE",
42
+ 259200, # Default to 3 days
43
+ )
44
+ REACTPY_CACHE: str = getattr(
45
+ settings,
46
+ "REACTPY_CACHE",
47
+ DEFAULT_CACHE_ALIAS,
48
+ )
49
+ REACTPY_DATABASE: str = getattr(
50
+ settings,
51
+ "REACTPY_DATABASE",
52
+ DEFAULT_DB_ALIAS,
53
+ )
54
+ _default_query_postprocessor = getattr(
55
+ settings,
56
+ "REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
57
+ "UNSET",
58
+ )
59
+ REACTPY_DEFAULT_QUERY_POSTPROCESSOR: AsyncPostprocessor | SyncPostprocessor | None
60
+ if _default_query_postprocessor is None:
61
+ REACTPY_DEFAULT_QUERY_POSTPROCESSOR = None
62
+ else:
63
+ REACTPY_DEFAULT_QUERY_POSTPROCESSOR = import_dotted_path(
64
+ "reactpy_django.utils.django_query_postprocessor"
65
+ if (
66
+ _default_query_postprocessor == "UNSET"
67
+ or not isinstance(_default_query_postprocessor, str)
68
+ )
69
+ else _default_query_postprocessor
70
+ )
71
+ REACTPY_AUTH_BACKEND: str | None = getattr(
72
+ settings,
73
+ "REACTPY_AUTH_BACKEND",
74
+ None,
75
+ )
76
+ REACTPY_BACKHAUL_THREAD: bool = getattr(
77
+ settings,
78
+ "REACTPY_BACKHAUL_THREAD",
79
+ False,
80
+ )
81
+ _default_hosts: list[str] | None = getattr(
82
+ settings,
83
+ "REACTPY_DEFAULT_HOSTS",
84
+ None,
85
+ )
86
+ REACTPY_DEFAULT_HOSTS: cycle[str] | None = (
87
+ cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)])
88
+ if _default_hosts
89
+ else None
90
+ )
91
+ REACTPY_RECONNECT_INTERVAL: int = getattr(
92
+ settings,
93
+ "REACTPY_RECONNECT_INTERVAL",
94
+ 750, # Default to 0.75 seconds
95
+ )
96
+ REACTPY_RECONNECT_MAX_INTERVAL: int = getattr(
97
+ settings,
98
+ "REACTPY_RECONNECT_MAX_INTERVAL",
99
+ 60000, # Default to 60 seconds
100
+ )
101
+ REACTPY_RECONNECT_MAX_RETRIES: int = getattr(
102
+ settings,
103
+ "REACTPY_RECONNECT_MAX_RETRIES",
104
+ 150,
105
+ )
106
+ REACTPY_RECONNECT_BACKOFF_MULTIPLIER: float | int = getattr(
107
+ settings,
108
+ "REACTPY_RECONNECT_BACKOFF_MULTIPLIER",
109
+ 1.25, # Default to 25% backoff per connection attempt
110
+ )
111
+ REACTPY_PRERENDER: bool = getattr(
112
+ settings,
113
+ "REACTPY_PRERENDER",
114
+ False,
115
+ )
116
+ REACTPY_AUTO_RELOGIN: bool = getattr(
117
+ settings,
118
+ "REACTPY_AUTO_RELOGIN",
119
+ False,
120
+ )
121
+ REACTPY_CLEAN_INTERVAL: int | None = getattr(
122
+ settings,
123
+ "REACTPY_CLEAN_INTERVAL",
124
+ 604800, # Default to 7 days
125
+ )
126
+ REACTPY_CLEAN_SESSIONS: bool = getattr(
127
+ settings,
128
+ "REACTPY_CLEAN_SESSIONS",
129
+ True,
130
+ )
131
+ REACTPY_CLEAN_USER_DATA: bool = getattr(
132
+ settings,
133
+ "REACTPY_CLEAN_USER_DATA",
134
+ True,
135
+ )
@@ -0,0 +1,31 @@
1
+ from reactpy_django.config import REACTPY_DATABASE
2
+
3
+
4
+ class Router:
5
+ """
6
+ A router to control all database operations on models in the
7
+ auth and contenttypes applications.
8
+ """
9
+
10
+ route_app_labels = {"reactpy_django"}
11
+
12
+ def db_for_read(self, model, **hints):
13
+ """Attempts to read go to REACTPY_DATABASE."""
14
+ if model._meta.app_label in self.route_app_labels:
15
+ return REACTPY_DATABASE
16
+
17
+ def db_for_write(self, model, **hints):
18
+ """Attempts to write go to REACTPY_DATABASE."""
19
+ if model._meta.app_label in self.route_app_labels:
20
+ return REACTPY_DATABASE
21
+
22
+ def allow_relation(self, obj1, obj2, **hints):
23
+ """Returning `None` only allow relations within the same database.
24
+ https://docs.djangoproject.com/en/dev/topics/db/multi-db/#limitations-of-multiple-databases
25
+ """
26
+ return None
27
+
28
+ def allow_migrate(self, db, app_label, model_name=None, **hints):
29
+ """Make sure ReactPy models only appear in REACTPY_DATABASE."""
30
+ if app_label in self.route_app_labels:
31
+ return db == REACTPY_DATABASE
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import wraps
4
+ from typing import TYPE_CHECKING, Any, Callable
5
+ from warnings import warn
6
+
7
+ from reactpy import component
8
+ from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict
9
+
10
+ from reactpy_django.exceptions import DecoratorParamError
11
+ from reactpy_django.hooks import use_scope, use_user
12
+
13
+ if TYPE_CHECKING:
14
+ from django.contrib.auth.models import AbstractUser
15
+
16
+
17
+ def auth_required(
18
+ component: Callable | None = None,
19
+ auth_attribute: str = "is_active",
20
+ fallback: ComponentType | Callable | VdomDict | None = None,
21
+ ) -> Callable:
22
+ """If the user passes authentication criteria, the decorated component will be rendered.
23
+ Otherwise, the fallback component will be rendered.
24
+
25
+ This decorator can be used with or without parentheses.
26
+
27
+ Args:
28
+ auth_attribute: The value to check within the user object. \
29
+ This is checked in the form of `UserModel.<auth_attribute>`. \
30
+ fallback: The component or VDOM (`reactpy.html` snippet) to render if the user is not authenticated.
31
+ """
32
+
33
+ warn(
34
+ "auth_required is deprecated and will be removed in the next major version. "
35
+ "An equivalent to this decorator's default is @user_passes_test(lambda user: user.is_active).",
36
+ DeprecationWarning,
37
+ )
38
+
39
+ def decorator(component):
40
+ @wraps(component)
41
+ def _wrapped_func(*args, **kwargs):
42
+ scope = use_scope()
43
+
44
+ if getattr(scope["user"], auth_attribute):
45
+ return component(*args, **kwargs)
46
+ return fallback(*args, **kwargs) if callable(fallback) else fallback
47
+
48
+ return _wrapped_func
49
+
50
+ # Return for @authenticated(...) and @authenticated respectively
51
+ return decorator if component is None else decorator(component)
52
+
53
+
54
+ def user_passes_test(
55
+ test_func: Callable[[AbstractUser], bool],
56
+ /,
57
+ fallback: Any | None = None,
58
+ ) -> ComponentConstructor:
59
+ """You can limit component access to users that pass a test function by using this decorator.
60
+
61
+ This decorator is inspired by Django's `user_passes_test` decorator, but works with ReactPy components.
62
+
63
+ Args:
64
+ test_func: A function that accepts a `User` returns a boolean.
65
+ fallback: The content to be rendered if the test fails. Typically is a ReactPy component or \
66
+ VDOM (`reactpy.html` snippet).
67
+ """
68
+
69
+ def decorator(user_component):
70
+ @wraps(user_component)
71
+ def _wrapper(*args, **kwargs):
72
+ return _user_passes_test(
73
+ user_component, fallback, test_func, *args, **kwargs
74
+ )
75
+
76
+ return _wrapper
77
+
78
+ return decorator
79
+
80
+
81
+ @component
82
+ def _user_passes_test(component_constructor, fallback, test_func, *args, **kwargs):
83
+ """Dedicated component for `user_passes_test` to allow us to always have access to hooks."""
84
+ user = use_user()
85
+
86
+ if test_func(user):
87
+ # Ensure that the component is a ReactPy component.
88
+ user_component = component_constructor(*args, **kwargs)
89
+ if not getattr(user_component, "render", None):
90
+ raise DecoratorParamError(
91
+ "`user_passes_test` is not decorating a ReactPy component. "
92
+ "Did you forget `@user_passes_test` must be ABOVE the `@component` decorator?"
93
+ )
94
+
95
+ # Render the component.
96
+ return user_component
97
+
98
+ # Render the fallback component.
99
+ # Returns an empty string if fallback is None, since ReactPy currently renders None as a string.
100
+ # TODO: Remove this fallback when ReactPy can render None properly.
101
+ return fallback(*args, **kwargs) if callable(fallback) else (fallback or "")
@@ -0,0 +1,34 @@
1
+ class ComponentParamError(TypeError):
2
+ ...
3
+
4
+
5
+ class ComponentDoesNotExistError(AttributeError):
6
+ ...
7
+
8
+
9
+ class OfflineComponentMissing(ComponentDoesNotExistError):
10
+ ...
11
+
12
+
13
+ class InvalidHostError(ValueError):
14
+ ...
15
+
16
+
17
+ class ComponentCarrierError(Exception):
18
+ ...
19
+
20
+
21
+ class UserNotFoundError(Exception):
22
+ ...
23
+
24
+
25
+ class ViewNotRegisteredError(AttributeError):
26
+ ...
27
+
28
+
29
+ class ViewDoesNotExistError(AttributeError):
30
+ ...
31
+
32
+
33
+ class DecoratorParamError(TypeError):
34
+ ...