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.

Files changed (91) hide show
  1. kinto/__init__.py +1 -0
  2. kinto/__main__.py +1 -19
  3. kinto/config/kinto.tpl +5 -15
  4. kinto/contribute.json +27 -0
  5. kinto/core/__init__.py +21 -8
  6. kinto/core/cornice/__init__.py +93 -0
  7. kinto/core/cornice/cors.py +144 -0
  8. kinto/core/cornice/errors.py +40 -0
  9. kinto/core/cornice/pyramidhook.py +373 -0
  10. kinto/core/cornice/renderer.py +89 -0
  11. kinto/core/cornice/resource.py +205 -0
  12. kinto/core/cornice/service.py +641 -0
  13. kinto/core/cornice/util.py +138 -0
  14. kinto/core/cornice/validators/__init__.py +94 -0
  15. kinto/core/cornice/validators/_colander.py +142 -0
  16. kinto/core/cornice/validators/_marshmallow.py +182 -0
  17. kinto/core/cornice_swagger/__init__.py +92 -0
  18. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  19. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  20. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  21. kinto/core/cornice_swagger/converters/schema.py +249 -0
  22. kinto/core/cornice_swagger/swagger.py +725 -0
  23. kinto/core/cornice_swagger/templates/index.html +73 -0
  24. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  25. kinto/core/cornice_swagger/util.py +42 -0
  26. kinto/core/cornice_swagger/views.py +78 -0
  27. kinto/core/errors.py +6 -4
  28. kinto/core/initialization.py +129 -59
  29. kinto/core/metrics.py +93 -0
  30. kinto/core/openapi.py +2 -3
  31. kinto/core/permission/memory.py +3 -2
  32. kinto/core/permission/postgresql/__init__.py +9 -9
  33. kinto/core/permission/testing.py +6 -0
  34. kinto/core/resource/__init__.py +9 -4
  35. kinto/core/resource/schema.py +1 -2
  36. kinto/core/resource/viewset.py +1 -1
  37. kinto/core/statsd.py +1 -63
  38. kinto/core/storage/__init__.py +15 -0
  39. kinto/core/storage/memory.py +20 -3
  40. kinto/core/storage/postgresql/__init__.py +31 -1
  41. kinto/core/storage/postgresql/client.py +2 -2
  42. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  43. kinto/core/storage/postgresql/pool.py +1 -1
  44. kinto/core/storage/postgresql/schema.sql +3 -2
  45. kinto/core/storage/testing.py +41 -1
  46. kinto/core/testing.py +6 -2
  47. kinto/core/utils.py +14 -4
  48. kinto/core/views/batch.py +1 -1
  49. kinto/core/views/errors.py +4 -3
  50. kinto/core/views/openapi.py +1 -1
  51. kinto/plugins/accounts/__init__.py +3 -21
  52. kinto/plugins/accounts/authentication.py +8 -54
  53. kinto/plugins/accounts/utils.py +0 -133
  54. kinto/plugins/accounts/{views/__init__.py → views.py} +7 -62
  55. kinto/plugins/admin/VERSION +1 -1
  56. kinto/plugins/admin/build/VERSION +1 -0
  57. kinto/plugins/admin/build/assets/asn1-EdZsLKOL.js +1 -0
  58. kinto/plugins/admin/build/assets/clojure-BMjYHr_A.js +1 -0
  59. kinto/plugins/admin/build/assets/css-BnMrqG3P.js +1 -0
  60. kinto/plugins/admin/build/assets/index-Cs7JVwIg.css +6 -0
  61. kinto/plugins/admin/build/assets/index-CylsivYB.js +165 -0
  62. kinto/plugins/admin/build/assets/javascript-qCveANmP.js +1 -0
  63. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  64. kinto/plugins/admin/build/assets/mllike-CXdrOF99.js +1 -0
  65. kinto/plugins/admin/build/assets/python-BuPzkPfP.js +1 -0
  66. kinto/plugins/admin/build/assets/rpm-CTu-6PCP.js +1 -0
  67. kinto/plugins/admin/build/assets/sql-D0XecflT.js +1 -0
  68. kinto/plugins/admin/build/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  69. kinto/plugins/admin/build/index.html +18 -0
  70. kinto/plugins/default_bucket/__init__.py +1 -2
  71. kinto/plugins/flush.py +2 -2
  72. kinto/plugins/history/__init__.py +15 -6
  73. kinto/plugins/history/listener.py +68 -5
  74. kinto/plugins/openid/views.py +1 -1
  75. kinto/plugins/prometheus.py +203 -0
  76. kinto/plugins/statsd.py +78 -0
  77. kinto/views/contribute.py +14 -13
  78. {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/METADATA +31 -32
  79. kinto-20.4.0.dist-info/RECORD +149 -0
  80. {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/WHEEL +1 -1
  81. kinto/plugins/accounts/mails.py +0 -96
  82. kinto/plugins/accounts/views/validation.py +0 -136
  83. kinto/plugins/quotas/__init__.py +0 -22
  84. kinto/plugins/quotas/listener.py +0 -226
  85. kinto/plugins/quotas/scripts.py +0 -80
  86. kinto/plugins/quotas/utils.py +0 -7
  87. kinto/scripts.py +0 -41
  88. kinto-18.1.0.dist-info/RECORD +0 -116
  89. {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info}/entry_points.txt +0 -0
  90. {kinto-18.1.0.dist-info → kinto-20.4.0.dist-info/licenses}/LICENSE +0 -0
  91. {kinto-18.1.0.dist-info → kinto-20.4.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