kinto 23.2.1__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.
- kinto/__init__.py +92 -0
- kinto/__main__.py +249 -0
- kinto/authorization.py +134 -0
- kinto/config/__init__.py +94 -0
- kinto/config/kinto.tpl +270 -0
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +246 -0
- kinto/core/authentication.py +48 -0
- kinto/core/authorization.py +311 -0
- kinto/core/cache/__init__.py +131 -0
- kinto/core/cache/memcached.py +112 -0
- kinto/core/cache/memory.py +104 -0
- kinto/core/cache/postgresql/__init__.py +178 -0
- kinto/core/cache/postgresql/schema.sql +23 -0
- kinto/core/cache/testing.py +208 -0
- kinto/core/cornice/__init__.py +93 -0
- kinto/core/cornice/cors.py +144 -0
- kinto/core/cornice/errors.py +40 -0
- kinto/core/cornice/pyramidhook.py +373 -0
- kinto/core/cornice/renderer.py +89 -0
- kinto/core/cornice/resource.py +205 -0
- kinto/core/cornice/service.py +641 -0
- kinto/core/cornice/util.py +138 -0
- kinto/core/cornice/validators/__init__.py +94 -0
- kinto/core/cornice/validators/_colander.py +142 -0
- kinto/core/cornice/validators/_marshmallow.py +182 -0
- kinto/core/cornice_swagger/__init__.py +92 -0
- kinto/core/cornice_swagger/converters/__init__.py +21 -0
- kinto/core/cornice_swagger/converters/exceptions.py +6 -0
- kinto/core/cornice_swagger/converters/parameters.py +90 -0
- kinto/core/cornice_swagger/converters/schema.py +249 -0
- kinto/core/cornice_swagger/swagger.py +725 -0
- kinto/core/cornice_swagger/templates/index.html +73 -0
- kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
- kinto/core/cornice_swagger/util.py +42 -0
- kinto/core/cornice_swagger/views.py +78 -0
- kinto/core/decorators.py +74 -0
- kinto/core/errors.py +216 -0
- kinto/core/events.py +301 -0
- kinto/core/initialization.py +738 -0
- kinto/core/listeners/__init__.py +9 -0
- kinto/core/metrics.py +94 -0
- kinto/core/openapi.py +115 -0
- kinto/core/permission/__init__.py +202 -0
- kinto/core/permission/memory.py +167 -0
- kinto/core/permission/postgresql/__init__.py +489 -0
- kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
- kinto/core/permission/postgresql/schema.sql +41 -0
- kinto/core/permission/testing.py +487 -0
- kinto/core/resource/__init__.py +1311 -0
- kinto/core/resource/model.py +412 -0
- kinto/core/resource/schema.py +502 -0
- kinto/core/resource/viewset.py +230 -0
- kinto/core/schema.py +119 -0
- kinto/core/scripts.py +50 -0
- kinto/core/statsd.py +1 -0
- kinto/core/storage/__init__.py +436 -0
- kinto/core/storage/exceptions.py +53 -0
- kinto/core/storage/generators.py +58 -0
- kinto/core/storage/memory.py +651 -0
- kinto/core/storage/postgresql/__init__.py +1131 -0
- kinto/core/storage/postgresql/client.py +120 -0
- kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
- kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
- kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
- kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
- kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
- kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
- kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
- kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
- kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
- kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
- kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
- kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
- kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
- kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
- kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
- kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
- kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
- kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
- kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
- kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
- kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
- kinto/core/storage/postgresql/migrator.py +98 -0
- kinto/core/storage/postgresql/pool.py +55 -0
- kinto/core/storage/postgresql/schema.sql +143 -0
- kinto/core/storage/testing.py +1857 -0
- kinto/core/storage/utils.py +37 -0
- kinto/core/testing.py +182 -0
- kinto/core/utils.py +553 -0
- kinto/core/views/__init__.py +0 -0
- kinto/core/views/batch.py +163 -0
- kinto/core/views/errors.py +145 -0
- kinto/core/views/heartbeat.py +106 -0
- kinto/core/views/hello.py +69 -0
- kinto/core/views/openapi.py +35 -0
- kinto/core/views/version.py +50 -0
- kinto/events.py +3 -0
- kinto/plugins/__init__.py +0 -0
- kinto/plugins/accounts/__init__.py +94 -0
- kinto/plugins/accounts/authentication.py +63 -0
- kinto/plugins/accounts/scripts.py +61 -0
- kinto/plugins/accounts/utils.py +13 -0
- kinto/plugins/accounts/views.py +136 -0
- kinto/plugins/admin/README.md +3 -0
- kinto/plugins/admin/VERSION +1 -0
- kinto/plugins/admin/__init__.py +40 -0
- kinto/plugins/admin/build/VERSION +1 -0
- kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
- kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/index.html +18 -0
- kinto/plugins/admin/public/help.html +25 -0
- kinto/plugins/admin/views.py +42 -0
- kinto/plugins/default_bucket/__init__.py +191 -0
- kinto/plugins/flush.py +28 -0
- kinto/plugins/history/__init__.py +65 -0
- kinto/plugins/history/listener.py +181 -0
- kinto/plugins/history/views.py +66 -0
- kinto/plugins/openid/__init__.py +131 -0
- kinto/plugins/openid/utils.py +14 -0
- kinto/plugins/openid/views.py +193 -0
- kinto/plugins/prometheus.py +300 -0
- kinto/plugins/statsd.py +85 -0
- kinto/schema_validation.py +135 -0
- kinto/views/__init__.py +34 -0
- kinto/views/admin.py +195 -0
- kinto/views/buckets.py +45 -0
- kinto/views/collections.py +58 -0
- kinto/views/contribute.py +39 -0
- kinto/views/groups.py +90 -0
- kinto/views/permissions.py +235 -0
- kinto/views/records.py +133 -0
- kinto-23.2.1.dist-info/METADATA +232 -0
- kinto-23.2.1.dist-info/RECORD +142 -0
- kinto-23.2.1.dist-info/WHEEL +5 -0
- kinto-23.2.1.dist-info/entry_points.txt +5 -0
- kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
- kinto-23.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
import colander
|
|
4
|
+
|
|
5
|
+
from kinto.core.errors import ErrorSchema
|
|
6
|
+
from kinto.core.schema import (
|
|
7
|
+
URL,
|
|
8
|
+
Any,
|
|
9
|
+
FieldList,
|
|
10
|
+
HeaderField,
|
|
11
|
+
HeaderQuotedInteger,
|
|
12
|
+
QueryField,
|
|
13
|
+
TimeStamp,
|
|
14
|
+
)
|
|
15
|
+
from kinto.core.utils import native_value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
POSTGRESQL_MAX_INTEGER_VALUE = 2**63
|
|
19
|
+
|
|
20
|
+
positive_big_integer = colander.Range(min=0, max=POSTGRESQL_MAX_INTEGER_VALUE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TimeStamp(TimeStamp):
|
|
24
|
+
"""This schema is deprecated, you should use `kinto.core.schema.TimeStamp` instead."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *args, **kwargs):
|
|
27
|
+
message = (
|
|
28
|
+
"`kinto.core.resource.schema.TimeStamp` is deprecated, "
|
|
29
|
+
"use `kinto.core.schema.TimeStamp` instead."
|
|
30
|
+
)
|
|
31
|
+
warnings.warn(message, DeprecationWarning)
|
|
32
|
+
super().__init__(*args, **kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class URL(URL):
|
|
36
|
+
"""This schema is deprecated, you should use `kinto.core.schema.URL` instead."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args, **kwargs):
|
|
39
|
+
message = (
|
|
40
|
+
"`kinto.core.resource.schema.URL` is deprecated, use `kinto.core.schema.URL` instead."
|
|
41
|
+
)
|
|
42
|
+
warnings.warn(message, DeprecationWarning)
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Resource related schemas
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ResourceSchema(colander.MappingSchema):
|
|
50
|
+
"""Base resource schema, with *Cliquet* specific built-in options."""
|
|
51
|
+
|
|
52
|
+
class Options:
|
|
53
|
+
"""
|
|
54
|
+
Resource schema options.
|
|
55
|
+
|
|
56
|
+
This is meant to be overriden for changing values:
|
|
57
|
+
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
class Product(ResourceSchema):
|
|
61
|
+
reference = colander.SchemaNode(colander.String())
|
|
62
|
+
|
|
63
|
+
class Options:
|
|
64
|
+
readonly_fields = ('reference',)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
readonly_fields = tuple()
|
|
68
|
+
"""Fields that cannot be updated. Values for fields will have to be
|
|
69
|
+
provided either during object creation, through default values using
|
|
70
|
+
``missing`` attribute or implementing a custom logic in
|
|
71
|
+
:meth:`kinto.core.resource.Resource.process_object`.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
preserve_unknown = True
|
|
75
|
+
"""Define if unknown fields should be preserved or not.
|
|
76
|
+
|
|
77
|
+
The resource is schema-less by default. In other words, any field name
|
|
78
|
+
will be accepted on objects. Set this to ``False`` in order to limit
|
|
79
|
+
the accepted fields to the ones defined in the schema.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def get_option(cls, attr):
|
|
84
|
+
default_value = getattr(ResourceSchema.Options, attr)
|
|
85
|
+
return getattr(cls.Options, attr, default_value)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def is_readonly(cls, field):
|
|
89
|
+
"""Return True if specified field name is read-only.
|
|
90
|
+
|
|
91
|
+
:param str field: the field name in the schema
|
|
92
|
+
:returns: ``True`` if the specified field is read-only,
|
|
93
|
+
``False`` otherwise.
|
|
94
|
+
:rtype: bool
|
|
95
|
+
"""
|
|
96
|
+
return field in cls.get_option("readonly_fields")
|
|
97
|
+
|
|
98
|
+
def schema_type(self):
|
|
99
|
+
if self.get_option("preserve_unknown") is True:
|
|
100
|
+
unknown = "preserve"
|
|
101
|
+
else:
|
|
102
|
+
unknown = "ignore"
|
|
103
|
+
return colander.Mapping(unknown=unknown)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PermissionsSchema(colander.SchemaNode):
|
|
107
|
+
"""A permission mapping defines ACEs.
|
|
108
|
+
|
|
109
|
+
It has permission names as keys and principals as values.
|
|
110
|
+
|
|
111
|
+
::
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
"write": ["fxa:af3e077eb9f5444a949ad65aa86e82ff"],
|
|
115
|
+
"groups:create": ["fxa:70a9335eecfe440fa445ba752a750f3d"]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
def __init__(self, *args, **kwargs):
|
|
121
|
+
self.known_perms = kwargs.pop("permissions", tuple())
|
|
122
|
+
super().__init__(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
for perm in self.known_perms:
|
|
125
|
+
self[perm] = self._get_node_principals(perm)
|
|
126
|
+
|
|
127
|
+
def schema_type(self):
|
|
128
|
+
if self.known_perms:
|
|
129
|
+
return colander.Mapping(unknown="raise")
|
|
130
|
+
else:
|
|
131
|
+
return colander.Mapping(unknown="preserve")
|
|
132
|
+
|
|
133
|
+
def deserialize(self, cstruct=colander.null):
|
|
134
|
+
# If permissions are not a mapping (e.g null or invalid), try deserializing
|
|
135
|
+
if not isinstance(cstruct, dict):
|
|
136
|
+
return super().deserialize(cstruct)
|
|
137
|
+
|
|
138
|
+
# If using application/merge-patch+json we need to allow null values as they
|
|
139
|
+
# represent removing a key.
|
|
140
|
+
cstruct, removed_keys = self._preprocess_null_perms(cstruct)
|
|
141
|
+
|
|
142
|
+
# If permissions are listed, check fields and produce fancy error messages
|
|
143
|
+
if self.known_perms:
|
|
144
|
+
for perm in cstruct:
|
|
145
|
+
colander.OneOf(choices=self.known_perms)(self, perm)
|
|
146
|
+
permissions = super().deserialize(cstruct)
|
|
147
|
+
|
|
148
|
+
# Else deserialize the fields that are not on the schema
|
|
149
|
+
else:
|
|
150
|
+
permissions = {}
|
|
151
|
+
perm_schema = colander.SequenceSchema(colander.SchemaNode(colander.String()))
|
|
152
|
+
for perm, principals in cstruct.items():
|
|
153
|
+
permissions[perm] = perm_schema.deserialize(principals)
|
|
154
|
+
|
|
155
|
+
return self._postprocess_null_perms(permissions, removed_keys)
|
|
156
|
+
|
|
157
|
+
def _get_node_principals(self, perm):
|
|
158
|
+
principal = colander.SchemaNode(colander.String())
|
|
159
|
+
return colander.SchemaNode(
|
|
160
|
+
colander.Sequence(), principal, name=perm, missing=colander.drop
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _preprocess_null_perms(cstruct):
|
|
165
|
+
keys = {k for k, v in cstruct.items() if v is None}
|
|
166
|
+
cleaned = {k: v for k, v in cstruct.items() if v is not None}
|
|
167
|
+
return cleaned, keys
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _postprocess_null_perms(validated, keys):
|
|
171
|
+
validated.update({k: None for k in keys})
|
|
172
|
+
return validated
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Header schemas
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class HeaderSchema(colander.MappingSchema):
|
|
179
|
+
"""Base schema used for validating and deserializing request headers."""
|
|
180
|
+
|
|
181
|
+
missing = colander.drop
|
|
182
|
+
|
|
183
|
+
if_match = HeaderQuotedInteger(name="If-Match")
|
|
184
|
+
if_none_match = HeaderQuotedInteger(name="If-None-Match")
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def schema_type():
|
|
188
|
+
return colander.Mapping(unknown="preserve")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class PatchHeaderSchema(HeaderSchema):
|
|
192
|
+
"""Header schema used with PATCH requests."""
|
|
193
|
+
|
|
194
|
+
def response_behavior_validator():
|
|
195
|
+
return colander.OneOf(["full", "light", "diff"])
|
|
196
|
+
|
|
197
|
+
response_behaviour = HeaderField(
|
|
198
|
+
colander.String(), name="Response-Behavior", validator=response_behavior_validator()
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# Querystring schemas
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class QuerySchema(colander.MappingSchema):
|
|
206
|
+
"""
|
|
207
|
+
Schema used for validating and deserializing querystrings. It will include
|
|
208
|
+
and try to guess the type of unknown fields (field filters) on deserialization.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
missing = colander.drop
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def schema_type():
|
|
215
|
+
return colander.Mapping(unknown="ignore")
|
|
216
|
+
|
|
217
|
+
def deserialize(self, cstruct=colander.null):
|
|
218
|
+
"""
|
|
219
|
+
Deserialize and validate the QuerySchema fields and try to deserialize and
|
|
220
|
+
get the native value of additional filds (field filters) that may be present
|
|
221
|
+
on the cstruct.
|
|
222
|
+
|
|
223
|
+
e.g:: ?exclude_id=a,b&deleted=true -> {'exclude_id': ['a', 'b'], deleted: True}
|
|
224
|
+
"""
|
|
225
|
+
values = {}
|
|
226
|
+
|
|
227
|
+
schema_values = super().deserialize(cstruct)
|
|
228
|
+
|
|
229
|
+
# Deserialize querystring field filters (see docstring e.g)
|
|
230
|
+
for k, v in cstruct.items():
|
|
231
|
+
# Deserialize lists used on contains_ and contains_any_ filters
|
|
232
|
+
if k.startswith("contains_"):
|
|
233
|
+
as_list = native_value(v)
|
|
234
|
+
|
|
235
|
+
if not isinstance(as_list, list):
|
|
236
|
+
values[k] = [as_list]
|
|
237
|
+
else:
|
|
238
|
+
values[k] = as_list
|
|
239
|
+
|
|
240
|
+
# Deserialize lists used on in_ and exclude_ filters
|
|
241
|
+
elif k.startswith("in_") or k.startswith("exclude_"):
|
|
242
|
+
as_list = FieldList().deserialize(v)
|
|
243
|
+
values[k] = [native_value(v) for v in as_list]
|
|
244
|
+
else:
|
|
245
|
+
values[k] = native_value(v)
|
|
246
|
+
|
|
247
|
+
values.update(schema_values)
|
|
248
|
+
return values
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class PluralQuerySchema(QuerySchema):
|
|
252
|
+
"""Querystring schema used with plural endpoints."""
|
|
253
|
+
|
|
254
|
+
_limit = QueryField(colander.Integer(), validator=positive_big_integer)
|
|
255
|
+
_sort = FieldList()
|
|
256
|
+
_token = QueryField(colander.String())
|
|
257
|
+
_since = QueryField(colander.Integer(), validator=positive_big_integer)
|
|
258
|
+
_to = QueryField(colander.Integer(), validator=positive_big_integer)
|
|
259
|
+
_before = QueryField(colander.Integer(), validator=positive_big_integer)
|
|
260
|
+
id = QueryField(colander.String())
|
|
261
|
+
last_modified = QueryField(colander.Integer(), validator=positive_big_integer)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ObjectGetQuerySchema(QuerySchema):
|
|
265
|
+
"""Querystring schema for GET object requests."""
|
|
266
|
+
|
|
267
|
+
_fields = FieldList()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class PluralGetQuerySchema(PluralQuerySchema):
|
|
271
|
+
"""Querystring schema for GET plural endpoints requests."""
|
|
272
|
+
|
|
273
|
+
_fields = FieldList()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# Body Schemas
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ObjectSchema(colander.MappingSchema):
|
|
280
|
+
@colander.deferred
|
|
281
|
+
def data(node, kwargs):
|
|
282
|
+
data = kwargs.get("data")
|
|
283
|
+
if data:
|
|
284
|
+
# Check if empty object is allowed.
|
|
285
|
+
# (e.g every schema fields have defaults)
|
|
286
|
+
try:
|
|
287
|
+
data.deserialize({})
|
|
288
|
+
except colander.Invalid:
|
|
289
|
+
pass
|
|
290
|
+
else:
|
|
291
|
+
data.default = {}
|
|
292
|
+
data.missing = colander.drop
|
|
293
|
+
return data
|
|
294
|
+
|
|
295
|
+
@colander.deferred
|
|
296
|
+
def permissions(node, kwargs):
|
|
297
|
+
def get_perms(node, kwargs):
|
|
298
|
+
return kwargs.get("permissions")
|
|
299
|
+
|
|
300
|
+
# Set if node is provided, else keep deferred. This allows binding the body
|
|
301
|
+
# on Resource first and bind permissions later.
|
|
302
|
+
# XXX: probably not necessary now that UserResource is gone.
|
|
303
|
+
return get_perms(node, kwargs) or colander.deferred(get_perms)
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def schema_type():
|
|
307
|
+
return colander.Mapping(unknown="raise")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class JsonPatchOperationSchema(colander.MappingSchema):
|
|
311
|
+
"""Single JSON Patch Operation."""
|
|
312
|
+
|
|
313
|
+
def op_validator():
|
|
314
|
+
op_values = ["test", "add", "remove", "replace", "move", "copy"]
|
|
315
|
+
return colander.OneOf(op_values)
|
|
316
|
+
|
|
317
|
+
def path_validator():
|
|
318
|
+
return colander.Regex("(/\\w*)+")
|
|
319
|
+
|
|
320
|
+
op = colander.SchemaNode(colander.String(), validator=op_validator())
|
|
321
|
+
path = colander.SchemaNode(colander.String(), validator=path_validator())
|
|
322
|
+
from_ = colander.SchemaNode(
|
|
323
|
+
colander.String(), name="from", validator=path_validator(), missing=colander.drop
|
|
324
|
+
)
|
|
325
|
+
value = colander.SchemaNode(Any(), missing=colander.drop)
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def schema_type():
|
|
329
|
+
return colander.Mapping(unknown="raise")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class JsonPatchBodySchema(colander.SequenceSchema):
|
|
333
|
+
"""Body used with JSON Patch (application/json-patch+json) as in RFC 6902."""
|
|
334
|
+
|
|
335
|
+
operations = JsonPatchOperationSchema(missing=colander.drop)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Request schemas
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class RequestSchema(colander.MappingSchema):
|
|
342
|
+
"""Base schema for kinto requests."""
|
|
343
|
+
|
|
344
|
+
@colander.deferred
|
|
345
|
+
def header(node, kwargs):
|
|
346
|
+
return kwargs.get("header")
|
|
347
|
+
|
|
348
|
+
@colander.deferred
|
|
349
|
+
def querystring(node, kwargs):
|
|
350
|
+
return kwargs.get("querystring")
|
|
351
|
+
|
|
352
|
+
def after_bind(self, node, kw):
|
|
353
|
+
# Set default bindings
|
|
354
|
+
if not self.get("header"):
|
|
355
|
+
self["header"] = HeaderSchema()
|
|
356
|
+
if not self.get("querystring"):
|
|
357
|
+
self["querystring"] = QuerySchema()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class PayloadRequestSchema(RequestSchema):
|
|
361
|
+
"""Base schema for methods that use a JSON request body."""
|
|
362
|
+
|
|
363
|
+
@colander.deferred
|
|
364
|
+
def body(node, kwargs):
|
|
365
|
+
def get_body(node, kwargs):
|
|
366
|
+
return kwargs.get("body")
|
|
367
|
+
|
|
368
|
+
# Set if node is provided, else keep deferred (and allow bindind later)
|
|
369
|
+
return get_body(node, kwargs) or colander.deferred(get_body)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class JsonPatchRequestSchema(RequestSchema):
|
|
373
|
+
"""JSON Patch (application/json-patch+json) request schema."""
|
|
374
|
+
|
|
375
|
+
body = JsonPatchBodySchema()
|
|
376
|
+
querystring = QuerySchema()
|
|
377
|
+
header = PatchHeaderSchema()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# Response schemas
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class ResponseHeaderSchema(colander.MappingSchema):
|
|
384
|
+
"""Kinto API custom response headers."""
|
|
385
|
+
|
|
386
|
+
etag = HeaderQuotedInteger(name="Etag")
|
|
387
|
+
last_modified = colander.SchemaNode(colander.String(), name="Last-Modified")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class ErrorResponseSchema(colander.MappingSchema):
|
|
391
|
+
"""Response schema used on 4xx and 5xx errors."""
|
|
392
|
+
|
|
393
|
+
body = ErrorSchema()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class NotModifiedResponseSchema(colander.MappingSchema):
|
|
397
|
+
"""Response schema used on 304 Not Modified responses."""
|
|
398
|
+
|
|
399
|
+
header = ResponseHeaderSchema()
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class ObjectResponseSchema(colander.MappingSchema):
|
|
403
|
+
"""Response schema used with sigle resource endpoints."""
|
|
404
|
+
|
|
405
|
+
header = ResponseHeaderSchema()
|
|
406
|
+
|
|
407
|
+
@colander.deferred
|
|
408
|
+
def body(node, kwargs):
|
|
409
|
+
return kwargs.get("object")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class PluralResponseSchema(colander.MappingSchema):
|
|
413
|
+
"""Response schema used with plural endpoints."""
|
|
414
|
+
|
|
415
|
+
header = ResponseHeaderSchema()
|
|
416
|
+
|
|
417
|
+
@colander.deferred
|
|
418
|
+
def body(node, kwargs):
|
|
419
|
+
resource = kwargs.get("object")["data"]
|
|
420
|
+
datalist = colander.MappingSchema()
|
|
421
|
+
datalist["data"] = colander.SequenceSchema(resource, missing=[])
|
|
422
|
+
return datalist
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class ResourceResponses:
|
|
426
|
+
"""Class that wraps and handles Resource responses."""
|
|
427
|
+
|
|
428
|
+
default_schemas = {
|
|
429
|
+
"400": ErrorResponseSchema(description="The request is invalid."),
|
|
430
|
+
"401": ErrorResponseSchema(description="The request is missing authentication headers."),
|
|
431
|
+
"403": ErrorResponseSchema(
|
|
432
|
+
description=(
|
|
433
|
+
"The user is not allowed to perform the operation, "
|
|
434
|
+
"or the resource is not accessible."
|
|
435
|
+
)
|
|
436
|
+
),
|
|
437
|
+
"406": ErrorResponseSchema(
|
|
438
|
+
description="The client doesn't accept supported responses Content-Type."
|
|
439
|
+
),
|
|
440
|
+
"412": ErrorResponseSchema(
|
|
441
|
+
description="Object was changed or deleted since value in `If-Match` header."
|
|
442
|
+
),
|
|
443
|
+
"default": ErrorResponseSchema(description="Unexpected error."),
|
|
444
|
+
}
|
|
445
|
+
default_object_schemas = {"200": ObjectResponseSchema(description="Return the target object.")}
|
|
446
|
+
default_plural_schemas = {
|
|
447
|
+
"200": PluralResponseSchema(description="Return a list of matching objects.")
|
|
448
|
+
}
|
|
449
|
+
default_get_schemas = {
|
|
450
|
+
"304": NotModifiedResponseSchema(
|
|
451
|
+
description="Response has not changed since value in If-None-Match header"
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
default_post_schemas = {
|
|
455
|
+
"200": ObjectResponseSchema(description="Return an existing object."),
|
|
456
|
+
"201": ObjectResponseSchema(description="Return a created object."),
|
|
457
|
+
"415": ErrorResponseSchema(
|
|
458
|
+
description="The client request was not sent with a correct Content-Type."
|
|
459
|
+
),
|
|
460
|
+
}
|
|
461
|
+
default_put_schemas = {
|
|
462
|
+
"201": ObjectResponseSchema(description="Return created object."),
|
|
463
|
+
"415": ErrorResponseSchema(
|
|
464
|
+
description="The client request was not sent with a correct Content-Type."
|
|
465
|
+
),
|
|
466
|
+
}
|
|
467
|
+
default_patch_schemas = {
|
|
468
|
+
"415": ErrorResponseSchema(
|
|
469
|
+
description="The client request was not sent with a correct Content-Type."
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
default_delete_schemas = {}
|
|
473
|
+
object_get_schemas = {
|
|
474
|
+
"404": ErrorResponseSchema(description="The object does not exist or was deleted.")
|
|
475
|
+
}
|
|
476
|
+
object_patch_schemas = {
|
|
477
|
+
"404": ErrorResponseSchema(description="The object does not exist or was deleted.")
|
|
478
|
+
}
|
|
479
|
+
object_delete_schemas = {
|
|
480
|
+
"404": ErrorResponseSchema(description="The object does not exist or was already deleted.")
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
def get_and_bind(self, endpoint_type, method, **kwargs):
|
|
484
|
+
"""Wrap resource colander response schemas for an endpoint and return a dict
|
|
485
|
+
of status codes mapping cloned and binded responses."""
|
|
486
|
+
|
|
487
|
+
responses = self.default_schemas.copy()
|
|
488
|
+
type_responses = getattr(self, f"default_{endpoint_type}_schemas")
|
|
489
|
+
responses.update(**type_responses)
|
|
490
|
+
|
|
491
|
+
verb_responses = f"default_{method.lower()}_schemas"
|
|
492
|
+
method_args = getattr(self, verb_responses, {})
|
|
493
|
+
responses.update(**method_args)
|
|
494
|
+
|
|
495
|
+
method_responses = f"{endpoint_type}_{method.lower()}_schemas"
|
|
496
|
+
endpoint_args = getattr(self, method_responses, {})
|
|
497
|
+
responses.update(**endpoint_args)
|
|
498
|
+
|
|
499
|
+
# Bind and clone schemas into a new dict
|
|
500
|
+
bound = {code: resp.bind(**kwargs) for code, resp in responses.items()}
|
|
501
|
+
|
|
502
|
+
return bound
|