kinto 23.2.1__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.
Files changed (142) hide show
  1. kinto/__init__.py +92 -0
  2. kinto/__main__.py +249 -0
  3. kinto/authorization.py +134 -0
  4. kinto/config/__init__.py +94 -0
  5. kinto/config/kinto.tpl +270 -0
  6. kinto/contribute.json +27 -0
  7. kinto/core/__init__.py +246 -0
  8. kinto/core/authentication.py +48 -0
  9. kinto/core/authorization.py +311 -0
  10. kinto/core/cache/__init__.py +131 -0
  11. kinto/core/cache/memcached.py +112 -0
  12. kinto/core/cache/memory.py +104 -0
  13. kinto/core/cache/postgresql/__init__.py +178 -0
  14. kinto/core/cache/postgresql/schema.sql +23 -0
  15. kinto/core/cache/testing.py +208 -0
  16. kinto/core/cornice/__init__.py +93 -0
  17. kinto/core/cornice/cors.py +144 -0
  18. kinto/core/cornice/errors.py +40 -0
  19. kinto/core/cornice/pyramidhook.py +373 -0
  20. kinto/core/cornice/renderer.py +89 -0
  21. kinto/core/cornice/resource.py +205 -0
  22. kinto/core/cornice/service.py +641 -0
  23. kinto/core/cornice/util.py +138 -0
  24. kinto/core/cornice/validators/__init__.py +94 -0
  25. kinto/core/cornice/validators/_colander.py +142 -0
  26. kinto/core/cornice/validators/_marshmallow.py +182 -0
  27. kinto/core/cornice_swagger/__init__.py +92 -0
  28. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  29. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  30. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  31. kinto/core/cornice_swagger/converters/schema.py +249 -0
  32. kinto/core/cornice_swagger/swagger.py +725 -0
  33. kinto/core/cornice_swagger/templates/index.html +73 -0
  34. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  35. kinto/core/cornice_swagger/util.py +42 -0
  36. kinto/core/cornice_swagger/views.py +78 -0
  37. kinto/core/decorators.py +74 -0
  38. kinto/core/errors.py +216 -0
  39. kinto/core/events.py +301 -0
  40. kinto/core/initialization.py +738 -0
  41. kinto/core/listeners/__init__.py +9 -0
  42. kinto/core/metrics.py +94 -0
  43. kinto/core/openapi.py +115 -0
  44. kinto/core/permission/__init__.py +202 -0
  45. kinto/core/permission/memory.py +167 -0
  46. kinto/core/permission/postgresql/__init__.py +489 -0
  47. kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
  48. kinto/core/permission/postgresql/schema.sql +41 -0
  49. kinto/core/permission/testing.py +487 -0
  50. kinto/core/resource/__init__.py +1311 -0
  51. kinto/core/resource/model.py +412 -0
  52. kinto/core/resource/schema.py +502 -0
  53. kinto/core/resource/viewset.py +230 -0
  54. kinto/core/schema.py +119 -0
  55. kinto/core/scripts.py +50 -0
  56. kinto/core/statsd.py +1 -0
  57. kinto/core/storage/__init__.py +436 -0
  58. kinto/core/storage/exceptions.py +53 -0
  59. kinto/core/storage/generators.py +58 -0
  60. kinto/core/storage/memory.py +651 -0
  61. kinto/core/storage/postgresql/__init__.py +1131 -0
  62. kinto/core/storage/postgresql/client.py +120 -0
  63. kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
  64. kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
  65. kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
  66. kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
  67. kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
  68. kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
  69. kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
  70. kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
  71. kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
  72. kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
  73. kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
  74. kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
  75. kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
  76. kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
  77. kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
  78. kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
  79. kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
  80. kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
  81. kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
  82. kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
  83. kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
  84. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  85. kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
  86. kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
  87. kinto/core/storage/postgresql/migrator.py +98 -0
  88. kinto/core/storage/postgresql/pool.py +55 -0
  89. kinto/core/storage/postgresql/schema.sql +143 -0
  90. kinto/core/storage/testing.py +1857 -0
  91. kinto/core/storage/utils.py +37 -0
  92. kinto/core/testing.py +182 -0
  93. kinto/core/utils.py +553 -0
  94. kinto/core/views/__init__.py +0 -0
  95. kinto/core/views/batch.py +163 -0
  96. kinto/core/views/errors.py +145 -0
  97. kinto/core/views/heartbeat.py +106 -0
  98. kinto/core/views/hello.py +69 -0
  99. kinto/core/views/openapi.py +35 -0
  100. kinto/core/views/version.py +50 -0
  101. kinto/events.py +3 -0
  102. kinto/plugins/__init__.py +0 -0
  103. kinto/plugins/accounts/__init__.py +94 -0
  104. kinto/plugins/accounts/authentication.py +63 -0
  105. kinto/plugins/accounts/scripts.py +61 -0
  106. kinto/plugins/accounts/utils.py +13 -0
  107. kinto/plugins/accounts/views.py +136 -0
  108. kinto/plugins/admin/README.md +3 -0
  109. kinto/plugins/admin/VERSION +1 -0
  110. kinto/plugins/admin/__init__.py +40 -0
  111. kinto/plugins/admin/build/VERSION +1 -0
  112. kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
  113. kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
  114. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  115. kinto/plugins/admin/build/index.html +18 -0
  116. kinto/plugins/admin/public/help.html +25 -0
  117. kinto/plugins/admin/views.py +42 -0
  118. kinto/plugins/default_bucket/__init__.py +191 -0
  119. kinto/plugins/flush.py +28 -0
  120. kinto/plugins/history/__init__.py +65 -0
  121. kinto/plugins/history/listener.py +181 -0
  122. kinto/plugins/history/views.py +66 -0
  123. kinto/plugins/openid/__init__.py +131 -0
  124. kinto/plugins/openid/utils.py +14 -0
  125. kinto/plugins/openid/views.py +193 -0
  126. kinto/plugins/prometheus.py +300 -0
  127. kinto/plugins/statsd.py +85 -0
  128. kinto/schema_validation.py +135 -0
  129. kinto/views/__init__.py +34 -0
  130. kinto/views/admin.py +195 -0
  131. kinto/views/buckets.py +45 -0
  132. kinto/views/collections.py +58 -0
  133. kinto/views/contribute.py +39 -0
  134. kinto/views/groups.py +90 -0
  135. kinto/views/permissions.py +235 -0
  136. kinto/views/records.py +133 -0
  137. kinto-23.2.1.dist-info/METADATA +232 -0
  138. kinto-23.2.1.dist-info/RECORD +142 -0
  139. kinto-23.2.1.dist-info/WHEEL +5 -0
  140. kinto-23.2.1.dist-info/entry_points.txt +5 -0
  141. kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
  142. kinto-23.2.1.dist-info/top_level.txt +1 -0
kinto/core/utils.py ADDED
@@ -0,0 +1,553 @@
1
+ import collections.abc as collections_abc
2
+ import functools
3
+ import hashlib
4
+ import hmac
5
+ import os
6
+ import re
7
+ import time
8
+ from base64 import b64decode, b64encode
9
+ from binascii import hexlify
10
+ from enum import Enum
11
+ from urllib.parse import unquote
12
+
13
+ import jsonpatch
14
+ import rapidjson
15
+ from colander import null
16
+ from pyramid import httpexceptions
17
+ from pyramid.authorization import Authenticated
18
+ from pyramid.interfaces import IRoutesMapper
19
+ from pyramid.request import Request, apply_request_extensions
20
+ from pyramid.settings import aslist
21
+ from pyramid.view import render_view_to_response
22
+
23
+ from kinto.core.cornice import cors
24
+
25
+
26
+ try:
27
+ import sqlalchemy
28
+ except ImportError: # pragma: no cover
29
+ sqlalchemy = None
30
+
31
+ try:
32
+ import memcache
33
+ except ImportError: # pragma: no cover
34
+ memcache = None
35
+
36
+
37
+ class json:
38
+ def dumps(v, **kw):
39
+ kw.setdefault("bytes_mode", rapidjson.BM_NONE)
40
+ return rapidjson.dumps(v, **kw)
41
+
42
+ def load(v, **kw):
43
+ kw.setdefault("number_mode", rapidjson.NM_NATIVE)
44
+ return rapidjson.load(v, **kw)
45
+
46
+ def loads(v, **kw):
47
+ kw.setdefault("number_mode", rapidjson.NM_NATIVE)
48
+ return rapidjson.loads(v, **kw)
49
+
50
+
51
+ json_serializer = json.dumps
52
+
53
+
54
+ def strip_whitespace(v):
55
+ """Remove whitespace, newlines, and tabs from the beginning/end
56
+ of a string.
57
+
58
+ :param str v: the string to strip.
59
+ :rtype: str
60
+ """
61
+ return v.strip(" \t\n\r") if v is not null else v
62
+
63
+
64
+ def msec_time():
65
+ """Return current epoch time in milliseconds.
66
+
67
+ :rtype: int
68
+ """
69
+ return int(time.time() * 1000.0) # floor
70
+
71
+
72
+ def classname(obj):
73
+ """Get a classname from an object.
74
+
75
+ :rtype: str
76
+ """
77
+ return obj.__class__.__name__.lower()
78
+
79
+
80
+ def merge_dicts(a, b):
81
+ """Merge b into a recursively, without overwriting values.
82
+
83
+ :param dict a: the dict that will be altered with values of `b`.
84
+ """
85
+ for k, v in b.items():
86
+ if isinstance(v, dict):
87
+ merge_dicts(a.setdefault(k, {}), v)
88
+ else:
89
+ a.setdefault(k, v)
90
+
91
+
92
+ def recursive_update_dict(root, changes, ignores=()):
93
+ """Update recursively all the entries from a dict and it's children dicts.
94
+
95
+ :param dict root: root dictionary
96
+ :param dict changes: dictonary where changes should be made (default=root)
97
+ :returns dict newd: dictionary with removed entries of val.
98
+ """
99
+ if isinstance(changes, dict):
100
+ for k, v in changes.items():
101
+ if isinstance(v, dict):
102
+ if k not in root:
103
+ root[k] = {}
104
+ recursive_update_dict(root[k], v, ignores)
105
+ elif v in ignores:
106
+ if k in root:
107
+ root.pop(k)
108
+ else:
109
+ root[k] = v
110
+
111
+
112
+ def random_bytes_hex(bytes_length):
113
+ """Return a hexstring of bytes_length cryptographic-friendly random bytes.
114
+
115
+ :param int bytes_length: number of random bytes.
116
+ :rtype: str
117
+ """
118
+ return hexlify(os.urandom(bytes_length)).decode("utf-8")
119
+
120
+
121
+ def native_value(value):
122
+ """Convert string value to native python values.
123
+
124
+ :param str value: value to interprete.
125
+ :returns: the value coerced to python type
126
+ """
127
+ if isinstance(value, str):
128
+ try:
129
+ value = json.loads(value)
130
+ except ValueError:
131
+ return value
132
+ return value
133
+
134
+
135
+ def read_env(key, value):
136
+ """Read the setting key from environment variables.
137
+
138
+ :param key: the setting name
139
+ :param value: default value if undefined in environment
140
+ :returns: the value from environment, coerced to python type, or the (uncoerced) default value
141
+ """
142
+ envkey = key.replace(".", "_").replace("-", "_").upper()
143
+ if envkey in os.environ:
144
+ return native_value(os.environ[envkey])
145
+ return value
146
+
147
+
148
+ def encode64(content, encoding="utf-8"):
149
+ """Encode some content in base64.
150
+
151
+ :rtype: str
152
+ """
153
+ return b64encode(content.encode(encoding)).decode(encoding)
154
+
155
+
156
+ def decode64(encoded_content, encoding="utf-8"):
157
+ """Decode some base64 encoded content.
158
+
159
+ :rtype: str
160
+ """
161
+ return b64decode(encoded_content.encode(encoding)).decode(encoding)
162
+
163
+
164
+ def hmac_digest(secret, message, encoding="utf-8"):
165
+ """Return hex digest of a message HMAC using secret"""
166
+ if isinstance(secret, str):
167
+ secret = secret.encode(encoding)
168
+ return hmac.new(secret, message.encode(encoding), hashlib.sha256).hexdigest()
169
+
170
+
171
+ def dict_subset(d, keys):
172
+ """Return a dict with the specified keys"""
173
+ result = {}
174
+
175
+ for key in keys:
176
+ if "." in key:
177
+ field, subfield = key.split(".", 1)
178
+ if isinstance(d.get(field), collections_abc.Mapping):
179
+ subvalue = dict_subset(d[field], [subfield])
180
+ result[field] = dict_merge(subvalue, result.get(field, {}))
181
+ elif field in d:
182
+ result[field] = d[field]
183
+ else:
184
+ if key in d:
185
+ result[key] = d[key]
186
+
187
+ return result
188
+
189
+
190
+ def dict_merge(a, b):
191
+ """Merge the two specified dicts"""
192
+ result = dict(**b)
193
+ for key, value in a.items():
194
+ if isinstance(value, collections_abc.Mapping):
195
+ value = dict_merge(value, result.setdefault(key, {}))
196
+ result[key] = value
197
+ return result
198
+
199
+
200
+ def find_nested_value(d, path, default=None):
201
+ """Finds a nested value in a dict from a dotted path key string.
202
+
203
+ :param dict d: the dict to retrieve nested value from
204
+ :param str path: the path to the nested value, in dot notation
205
+ :returns: the nested value if any was found, or None
206
+ """
207
+ if path in d:
208
+ return d.get(path)
209
+
210
+ # the challenge is to identify what is the root key, as dict keys may
211
+ # contain dot characters themselves
212
+ parts = path.split(".")
213
+
214
+ # build a list of all possible root keys from all the path parts
215
+ candidates = [".".join(parts[: i + 1]) for i in range(len(parts))]
216
+
217
+ # we start with the longest candidate paths as they're most likely to be the
218
+ # ones we want if they match
219
+ root = next((key for key in reversed(candidates) if key in d), None)
220
+
221
+ # if no valid root candidates were found, the path is invalid; abandon
222
+ if root is None or not isinstance(d.get(root), dict):
223
+ return default
224
+
225
+ # we have our root key, extract the new subpath and recur
226
+ subpath = path.replace(root + ".", "", 1)
227
+ return find_nested_value(d.get(root), subpath, default=default)
228
+
229
+
230
+ class COMPARISON(Enum):
231
+ LT = "<"
232
+ MIN = ">="
233
+ MAX = "<="
234
+ NOT = "!="
235
+ EQ = "=="
236
+ GT = ">"
237
+ IN = "in"
238
+ EXCLUDE = "exclude"
239
+ LIKE = "like"
240
+ HAS = "has"
241
+ # The order matters here because we want to match
242
+ # contains_any before contains_
243
+ CONTAINS_ANY = "contains_any"
244
+ CONTAINS = "contains"
245
+
246
+
247
+ def reapply_cors(request, response):
248
+ """Reapply cors headers to the new response with regards to the request.
249
+
250
+ We need to re-apply the CORS checks done by Cornice, in case we're
251
+ recreating the response from scratch.
252
+
253
+ """
254
+ service = request.current_service
255
+ if service:
256
+ request.info["cors_checked"] = False
257
+ cors.apply_cors_post_request(service, request, response)
258
+ response = cors.ensure_origin(service, request, response)
259
+ else:
260
+ # No existing service is concerned, and Cornice is not implied.
261
+ origin = request.headers.get("Origin")
262
+ if origin:
263
+ settings = request.registry.settings
264
+ allowed_origins = set(aslist(settings["cors_origins"]))
265
+ required_origins = {"*", origin}
266
+ matches = allowed_origins.intersection(required_origins)
267
+ if matches:
268
+ response.headers["Access-Control-Allow-Origin"] = matches.pop()
269
+
270
+ # Import service here because kinto.core import utils
271
+ from kinto.core import Service
272
+
273
+ if Service.default_cors_headers: # pragma: no branch
274
+ headers = ",".join(Service.default_cors_headers)
275
+ response.headers["Access-Control-Expose-Headers"] = headers
276
+ return response
277
+
278
+
279
+ def log_context(request, **kwargs):
280
+ """Bind information to the current request summary log."""
281
+ non_empty = {k: v for k, v in kwargs.items() if v is not None}
282
+ try:
283
+ request._log_context.update(**non_empty)
284
+ except AttributeError:
285
+ request._log_context = non_empty
286
+ return request._log_context
287
+
288
+
289
+ def current_service(request):
290
+ """Return the Cornice service matching the specified request.
291
+
292
+ :returns: the service or None if unmatched.
293
+ :rtype: kinto.core.cornice.Service
294
+ """
295
+ if request.matched_route:
296
+ services = request.registry.cornice_services
297
+ pattern = request.matched_route.pattern
298
+ try:
299
+ service = services[pattern]
300
+ except KeyError:
301
+ return None
302
+ else:
303
+ return service
304
+
305
+
306
+ def current_resource_name(request):
307
+ """Return the name used when the kinto.core resource was registered along its
308
+ viewset.
309
+
310
+ :returns: the resource identifier.
311
+ :rtype: str
312
+ """
313
+ service = current_service(request)
314
+ resource_name = service.viewset.get_name(service.resource)
315
+ return resource_name
316
+
317
+
318
+ def prefixed_userid(request):
319
+ """In Kinto users ids are prefixed with the policy name that is
320
+ contained in Pyramid Multiauth.
321
+ If a custom authn policy is used, without authn_type, this method returns
322
+ the user id without prefix.
323
+ """
324
+ # If pyramid_multiauth is used, a ``authn_type`` is set on request
325
+ # when a policy succesfully authenticates a user.
326
+ # (see :func:`kinto.core.initialization.setup_authentication`)
327
+ authn_type = getattr(request, "authn_type", None)
328
+ if authn_type is not None:
329
+ return f"{authn_type}:{request.selected_userid}"
330
+
331
+
332
+ def prefixed_principals(request):
333
+ """
334
+ :returns: the list principals with prefixed user id.
335
+ """
336
+ principals = request.effective_principals
337
+ if Authenticated not in principals:
338
+ return principals
339
+
340
+ # Remove unprefixed user id on effective_principals to avoid conflicts.
341
+ # (it is added via Pyramid Authn policy effective principals)
342
+ prefix, userid = request.prefixed_userid.split(":", 1)
343
+ principals = [p for p in principals if p != userid]
344
+
345
+ if request.prefixed_userid not in principals:
346
+ principals = [request.prefixed_userid] + principals
347
+
348
+ return principals
349
+
350
+
351
+ def build_request(original, dict_obj):
352
+ """
353
+ Transform a dict object into a :class:`pyramid.request.Request` object.
354
+
355
+ It sets a ``parent`` attribute on the resulting request assigned with
356
+ the `original` request specified.
357
+
358
+ :param original: the original request.
359
+ :param dict_obj: a dict object with the sub-request specifications.
360
+ """
361
+ api_prefix = "/{}".format(original.upath_info.split("/")[1])
362
+ path = dict_obj["path"]
363
+ if not path.startswith(api_prefix):
364
+ path = api_prefix + path
365
+
366
+ path = path.encode("utf-8")
367
+
368
+ method = dict_obj.get("method") or "GET"
369
+
370
+ headers = dict(original.headers)
371
+ headers.update(**dict_obj.get("headers") or {})
372
+ # Body can have different length, do not use original header.
373
+ headers.pop("Content-Length", None)
374
+
375
+ payload = dict_obj.get("body") or ""
376
+
377
+ # Payload is always a dict (from ``BatchRequestSchema.body``).
378
+ # Send it as JSON for subrequests.
379
+ if isinstance(payload, dict):
380
+ headers["Content-Type"] = "application/json; charset=utf-8"
381
+ payload = json.dumps(payload)
382
+
383
+ request = Request.blank(
384
+ path=path.decode("latin-1"), headers=headers, POST=payload, method=method
385
+ )
386
+ request.registry = original.registry
387
+ apply_request_extensions(request)
388
+
389
+ # This is used to distinguish subrequests from direct incoming requests.
390
+ # See :func:`kinto.core.initialization.setup_logging()`
391
+ request.parent = original
392
+
393
+ return request
394
+
395
+
396
+ def build_response(response, request):
397
+ """
398
+ Transform a :class:`pyramid.response.Response` object into a serializable
399
+ dict.
400
+
401
+ :param response: a response object, returned by Pyramid.
402
+ :param request: the request that was used to get the response.
403
+ """
404
+ dict_obj = {}
405
+ dict_obj["path"] = unquote(request.path)
406
+ dict_obj["status"] = response.status_code
407
+ dict_obj["headers"] = dict(response.headers)
408
+
409
+ body = ""
410
+ if request.method != "HEAD":
411
+ # XXX : Pyramid should not have built response body for HEAD!
412
+ try:
413
+ body = response.json
414
+ except ValueError:
415
+ body = response.body
416
+ dict_obj["body"] = body
417
+
418
+ return dict_obj
419
+
420
+
421
+ def follow_subrequest(request, subrequest, **kwargs):
422
+ """Run a subrequest (e.g. batch), and follow the redirection if any.
423
+
424
+ :rtype: tuple
425
+ :returns: the response and the redirection request (or `subrequest`
426
+ if no redirection happened.)
427
+ """
428
+ try:
429
+ try:
430
+ return request.invoke_subrequest(subrequest, **kwargs), subrequest
431
+ except Exception as e:
432
+ resp = render_view_to_response(e, subrequest)
433
+ if not resp or resp.status_code >= 500:
434
+ raise e
435
+ raise resp
436
+ except httpexceptions.HTTPRedirection as e:
437
+ new_location = e.headers["Location"]
438
+ new_request = Request.blank(
439
+ path=new_location,
440
+ headers=subrequest.headers,
441
+ POST=subrequest.body,
442
+ method=subrequest.method,
443
+ )
444
+ new_request.bound_data = subrequest.bound_data
445
+ new_request.parent = getattr(subrequest, "parent", None)
446
+ return request.invoke_subrequest(new_request, **kwargs), new_request
447
+
448
+
449
+ def strip_uri_prefix(path):
450
+ """
451
+ Remove potential version prefix in URI.
452
+ """
453
+ return re.sub(r"^(/v\d+)?", "", str(path))
454
+
455
+
456
+ def view_lookup(request, uri):
457
+ """
458
+ A convenience method for view_lookup_registry when you have a request.
459
+
460
+ :param request: the current request (used to obtain registry).
461
+ :param uri: a plural or object endpoint URI.
462
+ :rtype: tuple
463
+ :returns: the resource name and the associated matchdict.
464
+ """
465
+ return view_lookup_registry(request.registry, uri)
466
+
467
+
468
+ def view_lookup_registry(registry, uri):
469
+ """
470
+ Look-up the specified `uri` and return the associated resource name
471
+ along the match dict.
472
+
473
+ :param registry: the application's registry.
474
+ :param uri: a plural or object endpoint URI.
475
+ :rtype: tuple
476
+ :returns: the resource name and the associated matchdict.
477
+ """
478
+ api_prefix = f"/{registry.route_prefix}"
479
+ path = api_prefix + uri
480
+
481
+ q = registry.queryUtility
482
+ routes_mapper = q(IRoutesMapper)
483
+
484
+ fakerequest = Request.blank(path=path)
485
+ info = routes_mapper(fakerequest)
486
+ matchdict, route = info["match"], info["route"]
487
+ if route is None:
488
+ raise ValueError("URI has no route")
489
+
490
+ resource_name = route.name.replace("-object", "").replace("-plural", "")
491
+ return resource_name, matchdict
492
+
493
+
494
+ def instance_uri(request, resource_name, **params):
495
+ """Return the URI for the given resource."""
496
+ return strip_uri_prefix(request.route_path(f"{resource_name}-object", **params))
497
+
498
+
499
+ def instance_uri_registry(registry, resource_name, **params):
500
+ """Return the URI for the given resource, even if you don't have a request.
501
+
502
+ This gins up a request using Request.blank and so does not support
503
+ any routes with pregenerators.
504
+ """
505
+ request = Request.blank(path="")
506
+ request.registry = registry
507
+ return instance_uri(request, resource_name, **params)
508
+
509
+
510
+ def apply_json_patch(obj, ops):
511
+ """
512
+ Apply JSON Patch operations using jsonpatch.
513
+
514
+ :param object: base object where changes should be applied (not in-place).
515
+ :param list changes: list of JSON patch operations.
516
+ :param bool only_data: param to limit the scope of the patch only to 'data'.
517
+ :returns dict data: patched object data.
518
+ dict permissions: patched object permissions
519
+ """
520
+ data = {**obj}
521
+
522
+ # Permissions should always have read and write fields defined (to allow add)
523
+ permissions = {"read": set(), "write": set()}
524
+
525
+ # Get permissions if available on the resource (using SharableResource)
526
+ permissions.update(data.pop("__permissions__", {}))
527
+
528
+ # Permissions should be mapped as a dict, since jsonpatch doesn't accept
529
+ # sets and lists are mapped as JSON arrays (not indexed by value)
530
+ permissions = {k: {i: i for i in v} for k, v in permissions.items()}
531
+
532
+ resource = {"data": data, "permissions": permissions}
533
+
534
+ # Allow patch permissions without value since key and value are equal on sets
535
+ for op in ops:
536
+ # 'path' is here since it was validated.
537
+ if op["path"].startswith(("/permissions/read/", "/permissions/write/")):
538
+ op["value"] = op["path"].split("/")[-1]
539
+
540
+ try:
541
+ result = jsonpatch.apply_patch(resource, ops)
542
+
543
+ except (jsonpatch.JsonPatchException, jsonpatch.JsonPointerException) as e:
544
+ raise ValueError(e)
545
+
546
+ return result
547
+
548
+
549
+ def safe_wraps(wrapper, *args, **kwargs):
550
+ """Safely wraps partial functions."""
551
+ while isinstance(wrapper, functools.partial):
552
+ wrapper = wrapper.func
553
+ return functools.wraps(wrapper, *args, **kwargs)
File without changes