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
@@ -0,0 +1,311 @@
1
+ import functools
2
+ import logging
3
+
4
+ from pyramid.authorization import Authenticated
5
+ from pyramid.interfaces import IAuthorizationPolicy
6
+ from pyramid.settings import aslist
7
+ from zope.interface import implementer
8
+
9
+ from kinto.core import utils
10
+ from kinto.core.storage import exceptions as storage_exceptions
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # When permission is set to "private", only the current user is allowed.
16
+ PRIVATE = "private"
17
+
18
+ # A permission is called "dynamic" when it's computed at request time.
19
+ DYNAMIC = "dynamic"
20
+
21
+
22
+ def groupfinder(userid, request):
23
+ """Fetch principals from permission backend for the specified `userid`.
24
+
25
+ This is plugged by default using the ``multiauth.groupfinder`` setting.
26
+ """
27
+ backend = getattr(request.registry, "permission", None)
28
+ # Permission backend not configured. Ignore.
29
+ if not backend:
30
+ return []
31
+
32
+ # Safety check when Kinto-Core is used without pyramid_multiauth.
33
+ if request.prefixed_userid:
34
+ userid = request.prefixed_userid
35
+
36
+ # Query the permission backend only once per request (e.g. batch).
37
+ reify_key = userid + "_principals"
38
+ if reify_key not in request.bound_data:
39
+ principals = backend.get_user_principals(userid)
40
+ request.bound_data[reify_key] = principals
41
+
42
+ return request.bound_data[reify_key]
43
+
44
+
45
+ @implementer(IAuthorizationPolicy)
46
+ class AuthorizationPolicy:
47
+ """Default authorization class, that leverages the permission backend
48
+ for shareable resources.
49
+ """
50
+
51
+ get_bound_permissions = None
52
+ """Callable that takes an object id and a permission and returns
53
+ a list of tuples (<object id>, <permission>). Useful when objects
54
+ permission depend on others."""
55
+
56
+ def permits(self, context, principals, permission):
57
+ if permission == PRIVATE:
58
+ # When using the private permission, we bypass the permissions
59
+ # backend, and simply authorize if authenticated.
60
+ return Authenticated in principals
61
+
62
+ principals = context.get_prefixed_principals()
63
+
64
+ create_permission = f"{context.resource_name}:create"
65
+
66
+ permission = context.required_permission
67
+ if permission == "create":
68
+ permission = create_permission
69
+
70
+ object_id = context.permission_object_id
71
+ bound_perms = self._get_bound_permissions(object_id, permission)
72
+
73
+ allowed = context.check_permission(principals, bound_perms)
74
+
75
+ # Here we consider that parent URI is one path level above.
76
+ parent_uri = "/".join(object_id.split("/")[:-1]) if object_id else None
77
+
78
+ # If not allowed to delete/patch, and target object is missing, and
79
+ # allowed to read the parent, then view is permitted (will raise 404
80
+ # later anyway). See Kinto/kinto#918
81
+ is_object_unknown = not context.on_plural_endpoint and context.current_object is None
82
+ if context.required_permission == "write" and is_object_unknown:
83
+ bound_perms = self._get_bound_permissions(parent_uri, "read")
84
+ allowed = context.check_permission(principals, bound_perms)
85
+
86
+ # If not allowed on this plural endpoint, but some objects are shared with
87
+ # the current user, then authorize.
88
+ # The :class:`kinto.core.resource.Resource` class will take care of the filtering.
89
+ is_list_operation = (
90
+ context.on_plural_endpoint
91
+ and not permission.endswith("create")
92
+ and context.current_object is None
93
+ )
94
+ if not allowed and is_list_operation:
95
+ allowed = bool(
96
+ context.fetch_shared_objects(permission, principals, self.get_bound_permissions)
97
+ )
98
+ if not allowed:
99
+ # If allowed to create this kind of object on parent,
100
+ # then allow to obtain the list.
101
+ if len(bound_perms) > 0:
102
+ bound_perms = [(parent_uri, create_permission)]
103
+ else:
104
+ bound_perms = [("", "create")] # Root object.
105
+ allowed = context.check_permission(principals, bound_perms)
106
+
107
+ if not allowed:
108
+ logger.info(
109
+ "Permission %r on %r not granted to %r.",
110
+ permission,
111
+ object_id,
112
+ principals[0],
113
+ extra=dict(userid=principals[0], uri=object_id, perm=permission),
114
+ )
115
+
116
+ return allowed
117
+
118
+ def _get_bound_permissions(self, object_id, permission):
119
+ if self.get_bound_permissions is None:
120
+ # Permission to 'write' gives permission to 'read'.
121
+ bound = [(object_id, permission)]
122
+ if permission == "read":
123
+ bound += [(object_id, "write")]
124
+ return bound
125
+ return self.get_bound_permissions(object_id, permission)
126
+
127
+ def principals_allowed_by_permission(self, context, permission):
128
+ raise NotImplementedError() # PRAGMA NOCOVER
129
+
130
+
131
+ class RouteFactory:
132
+ resource_name = None
133
+ on_plural_endpoint = False
134
+ required_permission = None
135
+ permission_object_id = None
136
+ current_object = None
137
+ shared_ids = None
138
+
139
+ method_permissions = {
140
+ "head": "read",
141
+ "get": "read",
142
+ "post": "create",
143
+ "delete": "write",
144
+ "patch": "write",
145
+ }
146
+
147
+ def __init__(self, request):
148
+ # Store some shortcuts.
149
+ permission = request.registry.permission
150
+ self._check_permission = permission.check_permission
151
+ self._get_accessible_objects = permission.get_accessible_objects
152
+
153
+ self.get_prefixed_principals = functools.partial(utils.prefixed_principals, request)
154
+
155
+ # Store current resource and required permission.
156
+ service = utils.current_service(request)
157
+ is_on_resource = (
158
+ service is not None and hasattr(service, "viewset") and hasattr(service, "resource")
159
+ )
160
+ self._resource = None
161
+ if is_on_resource:
162
+ self.resource_name = request.current_resource_name
163
+ self.on_plural_endpoint = getattr(service, "type", None) == "plural"
164
+
165
+ # Check if this request targets an individual object.
166
+ # Its existence will affect permissions checking (cf `_find_required_permission()`).
167
+ # There are cases where the permission is not directly related to the HTTP method,
168
+ # For example:
169
+ # - with POST on plural endpoint, with an id supplied
170
+ # - with PUT on an object, which can either be creation or update
171
+ is_write_on_object = not self.on_plural_endpoint and request.method.lower() in (
172
+ "put",
173
+ "delete",
174
+ "patch",
175
+ )
176
+ is_post_on_plural = self.on_plural_endpoint and request.method.lower() == "post"
177
+ if is_write_on_object or is_post_on_plural:
178
+ # We instantiate the resource to determine the object targeted by the request.
179
+ self._resource = resource = service.resource(request=request, context=self)
180
+ if resource.object_id is not None: # Skip POST on plural without id.
181
+ try:
182
+ # Save a reference, to avoid refetching from storage in resource.
183
+ self.current_object = resource.model.get_object(resource.object_id)
184
+ except storage_exceptions.ObjectNotFoundError:
185
+ pass
186
+
187
+ self.permission_object_id, self.required_permission = self._find_required_permission(
188
+ request, service
189
+ )
190
+
191
+ # To obtain shared objects on a plural endpoint, use a match:
192
+ self._object_id_match = self.get_permission_object_id(request, "*")
193
+
194
+ self._settings = request.registry.settings
195
+
196
+ def check_permission(self, principals, bound_perms):
197
+ """Read allowed principals from settings, if not any, query the permission
198
+ backend to check if view is allowed.
199
+ """
200
+ if not bound_perms:
201
+ bound_perms = [(self.resource_name, self.required_permission)]
202
+ for _, permission in bound_perms:
203
+ # With Kinto inheritance tree, we can have: `permission = "record:create"`
204
+ if self.resource_name and permission.startswith(self.resource_name):
205
+ setting = f"{permission.replace(':', '_')}_principals"
206
+ else:
207
+ setting = f"{self.resource_name}_{permission}_principals"
208
+ allowed_principals = aslist(self._settings.get(setting, ""))
209
+ if allowed_principals:
210
+ if bool(set(allowed_principals) & set(principals)):
211
+ return True
212
+ return self._check_permission(principals, bound_perms)
213
+
214
+ def fetch_shared_objects(self, perm, principals, get_bound_permissions):
215
+ """Fetch objects that are readable or writable for the current
216
+ principals.
217
+
218
+ See :meth:`kinto.core.authorization.AuthorizationPolicy.permits`
219
+
220
+ If no object is shared, it returns None.
221
+
222
+ .. warning::
223
+ This sets the ``shared_ids`` attribute to the context with the
224
+ return value. The attribute is then read by
225
+ :class:`kinto.core.resource.Resource`
226
+ """
227
+ if get_bound_permissions:
228
+ bound_perms = get_bound_permissions(self._object_id_match, perm)
229
+ else:
230
+ bound_perms = [(self._object_id_match, perm)]
231
+ by_obj_id = self._get_accessible_objects(principals, bound_perms, with_children=False)
232
+ ids = by_obj_id.keys()
233
+ # Store for later use in ``Resource``.
234
+ self.shared_ids = [self._extract_object_id(id_) for id_ in ids]
235
+ return self.shared_ids
236
+
237
+ def get_permission_object_id(self, request, object_id=None):
238
+ """Returns the permission object id for the current request.
239
+ In the nominal case, it is just the current URI without version prefix.
240
+ For plural endpoint, it is the related object URI using the specified
241
+ `object_id`.
242
+
243
+ See :meth:`kinto.core.resource.model.SharableModel` and
244
+ :meth:`kinto.core.authorization.RouteFactory.__init__`
245
+ """
246
+ object_uri = utils.strip_uri_prefix(request.path)
247
+
248
+ if self.on_plural_endpoint and object_id is not None:
249
+ # With the current request on a plural endpoint, the object URI must
250
+ # be found out by inspecting the "plural" service and its sibling
251
+ # "object" service. (see `register_resource()`)
252
+ matchdict = {**request.matchdict, "id": object_id}
253
+ try:
254
+ object_uri = utils.instance_uri(request, self.resource_name, **matchdict)
255
+ object_uri = object_uri.replace("%2A", "*")
256
+ except KeyError:
257
+ # Maybe the resource has no single object endpoint.
258
+ # We consider that object URIs in permissions backend will
259
+ # be stored naively:
260
+ object_uri = f"{object_uri}/{object_id}"
261
+
262
+ return object_uri
263
+
264
+ def _extract_object_id(self, object_uri):
265
+ # XXX: Rewrite using kinto.core.utils.view_lookup() and matchdict['id']
266
+ return object_uri.split("/")[-1]
267
+
268
+ def _find_required_permission(self, request, service):
269
+ """Find out what is the permission object id and the required
270
+ permission.
271
+
272
+ .. note::
273
+ This method saves an attribute ``self.current_object`` used
274
+ in :class:`kinto.core.resource.Resource`.
275
+ """
276
+ # By default, it's a URI a and permission associated to the method.
277
+ permission_object_id = self.get_permission_object_id(request)
278
+ method = request.method.lower()
279
+ required_permission = self.method_permissions.get(method)
280
+
281
+ # For create permission, the object id is the plural endpoint.
282
+ plural_path = str(service.plural_path)
283
+ plural_path = plural_path.format_map(request.matchdict)
284
+
285
+ # In the case of a "PUT", check if the targetted object already
286
+ # exists, return "write" if it does, "create" otherwise.
287
+ if request.method.lower() == "put":
288
+ if self.current_object is None:
289
+ # The object does not exist, the permission to create on
290
+ # the related plural endpoint is required.
291
+ permission_object_id = plural_path
292
+ required_permission = "create"
293
+ else:
294
+ # For safe creations, the user needs a create permission.
295
+ # See Kinto/kinto#792
296
+ if request.headers.get("If-None-Match") == "*":
297
+ permission_object_id = plural_path
298
+ required_permission = "create"
299
+ else:
300
+ required_permission = "write"
301
+
302
+ # In the case of a "POST" on a plural endpoint, if an "id" was
303
+ # specified, then the object is returned. The required permission
304
+ # is thus "read" on this object.
305
+ if request.method.lower() == "post" and self.current_object is not None:
306
+ permission_object_id = self.get_permission_object_id(
307
+ request, object_id=self._resource.object_id
308
+ )
309
+ required_permission = "read"
310
+
311
+ return (permission_object_id, required_permission)
@@ -0,0 +1,131 @@
1
+ import logging
2
+ import random
3
+
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ _HEARTBEAT_DELETE_RATE = 0.5
9
+ _HEARTBEAT_KEY = "__heartbeat__"
10
+ _HEARTBEAT_TTL_SECONDS = 3600
11
+
12
+
13
+ _CACHE_HIT_METRIC_KEY = "cache_hits"
14
+ _CACHE_MISS_METRIC_KEY = "cache_misses"
15
+
16
+
17
+ class CacheBase:
18
+ def __init__(self, *args, **kwargs):
19
+ self.prefix = kwargs["cache_prefix"]
20
+ self.max_size_bytes = kwargs.get("cache_max_size_bytes")
21
+ self.set_metrics_backend(kwargs.get("metrics_backend"))
22
+
23
+ def initialize_schema(self, dry_run=False):
24
+ """Create every necessary objects (like tables or indices) in the
25
+ backend.
26
+
27
+ This is executed when the ``kinto migrate`` command is run.
28
+
29
+ :param bool dry_run: simulate instead of executing the operations.
30
+ """
31
+ raise NotImplementedError
32
+
33
+ def flush(self):
34
+ """Delete every values."""
35
+ raise NotImplementedError
36
+
37
+ def ttl(self, key):
38
+ """Obtain the expiration value of the specified `key`.
39
+
40
+ :param str key: key
41
+ :returns: number of seconds or negative if no TTL.
42
+ :rtype: float
43
+ """
44
+ raise NotImplementedError
45
+
46
+ def expire(self, key, ttl):
47
+ """Set the expiration value `ttl` for the specified `key`.
48
+
49
+ :param str key: key
50
+ :param float ttl: number of seconds
51
+ """
52
+ raise NotImplementedError
53
+
54
+ def set(self, key, value, ttl):
55
+ """Store a value with the specified `key`.
56
+
57
+ :param str key: key
58
+ :param str value: value to store
59
+ :param float ttl: expire after number of seconds
60
+ """
61
+ raise NotImplementedError
62
+
63
+ def get(self, key):
64
+ """Obtain the value of the specified `key`.
65
+
66
+ :param str key: key
67
+ :returns: the stored value or None if missing.
68
+ :rtype: str
69
+ """
70
+ raise NotImplementedError
71
+
72
+ def delete(self, key):
73
+ """Delete the value of the specified `key`.
74
+
75
+ :param str key: key
76
+ """
77
+ raise NotImplementedError
78
+
79
+ def set_metrics_backend(self, metrics_backend):
80
+ """Set a metrics backend via the `CacheMetricsBackend` adapter.
81
+
82
+ :param metrics_backend: A metrics backend implementing the IMetricsService interface.
83
+ """
84
+ self.metrics_backend = CacheMetricsBackend(metrics_backend)
85
+
86
+
87
+ class CacheMetricsBackend:
88
+ """
89
+ A simple adapter for tracking cache-related metrics.
90
+ """
91
+
92
+ def __init__(self, metrics_backend, *args, **kwargs):
93
+ """Initialize with a given metrics backend.
94
+
95
+ :param metrics_backend: A metrics backend implementing the IMetricsService interface.
96
+ """
97
+ self._backend = metrics_backend
98
+
99
+ def count_hit(self):
100
+ """Increment the cache hit counter."""
101
+ if self._backend:
102
+ self._backend.count(key=_CACHE_HIT_METRIC_KEY)
103
+
104
+ def count_miss(self):
105
+ """Increment the cache miss counter."""
106
+ if self._backend:
107
+ self._backend.count(key=_CACHE_MISS_METRIC_KEY)
108
+
109
+
110
+ def heartbeat(backend):
111
+ def ping(request):
112
+ """Test that cache backend is operational.
113
+
114
+ :param request: current request object
115
+ :type request: :class:`~pyramid:pyramid.request.Request`
116
+ :returns: ``True`` is everything is ok, ``False`` otherwise.
117
+ :rtype: bool
118
+ """
119
+ # No specific case for readonly mode because the cache should
120
+ # continue to work in that mode.
121
+ try:
122
+ if random.SystemRandom().random() < _HEARTBEAT_DELETE_RATE:
123
+ backend.delete(_HEARTBEAT_KEY)
124
+ return backend.get(_HEARTBEAT_KEY) is None
125
+ backend.set(_HEARTBEAT_KEY, "alive", _HEARTBEAT_TTL_SECONDS)
126
+ return backend.get(_HEARTBEAT_KEY) == "alive"
127
+ except Exception:
128
+ logger.exception("Heartbeat Failure")
129
+ return False
130
+
131
+ return ping
@@ -0,0 +1,112 @@
1
+ import logging
2
+ from functools import wraps
3
+ from math import ceil, floor
4
+ from time import time
5
+
6
+ from pyramid.settings import aslist
7
+
8
+ from kinto.core.cache import CacheBase
9
+ from kinto.core.storage import exceptions
10
+ from kinto.core.utils import json, memcache
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def wrap_memcached_error(func):
17
+ @wraps(func)
18
+ def wrapped(*args, **kwargs):
19
+ try:
20
+ return func(*args, **kwargs)
21
+ except TypeError:
22
+ raise
23
+ except (
24
+ memcache.Client.MemcachedKeyError,
25
+ memcache.Client.MemcachedStringEncodingError,
26
+ ) as e:
27
+ logger.exception(e)
28
+ raise exceptions.BackendError(original=e)
29
+
30
+ return wrapped
31
+
32
+
33
+ def create_from_config(config, prefix=""):
34
+ """Memcached client instantiation from settings."""
35
+ settings = config.get_settings()
36
+ hosts = aslist(settings[prefix + "hosts"])
37
+ return memcache.Client(hosts)
38
+
39
+
40
+ class Cache(CacheBase):
41
+ """Cache backend implementation using Memcached.
42
+
43
+ Enable in configuration::
44
+
45
+ kinto.cache_backend = kinto.core.cache.memcached
46
+
47
+ *(Optional)* Instance location URI can be customized::
48
+
49
+ kinto.cache_hosts = 127.0.0.1:11211 127.0.0.1:11212
50
+
51
+ :noindex:
52
+
53
+ """
54
+
55
+ def __init__(self, client, *args, **kwargs):
56
+ super(Cache, self).__init__(*args, **kwargs)
57
+ self._client = client
58
+
59
+ def initialize_schema(self, dry_run=False):
60
+ # Nothing to do.
61
+ pass
62
+
63
+ @wrap_memcached_error
64
+ def flush(self):
65
+ self._client.flush_all()
66
+
67
+ @wrap_memcached_error
68
+ def _get(self, key):
69
+ value = self._client.get(self.prefix + key)
70
+ if not value:
71
+ self.metrics_backend.count_miss()
72
+ return None, 0
73
+ self.metrics_backend.count_hit()
74
+ data = json.loads(value)
75
+ return data["value"], data["ttl"]
76
+
77
+ def ttl(self, key):
78
+ _, ttl = self._get(key)
79
+ val = ttl - time()
80
+ return floor(val)
81
+
82
+ def get(self, key):
83
+ value, _ = self._get(key)
84
+ return value
85
+
86
+ @wrap_memcached_error
87
+ def expire(self, key, ttl):
88
+ if ttl == 0:
89
+ self.delete(key)
90
+ else:
91
+ # We can't use touch here because we need to update the TTL value in the record.
92
+ value = self.get(key)
93
+ self.set(key, value, ttl)
94
+
95
+ @wrap_memcached_error
96
+ def set(self, key, value, ttl):
97
+ if isinstance(value, bytes):
98
+ raise TypeError("a string-like object is required, not 'bytes'")
99
+ value = json.dumps({"value": value, "ttl": ceil(time() + ttl)})
100
+ self._client.set(self.prefix + key, value, int(ttl))
101
+
102
+ @wrap_memcached_error
103
+ def delete(self, key):
104
+ value = self.get(key)
105
+ self._client.delete(self.prefix + key)
106
+ return value
107
+
108
+
109
+ def load_from_config(config):
110
+ settings = config.get_settings()
111
+ client = create_from_config(config, prefix="cache_")
112
+ return Cache(client, cache_prefix=settings["cache_prefix"])
@@ -0,0 +1,104 @@
1
+ import logging
2
+
3
+ from kinto.core.cache import CacheBase
4
+ from kinto.core.decorators import synchronized
5
+ from kinto.core.utils import msec_time
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class Cache(CacheBase):
12
+ """Cache backend implementation in local process memory.
13
+
14
+ Enable in configuration::
15
+
16
+ kinto.cache_backend = kinto.core.cache.memory
17
+
18
+ :noindex:
19
+ """
20
+
21
+ def __init__(self, *args, **kwargs):
22
+ super().__init__(*args, **kwargs)
23
+ self.flush()
24
+
25
+ def initialize_schema(self, dry_run=False):
26
+ # Nothing to do.
27
+ pass
28
+
29
+ def flush(self):
30
+ self._created_at = {}
31
+ self._ttl = {}
32
+ self._store = {}
33
+ self._quota = 0
34
+
35
+ def _clean_expired(self):
36
+ current = msec_time()
37
+ expired = [k for k, v in self._ttl.items() if current >= v]
38
+ for expired_item_key in expired:
39
+ self.delete(expired_item_key[len(self.prefix) :])
40
+
41
+ def _clean_oversized(self):
42
+ if self._quota < self.max_size_bytes:
43
+ return
44
+
45
+ for key, value in sorted(self._created_at.items(), key=lambda k: k[1]):
46
+ if self._quota < (self.max_size_bytes * 0.8):
47
+ break
48
+ self.delete(key[len(self.prefix) :])
49
+
50
+ @synchronized
51
+ def ttl(self, key):
52
+ ttl = self._ttl.get(self.prefix + key)
53
+ if ttl is not None:
54
+ return (ttl - msec_time()) / 1000.0
55
+ return -1
56
+
57
+ @synchronized
58
+ def expire(self, key, ttl):
59
+ self._ttl[self.prefix + key] = msec_time() + int(ttl * 1000.0)
60
+
61
+ @synchronized
62
+ def set(self, key, value, ttl):
63
+ if isinstance(value, bytes):
64
+ raise TypeError("a string-like object is required, not 'bytes'")
65
+ self._clean_expired()
66
+ self._clean_oversized()
67
+ self.expire(key, ttl)
68
+ item_key = self.prefix + key
69
+ self._store[item_key] = value
70
+ self._created_at[item_key] = msec_time()
71
+ self._quota += size_of(item_key, value)
72
+
73
+ @synchronized
74
+ def get(self, key):
75
+ self._clean_expired()
76
+ value = self._store.get(self.prefix + key)
77
+ if value is None:
78
+ self.metrics_backend.count_miss()
79
+ return None
80
+ self.metrics_backend.count_hit()
81
+ return value
82
+
83
+ @synchronized
84
+ def delete(self, key):
85
+ key = self.prefix + key
86
+ self._ttl.pop(key, None)
87
+ self._created_at.pop(key, None)
88
+ value = self._store.pop(key, None)
89
+ self._quota -= size_of(key, value)
90
+ return value
91
+
92
+
93
+ def load_from_config(config):
94
+ settings = config.get_settings()
95
+ return Cache(
96
+ cache_prefix=settings["cache_prefix"],
97
+ cache_max_size_bytes=settings["cache_max_size_bytes"],
98
+ )
99
+
100
+
101
+ def size_of(key, value):
102
+ # Key used for ttl, created_at and store.
103
+ # Int size is 24 bytes one for ttl and one for created_at values
104
+ return len(key) * 3 + len(str(value)) + 24 * 2