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.

@@ -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):
@@ -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
 
@@ -73,7 +73,12 @@ class Cache(CacheBase):
73
73
  @synchronized
74
74
  def get(self, key):
75
75
  self._clean_expired()
76
- return self._store.get(self.prefix + key)
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;"
@@ -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(), errno=errors.ERRORS.SERVICE_DEPRECATED, message=deprecation_msg
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
- # Get rid of colons in metric packet (see #1282).
472
- auth, user_id = user_id.split(":")
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
@@ -319,7 +319,7 @@ class Resource:
319
319
  #
320
320
 
321
321
  def plural_head(self):
322
- """Model ``HEAD`` endpoint: empty reponse with a ``Total-Objects`` header.
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
@@ -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 shoud use `kinto.core.schema.TimeStamp` instead."""
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 shoud use `kinto.core.schema.URL` instead."""
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 ResourceReponses:
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="Reponse has not changed since value in If-None-Match header"
451
+ description="Response has not changed since value in If-None-Match header"
452
452
  )
453
453
  }
454
454
  default_post_schemas = {
@@ -16,7 +16,7 @@ from .schema import (
16
16
  PluralGetQuerySchema,
17
17
  PluralQuerySchema,
18
18
  RequestSchema,
19
- ResourceReponses,
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 = ResourceReponses()
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
@@ -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 reponse and the redirection request (or `subrequest`
425
+ :returns: the response and the redirection request (or `subrequest`
426
426
  if no redirection happened.)
427
427
  """
428
428
  try:
@@ -1 +1 @@
1
- 3.7.1
1
+ 4.3.0
@@ -1 +1 @@
1
- 3.7.1
1
+ 4.3.0