kinto 23.0.2__py3-none-any.whl → 23.1.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.
- kinto/core/cache/__init__.py +35 -0
- kinto/core/cache/memcached.py +2 -0
- kinto/core/cache/memory.py +6 -1
- kinto/core/cache/postgresql/__init__.py +3 -0
- kinto/core/initialization.py +11 -3
- kinto/core/resource/__init__.py +9 -1
- kinto/core/resource/schema.py +4 -4
- kinto/core/resource/viewset.py +2 -2
- kinto/core/storage/postgresql/__init__.py +8 -0
- kinto/core/storage/testing.py +8 -0
- kinto/core/utils.py +1 -1
- kinto/plugins/admin/VERSION +1 -1
- kinto/plugins/admin/build/VERSION +1 -1
- kinto/plugins/admin/build/assets/index-B_zMxEpZ.css +6 -0
- kinto/plugins/admin/build/assets/index-DBxDgrMX.js +154 -0
- kinto/plugins/admin/build/index.html +2 -2
- kinto/plugins/history/__init__.py +24 -0
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/METADATA +2 -1
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/RECORD +23 -23
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/WHEEL +1 -1
- kinto/plugins/admin/build/assets/index-Cs7JVwIg.css +0 -6
- kinto/plugins/admin/build/assets/index-CylsivYB.js +0 -165
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/entry_points.txt +0 -0
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/licenses/LICENSE +0 -0
- {kinto-23.0.2.dist-info → kinto-23.1.0.dist-info}/top_level.txt +0 -0
kinto/core/cache/__init__.py
CHANGED
|
@@ -10,10 +10,15 @@ _HEARTBEAT_KEY = "__heartbeat__"
|
|
|
10
10
|
_HEARTBEAT_TTL_SECONDS = 3600
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
_CACHE_HIT_METRIC_KEY = "cache_hits"
|
|
14
|
+
_CACHE_MISS_METRIC_KEY = "cache_misses"
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
class CacheBase:
|
|
14
18
|
def __init__(self, *args, **kwargs):
|
|
15
19
|
self.prefix = kwargs["cache_prefix"]
|
|
16
20
|
self.max_size_bytes = kwargs.get("cache_max_size_bytes")
|
|
21
|
+
self.set_metrics_backend(kwargs.get("metrics_backend"))
|
|
17
22
|
|
|
18
23
|
def initialize_schema(self, dry_run=False):
|
|
19
24
|
"""Create every necessary objects (like tables or indices) in the
|
|
@@ -71,6 +76,36 @@ class CacheBase:
|
|
|
71
76
|
"""
|
|
72
77
|
raise NotImplementedError
|
|
73
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
|
+
|
|
74
109
|
|
|
75
110
|
def heartbeat(backend):
|
|
76
111
|
def ping(request):
|
kinto/core/cache/memcached.py
CHANGED
|
@@ -68,7 +68,9 @@ class Cache(CacheBase):
|
|
|
68
68
|
def _get(self, key):
|
|
69
69
|
value = self._client.get(self.prefix + key)
|
|
70
70
|
if not value:
|
|
71
|
+
self.metrics_backend.count_miss()
|
|
71
72
|
return None, 0
|
|
73
|
+
self.metrics_backend.count_hit()
|
|
72
74
|
data = json.loads(value)
|
|
73
75
|
return data["value"], data["ttl"]
|
|
74
76
|
|
kinto/core/cache/memory.py
CHANGED
|
@@ -73,7 +73,12 @@ class Cache(CacheBase):
|
|
|
73
73
|
@synchronized
|
|
74
74
|
def get(self, key):
|
|
75
75
|
self._clean_expired()
|
|
76
|
-
|
|
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
|
|
77
82
|
|
|
78
83
|
@synchronized
|
|
79
84
|
def delete(self, key):
|
|
@@ -156,8 +156,11 @@ class Cache(CacheBase):
|
|
|
156
156
|
conn.execute(sa.text(purge))
|
|
157
157
|
result = conn.execute(sa.text(query), dict(key=self.prefix + key))
|
|
158
158
|
if result.rowcount > 0:
|
|
159
|
+
self.metrics_backend.count_hit()
|
|
159
160
|
value = result.fetchone().value
|
|
160
161
|
return json.loads(value)
|
|
162
|
+
self.metrics_backend.count_miss()
|
|
163
|
+
return None
|
|
161
164
|
|
|
162
165
|
def delete(self, key):
|
|
163
166
|
query = "DELETE FROM cache WHERE key = :key RETURNING value;"
|
kinto/core/initialization.py
CHANGED
|
@@ -228,7 +228,9 @@ def _end_of_life_tween_factory(handler, registry):
|
|
|
228
228
|
else:
|
|
229
229
|
code = "hard-eol"
|
|
230
230
|
request.response = errors.http_error(
|
|
231
|
-
HTTPGone(),
|
|
231
|
+
HTTPGone(),
|
|
232
|
+
errno=errors.ERRORS.SERVICE_DEPRECATED,
|
|
233
|
+
message=deprecation_msg,
|
|
232
234
|
)
|
|
233
235
|
|
|
234
236
|
errors.send_alert(request, eos_message, url=eos_url, code=code)
|
|
@@ -459,6 +461,11 @@ def setup_metrics(config):
|
|
|
459
461
|
else:
|
|
460
462
|
metrics.watch_execution_time(metrics_service, policy, prefix="authentication")
|
|
461
463
|
|
|
464
|
+
# Set cache metrics backend
|
|
465
|
+
cache_backend = config.registry.cache
|
|
466
|
+
if isinstance(cache_backend, cache.CacheBase):
|
|
467
|
+
cache_backend.set_metrics_backend(metrics_service)
|
|
468
|
+
|
|
462
469
|
config.add_subscriber(on_app_created, ApplicationCreated)
|
|
463
470
|
|
|
464
471
|
def on_new_response(event):
|
|
@@ -468,8 +475,9 @@ def setup_metrics(config):
|
|
|
468
475
|
# Count unique users.
|
|
469
476
|
user_id = request.prefixed_userid
|
|
470
477
|
if user_id:
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
auth, user_id = user_id.split(":", 1)
|
|
479
|
+
# Get rid of colons in metric packet (see #1282 and #3571).
|
|
480
|
+
user_id = user_id.replace(":", ".")
|
|
473
481
|
metrics_service.count("users", unique=[("auth", auth), ("userid", user_id)])
|
|
474
482
|
|
|
475
483
|
status = event.response.status_code
|
kinto/core/resource/__init__.py
CHANGED
|
@@ -319,7 +319,7 @@ class Resource:
|
|
|
319
319
|
#
|
|
320
320
|
|
|
321
321
|
def plural_head(self):
|
|
322
|
-
"""Model ``HEAD`` endpoint: empty
|
|
322
|
+
"""Model ``HEAD`` endpoint: empty response with a ``Total-Objects`` header.
|
|
323
323
|
|
|
324
324
|
:raises: :exc:`~pyramid:pyramid.httpexceptions.HTTPNotModified` if
|
|
325
325
|
``If-None-Match`` header is provided and collection not
|
|
@@ -1135,6 +1135,14 @@ class Resource:
|
|
|
1135
1135
|
if field == self.model.modified_field and not is_valid_timestamp(value):
|
|
1136
1136
|
raise_invalid(self.request, **error_details)
|
|
1137
1137
|
|
|
1138
|
+
if field in (self.model.modified_field, self.model.id_field) and operator in (
|
|
1139
|
+
COMPARISON.CONTAINS,
|
|
1140
|
+
COMPARISON.CONTAINS_ANY,
|
|
1141
|
+
):
|
|
1142
|
+
error_msg = f"Field '{field}' is not an array"
|
|
1143
|
+
error_details["description"] = error_msg
|
|
1144
|
+
raise_invalid(self.request, **error_details)
|
|
1145
|
+
|
|
1138
1146
|
filters.append(Filter(field, value, operator))
|
|
1139
1147
|
|
|
1140
1148
|
# If a plural endpoint is reached, and if the user does not have the
|
kinto/core/resource/schema.py
CHANGED
|
@@ -21,7 +21,7 @@ positive_big_integer = colander.Range(min=0, max=POSTGRESQL_MAX_INTEGER_VALUE)
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TimeStamp(TimeStamp):
|
|
24
|
-
"""This schema is deprecated, you
|
|
24
|
+
"""This schema is deprecated, you should use `kinto.core.schema.TimeStamp` instead."""
|
|
25
25
|
|
|
26
26
|
def __init__(self, *args, **kwargs):
|
|
27
27
|
message = (
|
|
@@ -33,7 +33,7 @@ class TimeStamp(TimeStamp):
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class URL(URL):
|
|
36
|
-
"""This schema is deprecated, you
|
|
36
|
+
"""This schema is deprecated, you should use `kinto.core.schema.URL` instead."""
|
|
37
37
|
|
|
38
38
|
def __init__(self, *args, **kwargs):
|
|
39
39
|
message = (
|
|
@@ -422,7 +422,7 @@ class PluralResponseSchema(colander.MappingSchema):
|
|
|
422
422
|
return datalist
|
|
423
423
|
|
|
424
424
|
|
|
425
|
-
class
|
|
425
|
+
class ResourceResponses:
|
|
426
426
|
"""Class that wraps and handles Resource responses."""
|
|
427
427
|
|
|
428
428
|
default_schemas = {
|
|
@@ -448,7 +448,7 @@ class ResourceReponses:
|
|
|
448
448
|
}
|
|
449
449
|
default_get_schemas = {
|
|
450
450
|
"304": NotModifiedResponseSchema(
|
|
451
|
-
description="
|
|
451
|
+
description="Response has not changed since value in If-None-Match header"
|
|
452
452
|
)
|
|
453
453
|
}
|
|
454
454
|
default_post_schemas = {
|
kinto/core/resource/viewset.py
CHANGED
|
@@ -16,7 +16,7 @@ from .schema import (
|
|
|
16
16
|
PluralGetQuerySchema,
|
|
17
17
|
PluralQuerySchema,
|
|
18
18
|
RequestSchema,
|
|
19
|
-
|
|
19
|
+
ResourceResponses,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -64,7 +64,7 @@ class ViewSet:
|
|
|
64
64
|
|
|
65
65
|
factory = authorization.RouteFactory
|
|
66
66
|
|
|
67
|
-
responses =
|
|
67
|
+
responses = ResourceResponses()
|
|
68
68
|
|
|
69
69
|
service_arguments = {"description": "Set of {resource_name}"}
|
|
70
70
|
|
|
@@ -884,6 +884,14 @@ class Storage(StorageBase, MigratorMixin):
|
|
|
884
884
|
operator = "IS NOT NULL" if filtr.value else "IS NULL"
|
|
885
885
|
cond = f"{sql_field} {operator}"
|
|
886
886
|
|
|
887
|
+
elif filtr.operator == COMPARISON.CONTAINS:
|
|
888
|
+
value_holder = f"{prefix}_value_{i}"
|
|
889
|
+
holders[value_holder] = value
|
|
890
|
+
# In case the field is not a sequence, we ignore the object.
|
|
891
|
+
is_json_sequence = f"jsonb_typeof({sql_field}) = 'array'"
|
|
892
|
+
sql_operator = operators[filtr.operator]
|
|
893
|
+
cond = f"{is_json_sequence} AND {sql_field} {sql_operator} :{value_holder}"
|
|
894
|
+
|
|
887
895
|
elif filtr.operator == COMPARISON.CONTAINS_ANY:
|
|
888
896
|
value_holder = f"{prefix}_value_{i}"
|
|
889
897
|
holders[value_holder] = value
|
kinto/core/storage/testing.py
CHANGED
|
@@ -491,6 +491,14 @@ class BaseTestStorage:
|
|
|
491
491
|
objects = self.storage.list_all(filters=filters, **self.storage_kw)
|
|
492
492
|
self.assertEqual(len(objects), 0)
|
|
493
493
|
|
|
494
|
+
def test_list_all_contains_ignores_object_if_field_is_not_array(self):
|
|
495
|
+
self.create_object({"code": "black"})
|
|
496
|
+
self.create_object({"fib": ["a", "b", "c"]})
|
|
497
|
+
|
|
498
|
+
filters = [Filter("fib", ["a"], utils.COMPARISON.CONTAINS)]
|
|
499
|
+
objects = self.storage.list_all(filters=filters, **self.storage_kw)
|
|
500
|
+
self.assertEqual(len(objects), 1)
|
|
501
|
+
|
|
494
502
|
def test_list_all_can_filter_on_array_with_contains_any_and_unsupported_type(self):
|
|
495
503
|
self.create_object({"code": "black"})
|
|
496
504
|
self.create_object({"fib": [2, 3, 5]})
|
kinto/core/utils.py
CHANGED
|
@@ -422,7 +422,7 @@ def follow_subrequest(request, subrequest, **kwargs):
|
|
|
422
422
|
"""Run a subrequest (e.g. batch), and follow the redirection if any.
|
|
423
423
|
|
|
424
424
|
:rtype: tuple
|
|
425
|
-
:returns: the
|
|
425
|
+
:returns: the response and the redirection request (or `subrequest`
|
|
426
426
|
if no redirection happened.)
|
|
427
427
|
"""
|
|
428
428
|
try:
|
kinto/plugins/admin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
4.3.0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
4.3.0
|