drf-parcel 0.1.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.
- drf_parcel/__init__.py +0 -0
- drf_parcel/apps.py +15 -0
- drf_parcel/checks.py +167 -0
- drf_parcel/conf.py +106 -0
- drf_parcel/envelope.py +99 -0
- drf_parcel/exceptions.py +109 -0
- drf_parcel/handlers.py +270 -0
- drf_parcel/integrity.py +75 -0
- drf_parcel/locale/fa/LC_MESSAGES/django.mo +0 -0
- drf_parcel/locale/fa/LC_MESSAGES/django.po +79 -0
- drf_parcel/messages.py +54 -0
- drf_parcel/pagination.py +271 -0
- drf_parcel/py.typed +0 -0
- drf_parcel/responses.py +196 -0
- drf_parcel/schema.py +244 -0
- drf_parcel-0.1.0.dist-info/METADATA +198 -0
- drf_parcel-0.1.0.dist-info/RECORD +19 -0
- drf_parcel-0.1.0.dist-info/WHEEL +4 -0
- drf_parcel-0.1.0.dist-info/licenses/LICENSE +28 -0
drf_parcel/__init__.py
ADDED
|
File without changes
|
drf_parcel/apps.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.core import checks
|
|
3
|
+
from django.utils.translation import gettext_lazy as _
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DrfParcelConfig(AppConfig):
|
|
7
|
+
"""Installed so Django finds the library's catalogue and runs its checks."""
|
|
8
|
+
|
|
9
|
+
name = 'drf_parcel'
|
|
10
|
+
verbose_name = _('Response envelope')
|
|
11
|
+
|
|
12
|
+
def ready(self) -> None:
|
|
13
|
+
from drf_parcel.checks import check_settings
|
|
14
|
+
|
|
15
|
+
checks.register(check_settings)
|
drf_parcel/checks.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What is wrong with the ``DRF_PARCEL`` setting, said when the project starts.
|
|
3
|
+
|
|
4
|
+
A setting that would only fail on the first request that reads it fails
|
|
5
|
+
instead on ``manage.py check`` and ``runserver`` - before a client finds it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from django.apps import AppConfig
|
|
12
|
+
from django.core.checks import CheckMessage, Error
|
|
13
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
14
|
+
from django.utils.module_loading import import_string
|
|
15
|
+
|
|
16
|
+
from drf_parcel import conf
|
|
17
|
+
from drf_parcel.handlers import STANDARD_CODES
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_settings(
|
|
21
|
+
app_configs: Sequence[AppConfig] | None = None, **kwargs: Any
|
|
22
|
+
) -> list[CheckMessage]:
|
|
23
|
+
"""Every problem with the setting, each with what to do about it."""
|
|
24
|
+
try:
|
|
25
|
+
conf.raw()
|
|
26
|
+
conf.messages()
|
|
27
|
+
except ImproperlyConfigured as refused:
|
|
28
|
+
return [Error(str(refused), id='drf_parcel.E001')]
|
|
29
|
+
|
|
30
|
+
return _page_size_problems() + _error_problems()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _page_size_problems() -> list[CheckMessage]:
|
|
34
|
+
problems: list[CheckMessage] = []
|
|
35
|
+
page_size = conf.page_size()
|
|
36
|
+
ceiling = conf.get('MAX_PAGE_SIZE')
|
|
37
|
+
|
|
38
|
+
# The page size may be DRF's own PAGE_SIZE, so it is named by what it is.
|
|
39
|
+
for name, value in (
|
|
40
|
+
('The page size', page_size),
|
|
41
|
+
("DRF_PARCEL['MAX_PAGE_SIZE']", ceiling),
|
|
42
|
+
):
|
|
43
|
+
if not _is_positive_whole_number(value):
|
|
44
|
+
problems.append(
|
|
45
|
+
Error(
|
|
46
|
+
f'{name} must be a whole number above zero, not {value!r}.',
|
|
47
|
+
hint="It is DRF_PARCEL['PAGE_SIZE'], else REST_FRAMEWORK['PAGE_SIZE'].",
|
|
48
|
+
id='drf_parcel.E002',
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if not problems and ceiling < page_size: # type: ignore[operator]
|
|
53
|
+
problems.append(
|
|
54
|
+
Error(
|
|
55
|
+
f"DRF_PARCEL['MAX_PAGE_SIZE'] ({ceiling}) is below the page size ({page_size}).",
|
|
56
|
+
hint='The ceiling on a size the client chooses cannot be below the default size.',
|
|
57
|
+
id='drf_parcel.E003',
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not isinstance(conf.get('CLIENT_PAGE_SIZE'), bool):
|
|
62
|
+
problems.append(
|
|
63
|
+
Error("DRF_PARCEL['CLIENT_PAGE_SIZE'] must be True or False.", id='drf_parcel.E004')
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
param = conf.get('PAGE_SIZE_QUERY_PARAM')
|
|
67
|
+
|
|
68
|
+
if not isinstance(param, str) or not param:
|
|
69
|
+
problems.append(
|
|
70
|
+
Error(
|
|
71
|
+
"DRF_PARCEL['PAGE_SIZE_QUERY_PARAM'] must be the name of a query parameter.",
|
|
72
|
+
id='drf_parcel.E005',
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return problems
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _error_problems() -> list[CheckMessage]:
|
|
80
|
+
problems: list[CheckMessage] = []
|
|
81
|
+
|
|
82
|
+
if conf.get('FIELD_ERRORS') not in ('detailed', 'messages'):
|
|
83
|
+
problems.append(
|
|
84
|
+
Error(
|
|
85
|
+
"DRF_PARCEL['FIELD_ERRORS'] must be 'detailed' or 'messages'.",
|
|
86
|
+
id='drf_parcel.E006',
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
separator = conf.get('FIELD_PATH_SEPARATOR')
|
|
91
|
+
|
|
92
|
+
if not isinstance(separator, str) or not separator:
|
|
93
|
+
problems.append(
|
|
94
|
+
Error(
|
|
95
|
+
"DRF_PARCEL['FIELD_PATH_SEPARATOR'] must be a character or more.",
|
|
96
|
+
id='drf_parcel.E007',
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
problems.extend(_error_code_problems())
|
|
101
|
+
|
|
102
|
+
for name in ('HANDLE_INTEGRITY_ERRORS', 'RAISE_UNHANDLED_IN_DEBUG'):
|
|
103
|
+
if not isinstance(conf.get(name), bool):
|
|
104
|
+
problems.append(
|
|
105
|
+
Error(f"DRF_PARCEL['{name}'] must be True or False.", id='drf_parcel.E009')
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
problems.extend(_reporter_problems())
|
|
109
|
+
return problems
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _error_code_problems() -> list[CheckMessage]:
|
|
113
|
+
renamed = conf.get('ERROR_CODES')
|
|
114
|
+
|
|
115
|
+
if not isinstance(renamed, Mapping):
|
|
116
|
+
return [Error("DRF_PARCEL['ERROR_CODES'] must be a dictionary.", id='drf_parcel.E008')]
|
|
117
|
+
|
|
118
|
+
problems: list[CheckMessage] = []
|
|
119
|
+
unknown = sorted(set(renamed) - set(STANDARD_CODES))
|
|
120
|
+
|
|
121
|
+
if unknown:
|
|
122
|
+
problems.append(
|
|
123
|
+
Error(
|
|
124
|
+
f"DRF_PARCEL['ERROR_CODES'] renames codes the library does not send: "
|
|
125
|
+
f'{", ".join(unknown)}.',
|
|
126
|
+
hint=f'It sends {", ".join(STANDARD_CODES)}.',
|
|
127
|
+
id='drf_parcel.E008',
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if not all(isinstance(new, str) and new for new in renamed.values()):
|
|
132
|
+
problems.append(
|
|
133
|
+
Error(
|
|
134
|
+
"Every new name in DRF_PARCEL['ERROR_CODES'] must be a word.",
|
|
135
|
+
id='drf_parcel.E008',
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return problems
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _reporter_problems() -> list[CheckMessage]:
|
|
143
|
+
path = conf.get('REPORTER')
|
|
144
|
+
|
|
145
|
+
if path is None:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
reporter = import_string(path) if isinstance(path, str) else None
|
|
150
|
+
except ImportError:
|
|
151
|
+
reporter = None
|
|
152
|
+
|
|
153
|
+
if not callable(reporter):
|
|
154
|
+
return [
|
|
155
|
+
Error(
|
|
156
|
+
f"DRF_PARCEL['REPORTER'] must be the dotted path to a function, not {path!r}.",
|
|
157
|
+
hint='It is called with each unexpected exception and its context.',
|
|
158
|
+
id='drf_parcel.E010',
|
|
159
|
+
)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_positive_whole_number(value: object) -> bool:
|
|
166
|
+
# True is an int to Python, and is no page size.
|
|
167
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
drf_parcel/conf.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The ``DRF_PARCEL`` setting, read and checked.
|
|
3
|
+
|
|
4
|
+
Every key lives in one dictionary in the project's settings, and the default
|
|
5
|
+
needs none of them::
|
|
6
|
+
|
|
7
|
+
DRF_PARCEL = {
|
|
8
|
+
'PAGE_SIZE': 20,
|
|
9
|
+
'MESSAGES': {'created': _('Saved successfully.')},
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
A key the library does not know is refused rather than ignored: a misspelt
|
|
13
|
+
``PAGE_SZIE`` that is silently ignored is a page size that never changes, and
|
|
14
|
+
nobody can tell why.
|
|
15
|
+
|
|
16
|
+
The setting is read on every use rather than once, so a test that overrides
|
|
17
|
+
it - and a project that changes it - is answered by what it says now.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from types import MappingProxyType
|
|
22
|
+
from typing import Final
|
|
23
|
+
|
|
24
|
+
from django.conf import settings
|
|
25
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
26
|
+
from rest_framework.settings import api_settings
|
|
27
|
+
|
|
28
|
+
#: Every key the setting may hold, and what it is when left out.
|
|
29
|
+
DEFAULTS: Final[Mapping[str, object]] = MappingProxyType(
|
|
30
|
+
{
|
|
31
|
+
'PAGE_SIZE': 20,
|
|
32
|
+
'MAX_PAGE_SIZE': 100,
|
|
33
|
+
'CLIENT_PAGE_SIZE': False,
|
|
34
|
+
'PAGE_SIZE_QUERY_PARAM': 'page_size',
|
|
35
|
+
'FIELD_ERRORS': 'detailed',
|
|
36
|
+
'FIELD_PATH_SEPARATOR': '.',
|
|
37
|
+
'ERROR_CODES': MappingProxyType({}),
|
|
38
|
+
'HANDLE_INTEGRITY_ERRORS': False,
|
|
39
|
+
'RAISE_UNHANDLED_IN_DEBUG': True,
|
|
40
|
+
'REPORTER': None,
|
|
41
|
+
'MESSAGES': MappingProxyType({}),
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
#: The default success messages a project may replace, by key.
|
|
46
|
+
MESSAGE_KEYS: Final = ('created', 'saved', 'deleted', 'done')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def raw() -> Mapping[str, object]:
|
|
50
|
+
"""The setting as written, refused when it is not a dictionary of known keys."""
|
|
51
|
+
written = getattr(settings, 'DRF_PARCEL', {})
|
|
52
|
+
|
|
53
|
+
if not isinstance(written, Mapping):
|
|
54
|
+
raise ImproperlyConfigured('DRF_PARCEL must be a dictionary.')
|
|
55
|
+
|
|
56
|
+
unknown = sorted(set(written) - set(DEFAULTS))
|
|
57
|
+
|
|
58
|
+
if unknown:
|
|
59
|
+
raise ImproperlyConfigured(
|
|
60
|
+
f'DRF_PARCEL has keys the library does not know: {", ".join(unknown)}. '
|
|
61
|
+
f'It knows {", ".join(DEFAULTS)}.'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return written
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get(key: str) -> object:
|
|
68
|
+
"""One key of the setting, or its default when the project left it out."""
|
|
69
|
+
return raw().get(key, DEFAULTS[key])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def page_size() -> object:
|
|
73
|
+
"""
|
|
74
|
+
The page size when a view names none.
|
|
75
|
+
|
|
76
|
+
``DRF_PARCEL['PAGE_SIZE']`` when the project wrote it; else DRF's own
|
|
77
|
+
``PAGE_SIZE``, so a project that already set it there keeps its size when
|
|
78
|
+
it adopts the library; else the library's default.
|
|
79
|
+
"""
|
|
80
|
+
written = raw()
|
|
81
|
+
|
|
82
|
+
if 'PAGE_SIZE' in written:
|
|
83
|
+
return written['PAGE_SIZE']
|
|
84
|
+
|
|
85
|
+
if api_settings.PAGE_SIZE is not None:
|
|
86
|
+
return api_settings.PAGE_SIZE
|
|
87
|
+
|
|
88
|
+
return DEFAULTS['PAGE_SIZE']
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def messages() -> Mapping[str, object]:
|
|
92
|
+
"""The project's replacements for the default success messages, by key."""
|
|
93
|
+
replaced = get('MESSAGES')
|
|
94
|
+
|
|
95
|
+
if not isinstance(replaced, Mapping):
|
|
96
|
+
raise ImproperlyConfigured("DRF_PARCEL['MESSAGES'] must be a dictionary.")
|
|
97
|
+
|
|
98
|
+
unknown = sorted(set(replaced) - set(MESSAGE_KEYS))
|
|
99
|
+
|
|
100
|
+
if unknown:
|
|
101
|
+
raise ImproperlyConfigured(
|
|
102
|
+
f"DRF_PARCEL['MESSAGES'] has keys the library does not know: {', '.join(unknown)}. "
|
|
103
|
+
f'It knows {", ".join(MESSAGE_KEYS)}.'
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return replaced
|
drf_parcel/envelope.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The envelope - the one place its shape is assembled.
|
|
3
|
+
|
|
4
|
+
Every answer has the same top level, success and failure alike::
|
|
5
|
+
|
|
6
|
+
{"success": true, "status_code": 200, "message": "...", "data": {}}
|
|
7
|
+
{"success": true, "status_code": 200, "message": "...", "data": [], "pagination": {}}
|
|
8
|
+
{"success": false, "status_code": 409, "message": "...", "data": {}, "error_code": "..."}
|
|
9
|
+
|
|
10
|
+
Two places building the envelope become two shapes, so nothing else in the
|
|
11
|
+
library writes these keys.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
#: The keys every envelope carries, whatever it answers.
|
|
18
|
+
KEYS = ('success', 'status_code', 'message', 'data')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_envelope(
|
|
22
|
+
*,
|
|
23
|
+
success: bool,
|
|
24
|
+
status_code: int,
|
|
25
|
+
message: object,
|
|
26
|
+
data: Any = None,
|
|
27
|
+
pagination: Mapping[str, Any] | None = None,
|
|
28
|
+
error_code: str | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
The envelope, from its parts.
|
|
32
|
+
|
|
33
|
+
``message`` may be a lazy translation: it becomes text here, under the
|
|
34
|
+
language active when the answer is built. ``data`` that says nothing is
|
|
35
|
+
``{}``, never ``null``.
|
|
36
|
+
|
|
37
|
+
A key is either always there or there for a reason, so a failure must
|
|
38
|
+
name its ``error_code`` and a success must not, and only a success is a
|
|
39
|
+
page.
|
|
40
|
+
"""
|
|
41
|
+
if success and error_code is not None:
|
|
42
|
+
raise ValueError('A success carries no error_code.')
|
|
43
|
+
|
|
44
|
+
if not success and not error_code:
|
|
45
|
+
raise ValueError('A failure must name its error_code.')
|
|
46
|
+
|
|
47
|
+
if not success and pagination is not None:
|
|
48
|
+
raise ValueError('A failure is not a page.')
|
|
49
|
+
|
|
50
|
+
envelope: dict[str, Any] = {
|
|
51
|
+
'success': success,
|
|
52
|
+
'status_code': status_code,
|
|
53
|
+
'message': str(message),
|
|
54
|
+
'data': {} if data is None else data,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if pagination is not None:
|
|
58
|
+
envelope['pagination'] = dict(pagination)
|
|
59
|
+
|
|
60
|
+
if error_code is not None:
|
|
61
|
+
envelope['error_code'] = error_code
|
|
62
|
+
|
|
63
|
+
return envelope
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def envelope_schema(
|
|
67
|
+
data: Mapping[str, Any],
|
|
68
|
+
*,
|
|
69
|
+
pagination: Mapping[str, Any] | None = None,
|
|
70
|
+
error: bool = False,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
The OpenAPI schema of an envelope around ``data`` - the shape above, as a schema.
|
|
74
|
+
|
|
75
|
+
Kept beside ``build_envelope`` so that the shape and its description
|
|
76
|
+
change together.
|
|
77
|
+
"""
|
|
78
|
+
properties: dict[str, Any] = {
|
|
79
|
+
# Not an enum: drf-spectacular would lift each into a component of its own.
|
|
80
|
+
'success': {
|
|
81
|
+
'type': 'boolean',
|
|
82
|
+
'example': not error,
|
|
83
|
+
'description': 'Always false on a failure.' if error else 'Always true on a success.',
|
|
84
|
+
},
|
|
85
|
+
'status_code': {'type': 'integer'},
|
|
86
|
+
'message': {'type': 'string'},
|
|
87
|
+
'data': dict(data),
|
|
88
|
+
}
|
|
89
|
+
required = list(KEYS)
|
|
90
|
+
|
|
91
|
+
if pagination is not None:
|
|
92
|
+
properties['pagination'] = dict(pagination)
|
|
93
|
+
required.append('pagination')
|
|
94
|
+
|
|
95
|
+
if error:
|
|
96
|
+
properties['error_code'] = {'type': 'string'}
|
|
97
|
+
required.append('error_code')
|
|
98
|
+
|
|
99
|
+
return {'type': 'object', 'properties': properties, 'required': required}
|
drf_parcel/exceptions.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A project's own errors, and the common ones the library ships.
|
|
3
|
+
|
|
4
|
+
A ``DomainError`` is DRF's ``APIException`` with one thing added: ``data``,
|
|
5
|
+
what a client can act on - which record is in the way, how long to wait::
|
|
6
|
+
|
|
7
|
+
class ProvinceInUse(DomainError):
|
|
8
|
+
status_code = 409
|
|
9
|
+
default_code = 'province_in_use'
|
|
10
|
+
default_detail = _('This province has branches and cannot be deleted.')
|
|
11
|
+
|
|
12
|
+
raise ProvinceInUse(data={'branches': 3})
|
|
13
|
+
|
|
14
|
+
Its names are DRF's own: ``default_code`` is sent as ``error_code``,
|
|
15
|
+
``default_detail`` as ``message``. Being an ``APIException``, it answers
|
|
16
|
+
sensibly under DRF's own handler too, and a Celery task or a management
|
|
17
|
+
command catches it like any other exception.
|
|
18
|
+
|
|
19
|
+
Where DRF has an exception of the same meaning, the library's is also that
|
|
20
|
+
exception - ``NotFoundError`` is a ``NotFound`` - so code that catches DRF's,
|
|
21
|
+
and DRF's own handling of it, keep working.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from django.utils.translation import gettext_lazy as _
|
|
28
|
+
from rest_framework import exceptions, status
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DomainError(exceptions.APIException):
|
|
32
|
+
"""A rule of the project was broken, said in the envelope with ``data`` beside it."""
|
|
33
|
+
|
|
34
|
+
status_code: int = status.HTTP_400_BAD_REQUEST
|
|
35
|
+
default_code = 'error'
|
|
36
|
+
default_detail = _('The request could not be completed.')
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
detail: Any = None,
|
|
41
|
+
code: str | None = None,
|
|
42
|
+
*,
|
|
43
|
+
data: Mapping[str, Any] | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(detail, code)
|
|
46
|
+
#: What a client can act on. Never what a person must not read elsewhere.
|
|
47
|
+
self.data: dict[str, Any] = dict(data or {})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InvalidInputError(DomainError):
|
|
51
|
+
"""What was sent cannot be used - said by a service, where no serializer is."""
|
|
52
|
+
|
|
53
|
+
default_code = 'validation_error'
|
|
54
|
+
default_detail = _('Some of the information provided is not valid.')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class InvalidCursorError(DomainError):
|
|
58
|
+
"""A cursor the server cannot read: the client sent back something it was never given."""
|
|
59
|
+
|
|
60
|
+
default_code = 'invalid_cursor'
|
|
61
|
+
default_detail = _('The cursor is not valid.')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AuthenticationFailedError(DomainError, exceptions.AuthenticationFailed):
|
|
65
|
+
"""The credentials sent are wrong, or no longer good."""
|
|
66
|
+
|
|
67
|
+
status_code = status.HTTP_401_UNAUTHORIZED
|
|
68
|
+
default_code = 'authentication_failed'
|
|
69
|
+
default_detail = _('Sign in again to continue.')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PermissionDeniedError(DomainError, exceptions.PermissionDenied):
|
|
73
|
+
"""The caller is known, and may not do this."""
|
|
74
|
+
|
|
75
|
+
status_code = status.HTTP_403_FORBIDDEN
|
|
76
|
+
default_code = 'permission_denied'
|
|
77
|
+
default_detail = _('You do not have permission to do this.')
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NotFoundError(DomainError, exceptions.NotFound):
|
|
81
|
+
"""The record does not exist - or does not exist for this caller."""
|
|
82
|
+
|
|
83
|
+
status_code = status.HTTP_404_NOT_FOUND
|
|
84
|
+
default_code = 'not_found'
|
|
85
|
+
default_detail = _('It was not found.')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ConflictError(DomainError):
|
|
89
|
+
"""The request is right, and the state of things refuses it."""
|
|
90
|
+
|
|
91
|
+
status_code = status.HTTP_409_CONFLICT
|
|
92
|
+
default_code = 'conflict'
|
|
93
|
+
default_detail = _('This conflicts with a change made in the meantime.')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class RateLimitedError(DomainError):
|
|
97
|
+
"""Too many attempts, too quickly. Put ``retry_after`` in ``data`` when it is known."""
|
|
98
|
+
|
|
99
|
+
status_code = status.HTTP_429_TOO_MANY_REQUESTS
|
|
100
|
+
default_code = 'rate_limited'
|
|
101
|
+
default_detail = _('Too many attempts. Please wait and try again.')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ServiceUnavailableError(DomainError):
|
|
105
|
+
"""Something the server depends on is not answering."""
|
|
106
|
+
|
|
107
|
+
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|
108
|
+
default_code = 'service_unavailable'
|
|
109
|
+
default_detail = _('This service is not available right now. Please try again later.')
|