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
kinto/__init__.py CHANGED
@@ -38,6 +38,7 @@ DEFAULT_SETTINGS = {
38
38
  "record_id_generator": "kinto.views.RelaxedUUID",
39
39
  "project_name": "kinto",
40
40
  "admin_assets_path": None,
41
+ "metrics_matchdict_fields": ["bucket_id", "collection_id", "group_id", "record_id"],
41
42
  }
42
43
 
43
44
 
kinto/__main__.py CHANGED
@@ -9,7 +9,6 @@ from pyramid.paster import bootstrap
9
9
  from pyramid.scripts import pserve
10
10
 
11
11
  from kinto import __version__
12
- from kinto import scripts as kinto_scripts
13
12
  from kinto.config import init
14
13
  from kinto.core import scripts as core_scripts
15
14
  from kinto.plugins.accounts import scripts as accounts_scripts
@@ -33,7 +32,6 @@ def main(args=None):
33
32
  "migrate",
34
33
  "flush-cache",
35
34
  "version",
36
- "rebuild-quotas",
37
35
  "create-user",
38
36
  )
39
37
  subparsers = parser.add_subparsers(
@@ -107,16 +105,6 @@ def main(args=None):
107
105
  default=False,
108
106
  )
109
107
 
110
- elif command == "rebuild-quotas":
111
- subparser.add_argument(
112
- "--dry-run",
113
- action="store_true",
114
- help="Simulate the rebuild operation and show information",
115
- dest="dry_run",
116
- required=False,
117
- default=False,
118
- )
119
-
120
108
  elif command == "start":
121
109
  subparser.add_argument(
122
110
  "--reload",
@@ -161,8 +149,7 @@ def main(args=None):
161
149
  if not backend:
162
150
  while True:
163
151
  prompt = (
164
- "Select the backend you would like to use: "
165
- "(1 - postgresql, default - memory) "
152
+ "Select the backend you would like to use: (1 - postgresql, default - memory) "
166
153
  )
167
154
  answer = input(prompt).strip()
168
155
  try:
@@ -215,11 +202,6 @@ def main(args=None):
215
202
  env = bootstrap(config_file, options={"command": "flush-cache"})
216
203
  core_scripts.flush_cache(env)
217
204
 
218
- elif which_command == "rebuild-quotas":
219
- dry_run = parsed_args["dry_run"]
220
- env = bootstrap(config_file, options={"command": "rebuild-quotas"})
221
- return kinto_scripts.rebuild_quotas(env, dry_run=dry_run)
222
-
223
205
  elif which_command == "create-user":
224
206
  username = parsed_args["username"]
225
207
  password = parsed_args["password"]
kinto/config/kinto.tpl CHANGED
@@ -36,7 +36,6 @@ kinto.includes = kinto.plugins.default_bucket
36
36
  kinto.plugins.admin
37
37
  kinto.plugins.accounts
38
38
  # kinto.plugins.history
39
- # kinto.plugins.quotas
40
39
 
41
40
  # Backends
42
41
  # https://kinto.readthedocs.io/en/latest/configuration/settings.html#storage
@@ -104,18 +103,6 @@ kinto.account_create_principals = system.Everyone
104
103
  kinto.account_write_principals = account:admin
105
104
  # Allow administrators to create buckets
106
105
  kinto.bucket_create_principals = account:admin
107
- # Enable the "account_validation" option.
108
- # kinto.account_validation = true
109
- # Set the sender for the validation email.
110
- # kinto.account_validation.email_sender = "admin@example.com"
111
- # Set the regular expression used to validate a proper email address.
112
- # kinto.account_validation.email_regexp = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
113
-
114
- # Mail configuration (needed for the account validation option), see https://docs.pylonsproject.org/projects/pyramid_mailer/en/latest/#configuration
115
- # mail.host = localhost
116
- # mail.port = 25
117
- # mail.username = someusername
118
- # mail.password = somepassword
119
106
 
120
107
  # Notifications
121
108
  # https://kinto.readthedocs.io/en/latest/configuration/settings.html#notifications
@@ -258,7 +245,7 @@ keys = root, kinto
258
245
  keys = console
259
246
 
260
247
  [formatters]
261
- keys = color
248
+ keys = color, json
262
249
 
263
250
  [logger_root]
264
251
  level = INFO
@@ -271,10 +258,13 @@ qualname = kinto
271
258
  propagate = 0
272
259
 
273
260
  [handler_console]
274
- class = StreamHandler
261
+ class = kinto.core.StreamHandlerWithRequestID
275
262
  args = (sys.stderr,)
276
263
  level = NOTSET
277
264
  formatter = color
278
265
 
279
266
  [formatter_color]
280
267
  class = logging_color_formatter.ColorFormatter
268
+
269
+ [formatter_json]
270
+ class = kinto.core.JsonLogFormatter
kinto/contribute.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "Kinto",
3
+ "description": "Kinto is a minimalist JSON storage service with synchronisation and sharing abilities.",
4
+ "repository": {
5
+ "url": "https://github.com/Kinto/kinto",
6
+ "license": "Apache License (2.0)",
7
+ "tests": "https://github.com/Kinto/kinto/actions"
8
+ },
9
+ "participate": {
10
+ "home": "https://kinto.readthedocs.io/en/latest/community.html#how-to-contribute",
11
+ "docs": "https://kinto.readthedocs.io/",
12
+ "irc": "irc://irc.freenode.org/#kinto",
13
+ "irc-contacts": ["alexis", "leplatrem", "magopian", "natim", "NiKo`"]
14
+ },
15
+ "bugs": {
16
+ "list": "https://github.com/Kinto/kinto/issues",
17
+ "report": "https://github.com/Kinto/kinto/issues/new"
18
+ },
19
+ "keywords": [
20
+ "JSON",
21
+ "Python",
22
+ "Pyramid",
23
+ "REST",
24
+ "API",
25
+ "Database"
26
+ ]
27
+ }
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,
@@ -66,8 +66,8 @@ DEFAULT_SETTINGS = {
66
66
  "kinto.core.initialization.setup_authentication",
67
67
  "kinto.core.initialization.setup_backoff",
68
68
  "kinto.core.initialization.setup_sentry",
69
- "kinto.core.initialization.setup_statsd",
70
69
  "kinto.core.initialization.setup_listeners",
70
+ "kinto.core.initialization.setup_metrics",
71
71
  "kinto.core.events.setup_transaction_hook",
72
72
  ),
73
73
  "event_listeners": "",
@@ -96,6 +96,7 @@ DEFAULT_SETTINGS = {
96
96
  "statsd_backend": "kinto.core.statsd",
97
97
  "statsd_prefix": "kinto.core",
98
98
  "statsd_url": None,
99
+ "metrics_matchdict_fields": [],
99
100
  "storage_backend": "",
100
101
  "storage_url": "",
101
102
  "storage_max_fetch_size": 10000,
@@ -109,10 +110,8 @@ DEFAULT_SETTINGS = {
109
110
  "trailing_slash_redirect_ttl_seconds": 3600,
110
111
  "multiauth.groupfinder": "kinto.core.authorization.groupfinder",
111
112
  "multiauth.policies": "",
112
- "multiauth.policy.basicauth.use": (
113
- "kinto.core.authentication." "BasicAuthAuthenticationPolicy"
114
- ),
115
- "multiauth.authorization_policy": ("kinto.core.authorization." "AuthorizationPolicy"),
113
+ "multiauth.policy.basicauth.use": "kinto.core.authentication.BasicAuthAuthenticationPolicy",
114
+ "multiauth.authorization_policy": "kinto.core.authorization.AuthorizationPolicy",
116
115
  }
117
116
 
118
117
 
@@ -152,6 +151,20 @@ class JsonLogFormatter(dockerflow_logging.JsonLogFormatter):
152
151
  self.logger_name = logger_name
153
152
 
154
153
 
154
+ class StreamHandlerWithRequestID(logging.StreamHandler):
155
+ """
156
+ A custom StreamHandler that adds the Dockerflow's `RequestIdLogFilter`.
157
+
158
+ Defining a custom handler seems to be the only way to bypass the fact that
159
+ ``logging.config.fileConfig()`` does not load filters from ``.ini`` files.
160
+ """
161
+
162
+ def __init__(self, *args, **kwargs):
163
+ super().__init__(*args, **kwargs)
164
+ filter_ = dockerflow_logging.RequestIdLogFilter()
165
+ self.addFilter(filter_)
166
+
167
+
155
168
  def get_user_info(request):
156
169
  # Default user info (shown in hello view for example).
157
170
  user_info = {"id": request.prefixed_userid, "principals": request.prefixed_principals}
@@ -187,10 +200,10 @@ def includeme(config):
187
200
  config.add_request_method(events.notify_resource_event, name="notify_resource_event")
188
201
 
189
202
  # Setup cornice.
190
- config.include("cornice")
203
+ config.include("kinto.core.cornice")
191
204
 
192
205
  # Setup cornice api documentation
193
- config.include("cornice_swagger")
206
+ config.include("kinto.core.cornice_swagger")
194
207
 
195
208
  # Per-request transaction.
196
209
  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