kinto 19.5.0__py3-none-any.whl → 19.6.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 (37) hide show
  1. kinto/core/__init__.py +3 -3
  2. kinto/core/cornice/__init__.py +93 -0
  3. kinto/core/cornice/cors.py +144 -0
  4. kinto/core/cornice/errors.py +40 -0
  5. kinto/core/cornice/pyramidhook.py +373 -0
  6. kinto/core/cornice/renderer.py +89 -0
  7. kinto/core/cornice/resource.py +205 -0
  8. kinto/core/cornice/service.py +641 -0
  9. kinto/core/cornice/util.py +138 -0
  10. kinto/core/cornice/validators/__init__.py +94 -0
  11. kinto/core/cornice/validators/_colander.py +142 -0
  12. kinto/core/cornice/validators/_marshmallow.py +182 -0
  13. kinto/core/cornice_swagger/__init__.py +92 -0
  14. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  15. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  16. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  17. kinto/core/cornice_swagger/converters/schema.py +249 -0
  18. kinto/core/cornice_swagger/swagger.py +725 -0
  19. kinto/core/cornice_swagger/templates/index.html +73 -0
  20. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  21. kinto/core/cornice_swagger/util.py +42 -0
  22. kinto/core/cornice_swagger/views.py +78 -0
  23. kinto/core/openapi.py +2 -3
  24. kinto/core/resource/viewset.py +1 -1
  25. kinto/core/testing.py +1 -1
  26. kinto/core/utils.py +3 -2
  27. kinto/core/views/batch.py +1 -1
  28. kinto/core/views/openapi.py +1 -1
  29. kinto/plugins/flush.py +1 -1
  30. kinto/plugins/openid/views.py +1 -1
  31. kinto/views/contribute.py +2 -1
  32. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/METADATA +2 -4
  33. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/RECORD +37 -16
  34. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/LICENSE +0 -0
  35. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/WHEEL +0 -0
  36. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/entry_points.txt +0 -0
  37. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/top_level.txt +0 -0
kinto/core/__init__.py CHANGED
@@ -4,11 +4,11 @@ import logging
4
4
  import tempfile
5
5
 
6
6
  import pkg_resources
7
- from cornice import Service as CorniceService
8
7
  from dockerflow import logging as dockerflow_logging
9
8
  from pyramid.settings import aslist
10
9
 
11
10
  from kinto.core import errors, events
11
+ from kinto.core.cornice import Service as CorniceService
12
12
  from kinto.core.initialization import ( # NOQA
13
13
  initialize,
14
14
  install_middlewares,
@@ -186,10 +186,10 @@ def includeme(config):
186
186
  config.add_request_method(events.notify_resource_event, name="notify_resource_event")
187
187
 
188
188
  # Setup cornice.
189
- config.include("cornice")
189
+ config.include("kinto.core.cornice")
190
190
 
191
191
  # Setup cornice api documentation
192
- config.include("cornice_swagger")
192
+ config.include("kinto.core.cornice_swagger")
193
193
 
194
194
  # Per-request transaction.
195
195
  config.include("pyramid_tm")
@@ -0,0 +1,93 @@
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 logging
5
+ from functools import partial
6
+
7
+ from pyramid.events import NewRequest
8
+ from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound
9
+ from pyramid.security import NO_PERMISSION_REQUIRED
10
+ from pyramid.settings import asbool, aslist
11
+
12
+ from kinto.core.cornice.errors import Errors # NOQA
13
+ from kinto.core.cornice.pyramidhook import (
14
+ handle_exceptions,
15
+ register_resource_views,
16
+ register_service_views,
17
+ wrap_request,
18
+ )
19
+ from kinto.core.cornice.renderer import CorniceRenderer
20
+ from kinto.core.cornice.service import Service # NOQA
21
+ from kinto.core.cornice.util import ContentTypePredicate, current_service
22
+
23
+
24
+ logger = logging.getLogger("cornice")
25
+
26
+
27
+ def set_localizer_for_languages(event, available_languages, default_locale_name):
28
+ """
29
+ Sets the current locale based on the incoming Accept-Language header, if
30
+ present, and sets a localizer attribute on the request object based on
31
+ the current locale.
32
+
33
+ To be used as an event handler, this function needs to be partially applied
34
+ with the available_languages and default_locale_name arguments. The
35
+ resulting function will be an event handler which takes an event object as
36
+ its only argument.
37
+ """
38
+ request = event.request
39
+ if request.accept_language:
40
+ accepted = request.accept_language.lookup(available_languages, default=default_locale_name)
41
+ request._LOCALE_ = accepted
42
+
43
+
44
+ def setup_localization(config):
45
+ """
46
+ Setup localization based on the available_languages and
47
+ pyramid.default_locale_name settings.
48
+
49
+ These settings are named after suggestions from the "Internationalization
50
+ and Localization" section of the Pyramid documentation.
51
+ """
52
+ try:
53
+ config.add_translation_dirs("colander:locale/")
54
+ settings = config.get_settings()
55
+ available_languages = aslist(settings["available_languages"])
56
+ default_locale_name = settings.get("pyramid.default_locale_name", "en")
57
+ set_localizer = partial(
58
+ set_localizer_for_languages,
59
+ available_languages=available_languages,
60
+ default_locale_name=default_locale_name,
61
+ )
62
+ config.add_subscriber(set_localizer, NewRequest)
63
+ except ImportError: # pragma: no cover
64
+ # add_translation_dirs raises an ImportError if colander is not
65
+ # installed
66
+ pass
67
+
68
+
69
+ def includeme(config):
70
+ """Include the Cornice definitions"""
71
+ # attributes required to maintain services
72
+ config.registry.cornice_services = {}
73
+
74
+ settings = config.get_settings()
75
+
76
+ # localization request subscriber must be set before first call
77
+ # for request.localizer (in wrap_request)
78
+ if settings.get("available_languages"):
79
+ setup_localization(config)
80
+
81
+ config.add_directive("add_cornice_service", register_service_views)
82
+ config.add_directive("add_cornice_resource", register_resource_views)
83
+ config.add_subscriber(wrap_request, NewRequest)
84
+ config.add_renderer("cornicejson", CorniceRenderer())
85
+ config.add_view_predicate("content_type", ContentTypePredicate)
86
+ config.add_request_method(current_service, reify=True)
87
+
88
+ if asbool(settings.get("handle_exceptions", True)):
89
+ config.add_view(handle_exceptions, context=Exception, permission=NO_PERMISSION_REQUIRED)
90
+ config.add_view(handle_exceptions, context=HTTPNotFound, permission=NO_PERMISSION_REQUIRED)
91
+ config.add_view(
92
+ handle_exceptions, context=HTTPForbidden, permission=NO_PERMISSION_REQUIRED
93
+ )
@@ -0,0 +1,144 @@
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 fnmatch
5
+ import functools
6
+
7
+ from pyramid.settings import asbool
8
+
9
+
10
+ CORS_PARAMETERS = (
11
+ "cors_headers",
12
+ "cors_enabled",
13
+ "cors_origins",
14
+ "cors_credentials",
15
+ "cors_max_age",
16
+ "cors_expose_all_headers",
17
+ )
18
+
19
+
20
+ def get_cors_preflight_view(service):
21
+ """Return a view for the OPTION method.
22
+
23
+ Checks that the User-Agent is authorized to do a request to the server, and
24
+ to this particular service, and add the various checks that are specified
25
+ in http://www.w3.org/TR/cors/#resource-processing-model.
26
+ """
27
+
28
+ def _preflight_view(request):
29
+ response = request.response
30
+ origin = request.headers.get("Origin")
31
+ supported_headers = service.cors_supported_headers_for()
32
+
33
+ if not origin:
34
+ request.errors.add("header", "Origin", "this header is mandatory")
35
+
36
+ requested_method = request.headers.get("Access-Control-Request-Method")
37
+ if not requested_method:
38
+ request.errors.add(
39
+ "header", "Access-Control-Request-Method", "this header is mandatory"
40
+ )
41
+
42
+ if not (requested_method and origin):
43
+ return
44
+
45
+ requested_headers = request.headers.get("Access-Control-Request-Headers", ())
46
+
47
+ if requested_headers:
48
+ requested_headers = map(str.strip, requested_headers.split(","))
49
+
50
+ if requested_method not in service.cors_supported_methods:
51
+ request.errors.add("header", "Access-Control-Request-Method", "Method not allowed")
52
+
53
+ if not service.cors_expose_all_headers:
54
+ for h in requested_headers:
55
+ if h.lower() not in [s.lower() for s in supported_headers]:
56
+ request.errors.add(
57
+ "header", "Access-Control-Request-Headers", 'Header "%s" not allowed' % h
58
+ )
59
+
60
+ supported_headers = set(supported_headers) | set(requested_headers)
61
+
62
+ response.headers["Access-Control-Allow-Headers"] = ",".join(supported_headers)
63
+
64
+ response.headers["Access-Control-Allow-Methods"] = ",".join(service.cors_supported_methods)
65
+
66
+ max_age = service.cors_max_age_for(requested_method)
67
+ if max_age is not None:
68
+ response.headers["Access-Control-Max-Age"] = str(max_age)
69
+
70
+ return None
71
+
72
+ return _preflight_view
73
+
74
+
75
+ def _get_method(request):
76
+ """Return what's supposed to be the method for CORS operations.
77
+ (e.g if the verb is options, look at the A-C-Request-Method header,
78
+ otherwise return the HTTP verb).
79
+ """
80
+ if request.method == "OPTIONS":
81
+ method = request.headers.get("Access-Control-Request-Method", request.method)
82
+ else:
83
+ method = request.method
84
+ return method
85
+
86
+
87
+ def ensure_origin(service, request, response=None, **kwargs):
88
+ """Ensure that the origin header is set and allowed."""
89
+ response = response or request.response
90
+
91
+ # Don't check this twice.
92
+ if not request.info.get("cors_checked", False):
93
+ method = _get_method(request)
94
+
95
+ origin = request.headers.get("Origin")
96
+
97
+ if not origin:
98
+ always_cors = asbool(request.registry.settings.get("cornice.always_cors"))
99
+ # With this setting, if the service origins has "*", then
100
+ # always return CORS headers.
101
+ origins = getattr(service, "cors_origins", [])
102
+ if always_cors and "*" in origins:
103
+ origin = "*"
104
+
105
+ if origin:
106
+ if not any([fnmatch.fnmatchcase(origin, o) for o in service.cors_origins_for(method)]):
107
+ request.errors.add("header", "Origin", "%s not allowed" % origin)
108
+ elif service.cors_support_credentials_for(method):
109
+ response.headers["Access-Control-Allow-Origin"] = origin
110
+ else:
111
+ if any([o == "*" for o in service.cors_origins_for(method)]):
112
+ response.headers["Access-Control-Allow-Origin"] = "*"
113
+ else:
114
+ response.headers["Access-Control-Allow-Origin"] = origin
115
+ request.info["cors_checked"] = True
116
+ return response
117
+
118
+
119
+ def get_cors_validator(service):
120
+ return functools.partial(ensure_origin, service)
121
+
122
+
123
+ def apply_cors_post_request(service, request, response):
124
+ """Handles CORS-related post-request things.
125
+
126
+ Add some response headers, such as the Expose-Headers and the
127
+ Allow-Credentials ones.
128
+ """
129
+ response = ensure_origin(service, request, response)
130
+ method = _get_method(request)
131
+
132
+ if (
133
+ service.cors_support_credentials_for(method)
134
+ and "Access-Control-Allow-Credentials" not in response.headers
135
+ ):
136
+ response.headers["Access-Control-Allow-Credentials"] = "true"
137
+
138
+ if request.method != "OPTIONS":
139
+ # Which headers are exposed?
140
+ supported_headers = service.cors_supported_headers_for(request.method)
141
+ if supported_headers:
142
+ response.headers["Access-Control-Expose-Headers"] = ", ".join(supported_headers)
143
+
144
+ return response
@@ -0,0 +1,40 @@
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 json
5
+
6
+ from pyramid.i18n import TranslationString
7
+
8
+
9
+ class Errors(list):
10
+ """Holds Request errors"""
11
+
12
+ def __init__(self, status=400, localizer=None):
13
+ self.status = status
14
+ self.localizer = localizer
15
+ super(Errors, self).__init__()
16
+
17
+ def add(self, location, name=None, description=None, **kw):
18
+ """Registers a new error."""
19
+ allowed = ("body", "querystring", "url", "header", "path", "cookies", "method")
20
+ if location != "" and location not in allowed:
21
+ raise ValueError("%r not in %s" % (location, allowed))
22
+
23
+ if isinstance(description, TranslationString) and self.localizer:
24
+ description = self.localizer.translate(description)
25
+
26
+ self.append(dict(location=location, name=name, description=description, **kw))
27
+
28
+ @classmethod
29
+ def from_json(cls, string):
30
+ """Transforms a json string into an `Errors` instance"""
31
+ obj = json.loads(string.decode())
32
+ return Errors.from_list(obj.get("errors", []))
33
+
34
+ @classmethod
35
+ def from_list(cls, obj):
36
+ """Transforms a python list into an `Errors` instance"""
37
+ errors = Errors()
38
+ for error in obj:
39
+ errors.add(**error)
40
+ return errors
@@ -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)