kinto 18.1.0__py3-none-any.whl → 20.4.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/__init__.py +1 -0
- kinto/__main__.py +1 -19
- kinto/config/kinto.tpl +5 -15
- kinto/contribute.json +27 -0
- kinto/core/__init__.py +21 -8
- 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/errors.py +6 -4
- kinto/core/initialization.py +129 -59
- kinto/core/metrics.py +93 -0
- kinto/core/openapi.py +2 -3
- kinto/core/permission/memory.py +3 -2
- kinto/core/permission/postgresql/__init__.py +9 -9
- kinto/core/permission/testing.py +6 -0
- kinto/core/resource/__init__.py +9 -4
- kinto/core/resource/schema.py +1 -2
- kinto/core/resource/viewset.py +1 -1
- kinto/core/statsd.py +1 -63
- kinto/core/storage/__init__.py +15 -0
- kinto/core/storage/memory.py +20 -3
- kinto/core/storage/postgresql/__init__.py +31 -1
- kinto/core/storage/postgresql/client.py +2 -2
- kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
- kinto/core/storage/postgresql/pool.py +1 -1
- kinto/core/storage/postgresql/schema.sql +3 -2
- kinto/core/storage/testing.py +41 -1
- kinto/core/testing.py +6 -2
- kinto/core/utils.py +14 -4
- kinto/core/views/batch.py +1 -1
- kinto/core/views/errors.py +4 -3
- kinto/core/views/openapi.py +1 -1
- kinto/plugins/accounts/__init__.py +3 -21
- 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 -0
- kinto/plugins/admin/build/assets/asn1-EdZsLKOL.js +1 -0
- kinto/plugins/admin/build/assets/clojure-BMjYHr_A.js +1 -0
- kinto/plugins/admin/build/assets/css-BnMrqG3P.js +1 -0
- kinto/plugins/admin/build/assets/index-Cs7JVwIg.css +6 -0
- kinto/plugins/admin/build/assets/index-CylsivYB.js +165 -0
- kinto/plugins/admin/build/assets/javascript-qCveANmP.js +1 -0
- kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
- kinto/plugins/admin/build/assets/mllike-CXdrOF99.js +1 -0
- kinto/plugins/admin/build/assets/python-BuPzkPfP.js +1 -0
- kinto/plugins/admin/build/assets/rpm-CTu-6PCP.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 +18 -0
- kinto/plugins/default_bucket/__init__.py +1 -2
- kinto/plugins/flush.py +2 -2
- kinto/plugins/history/__init__.py +15 -6
- kinto/plugins/history/listener.py +68 -5
- kinto/plugins/openid/views.py +1 -1
- kinto/plugins/prometheus.py +203 -0
- kinto/plugins/statsd.py +78 -0
- kinto/views/contribute.py +14 -13
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/METADATA +31 -32
- kinto-20.4.0.dist-info/RECORD +149 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/WHEEL +1 -1
- kinto/plugins/accounts/mails.py +0 -96
- kinto/plugins/accounts/views/validation.py +0 -136
- kinto/plugins/quotas/__init__.py +0 -22
- 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-18.1.0.dist-info/RECORD +0 -116
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/entry_points.txt +0 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info/licenses}/LICENSE +0 -0
- {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,641 @@
|
|
|
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 functools
|
|
5
|
+
|
|
6
|
+
import venusian
|
|
7
|
+
from pyramid.exceptions import ConfigurationError
|
|
8
|
+
from pyramid.interfaces import IRendererFactory
|
|
9
|
+
from pyramid.response import Response
|
|
10
|
+
|
|
11
|
+
from kinto.core.cornice.util import func_name, is_string, to_list
|
|
12
|
+
from kinto.core.cornice.validators import (
|
|
13
|
+
DEFAULT_FILTERS,
|
|
14
|
+
DEFAULT_VALIDATORS,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SERVICES = []
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def clear_services():
|
|
22
|
+
SERVICES[:] = []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_services(names=None, exclude=None):
|
|
26
|
+
def _keep(service):
|
|
27
|
+
if exclude is not None and service.name in exclude:
|
|
28
|
+
# excluded !
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
# in white list or no white list provided
|
|
32
|
+
return names is None or service.name in names
|
|
33
|
+
|
|
34
|
+
return [service for service in SERVICES if _keep(service)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Service(object):
|
|
38
|
+
"""Contains a service definition (in the definition attribute).
|
|
39
|
+
|
|
40
|
+
A service is composed of a path and many potential methods, associated
|
|
41
|
+
with context.
|
|
42
|
+
|
|
43
|
+
All the class attributes defined in this class or in children are
|
|
44
|
+
considered default values.
|
|
45
|
+
|
|
46
|
+
:param name:
|
|
47
|
+
The name of the service. Should be unique among all the services.
|
|
48
|
+
|
|
49
|
+
:param path:
|
|
50
|
+
The path the service is available at. Should also be unique.
|
|
51
|
+
|
|
52
|
+
:param pyramid_route:
|
|
53
|
+
Use existing pyramid route instead of creating new one.
|
|
54
|
+
|
|
55
|
+
:param renderer:
|
|
56
|
+
The renderer that should be used by this service. Default value is
|
|
57
|
+
'cornicejson'.
|
|
58
|
+
|
|
59
|
+
:param description:
|
|
60
|
+
The description of what the webservice does. This is primarily intended
|
|
61
|
+
for documentation purposes.
|
|
62
|
+
|
|
63
|
+
:param validators:
|
|
64
|
+
A list of callables to pass the request into before passing it to the
|
|
65
|
+
associated view.
|
|
66
|
+
|
|
67
|
+
:param filters:
|
|
68
|
+
A list of callables to pass the response into before returning it to
|
|
69
|
+
the client.
|
|
70
|
+
|
|
71
|
+
:param accept:
|
|
72
|
+
A list of ``Accept`` header values accepted for this service
|
|
73
|
+
(or method if overwritten when defining a method).
|
|
74
|
+
It can also be a callable, in which case the values will be
|
|
75
|
+
discovered at runtime. If a callable is passed, it should be able
|
|
76
|
+
to take the request as a first argument.
|
|
77
|
+
|
|
78
|
+
:param content_type:
|
|
79
|
+
A list of ``Content-Type`` header values accepted for this service
|
|
80
|
+
(or method if overwritten when defining a method).
|
|
81
|
+
It can also be a callable, in which case the values will be
|
|
82
|
+
discovered at runtime. If a callable is passed, it should be able
|
|
83
|
+
to take the request as a first argument.
|
|
84
|
+
|
|
85
|
+
:param factory:
|
|
86
|
+
A factory returning callables which return boolean values. The
|
|
87
|
+
callables take the request as their first argument and return boolean
|
|
88
|
+
values.
|
|
89
|
+
|
|
90
|
+
:param permission:
|
|
91
|
+
As for ``pyramid.config.Configurator.add_view()``.
|
|
92
|
+
Note: `permission` can also be applied
|
|
93
|
+
to instance method decorators such as :meth:`~get` and :meth:`~put`.
|
|
94
|
+
|
|
95
|
+
:param klass:
|
|
96
|
+
The class to use when resolving views (if they are not callables)
|
|
97
|
+
|
|
98
|
+
:param error_handler:
|
|
99
|
+
A callable which is used to render responses following validation
|
|
100
|
+
failures. By default it will call the registered renderer
|
|
101
|
+
`render_errors` method.
|
|
102
|
+
|
|
103
|
+
:param traverse:
|
|
104
|
+
A traversal pattern that will be passed on route declaration and that
|
|
105
|
+
will be used as the traversal path.
|
|
106
|
+
|
|
107
|
+
There are also a number of parameters that are related to the support of
|
|
108
|
+
CORS (Cross Origin Resource Sharing). You can read the CORS specification
|
|
109
|
+
at http://www.w3.org/TR/cors/
|
|
110
|
+
|
|
111
|
+
:param cors_enabled:
|
|
112
|
+
To use if you especially want to disable CORS support for a particular
|
|
113
|
+
service / method.
|
|
114
|
+
|
|
115
|
+
:param cors_origins:
|
|
116
|
+
The list of origins for CORS. You can use wildcards here if needed,
|
|
117
|
+
e.g. ('list', 'of', '\\*.domain').
|
|
118
|
+
|
|
119
|
+
:param cors_headers:
|
|
120
|
+
The list of headers supported for the services.
|
|
121
|
+
|
|
122
|
+
:param cors_credentials:
|
|
123
|
+
Should the client send credential information (False by default).
|
|
124
|
+
|
|
125
|
+
:param cors_max_age:
|
|
126
|
+
Indicates how long the results of a preflight request can be cached in
|
|
127
|
+
a preflight result cache.
|
|
128
|
+
|
|
129
|
+
:param cors_expose_all_headers:
|
|
130
|
+
If set to True, all the headers will be exposed and considered valid
|
|
131
|
+
ones (Default: True). If set to False, all the headers need be
|
|
132
|
+
explicitly mentioned with the cors_headers parameter.
|
|
133
|
+
|
|
134
|
+
:param cors_policy:
|
|
135
|
+
It may be easier to have an external object containing all the policy
|
|
136
|
+
information related to CORS, e.g::
|
|
137
|
+
|
|
138
|
+
>>> cors_policy = {'origins': ('*',), 'max_age': 42,
|
|
139
|
+
... 'credentials': True}
|
|
140
|
+
|
|
141
|
+
You can pass a dict here and all the values will be
|
|
142
|
+
unpacked and considered rather than the parameters starting by `cors_`
|
|
143
|
+
here.
|
|
144
|
+
|
|
145
|
+
See
|
|
146
|
+
https://pyramid.readthedocs.io/en/1.0-branch/glossary.html#term-acl
|
|
147
|
+
for more information about ACLs.
|
|
148
|
+
|
|
149
|
+
Service cornice instances also have methods :meth:`~get`, :meth:`~post`,
|
|
150
|
+
:meth:`~put`, :meth:`~options` and :meth:`~delete` are decorators that can
|
|
151
|
+
be used to decorate views.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
renderer = "cornicejson"
|
|
155
|
+
default_validators = DEFAULT_VALIDATORS
|
|
156
|
+
default_filters = DEFAULT_FILTERS
|
|
157
|
+
|
|
158
|
+
mandatory_arguments = ("renderer",)
|
|
159
|
+
list_arguments = ("validators", "filters", "cors_headers", "cors_origins")
|
|
160
|
+
|
|
161
|
+
def __repr__(self):
|
|
162
|
+
return "<Service %s at %s>" % (self.name, self.pyramid_route or self.path)
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
name,
|
|
167
|
+
path=None,
|
|
168
|
+
description=None,
|
|
169
|
+
cors_policy=None,
|
|
170
|
+
depth=1,
|
|
171
|
+
pyramid_route=None,
|
|
172
|
+
**kw,
|
|
173
|
+
):
|
|
174
|
+
self.name = name
|
|
175
|
+
self.path = path
|
|
176
|
+
self.pyramid_route = pyramid_route
|
|
177
|
+
|
|
178
|
+
if not self.path and not self.pyramid_route:
|
|
179
|
+
raise TypeError("You need to pass path or pyramid_route arg")
|
|
180
|
+
|
|
181
|
+
self.description = description
|
|
182
|
+
self.cors_expose_all_headers = True
|
|
183
|
+
self._cors_enabled = None
|
|
184
|
+
|
|
185
|
+
if cors_policy:
|
|
186
|
+
for key, value in cors_policy.items():
|
|
187
|
+
kw.setdefault("cors_" + key, value)
|
|
188
|
+
|
|
189
|
+
for key in self.list_arguments:
|
|
190
|
+
# default_{validators,filters} and {filters,validators} don't
|
|
191
|
+
# have to be mutables, so we need to create a new list from them
|
|
192
|
+
extra = to_list(kw.get(key, []))
|
|
193
|
+
kw[key] = []
|
|
194
|
+
kw[key].extend(getattr(self, "default_%s" % key, []))
|
|
195
|
+
kw[key].extend(extra)
|
|
196
|
+
|
|
197
|
+
self.arguments = self.get_arguments(kw)
|
|
198
|
+
for key, value in self.arguments.items():
|
|
199
|
+
# avoid squashing Service.decorator if ``decorator``
|
|
200
|
+
# argument is used to specify a default pyramid view
|
|
201
|
+
# decorator
|
|
202
|
+
if key != "decorator":
|
|
203
|
+
setattr(self, key, value)
|
|
204
|
+
|
|
205
|
+
if hasattr(self, "acl"):
|
|
206
|
+
raise ConfigurationError("'acl' is not supported")
|
|
207
|
+
|
|
208
|
+
# instantiate some variables we use to keep track of what's defined for
|
|
209
|
+
# this service.
|
|
210
|
+
self.defined_methods = []
|
|
211
|
+
self.definitions = []
|
|
212
|
+
|
|
213
|
+
# add this service to the list of available services
|
|
214
|
+
SERVICES.append(self)
|
|
215
|
+
|
|
216
|
+
# this callback will be called when config.scan (from pyramid) will
|
|
217
|
+
# be triggered.
|
|
218
|
+
def callback(context, name, ob):
|
|
219
|
+
config = context.config.with_package(info.module)
|
|
220
|
+
config.add_cornice_service(self)
|
|
221
|
+
|
|
222
|
+
info = venusian.attach(self, callback, category="pyramid", depth=depth)
|
|
223
|
+
|
|
224
|
+
def default_error_handler(self, request):
|
|
225
|
+
"""Default error_handler.
|
|
226
|
+
|
|
227
|
+
Uses the renderer for the service to render `request.errors`.
|
|
228
|
+
Only works if the registered renderer for the Service exposes the
|
|
229
|
+
method `render_errors`, which is implemented by default by
|
|
230
|
+
:class:`cornice.renderer.CorniceRenderer`.
|
|
231
|
+
|
|
232
|
+
:param request: the current Request.
|
|
233
|
+
"""
|
|
234
|
+
renderer = request.registry.queryUtility(IRendererFactory, name=self.renderer)
|
|
235
|
+
return renderer.render_errors(request)
|
|
236
|
+
|
|
237
|
+
def get_arguments(self, conf=None):
|
|
238
|
+
"""Return a dictionary of arguments. Takes arguments from the :param
|
|
239
|
+
conf: param and merges it with the arguments passed in the constructor.
|
|
240
|
+
|
|
241
|
+
:param conf: the dictionary to use.
|
|
242
|
+
"""
|
|
243
|
+
if conf is None:
|
|
244
|
+
conf = {}
|
|
245
|
+
|
|
246
|
+
arguments = {}
|
|
247
|
+
for arg in self.mandatory_arguments:
|
|
248
|
+
# get the value from the passed conf, then from the instance, then
|
|
249
|
+
# from the default class settings.
|
|
250
|
+
arguments[arg] = conf.pop(arg, getattr(self, arg, None))
|
|
251
|
+
|
|
252
|
+
for arg in self.list_arguments:
|
|
253
|
+
# rather than overwriting, extend the defined lists if
|
|
254
|
+
# any. take care of re-creating the lists before appending
|
|
255
|
+
# items to them, to avoid modifications to the already
|
|
256
|
+
# existing ones
|
|
257
|
+
value = list(getattr(self, arg, []))
|
|
258
|
+
if arg in conf:
|
|
259
|
+
value.extend(to_list(conf.pop(arg)))
|
|
260
|
+
arguments[arg] = value
|
|
261
|
+
|
|
262
|
+
# Allow custom error handler
|
|
263
|
+
arguments["error_handler"] = conf.pop(
|
|
264
|
+
"error_handler", getattr(self, "error_handler", self.default_error_handler)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# exclude some validators or filters
|
|
268
|
+
if "exclude" in conf:
|
|
269
|
+
for item in to_list(conf.pop("exclude")):
|
|
270
|
+
for container in arguments["validators"], arguments["filters"]:
|
|
271
|
+
if item in container:
|
|
272
|
+
container.remove(item)
|
|
273
|
+
|
|
274
|
+
# also include the other key,value pair we don't know anything about
|
|
275
|
+
arguments.update(conf)
|
|
276
|
+
|
|
277
|
+
# if some keys have been defined service-wide, then we need to add
|
|
278
|
+
# them to the returned dict.
|
|
279
|
+
if hasattr(self, "arguments"):
|
|
280
|
+
for key, value in self.arguments.items():
|
|
281
|
+
if key not in arguments:
|
|
282
|
+
arguments[key] = value
|
|
283
|
+
|
|
284
|
+
return arguments
|
|
285
|
+
|
|
286
|
+
def add_view(self, method, view, **kwargs):
|
|
287
|
+
"""Add a view to a method and arguments.
|
|
288
|
+
|
|
289
|
+
All the :class:`Service` keyword params except `name` and `path`
|
|
290
|
+
can be overwritten here. Additionally,
|
|
291
|
+
:meth:`~cornice.service.Service.api` has following keyword params:
|
|
292
|
+
|
|
293
|
+
:param method: The request method. Should be one of 'GET', 'POST',
|
|
294
|
+
'PUT', 'DELETE', 'OPTIONS', 'TRACE', or 'CONNECT'.
|
|
295
|
+
:param view: the view to hook to
|
|
296
|
+
:param **kwargs: additional configuration for this view,
|
|
297
|
+
including `permission`.
|
|
298
|
+
"""
|
|
299
|
+
method = method.upper()
|
|
300
|
+
|
|
301
|
+
if "klass" in kwargs and not callable(view):
|
|
302
|
+
view = _UnboundView(kwargs["klass"], view)
|
|
303
|
+
|
|
304
|
+
args = self.get_arguments(kwargs)
|
|
305
|
+
|
|
306
|
+
# remove 'factory' if present,
|
|
307
|
+
# it's not a valid pyramid view param
|
|
308
|
+
if "factory" in args:
|
|
309
|
+
del args["factory"]
|
|
310
|
+
|
|
311
|
+
if hasattr(self, "get_view_wrapper"):
|
|
312
|
+
view = self.get_view_wrapper(kwargs)(view)
|
|
313
|
+
self.definitions.append((method, view, args))
|
|
314
|
+
|
|
315
|
+
# keep track of the defined methods for the service
|
|
316
|
+
if method not in self.defined_methods:
|
|
317
|
+
self.defined_methods.append(method)
|
|
318
|
+
|
|
319
|
+
# auto-define a HEAD method if we have a definition for GET.
|
|
320
|
+
if method == "GET":
|
|
321
|
+
self.definitions.append(("HEAD", view, args))
|
|
322
|
+
if "HEAD" not in self.defined_methods:
|
|
323
|
+
self.defined_methods.append("HEAD")
|
|
324
|
+
|
|
325
|
+
def decorator(self, method, **kwargs):
|
|
326
|
+
"""Add the ability to define methods using python's decorators
|
|
327
|
+
syntax.
|
|
328
|
+
|
|
329
|
+
For instance, it is possible to do this with this method::
|
|
330
|
+
|
|
331
|
+
service = Service("blah", "/blah")
|
|
332
|
+
@service.decorator("get", accept="application/json")
|
|
333
|
+
def my_view(request):
|
|
334
|
+
pass
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
def wrapper(view):
|
|
338
|
+
self.add_view(method, view, **kwargs)
|
|
339
|
+
return view
|
|
340
|
+
|
|
341
|
+
return wrapper
|
|
342
|
+
|
|
343
|
+
def get(self, **kwargs):
|
|
344
|
+
"""Add the ability to define get using python's decorators
|
|
345
|
+
syntax.
|
|
346
|
+
|
|
347
|
+
For instance, it is possible to do this with this method::
|
|
348
|
+
|
|
349
|
+
service = Service("blah", "/blah")
|
|
350
|
+
@service.get(accept="application/json")
|
|
351
|
+
def my_view(request):
|
|
352
|
+
pass
|
|
353
|
+
"""
|
|
354
|
+
return self.decorator("GET", **kwargs)
|
|
355
|
+
|
|
356
|
+
def post(self, **kwargs):
|
|
357
|
+
"""Add the ability to define post using python's decorators
|
|
358
|
+
syntax.
|
|
359
|
+
|
|
360
|
+
For instance, it is possible to do this with this method::
|
|
361
|
+
|
|
362
|
+
service = Service("blah", "/blah")
|
|
363
|
+
@service.post(accept="application/json")
|
|
364
|
+
def my_view(request):
|
|
365
|
+
pass
|
|
366
|
+
"""
|
|
367
|
+
return self.decorator("POST", **kwargs)
|
|
368
|
+
|
|
369
|
+
def put(self, **kwargs):
|
|
370
|
+
"""Add the ability to define put using python's decorators
|
|
371
|
+
syntax.
|
|
372
|
+
|
|
373
|
+
For instance, it is possible to do this with this method::
|
|
374
|
+
|
|
375
|
+
service = Service("blah", "/blah")
|
|
376
|
+
@service.put(accept="application/json")
|
|
377
|
+
def my_view(request):
|
|
378
|
+
pass
|
|
379
|
+
"""
|
|
380
|
+
return self.decorator("PUT", **kwargs)
|
|
381
|
+
|
|
382
|
+
def delete(self, **kwargs):
|
|
383
|
+
"""Add the ability to define delete using python's decorators
|
|
384
|
+
syntax.
|
|
385
|
+
|
|
386
|
+
For instance, it is possible to do this with this method::
|
|
387
|
+
|
|
388
|
+
service = Service("blah", "/blah")
|
|
389
|
+
@service.delete(accept="application/json")
|
|
390
|
+
def my_view(request):
|
|
391
|
+
pass
|
|
392
|
+
"""
|
|
393
|
+
return self.decorator("DELETE", **kwargs)
|
|
394
|
+
|
|
395
|
+
def options(self, **kwargs):
|
|
396
|
+
"""Add the ability to define options using python's decorators
|
|
397
|
+
syntax.
|
|
398
|
+
|
|
399
|
+
For instance, it is possible to do this with this method::
|
|
400
|
+
|
|
401
|
+
service = Service("blah", "/blah")
|
|
402
|
+
@service.options(accept="application/json")
|
|
403
|
+
def my_view(request):
|
|
404
|
+
pass
|
|
405
|
+
"""
|
|
406
|
+
return self.decorator("OPTIONS", **kwargs)
|
|
407
|
+
|
|
408
|
+
def patch(self, **kwargs):
|
|
409
|
+
"""Add the ability to define patch using python's decorators
|
|
410
|
+
syntax.
|
|
411
|
+
|
|
412
|
+
For instance, it is possible to do this with this method::
|
|
413
|
+
|
|
414
|
+
service = Service("blah", "/blah")
|
|
415
|
+
@service.patch(accept="application/json")
|
|
416
|
+
def my_view(request):
|
|
417
|
+
pass
|
|
418
|
+
"""
|
|
419
|
+
return self.decorator("PATCH", **kwargs)
|
|
420
|
+
|
|
421
|
+
def filter_argumentlist(self, method, argname, filter_callables=False):
|
|
422
|
+
"""
|
|
423
|
+
Helper method to ``get_acceptable`` and ``get_contenttypes``. DRY.
|
|
424
|
+
"""
|
|
425
|
+
result = []
|
|
426
|
+
for meth, view, args in self.definitions:
|
|
427
|
+
if meth.upper() == method.upper():
|
|
428
|
+
result_tmp = to_list(args.get(argname))
|
|
429
|
+
if filter_callables:
|
|
430
|
+
result_tmp = [a for a in result_tmp if not callable(a)]
|
|
431
|
+
result.extend(result_tmp)
|
|
432
|
+
return result
|
|
433
|
+
|
|
434
|
+
def get_acceptable(self, method, filter_callables=False):
|
|
435
|
+
"""return a list of acceptable egress content-type headers that were
|
|
436
|
+
defined for this service.
|
|
437
|
+
|
|
438
|
+
:param method: the method to get the acceptable egress content-types
|
|
439
|
+
for.
|
|
440
|
+
:param filter_callables: it is possible to give acceptable
|
|
441
|
+
content-types dynamically, with callables.
|
|
442
|
+
This toggles filtering the callables (default:
|
|
443
|
+
False)
|
|
444
|
+
"""
|
|
445
|
+
return self.filter_argumentlist(method, "accept", filter_callables)
|
|
446
|
+
|
|
447
|
+
def get_contenttypes(self, method, filter_callables=False):
|
|
448
|
+
"""return a list of supported ingress content-type headers that were
|
|
449
|
+
defined for this service.
|
|
450
|
+
|
|
451
|
+
:param method: the method to get the supported ingress content-types
|
|
452
|
+
for.
|
|
453
|
+
:param filter_callables: it is possible to give supported
|
|
454
|
+
content-types dynamically, with callables.
|
|
455
|
+
This toggles filtering the callables (default:
|
|
456
|
+
False)
|
|
457
|
+
"""
|
|
458
|
+
return self.filter_argumentlist(method, "content_type", filter_callables)
|
|
459
|
+
|
|
460
|
+
def get_validators(self, method):
|
|
461
|
+
"""return a list of validators for the given method.
|
|
462
|
+
|
|
463
|
+
:param method: the method to get the validators for.
|
|
464
|
+
"""
|
|
465
|
+
validators = []
|
|
466
|
+
for meth, view, args in self.definitions:
|
|
467
|
+
if meth.upper() == method.upper() and "validators" in args:
|
|
468
|
+
for validator in args["validators"]:
|
|
469
|
+
if validator not in validators:
|
|
470
|
+
validators.append(validator)
|
|
471
|
+
return validators
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def cors_enabled(self):
|
|
475
|
+
if self._cors_enabled is False:
|
|
476
|
+
return False
|
|
477
|
+
|
|
478
|
+
return bool(self.cors_origins or self._cors_enabled)
|
|
479
|
+
|
|
480
|
+
@cors_enabled.setter
|
|
481
|
+
def cors_enabled(self, value):
|
|
482
|
+
self._cors_enabled = value
|
|
483
|
+
|
|
484
|
+
def cors_supported_headers_for(self, method=None):
|
|
485
|
+
"""Return an iterable of supported headers for this service.
|
|
486
|
+
|
|
487
|
+
The supported headers are defined by the :param headers: argument
|
|
488
|
+
that is passed to services or methods, at definition time.
|
|
489
|
+
"""
|
|
490
|
+
headers = set()
|
|
491
|
+
for meth, _, args in self.definitions:
|
|
492
|
+
if args.get("cors_enabled", True):
|
|
493
|
+
exposed_headers = args.get("cors_headers", ())
|
|
494
|
+
if method is not None:
|
|
495
|
+
if meth.upper() == method.upper():
|
|
496
|
+
return set(exposed_headers)
|
|
497
|
+
else:
|
|
498
|
+
headers |= set(exposed_headers)
|
|
499
|
+
return headers
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def cors_supported_methods(self):
|
|
503
|
+
"""Return an iterable of methods supported by CORS"""
|
|
504
|
+
methods = []
|
|
505
|
+
for meth, _, args in self.definitions:
|
|
506
|
+
if args.get("cors_enabled", True) and meth not in methods:
|
|
507
|
+
methods.append(meth)
|
|
508
|
+
return methods
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def cors_supported_origins(self):
|
|
512
|
+
origins = set(getattr(self, "cors_origins", ()))
|
|
513
|
+
for _, _, args in self.definitions:
|
|
514
|
+
origins |= set(args.get("cors_origins", ()))
|
|
515
|
+
return origins
|
|
516
|
+
|
|
517
|
+
def cors_origins_for(self, method):
|
|
518
|
+
"""Return the list of origins supported for a given HTTP method"""
|
|
519
|
+
origins = set()
|
|
520
|
+
for meth, view, args in self.definitions:
|
|
521
|
+
if meth.upper() == method.upper():
|
|
522
|
+
origins |= set(args.get("cors_origins", ()))
|
|
523
|
+
|
|
524
|
+
if not origins:
|
|
525
|
+
origins = self.cors_origins
|
|
526
|
+
return origins
|
|
527
|
+
|
|
528
|
+
def cors_support_credentials_for(self, method=None):
|
|
529
|
+
"""Returns if the given method support credentials.
|
|
530
|
+
|
|
531
|
+
:param method:
|
|
532
|
+
The method to check the credentials support for
|
|
533
|
+
"""
|
|
534
|
+
for meth, view, args in self.definitions:
|
|
535
|
+
if method and meth.upper() == method.upper():
|
|
536
|
+
return args.get("cors_credentials", False)
|
|
537
|
+
|
|
538
|
+
if getattr(self, "cors_credentials", False):
|
|
539
|
+
return self.cors_credentials
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
def cors_max_age_for(self, method=None):
|
|
543
|
+
max_age = None
|
|
544
|
+
for meth, view, args in self.definitions:
|
|
545
|
+
if method and meth.upper() == method.upper():
|
|
546
|
+
max_age = args.get("cors_max_age", None)
|
|
547
|
+
break
|
|
548
|
+
|
|
549
|
+
if max_age is None:
|
|
550
|
+
max_age = getattr(self, "cors_max_age", None)
|
|
551
|
+
return max_age
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def decorate_view(view, args, method, route_args={}):
|
|
555
|
+
"""Decorate a given view with cornice niceties.
|
|
556
|
+
|
|
557
|
+
This function returns a function with the same signature than the one
|
|
558
|
+
you give as :param view:
|
|
559
|
+
|
|
560
|
+
:param view: the view to decorate
|
|
561
|
+
:param args: the args to use for the decoration
|
|
562
|
+
:param method: the HTTP method
|
|
563
|
+
:param route_args: the args used for the associated route
|
|
564
|
+
"""
|
|
565
|
+
|
|
566
|
+
def wrapper(request):
|
|
567
|
+
# if the args contain a klass argument then use it to resolve the view
|
|
568
|
+
# location (if the view argument isn't a callable)
|
|
569
|
+
ob = None
|
|
570
|
+
view_ = view
|
|
571
|
+
if "klass" in args and not callable(view):
|
|
572
|
+
# XXX: given that request.context exists and root-factory
|
|
573
|
+
# only expects request param, having params seems unnecessary
|
|
574
|
+
# ob = args['klass'](request)
|
|
575
|
+
params = dict(request=request)
|
|
576
|
+
if "factory" in route_args:
|
|
577
|
+
params["context"] = request.context
|
|
578
|
+
ob = args["klass"](**params)
|
|
579
|
+
if is_string(view):
|
|
580
|
+
view_ = getattr(ob, view.lower())
|
|
581
|
+
elif isinstance(view, _UnboundView):
|
|
582
|
+
view_ = view.make_bound_view(ob)
|
|
583
|
+
|
|
584
|
+
# the validators can either be a list of callables or contain some
|
|
585
|
+
# non-callable values. In which case we want to resolve them using the
|
|
586
|
+
# object if any
|
|
587
|
+
validators = args.get("validators", ())
|
|
588
|
+
for validator in validators:
|
|
589
|
+
if is_string(validator) and ob is not None:
|
|
590
|
+
validator = getattr(ob, validator)
|
|
591
|
+
validator(request, **args)
|
|
592
|
+
|
|
593
|
+
# only call the view if we don't have validation errors
|
|
594
|
+
if len(request.errors) == 0:
|
|
595
|
+
try:
|
|
596
|
+
# If we have an object, it already has the request.
|
|
597
|
+
if ob:
|
|
598
|
+
response = view_()
|
|
599
|
+
else:
|
|
600
|
+
response = view_(request)
|
|
601
|
+
except Exception:
|
|
602
|
+
# cors headers need to be set if an exception was raised
|
|
603
|
+
request.info["cors_checked"] = False
|
|
604
|
+
raise
|
|
605
|
+
|
|
606
|
+
# check for errors and return them if any
|
|
607
|
+
if len(request.errors) > 0:
|
|
608
|
+
# We already checked for CORS, but since the response is created
|
|
609
|
+
# again, we want to do that again before returning the response.
|
|
610
|
+
request.info["cors_checked"] = False
|
|
611
|
+
return args["error_handler"](request)
|
|
612
|
+
|
|
613
|
+
# if the view returns its own response, cors headers need to be set
|
|
614
|
+
if isinstance(response, Response):
|
|
615
|
+
request.info["cors_checked"] = False
|
|
616
|
+
|
|
617
|
+
# We can't apply filters at this level, since "response" may not have
|
|
618
|
+
# been rendered into a proper Response object yet. Instead, give the
|
|
619
|
+
# request a reference to its api_kwargs so that a tween can apply them.
|
|
620
|
+
# We also pass the object we created (if any) so we can use it to find
|
|
621
|
+
# the filters that are in fact methods.
|
|
622
|
+
request.cornice_args = (args, ob)
|
|
623
|
+
return response
|
|
624
|
+
|
|
625
|
+
# return the wrapper, not the function, keep the same signature
|
|
626
|
+
if not is_string(view):
|
|
627
|
+
functools.update_wrapper(wrapper, view)
|
|
628
|
+
|
|
629
|
+
# Set the wrapper name to something useful
|
|
630
|
+
wrapper.__name__ = "{0}__{1}".format(func_name(view), method)
|
|
631
|
+
return wrapper
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class _UnboundView(object):
|
|
635
|
+
def __init__(self, klass, view):
|
|
636
|
+
self.unbound_view = getattr(klass, view.lower())
|
|
637
|
+
functools.update_wrapper(self, self.unbound_view)
|
|
638
|
+
self.__name__ = func_name(self.unbound_view)
|
|
639
|
+
|
|
640
|
+
def make_bound_view(self, ob):
|
|
641
|
+
return functools.partial(self.unbound_view, ob)
|