kinto 20.5.0__py3-none-any.whl → 20.6.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/__main__.py CHANGED
@@ -33,6 +33,7 @@ def main(args=None):
33
33
  "flush-cache",
34
34
  "version",
35
35
  "create-user",
36
+ "purge-deleted",
36
37
  )
37
38
  subparsers = parser.add_subparsers(
38
39
  title="subcommands",
@@ -128,6 +129,18 @@ def main(args=None):
128
129
  subparser.add_argument(
129
130
  "-p", "--password", help="Superuser password", required=False, default=None
130
131
  )
132
+ elif command == "purge-deleted":
133
+ subparser.add_argument(
134
+ "resources", # No '--' → positional
135
+ nargs="+", # Accepts one or more
136
+ help="List of resources (e.g. record bucket group)",
137
+ default=["record"],
138
+ )
139
+ subparser.add_argument(
140
+ "max-retained",
141
+ help="The maximum number of tombstones to keep per resource and per parent",
142
+ type=int,
143
+ )
131
144
 
132
145
  # Parse command-line arguments
133
146
  parsed_args = vars(parser.parse_args(args))
@@ -208,6 +221,12 @@ def main(args=None):
208
221
  env = bootstrap(config_file, options={"command": "create-user"})
209
222
  return accounts_scripts.create_user(env, username=username, password=password)
210
223
 
224
+ elif which_command == "purge-deleted":
225
+ env = bootstrap(config_file)
226
+ return core_scripts.purge_deleted(
227
+ env, parsed_args["resources"], parsed_args["max-retained"]
228
+ )
229
+
211
230
  elif which_command == "start":
212
231
  pserve_argv = ["pserve"]
213
232
 
kinto/core/scripts.py CHANGED
@@ -28,6 +28,21 @@ def migrate(env, dry_run=False):
28
28
  getattr(registry, backend).initialize_schema(dry_run=dry_run)
29
29
 
30
30
 
31
+ def purge_deleted(env, resource_names, max_retained):
32
+ logger.info("Keep only %r tombstones per parent and resource." % max_retained)
33
+
34
+ registry = env["registry"]
35
+
36
+ count = 0
37
+ for resource_name in resource_names:
38
+ count += registry.storage.purge_deleted(
39
+ resource_name=resource_name, parent_id="*", max_retained=max_retained
40
+ )
41
+
42
+ logger.info("%s tombstone(s) deleted." % count)
43
+ return 0
44
+
45
+
31
46
  def flush_cache(env):
32
47
  registry = env["registry"]
33
48
  registry.cache.flush()
@@ -264,6 +264,7 @@ class StorageBase:
264
264
  resource_name,
265
265
  parent_id,
266
266
  before=None,
267
+ max_retained=None,
267
268
  id_field=DEFAULT_ID_FIELD,
268
269
  modified_field=DEFAULT_MODIFIED_FIELD,
269
270
  ):
@@ -273,7 +274,8 @@ class StorageBase:
273
274
  :param str resource_name: the resource name.
274
275
  :param str parent_id: the resource parent.
275
276
 
276
- :param int before: Optionnal timestamp to limit deletion (exclusive)
277
+ :param int before: Optional timestamp to limit deletion (exclusive).
278
+ :param int max_count: Optional maximum of tombstones to keep per collection.
277
279
 
278
280
  :returns: The number of deleted objects.
279
281
  :rtype: int
@@ -284,9 +284,13 @@ class Storage(MemoryBasedStorage):
284
284
  resource_name,
285
285
  parent_id,
286
286
  before=None,
287
+ max_retained=None,
287
288
  id_field=DEFAULT_ID_FIELD,
288
289
  modified_field=DEFAULT_MODIFIED_FIELD,
289
290
  ):
291
+ if max_retained is not None and before is not None:
292
+ raise ValueError("`before` and `max_retained` are exclusive arguments. Pick one.")
293
+
290
294
  parent_id_match = re.compile(parent_id.replace("*", ".*"))
291
295
 
292
296
  timestamps_by_parent_id = {
@@ -312,7 +316,20 @@ class Storage(MemoryBasedStorage):
312
316
  resources = {resource_name: resources[resource_name]}
313
317
  for resource, resource_objects in resources.items():
314
318
  if before is None:
315
- kept = {}
319
+ if max_retained is None:
320
+ kept = {}
321
+ else:
322
+ kept = {
323
+ key: value
324
+ for i, (key, value) in enumerate(
325
+ sorted(
326
+ resource_objects.items(),
327
+ key=lambda i: i[1]["last_modified"],
328
+ reverse=True,
329
+ )
330
+ )
331
+ if i < max_retained
332
+ }
316
333
  else:
317
334
  kept = {
318
335
  key: value
@@ -598,6 +598,7 @@ class Storage(StorageBase, MigratorMixin):
598
598
  resource_name,
599
599
  parent_id,
600
600
  before=None,
601
+ max_retained=None,
601
602
  id_field=DEFAULT_ID_FIELD,
602
603
  modified_field=DEFAULT_MODIFIED_FIELD,
603
604
  ):
@@ -608,9 +609,39 @@ class Storage(StorageBase, MigratorMixin):
608
609
  {resource_name_filter}
609
610
  {conditions_filter}
610
611
  """
612
+
613
+ if max_retained is not None:
614
+ if before is not None:
615
+ raise ValueError("`before` and `max_retained` are exclusive arguments. Pick one.")
616
+
617
+ delete_tombstones = """
618
+ WITH ranked AS (
619
+ SELECT
620
+ id AS objid,
621
+ parent_id,
622
+ resource_name,
623
+ ROW_NUMBER() OVER (
624
+ PARTITION BY parent_id, resource_name
625
+ ORDER BY last_modified DESC
626
+ ) AS rn
627
+ FROM objects
628
+ )
629
+ DELETE FROM objects
630
+ WHERE id IN (
631
+ SELECT objid
632
+ FROM ranked
633
+ WHERE
634
+ {parent_id_filter}
635
+ {resource_name_filter}
636
+ AND rn > :max_retained
637
+ )
638
+ """
639
+
611
640
  id_field = id_field or self.id_field
612
641
  modified_field = modified_field or self.modified_field
613
- placeholders = dict(parent_id=parent_id, resource_name=resource_name)
642
+ placeholders = dict(
643
+ parent_id=parent_id, resource_name=resource_name, max_retained=max_retained
644
+ )
614
645
  # Safe strings
615
646
  safeholders = defaultdict(str)
616
647
  # Handle parent_id as a regex only if it contains *
@@ -1342,6 +1342,60 @@ class DeletedObjectsTest:
1342
1342
  count = self.storage.count_all(**self.storage_kw)
1343
1343
  self.assertEqual(count, 0)
1344
1344
 
1345
+ def test_purge_deleted_does_not_support_before_and_max_retained(self):
1346
+ self.assertRaises(
1347
+ ValueError,
1348
+ self.storage.purge_deleted,
1349
+ resource_name="r",
1350
+ parent_id="p",
1351
+ before=42,
1352
+ max_retained=1,
1353
+ )
1354
+
1355
+ def test_purge_deleted_remove_with_max_count_per_collection(self):
1356
+ cid1_kw = dict(parent_id="cid1", resource_name="one")
1357
+ for i in range(5):
1358
+ record = self.create_object(**cid1_kw)
1359
+ self.storage.delete(object_id=record["id"], **cid1_kw)
1360
+ cid2_kw = dict(parent_id="cid2", resource_name="one")
1361
+ for i in range(4):
1362
+ record = self.create_object(**cid2_kw)
1363
+ self.storage.delete(object_id=record["id"], **cid2_kw)
1364
+ cid1_other_kw = dict(parent_id="cid1", resource_name="other")
1365
+ for i in range(5):
1366
+ record = self.create_object(**cid1_other_kw)
1367
+ self.storage.delete(object_id=record["id"], **cid1_other_kw)
1368
+
1369
+ # Consistency checks first.
1370
+ objects_c1_before = self.storage.list_all(include_deleted=True, **cid1_kw)
1371
+ self.assertEqual(len(objects_c1_before), 5)
1372
+ objects_c2_before = self.storage.list_all(include_deleted=True, **cid2_kw)
1373
+ self.assertEqual(len(objects_c2_before), 4)
1374
+ objects_c1_other_before = self.storage.list_all(include_deleted=True, **cid1_other_kw)
1375
+ self.assertEqual(len(objects_c1_other_before), 5)
1376
+
1377
+ num_removed = self.storage.purge_deleted(
1378
+ resource_name="one", parent_id="*", max_retained=3
1379
+ )
1380
+
1381
+ self.assertEqual(num_removed, 3) # 2 for one/cid1, 1 for one/cid2, 0 for other/cid1
1382
+
1383
+ # It kept 3 tombstones for each resource/parent.
1384
+ objects = self.storage.list_all(include_deleted=True, **cid1_kw)
1385
+ self.assertEqual(len(objects), 3)
1386
+ self.assertEqual(
1387
+ min(obj["last_modified"] for obj in objects), objects_c1_before[2]["last_modified"]
1388
+ )
1389
+
1390
+ objects = self.storage.list_all(include_deleted=True, **cid2_kw)
1391
+ self.assertEqual(len(objects), 3)
1392
+ self.assertNotEqual(
1393
+ min(obj["last_modified"] for obj in objects), objects_c2_before[2]["last_modified"]
1394
+ )
1395
+
1396
+ objects = self.storage.list_all(include_deleted=True, **cid1_other_kw)
1397
+ self.assertEqual(len(objects), 5)
1398
+
1345
1399
  #
1346
1400
  # Sorting
1347
1401
  #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kinto
3
- Version: 20.5.0
3
+ Version: 20.6.0
4
4
  Summary: Kinto Web Service - Store, Sync, Share, and Self-Host.
5
5
  Author-email: Mozilla Services <developers@kinto-storage.org>
6
6
  License: Copyright 2012 - Mozilla Foundation
@@ -1,5 +1,5 @@
1
1
  kinto/__init__.py,sha256=XG7cOyHN0ipQeOSPMW-XFax7uFgAjnCd3Slq0jSFGC4,3299
2
- kinto/__main__.py,sha256=0vBSUT2pAMg78HyRpfGbk3R6Z_fpg12pcGi3pi1XJ1c,7181
2
+ kinto/__main__.py,sha256=6XDjWYGtJ36qepn9Of4xL9zc35TfdSeEvHh3s26Aw9c,7913
3
3
  kinto/authorization.py,sha256=i3ttdPTFGzE9CqUQmfC4rR_6dDZJu0jWJMLGl_jFzIE,4919
4
4
  kinto/contribute.json,sha256=HT9QVB8rA8jWIREQoqWfMibfJXMAzbRsixW8F6O6cQY,792
5
5
  kinto/events.py,sha256=NMPvKUdbi25aYHhu9svzQsrEZMa9nyO4mtuMZC5871Q,85
@@ -16,7 +16,7 @@ kinto/core/initialization.py,sha256=ZjmCKc8MYZXv63W2mv--fY8rXSLAnJa7RtCYdfK4jsg,
16
16
  kinto/core/metrics.py,sha256=Y6Mt4PUzy2-oudeGr_oCmtX8nIR4SZkzUlPxr58jr-g,2619
17
17
  kinto/core/openapi.py,sha256=92sZviff4NCxN0jMnu5lPUnF5iQbrKMGy7Cegf-VAME,3876
18
18
  kinto/core/schema.py,sha256=d5L5TQynRYJPkZ8Mu2X7F72xEh6SKDbrHK1CNTdOf2E,3646
19
- kinto/core/scripts.py,sha256=5HSq5QAuin7HuU6icNYkPisny-4JpcdBvjf8X4JImrE,978
19
+ kinto/core/scripts.py,sha256=02SXVjo579W82AsDF8dyVCRxYVcrMFkjjaNVIgLChh0,1412
20
20
  kinto/core/statsd.py,sha256=2f4s2opiHVdrA02ZlBa5pxIHaEjPuG8tdVLsmdII27s,64
21
21
  kinto/core/testing.py,sha256=kZ-75EiiZwTNDBpHZyKBBHute6jEmUwXd7nRMK9kwr4,5908
22
22
  kinto/core/utils.py,sha256=EDlZfHsgcaUvxg-AG_dn9IcQCq7j7RxpBjnFrx8b11E,17069
@@ -58,13 +58,13 @@ kinto/core/resource/__init__.py,sha256=B-1X3nPzIZOiN8Dc_tSersAP-Wn4jXf3GcrctN2Ck
58
58
  kinto/core/resource/model.py,sha256=xjZ6shnhelXCdWvgw6yeOWXodxiKMm9iyDqLTk0i8Bs,15626
59
59
  kinto/core/resource/schema.py,sha256=EhPKDMlBjx60hXztMvywfo4IrRfwaZ3V-7sY_sl-BYk,16126
60
60
  kinto/core/resource/viewset.py,sha256=Wo7mQwmI08IGnSetaqGF66fCqYPB1pDUdZa3U92NIic,7613
61
- kinto/core/storage/__init__.py,sha256=Bo9q5PCDQ7KkBuBAHt7UoZFr5WfVWwEbFJjGsuq4Oo4,13704
61
+ kinto/core/storage/__init__.py,sha256=xELryXjUvmYon4qS3GJB6nCgVcLyGJoV0rJ5EWjGiVY,13816
62
62
  kinto/core/storage/exceptions.py,sha256=o10f7LohwyCHOTlR-dOdnB4us_MdCGOJUxZO8HZ3Akc,1304
63
63
  kinto/core/storage/generators.py,sha256=rxWN9hOfOsB-PLeryhPGO-aE670sivr3LFRIExImpxc,1829
64
- kinto/core/storage/memory.py,sha256=DR6gpqSYh4oGPBX9x4-dGLNITZi33T0VODsNWsPtgho,19875
65
- kinto/core/storage/testing.py,sha256=4hGR6xc54M6H81IQb8XP7NP_iBP-X8m8PqKqixDs5nU,78411
64
+ kinto/core/storage/memory.py,sha256=--08jHpnFMtcyz1CD10ZFni36qe-Wzrwprl-VVI84IE,20630
65
+ kinto/core/storage/testing.py,sha256=5t61Li0M6XXCNu0vRMaT-_GaBNz7TPZ27eXTO1Bf9vI,80766
66
66
  kinto/core/storage/utils.py,sha256=BHpohIKVOCtURjRbUT7O5AEhIKfSEFv-pfgRzq8Q5zs,1082
67
- kinto/core/storage/postgresql/__init__.py,sha256=HMlUfTBsnsUQkLEhuhItU-9fgSOf9wTH6J09-1HW1fA,40157
67
+ kinto/core/storage/postgresql/__init__.py,sha256=egK7K5oQgRF03LxdQAzPj4L4yeysDVkkPBKZg5Ab4mQ,41111
68
68
  kinto/core/storage/postgresql/client.py,sha256=JTRxHK-8ipTXgs0E4PyiFiun-32yuzqFGsWTi-OuFfA,4270
69
69
  kinto/core/storage/postgresql/migrator.py,sha256=MQ_5aSrDi9-s2wlyyFyfhYP6HreCXjtlJzBI4b85u1I,3524
70
70
  kinto/core/storage/postgresql/pool.py,sha256=lOtclVagFqzzWbVxrGoWeKylpHlKdFgGz3Ef6cgGNJU,2219
@@ -141,9 +141,9 @@ kinto/views/contribute.py,sha256=PJoIMLj9_IszSjgZkaCd_TUjekDgNqjpmVTmRN9ztaA,983
141
141
  kinto/views/groups.py,sha256=jOq5fX0-4lwZE8k1q5HME2tU7x9052rtBPF7YqcJ-Qg,3181
142
142
  kinto/views/permissions.py,sha256=F0_eKx201WyLonXJ5vLdGKa9RcFKjvAihrEEhU1JuLw,9069
143
143
  kinto/views/records.py,sha256=lYfACW2L8qcQoyYBD5IX-fTPjFWmGp7GjHq_U4InlyE,5037
144
- kinto-20.5.0.dist-info/licenses/LICENSE,sha256=oNEIMTuTJzppR5ZEyi86yvvtSagveMYXTYFn56zF0Uk,561
145
- kinto-20.5.0.dist-info/METADATA,sha256=8hgp9glzPTbNRIWX6r2RXIy5xX_-V72uFHyjo27XlxQ,8731
146
- kinto-20.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
147
- kinto-20.5.0.dist-info/entry_points.txt,sha256=3KlqBWPKY81mrCe_oX0I5s1cRO7Q53nCLbnVr5P9LH4,85
148
- kinto-20.5.0.dist-info/top_level.txt,sha256=EG_YmbZL6FAug9VwopG7JtF9SvH_r0DEnFp-3twPPys,6
149
- kinto-20.5.0.dist-info/RECORD,,
144
+ kinto-20.6.0.dist-info/licenses/LICENSE,sha256=oNEIMTuTJzppR5ZEyi86yvvtSagveMYXTYFn56zF0Uk,561
145
+ kinto-20.6.0.dist-info/METADATA,sha256=ajUNuc134EmgReYQWZIKBRKtn6gWQEeIEEj8O1MQ3KU,8731
146
+ kinto-20.6.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
147
+ kinto-20.6.0.dist-info/entry_points.txt,sha256=3KlqBWPKY81mrCe_oX0I5s1cRO7Q53nCLbnVr5P9LH4,85
148
+ kinto-20.6.0.dist-info/top_level.txt,sha256=EG_YmbZL6FAug9VwopG7JtF9SvH_r0DEnFp-3twPPys,6
149
+ kinto-20.6.0.dist-info/RECORD,,
File without changes