kinto 19.5.0__py3-none-any.whl → 20.0.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.
Potentially problematic release.
This version of kinto might be problematic. Click here for more details.
- kinto/__main__.py +0 -17
- kinto/config/kinto.tpl +0 -13
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +3 -3
- 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/initialization.py +0 -14
- kinto/core/openapi.py +2 -3
- kinto/core/resource/viewset.py +1 -1
- kinto/core/storage/postgresql/pool.py +1 -1
- kinto/core/testing.py +1 -1
- kinto/core/utils.py +3 -2
- kinto/core/views/batch.py +1 -1
- kinto/core/views/errors.py +2 -0
- kinto/core/views/openapi.py +1 -1
- kinto/plugins/accounts/__init__.py +2 -19
- kinto/plugins/accounts/authentication.py +8 -54
- kinto/plugins/accounts/utils.py +0 -133
- kinto/plugins/accounts/{views/__init__.py → views.py} +7 -62
- kinto/plugins/admin/VERSION +1 -1
- kinto/plugins/admin/build/VERSION +1 -1
- kinto/plugins/admin/build/assets/asn1-EdZsLKOL.js +1 -0
- kinto/plugins/admin/build/assets/index-Bq62Gei8.js +165 -0
- kinto/plugins/admin/build/assets/{index-BdpYyatM.css → index-Cs7JVwIg.css} +1 -1
- kinto/plugins/admin/build/assets/javascript-qCveANmP.js +1 -0
- kinto/plugins/admin/build/assets/mllike-CXdrOF99.js +1 -0
- kinto/plugins/admin/build/assets/sql-D0XecflT.js +1 -0
- kinto/plugins/admin/build/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- kinto/plugins/admin/build/index.html +2 -2
- kinto/plugins/flush.py +1 -1
- kinto/plugins/openid/views.py +1 -1
- kinto/views/contribute.py +14 -13
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/METADATA +2 -6
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/RECORD +57 -42
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/WHEEL +1 -1
- kinto/plugins/accounts/mails.py +0 -96
- kinto/plugins/accounts/views/validation.py +0 -136
- kinto/plugins/admin/build/assets/asn1-CGOzndHr.js +0 -1
- kinto/plugins/admin/build/assets/index-n-QM_iZE.js +0 -165
- kinto/plugins/admin/build/assets/javascript-iSgyE4tI.js +0 -1
- kinto/plugins/admin/build/assets/mllike-C_8OmSiT.js +0 -1
- kinto/plugins/admin/build/assets/sql-C4g8LzGK.js +0 -1
- kinto/plugins/admin/build/assets/ttcn-cfg-BIkV9KBc.js +0 -1
- kinto/plugins/quotas/__init__.py +0 -21
- kinto/plugins/quotas/listener.py +0 -226
- kinto/plugins/quotas/scripts.py +0 -80
- kinto/plugins/quotas/utils.py +0 -7
- kinto/scripts.py +0 -41
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/LICENSE +0 -0
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/entry_points.txt +0 -0
- {kinto-19.5.0.dist-info → kinto-20.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
3
|
+
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
import copy
|
|
5
|
+
import functools
|
|
6
|
+
import itertools
|
|
7
|
+
|
|
8
|
+
from pyramid.exceptions import PredicateMismatch
|
|
9
|
+
from pyramid.httpexceptions import (
|
|
10
|
+
HTTPException,
|
|
11
|
+
HTTPMethodNotAllowed,
|
|
12
|
+
HTTPNotAcceptable,
|
|
13
|
+
HTTPUnsupportedMediaType,
|
|
14
|
+
)
|
|
15
|
+
from pyramid.security import NO_PERMISSION_REQUIRED
|
|
16
|
+
|
|
17
|
+
from kinto.core.cornice.cors import (
|
|
18
|
+
CORS_PARAMETERS,
|
|
19
|
+
apply_cors_post_request,
|
|
20
|
+
get_cors_preflight_view,
|
|
21
|
+
get_cors_validator,
|
|
22
|
+
)
|
|
23
|
+
from kinto.core.cornice.errors import Errors
|
|
24
|
+
from kinto.core.cornice.service import decorate_view
|
|
25
|
+
from kinto.core.cornice.util import (
|
|
26
|
+
content_type_matches,
|
|
27
|
+
current_service,
|
|
28
|
+
is_string,
|
|
29
|
+
match_accept_header,
|
|
30
|
+
match_content_type_header,
|
|
31
|
+
to_list,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_fallback_view(service):
|
|
36
|
+
"""Fallback view for a given service, called when nothing else matches.
|
|
37
|
+
|
|
38
|
+
This method provides the view logic to be executed when the request
|
|
39
|
+
does not match any explicitly-defined view. Its main responsibility
|
|
40
|
+
is to produce an accurate error response, such as HTTPMethodNotAllowed,
|
|
41
|
+
HTTPNotAcceptable or HTTPUnsupportedMediaType.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def _fallback_view(request):
|
|
45
|
+
# Maybe we failed to match any definitions for the request method?
|
|
46
|
+
if request.method not in service.defined_methods:
|
|
47
|
+
response = HTTPMethodNotAllowed()
|
|
48
|
+
response.allow = service.defined_methods
|
|
49
|
+
raise response
|
|
50
|
+
# Maybe we failed to match an acceptable content-type?
|
|
51
|
+
# First search all the definitions to find the acceptable types.
|
|
52
|
+
# XXX: precalculate this like the defined_methods list?
|
|
53
|
+
acceptable = []
|
|
54
|
+
supported_contenttypes = []
|
|
55
|
+
for method, _, args in service.definitions:
|
|
56
|
+
if method != request.method:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if "accept" in args:
|
|
60
|
+
acceptable.extend(service.get_acceptable(method, filter_callables=True))
|
|
61
|
+
acceptable.extend(request.info.get("acceptable", []))
|
|
62
|
+
acceptable = list(set(acceptable))
|
|
63
|
+
|
|
64
|
+
# Now check if that was actually the source of the problem.
|
|
65
|
+
if not request.accept.acceptable_offers(offers=acceptable):
|
|
66
|
+
request.errors.add(
|
|
67
|
+
"header",
|
|
68
|
+
"Accept",
|
|
69
|
+
"Accept header should be one of {0}".format(acceptable).encode("ascii"),
|
|
70
|
+
)
|
|
71
|
+
request.errors.status = HTTPNotAcceptable.code
|
|
72
|
+
error = service.error_handler(request)
|
|
73
|
+
raise error
|
|
74
|
+
|
|
75
|
+
if "content_type" in args:
|
|
76
|
+
supported_contenttypes.extend(
|
|
77
|
+
service.get_contenttypes(method, filter_callables=True)
|
|
78
|
+
)
|
|
79
|
+
supported_contenttypes.extend(request.info.get("supported_contenttypes", []))
|
|
80
|
+
supported_contenttypes = list(set(supported_contenttypes))
|
|
81
|
+
|
|
82
|
+
# Now check if that was actually the source of the problem.
|
|
83
|
+
if not content_type_matches(request, supported_contenttypes):
|
|
84
|
+
request.errors.add(
|
|
85
|
+
"header",
|
|
86
|
+
"Content-Type",
|
|
87
|
+
"Content-Type header should be one of {0}".format(
|
|
88
|
+
supported_contenttypes
|
|
89
|
+
).encode("ascii"),
|
|
90
|
+
)
|
|
91
|
+
request.errors.status = HTTPUnsupportedMediaType.code
|
|
92
|
+
error = service.error_handler(request)
|
|
93
|
+
raise error
|
|
94
|
+
|
|
95
|
+
# In the absence of further information about what went wrong,
|
|
96
|
+
# let upstream deal with the mismatch.
|
|
97
|
+
|
|
98
|
+
# After "custom predicates" feature has been added there is no need in
|
|
99
|
+
# this line. Instead requests will be filtered by "custom predicates"
|
|
100
|
+
# feature filter and exception "404 Not found" error will be raised. In
|
|
101
|
+
# order to avoid unpredictable cases, we left this line in place and
|
|
102
|
+
# excluded it from coverage.
|
|
103
|
+
raise PredicateMismatch(service.name) # pragma: no cover
|
|
104
|
+
|
|
105
|
+
return _fallback_view
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def apply_filters(request, response):
|
|
109
|
+
if request.matched_route is not None:
|
|
110
|
+
# do some sanity checking on the response using filters
|
|
111
|
+
service = current_service(request)
|
|
112
|
+
if service is not None:
|
|
113
|
+
kwargs, ob = getattr(request, "cornice_args", ({}, None))
|
|
114
|
+
for _filter in kwargs.get("filters", []):
|
|
115
|
+
if is_string(_filter) and ob is not None:
|
|
116
|
+
_filter = getattr(ob, _filter)
|
|
117
|
+
try:
|
|
118
|
+
response = _filter(response, request)
|
|
119
|
+
except TypeError:
|
|
120
|
+
response = _filter(response)
|
|
121
|
+
if service.cors_enabled:
|
|
122
|
+
apply_cors_post_request(service, request, response)
|
|
123
|
+
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def handle_exceptions(exc, request):
|
|
128
|
+
# At this stage, the checks done by the validators had been removed because
|
|
129
|
+
# a new response started (the exception), so we need to do that again.
|
|
130
|
+
if not isinstance(exc, HTTPException):
|
|
131
|
+
raise
|
|
132
|
+
request.info["cors_checked"] = False
|
|
133
|
+
return apply_filters(request, exc)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_nosniff_header(request, response):
|
|
137
|
+
"""IE has some rather unfortunately content-type-sniffing behaviour
|
|
138
|
+
that can be used to trigger XSS attacks via a JSON API, as described here:
|
|
139
|
+
|
|
140
|
+
* http://blog.watchfire.com/wfblog/2011/10/json-based-xss-exploitation.html
|
|
141
|
+
* https://superevr.com/blog/2012/exploiting-xss-in-ajax-web-applications/
|
|
142
|
+
|
|
143
|
+
Make cornice safe-by-default against this attack by including the header.
|
|
144
|
+
"""
|
|
145
|
+
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def wrap_request(event):
|
|
149
|
+
"""Adds a "validated" dict, a custom "errors" object and an "info" dict to
|
|
150
|
+
the request object if they don't already exists
|
|
151
|
+
"""
|
|
152
|
+
request = event.request
|
|
153
|
+
request.add_response_callback(apply_filters)
|
|
154
|
+
request.add_response_callback(add_nosniff_header)
|
|
155
|
+
|
|
156
|
+
if not hasattr(request, "validated"):
|
|
157
|
+
setattr(request, "validated", {})
|
|
158
|
+
|
|
159
|
+
if not hasattr(request, "errors"):
|
|
160
|
+
if request.registry.settings.get("available_languages"):
|
|
161
|
+
setattr(request, "errors", Errors(localizer=request.localizer))
|
|
162
|
+
else:
|
|
163
|
+
setattr(request, "errors", Errors())
|
|
164
|
+
|
|
165
|
+
if not hasattr(request, "info"):
|
|
166
|
+
setattr(request, "info", {})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def register_service_views(config, service):
|
|
170
|
+
"""Register the routes of the given service into the pyramid router.
|
|
171
|
+
|
|
172
|
+
:param config: the pyramid configuration object that will be populated.
|
|
173
|
+
:param service: the service object containing the definitions
|
|
174
|
+
"""
|
|
175
|
+
route_name = service.name
|
|
176
|
+
existing_route = service.pyramid_route
|
|
177
|
+
prefix = config.route_prefix or ""
|
|
178
|
+
services = config.registry.cornice_services
|
|
179
|
+
if existing_route:
|
|
180
|
+
route_name = existing_route
|
|
181
|
+
services["__cornice" + existing_route] = service
|
|
182
|
+
else:
|
|
183
|
+
services[prefix + service.path] = service
|
|
184
|
+
|
|
185
|
+
# before doing anything else, register a view for the OPTIONS method
|
|
186
|
+
# if we need to
|
|
187
|
+
if service.cors_enabled and "OPTIONS" not in service.defined_methods:
|
|
188
|
+
service.add_view(
|
|
189
|
+
"options", view=get_cors_preflight_view(service), permission=NO_PERMISSION_REQUIRED
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# register the fallback view, which takes care of returning good error
|
|
193
|
+
# messages to the user-agent
|
|
194
|
+
cors_validator = get_cors_validator(service)
|
|
195
|
+
|
|
196
|
+
# Cornice-specific arguments that pyramid does not know about
|
|
197
|
+
cornice_parameters = (
|
|
198
|
+
"filters",
|
|
199
|
+
"validators",
|
|
200
|
+
"schema",
|
|
201
|
+
"klass",
|
|
202
|
+
"error_handler",
|
|
203
|
+
"deserializer",
|
|
204
|
+
) + CORS_PARAMETERS
|
|
205
|
+
|
|
206
|
+
# 1. register route
|
|
207
|
+
|
|
208
|
+
route_args = {}
|
|
209
|
+
|
|
210
|
+
if hasattr(service, "factory"):
|
|
211
|
+
route_args["factory"] = service.factory
|
|
212
|
+
|
|
213
|
+
routes = config.get_predlist("route")
|
|
214
|
+
for predicate in routes.sorter.names:
|
|
215
|
+
# Do not let the custom predicates handle validation of Header Accept,
|
|
216
|
+
# which will pass it through to pyramid. It is handled by
|
|
217
|
+
# _fallback_view(), because it allows callable.
|
|
218
|
+
if predicate == "accept":
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
if hasattr(service, predicate):
|
|
222
|
+
route_args[predicate] = getattr(service, predicate)
|
|
223
|
+
|
|
224
|
+
# register route when not using exiting pyramid routes
|
|
225
|
+
if not existing_route:
|
|
226
|
+
config.add_route(route_name, service.path, **route_args)
|
|
227
|
+
|
|
228
|
+
# 2. register view(s)
|
|
229
|
+
|
|
230
|
+
for method, view, args in service.definitions:
|
|
231
|
+
args = copy.copy(args) # make a copy of the dict to not modify it
|
|
232
|
+
# Deepcopy only the params we're possibly passing on to pyramid
|
|
233
|
+
# (Some of those in cornice_parameters, e.g. ``schema``, may contain
|
|
234
|
+
# unpickleable values.)
|
|
235
|
+
for item in args:
|
|
236
|
+
if item not in cornice_parameters:
|
|
237
|
+
args[item] = copy.deepcopy(args[item])
|
|
238
|
+
|
|
239
|
+
args["request_method"] = method
|
|
240
|
+
|
|
241
|
+
if service.cors_enabled:
|
|
242
|
+
args["validators"].insert(0, cors_validator)
|
|
243
|
+
|
|
244
|
+
decorated_view = decorate_view(view, dict(args), method, route_args)
|
|
245
|
+
|
|
246
|
+
for item in cornice_parameters:
|
|
247
|
+
if item in args:
|
|
248
|
+
del args[item]
|
|
249
|
+
|
|
250
|
+
# filter predicates defined on Resource
|
|
251
|
+
route_predicates = config.get_predlist("route").sorter.names
|
|
252
|
+
view_predicates = config.get_predlist("view").sorter.names
|
|
253
|
+
for pred in set(route_predicates).difference(view_predicates):
|
|
254
|
+
if pred in args:
|
|
255
|
+
args.pop(pred)
|
|
256
|
+
|
|
257
|
+
# pop and compute predicates which get passed through to Pyramid 1:1
|
|
258
|
+
|
|
259
|
+
predicate_definitions = _pop_complex_predicates(args)
|
|
260
|
+
|
|
261
|
+
if predicate_definitions:
|
|
262
|
+
empty_contenttype = [({"kind": "content_type", "value": ""},)]
|
|
263
|
+
for predicate_list in predicate_definitions + empty_contenttype:
|
|
264
|
+
args = dict(args) # make a copy of the dict to not modify it
|
|
265
|
+
|
|
266
|
+
# prepare view args by evaluating complex predicates
|
|
267
|
+
_mungle_view_args(args, predicate_list)
|
|
268
|
+
|
|
269
|
+
# We register the same view multiple times with different
|
|
270
|
+
# accept / content_type / custom_predicates arguments
|
|
271
|
+
config.add_view(view=decorated_view, route_name=route_name, **args)
|
|
272
|
+
|
|
273
|
+
else:
|
|
274
|
+
# it is a simple view, we don't need to loop on the definitions
|
|
275
|
+
# and just add it one time.
|
|
276
|
+
config.add_view(view=decorated_view, route_name=route_name, **args)
|
|
277
|
+
|
|
278
|
+
if service.definitions:
|
|
279
|
+
# Add the fallback view last
|
|
280
|
+
config.add_view(
|
|
281
|
+
view=get_fallback_view(service),
|
|
282
|
+
route_name=route_name,
|
|
283
|
+
permission=NO_PERMISSION_REQUIRED,
|
|
284
|
+
require_csrf=False,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _pop_complex_predicates(args):
|
|
289
|
+
"""
|
|
290
|
+
Compute the cartesian product of "accept" and "content_type"
|
|
291
|
+
fields to establish all possible predicate combinations.
|
|
292
|
+
|
|
293
|
+
.. seealso::
|
|
294
|
+
|
|
295
|
+
https://github.com/mozilla-services/cornice/pull/91#discussion_r3441384
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
# pop and prepare individual predicate lists
|
|
299
|
+
accept_list = _pop_predicate_definition(args, "accept")
|
|
300
|
+
content_type_list = _pop_predicate_definition(args, "content_type")
|
|
301
|
+
|
|
302
|
+
# compute cartesian product of prepared lists, additionally
|
|
303
|
+
# remove empty elements of input and output lists
|
|
304
|
+
product_input = filter(None, [accept_list, content_type_list])
|
|
305
|
+
|
|
306
|
+
# In Python 3, the filter() function returns an iterator, not a list.
|
|
307
|
+
# http://getpython3.com/diveintopython3/ \
|
|
308
|
+
# porting-code-to-python-3-with-2to3.html#filter
|
|
309
|
+
predicate_product = list(filter(None, itertools.product(*product_input)))
|
|
310
|
+
|
|
311
|
+
return predicate_product
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _pop_predicate_definition(args, kind):
|
|
315
|
+
"""
|
|
316
|
+
Build a dictionary enriched by "kind" of predicate definition list.
|
|
317
|
+
This is required for evaluation in ``_mungle_view_args``.
|
|
318
|
+
"""
|
|
319
|
+
values = to_list(args.pop(kind, ()))
|
|
320
|
+
# In much the same way as filter(), the map() function [in Python 3] now
|
|
321
|
+
# returns an iterator. (In Python 2, it returned a list.)
|
|
322
|
+
# http://getpython3.com/diveintopython3/ \
|
|
323
|
+
# porting-code-to-python-3-with-2to3.html#map
|
|
324
|
+
values = list(map(lambda value: {"kind": kind, "value": value}, values))
|
|
325
|
+
return values
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _mungle_view_args(args, predicate_list):
|
|
329
|
+
"""
|
|
330
|
+
Prepare view args by evaluating complex predicates
|
|
331
|
+
which get passed through to Pyramid 1:1.
|
|
332
|
+
Also resolve predicate definitions passed as callables.
|
|
333
|
+
|
|
334
|
+
.. seealso::
|
|
335
|
+
|
|
336
|
+
https://github.com/mozilla-services/cornice/pull/91#discussion_r3441384
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
# map kind of argument value to function for resolving callables
|
|
340
|
+
callable_map = {
|
|
341
|
+
"accept": match_accept_header,
|
|
342
|
+
"content_type": match_content_type_header,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# iterate and resolve all predicates
|
|
346
|
+
for predicate_entry in predicate_list:
|
|
347
|
+
kind = predicate_entry["kind"]
|
|
348
|
+
value = predicate_entry["value"]
|
|
349
|
+
|
|
350
|
+
# we need to build a custom predicate if argument value is a callable
|
|
351
|
+
predicates = args.get("custom_predicates", [])
|
|
352
|
+
if callable(value):
|
|
353
|
+
func = callable_map[kind]
|
|
354
|
+
predicate_checker = functools.partial(func, value)
|
|
355
|
+
predicates.append(predicate_checker)
|
|
356
|
+
args["custom_predicates"] = predicates
|
|
357
|
+
else:
|
|
358
|
+
# otherwise argument value is just a scalar
|
|
359
|
+
args[kind] = value
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def register_resource_views(config, resource):
|
|
363
|
+
"""Register a resource and it's views.
|
|
364
|
+
|
|
365
|
+
:param config:
|
|
366
|
+
The pyramid configuration object that will be populated.
|
|
367
|
+
:param resource:
|
|
368
|
+
The resource class containing the definitions
|
|
369
|
+
"""
|
|
370
|
+
services = resource._services
|
|
371
|
+
|
|
372
|
+
for service in services.values():
|
|
373
|
+
config.add_cornice_service(service)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from pyramid import httpexceptions as exc
|
|
2
|
+
from pyramid.renderers import JSON
|
|
3
|
+
from pyramid.response import Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def bytes_adapter(obj, request):
|
|
7
|
+
"""Convert bytes objects to strings for json error renderer."""
|
|
8
|
+
if isinstance(obj, bytes):
|
|
9
|
+
return obj.decode("utf8")
|
|
10
|
+
return obj
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JSONError(exc.HTTPError):
|
|
14
|
+
def __init__(self, serializer, serializer_kw, errors, status=400):
|
|
15
|
+
body = {"status": "error", "errors": errors}
|
|
16
|
+
Response.__init__(self, serializer(body, **serializer_kw))
|
|
17
|
+
self.status = status
|
|
18
|
+
self.content_type = "application/json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CorniceRenderer(JSON):
|
|
22
|
+
"""We implement JSON serialization by extending Pyramid's default
|
|
23
|
+
JSON rendering machinery using our own custom Content-Type logic `[1]`_.
|
|
24
|
+
|
|
25
|
+
This allows developers to config the JSON renderer using Pyramid's
|
|
26
|
+
configuration machinery `[2]`_.
|
|
27
|
+
|
|
28
|
+
.. _`[1]`: https://github.com/mozilla-services/cornice/pull/116 \
|
|
29
|
+
#issuecomment-14355865
|
|
30
|
+
.. _`[2]`: http://pyramid.readthedocs.io/en/latest/narr/renderers.html \
|
|
31
|
+
#serializing-custom-objects
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
acceptable = ("application/json", "text/plain")
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args, **kwargs):
|
|
37
|
+
"""Adds a `bytes` adapter by default."""
|
|
38
|
+
super(CorniceRenderer, self).__init__(*args, **kwargs)
|
|
39
|
+
self.add_adapter(bytes, bytes_adapter)
|
|
40
|
+
|
|
41
|
+
def render_errors(self, request):
|
|
42
|
+
"""Returns an HTTPError with the given status and message.
|
|
43
|
+
|
|
44
|
+
The HTTP error content type is "application/json"
|
|
45
|
+
"""
|
|
46
|
+
default = self._make_default(request)
|
|
47
|
+
serializer_kw = self.kw.copy()
|
|
48
|
+
serializer_kw["default"] = default
|
|
49
|
+
return JSONError(
|
|
50
|
+
serializer=self.serializer,
|
|
51
|
+
serializer_kw=serializer_kw,
|
|
52
|
+
errors=request.errors,
|
|
53
|
+
status=request.errors.status,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def render(self, value, system):
|
|
57
|
+
"""Extends the default `_render` function of the pyramid JSON renderer.
|
|
58
|
+
|
|
59
|
+
Compared to the default `pyramid.renderers.JSON` renderer:
|
|
60
|
+
1. Overrides the response with an empty string and
|
|
61
|
+
no Content-Type in case of HTTP 204.
|
|
62
|
+
2. Overrides the default behavior of Content-Type handling,
|
|
63
|
+
forcing the use of `acceptable_offers`, instead of letting
|
|
64
|
+
the user specify the Content-Type manually.
|
|
65
|
+
TODO: maybe explain this a little better
|
|
66
|
+
"""
|
|
67
|
+
request = system.get("request")
|
|
68
|
+
if request is not None:
|
|
69
|
+
response = request.response
|
|
70
|
+
|
|
71
|
+
# Do not return content with ``204 No Content``
|
|
72
|
+
if response.status_code == 204:
|
|
73
|
+
response.content_type = None
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
ctypes = request.accept.acceptable_offers(offers=self.acceptable)
|
|
77
|
+
if not ctypes:
|
|
78
|
+
ctypes = [(self.acceptable[0], 1.0)]
|
|
79
|
+
response.content_type = ctypes[0][0]
|
|
80
|
+
default = self._make_default(request)
|
|
81
|
+
return self.serializer(value, default=default, **self.kw)
|
|
82
|
+
|
|
83
|
+
def __call__(self, info):
|
|
84
|
+
"""Overrides the default behavior of `pyramid.renderers.JSON`.
|
|
85
|
+
|
|
86
|
+
Uses a public `render()` method instead of defining render inside
|
|
87
|
+
`__call__`, to let the user extend it if necessary.
|
|
88
|
+
"""
|
|
89
|
+
return self.render
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
4
|
+
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
import functools
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
import venusian
|
|
9
|
+
|
|
10
|
+
from kinto.core.cornice import Service
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resource(depth=2, **kw):
|
|
14
|
+
"""Class decorator to declare resources.
|
|
15
|
+
|
|
16
|
+
All the methods of this class named by the name of HTTP resources
|
|
17
|
+
will be used as such. You can also prefix them by ``"collection_"`` and
|
|
18
|
+
they will be treated as HTTP methods for the given collection path
|
|
19
|
+
(collection_path), if any.
|
|
20
|
+
|
|
21
|
+
:param depth:
|
|
22
|
+
Which frame should be looked in default 2.
|
|
23
|
+
|
|
24
|
+
:param kw:
|
|
25
|
+
Keyword arguments configuring the resource.
|
|
26
|
+
|
|
27
|
+
Here is an example::
|
|
28
|
+
|
|
29
|
+
@resource(collection_path='/users', path='/users/{id}')
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def wrapper(klass):
|
|
33
|
+
return add_resource(klass, depth, **kw)
|
|
34
|
+
|
|
35
|
+
return wrapper
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def add_resource(klass, depth=2, **kw):
|
|
39
|
+
"""Function to declare resources of a Class.
|
|
40
|
+
|
|
41
|
+
All the methods of this class named by the name of HTTP resources
|
|
42
|
+
will be used as such. You can also prefix them by ``"collection_"`` and
|
|
43
|
+
they will be treated as HTTP methods for the given collection path
|
|
44
|
+
(collection_path), if any.
|
|
45
|
+
|
|
46
|
+
:param klass:
|
|
47
|
+
The class (resource) on which to register the service.
|
|
48
|
+
|
|
49
|
+
:param depth:
|
|
50
|
+
Which frame should be looked in default 2.
|
|
51
|
+
|
|
52
|
+
:param kw:
|
|
53
|
+
Keyword arguments configuring the resource.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Here is an example:
|
|
57
|
+
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
class User(object):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
add_resource(User, collection_path='/users', path='/users/{id}')
|
|
64
|
+
|
|
65
|
+
Alternatively if you want to reuse your existing pyramid routes:
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
class User(object):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
add_resource(User, collection_pyramid_route='users',
|
|
73
|
+
pyramid_route='user')
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
services = {}
|
|
78
|
+
|
|
79
|
+
if ("collection_pyramid_route" in kw or "pyramid_route" in kw) and (
|
|
80
|
+
"collection_path" in kw or "path" in kw
|
|
81
|
+
):
|
|
82
|
+
raise ValueError("You use either paths or route names, not both")
|
|
83
|
+
|
|
84
|
+
if "collection_path" in kw:
|
|
85
|
+
if kw["collection_path"] == kw["path"]:
|
|
86
|
+
msg = "Warning: collection_path and path are not distinct."
|
|
87
|
+
warnings.warn(msg)
|
|
88
|
+
|
|
89
|
+
prefixes = ("", "collection_")
|
|
90
|
+
else:
|
|
91
|
+
prefixes = ("",)
|
|
92
|
+
|
|
93
|
+
if "collection_pyramid_route" in kw:
|
|
94
|
+
if kw["collection_pyramid_route"] == kw["pyramid_route"]:
|
|
95
|
+
msg = "Warning: collection_pyramid_route and pyramid_route are not distinct."
|
|
96
|
+
warnings.warn(msg)
|
|
97
|
+
|
|
98
|
+
prefixes = ("", "collection_")
|
|
99
|
+
|
|
100
|
+
for prefix in prefixes:
|
|
101
|
+
# get clean view arguments
|
|
102
|
+
service_args = {}
|
|
103
|
+
for k in list(kw):
|
|
104
|
+
if k.startswith("collection_"):
|
|
105
|
+
if prefix == "collection_":
|
|
106
|
+
service_args[k[len(prefix) :]] = kw[k]
|
|
107
|
+
elif k not in service_args:
|
|
108
|
+
service_args[k] = kw[k]
|
|
109
|
+
|
|
110
|
+
# auto-wire klass as its own view factory, unless one
|
|
111
|
+
# is explicitly declared.
|
|
112
|
+
if "factory" not in kw:
|
|
113
|
+
service_args["factory"] = klass
|
|
114
|
+
|
|
115
|
+
# create service
|
|
116
|
+
service_name = service_args.pop("name", None) or klass.__name__.lower()
|
|
117
|
+
service_name = prefix + service_name
|
|
118
|
+
service = services[service_name] = Service(name=service_name, depth=depth, **service_args)
|
|
119
|
+
# ensure the service comes with the same properties as the wrapped
|
|
120
|
+
# resource
|
|
121
|
+
functools.update_wrapper(service, klass)
|
|
122
|
+
|
|
123
|
+
# initialize views
|
|
124
|
+
for verb in ("get", "post", "put", "delete", "options", "patch"):
|
|
125
|
+
view_attr = prefix + verb
|
|
126
|
+
meth = getattr(klass, view_attr, None)
|
|
127
|
+
|
|
128
|
+
if meth is not None:
|
|
129
|
+
# if the method has a __views__ arguments, then it had
|
|
130
|
+
# been decorated by a @view decorator. get back the name of
|
|
131
|
+
# the decorated method so we can register it properly
|
|
132
|
+
views = getattr(meth, "__views__", [])
|
|
133
|
+
if views:
|
|
134
|
+
for view_args in views:
|
|
135
|
+
service.add_view(verb, view_attr, klass=klass, **view_args)
|
|
136
|
+
else:
|
|
137
|
+
service.add_view(verb, view_attr, klass=klass)
|
|
138
|
+
|
|
139
|
+
setattr(klass, "_services", services)
|
|
140
|
+
|
|
141
|
+
def callback(context, name, ob):
|
|
142
|
+
# get the callbacks registered by the inner services
|
|
143
|
+
# and call them from here when the @resource classes are being
|
|
144
|
+
# scanned by venusian.
|
|
145
|
+
for service in services.values():
|
|
146
|
+
config = context.config.with_package(info.module)
|
|
147
|
+
config.add_cornice_service(service)
|
|
148
|
+
|
|
149
|
+
info = venusian.attach(klass, callback, category="pyramid", depth=depth)
|
|
150
|
+
|
|
151
|
+
return klass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def view(**kw):
|
|
155
|
+
"""Method decorator to store view arguments when defining a resource with
|
|
156
|
+
the @resource class decorator
|
|
157
|
+
|
|
158
|
+
:param kw:
|
|
159
|
+
Keyword arguments configuring the view.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def wrapper(func):
|
|
163
|
+
return add_view(func, **kw)
|
|
164
|
+
|
|
165
|
+
return wrapper
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def add_view(func, **kw):
|
|
169
|
+
"""Method to store view arguments when defining a resource with
|
|
170
|
+
the add_resource class method
|
|
171
|
+
|
|
172
|
+
:param func:
|
|
173
|
+
The func to hook to
|
|
174
|
+
|
|
175
|
+
:param kw:
|
|
176
|
+
Keyword arguments configuring the view.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
|
|
180
|
+
.. code-block:: python
|
|
181
|
+
|
|
182
|
+
class User(object):
|
|
183
|
+
|
|
184
|
+
def __init__(self, request):
|
|
185
|
+
self.request = request
|
|
186
|
+
|
|
187
|
+
def collection_get(self):
|
|
188
|
+
return {'users': _USERS.keys()}
|
|
189
|
+
|
|
190
|
+
def get(self):
|
|
191
|
+
return _USERS.get(int(self.request.matchdict['id']))
|
|
192
|
+
|
|
193
|
+
add_view(User.get, renderer='json')
|
|
194
|
+
add_resource(User, collection_path='/users', path='/users/{id}')
|
|
195
|
+
"""
|
|
196
|
+
# XXX needed in py2 to set on instancemethod
|
|
197
|
+
if hasattr(func, "__func__"): # pragma: no cover
|
|
198
|
+
func = func.__func__
|
|
199
|
+
# store view argument to use them later in @resource
|
|
200
|
+
views = getattr(func, "__views__", None)
|
|
201
|
+
if views is None:
|
|
202
|
+
views = []
|
|
203
|
+
setattr(func, "__views__", views)
|
|
204
|
+
views.append(kw)
|
|
205
|
+
return func
|