morango 0.6.11__py2.py3-none-any.whl → 0.8.6__py2.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 (64) hide show
  1. morango/__init__.py +1 -6
  2. morango/api/serializers.py +3 -0
  3. morango/api/viewsets.py +38 -23
  4. morango/apps.py +1 -2
  5. morango/constants/settings.py +3 -0
  6. morango/constants/transfer_stages.py +1 -1
  7. morango/constants/transfer_statuses.py +1 -1
  8. morango/errors.py +4 -0
  9. morango/management/commands/cleanupsyncs.py +64 -14
  10. morango/migrations/0001_initial.py +0 -2
  11. morango/migrations/0001_squashed_0024_auto_20240129_1757.py +583 -0
  12. morango/migrations/0002_auto_20170511_0400.py +0 -2
  13. morango/migrations/0002_store_idx_morango_deserialize.py +21 -0
  14. morango/migrations/0003_auto_20170519_0543.py +0 -2
  15. morango/migrations/0004_auto_20170520_2112.py +0 -2
  16. morango/migrations/0005_auto_20170629_2139.py +0 -2
  17. morango/migrations/0006_instanceidmodel_system_id.py +0 -2
  18. morango/migrations/0007_auto_20171018_1615.py +0 -2
  19. morango/migrations/0008_auto_20171114_2217.py +0 -2
  20. morango/migrations/0009_auto_20171205_0252.py +0 -2
  21. morango/migrations/0010_auto_20171206_1615.py +0 -2
  22. morango/migrations/0011_sharedkey.py +0 -2
  23. morango/migrations/0012_auto_20180927_1658.py +0 -2
  24. morango/migrations/0013_auto_20190627_1513.py +0 -2
  25. morango/migrations/0014_syncsession_extra_fields.py +0 -2
  26. morango/migrations/0015_auto_20200508_2104.py +2 -3
  27. morango/migrations/0016_store_deserialization_error.py +2 -3
  28. morango/migrations/0017_store_last_transfer_session_id.py +1 -2
  29. morango/migrations/0018_auto_20210714_2216.py +2 -3
  30. morango/migrations/0019_auto_20220113_1807.py +2 -3
  31. morango/migrations/0020_postgres_fix_nullable.py +0 -2
  32. morango/migrations/0021_store_partition_index_create.py +0 -2
  33. morango/migrations/0022_rename_instance_fields.py +23 -0
  34. morango/migrations/0023_add_instance_id_fields.py +24 -0
  35. morango/migrations/0024_auto_20240129_1757.py +28 -0
  36. morango/models/__init__.py +0 -6
  37. morango/models/certificates.py +137 -28
  38. morango/models/core.py +48 -46
  39. morango/models/fields/crypto.py +20 -6
  40. morango/models/fields/uuids.py +2 -1
  41. morango/models/utils.py +5 -6
  42. morango/proquint.py +2 -3
  43. morango/registry.py +28 -49
  44. morango/sync/backends/base.py +34 -0
  45. morango/sync/backends/postgres.py +129 -0
  46. morango/sync/backends/utils.py +10 -11
  47. morango/sync/context.py +198 -13
  48. morango/sync/controller.py +33 -11
  49. morango/sync/operations.py +324 -251
  50. morango/sync/session.py +11 -0
  51. morango/sync/syncsession.py +78 -85
  52. morango/sync/utils.py +18 -0
  53. morango/urls.py +3 -3
  54. morango/utils.py +2 -3
  55. {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/METADATA +29 -14
  56. morango-0.8.6.dist-info/RECORD +79 -0
  57. {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/WHEEL +1 -1
  58. morango/models/morango_mptt.py +0 -33
  59. morango/settings.py +0 -115
  60. morango/wsgi.py +0 -33
  61. morango-0.6.11.dist-info/RECORD +0 -77
  62. {morango-0.6.11.dist-info → morango-0.8.6.dist-info/licenses}/AUTHORS.md +0 -0
  63. {morango-0.6.11.dist-info → morango-0.8.6.dist-info/licenses}/LICENSE +0 -0
  64. {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.15 on 2018-09-27 16:58
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.21 on 2019-06-27 22:13
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
 
7
5
  import morango.models.fields.uuids
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.27 on 2019-12-30 18:28
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2020-05-08 21:04
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.28 on 2020-06-10 23:48
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.28 on 2021-06-25 23:13
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
4
+
6
5
  import morango.models.fields.uuids
7
6
 
8
7
 
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2021-07-14 22:16
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,8 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-01-13 18:07
3
- from __future__ import unicode_literals
4
-
5
- from django.db import migrations, models
3
+ from django.db import migrations
4
+ from django.db import models
6
5
 
7
6
 
8
7
  class Migration(migrations.Migration):
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-01-13 18:07
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
 
7
5
 
@@ -1,7 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  # Generated by Django 1.11.29 on 2022-04-27 16:59
3
- from __future__ import unicode_literals
4
-
5
3
  from django.db import migrations
6
4
  from django.db import models
7
5
 
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by Django 1.11.29 on 2023-01-31 18:53
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('morango', '0021_store_partition_index_create'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='syncsession',
15
+ old_name='client_instance',
16
+ new_name='client_instance_json',
17
+ ),
18
+ migrations.RenameField(
19
+ model_name='syncsession',
20
+ old_name='server_instance',
21
+ new_name='server_instance_json',
22
+ ),
23
+ ]
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by Django 1.11.29 on 2023-01-31 19:03
3
+ from django.db import migrations
4
+ from django.db import models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('morango', '0022_rename_instance_fields'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name='syncsession',
16
+ name='client_instance_id',
17
+ field=models.UUIDField(blank=True, null=True),
18
+ ),
19
+ migrations.AddField(
20
+ model_name='syncsession',
21
+ name='server_instance_id',
22
+ field=models.UUIDField(blank=True, null=True),
23
+ ),
24
+ ]
@@ -0,0 +1,28 @@
1
+ # Generated by Django 3.2.23 on 2024-01-29 17:57
2
+ from django.db import migrations
3
+ from django.db import models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('morango', '0023_add_instance_id_fields'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='certificate',
15
+ name='level',
16
+ field=models.PositiveIntegerField(editable=False),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='certificate',
20
+ name='lft',
21
+ field=models.PositiveIntegerField(editable=False),
22
+ ),
23
+ migrations.AlterField(
24
+ model_name='certificate',
25
+ name='rght',
26
+ field=models.PositiveIntegerField(editable=False),
27
+ ),
28
+ ]
@@ -21,9 +21,6 @@ from morango.models.fields import __all__ as fields_all
21
21
  from morango.models.fields.crypto import SharedKey
22
22
  from morango.models.fields.uuids import UUIDModelMixin
23
23
  from morango.models.manager import SyncableModelManager
24
- from morango.models.morango_mptt import MorangoMPTTModel
25
- from morango.models.morango_mptt import MorangoMPTTTreeManager
26
- from morango.models.morango_mptt import MorangoTreeQuerySet
27
24
  from morango.models.query import SyncableModelQuerySet
28
25
  from morango.registry import syncable_models
29
26
 
@@ -41,9 +38,6 @@ __all__ += [
41
38
  "SyncableModelManager",
42
39
  "SyncableModelQuerySet",
43
40
  "syncable_models",
44
- "MorangoTreeQuerySet",
45
- "MorangoMPTTTreeManager",
46
- "MorangoMPTTModel",
47
41
  "DatabaseIDModel",
48
42
  "InstanceIDModel",
49
43
  "SyncSession",
@@ -3,18 +3,18 @@
3
3
  Each certificate has a ``private_key`` used for signing (child) certificates (thus giving certain permissions)
4
4
  and a ``public_key`` used for verifying that a certificate(s) was properly signed.
5
5
  """
6
- from __future__ import unicode_literals
7
-
8
6
  import json
7
+ import logging
9
8
  import string
9
+ from contextlib import contextmanager
10
10
 
11
11
  import mptt.models
12
12
  from django.core.management import call_command
13
+ from django.db import connection
13
14
  from django.db import models
14
15
  from django.db import transaction
16
+ from django.db.utils import OperationalError
15
17
  from django.utils import timezone
16
- from django.utils.six import string_types
17
- from future.utils import python_2_unicode_compatible
18
18
 
19
19
  from .fields.crypto import Key
20
20
  from .fields.crypto import PrivateKeyField
@@ -27,21 +27,21 @@ from morango.errors import CertificateScopeNotSubset
27
27
  from morango.errors import CertificateSignatureInvalid
28
28
  from morango.errors import NonceDoesNotExist
29
29
  from morango.errors import NonceExpired
30
+ from morango.sync.backends.utils import load_backend
30
31
  from morango.utils import _assert
31
32
 
32
33
 
33
- @python_2_unicode_compatible
34
34
  class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
35
35
 
36
36
  uuid_input_fields = ("public_key", "profile", "salt")
37
37
 
38
- parent = models.ForeignKey("Certificate", blank=True, null=True)
38
+ parent = models.ForeignKey("Certificate", blank=True, null=True, on_delete=models.CASCADE)
39
39
 
40
40
  # the Morango profile with which this certificate is associated
41
41
  profile = models.CharField(max_length=20)
42
42
 
43
43
  # scope of this certificate, and version of the scope, along with associated params
44
- scope_definition = models.ForeignKey("ScopeDefinition")
44
+ scope_definition = models.ForeignKey("ScopeDefinition", on_delete=models.CASCADE)
45
45
  scope_version = models.IntegerField()
46
46
  scope_params = (
47
47
  models.TextField()
@@ -199,7 +199,7 @@ class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
199
199
  def save_certificate_chain(cls, cert_chain, expected_last_id=None):
200
200
 
201
201
  # parse the chain from json if needed
202
- if isinstance(cert_chain, string_types):
202
+ if isinstance(cert_chain, str):
203
203
  cert_chain = json.loads(cert_chain)
204
204
 
205
205
  # start from the bottom of the chain
@@ -251,6 +251,37 @@ class Certificate(mptt.models.MPTTModel, UUIDModelMixin):
251
251
  def get_scope(self):
252
252
  return self.scope_definition.get_scope(self.scope_params)
253
253
 
254
+ @contextmanager
255
+ def _attempt_lock_mptt(self):
256
+ from morango.sync.utils import lock_partitions
257
+
258
+ DBBackend = load_backend(connection)
259
+
260
+ with transaction.atomic():
261
+ # Call get_root on the parent as it is already saved in the DB
262
+ root_id = self.parent.get_root().id if self.parent else self.id
263
+
264
+ # lock the partitions in our scope to prevent MPTT tree corruption during concurrent certificate creation
265
+ lock_partitions(DBBackend, sync_filter=Filter(root_id) if root_id else None)
266
+ yield
267
+
268
+ @contextmanager
269
+ def _lock_mptt(self):
270
+ try:
271
+ with self._attempt_lock_mptt():
272
+ yield
273
+ except OperationalError as e:
274
+ if "deadlock detected" in e.args[0]:
275
+ logging.error("Deadlock detected when attempting to lock MPTT partitions, retrying once more")
276
+ with self._attempt_lock_mptt():
277
+ yield
278
+ else:
279
+ raise
280
+
281
+ def save(self, *args, **kwargs):
282
+ with self._lock_mptt():
283
+ super().save(*args, **kwargs)
284
+
254
285
  def __str__(self):
255
286
  if self.scope_definition:
256
287
  return self.scope_definition.get_description(self.scope_params)
@@ -324,52 +355,95 @@ class ScopeDefinition(models.Model):
324
355
  return Scope(definition=self, params=params)
325
356
 
326
357
  def get_description(self, params):
327
- if isinstance(params, string_types):
358
+ if isinstance(params, str):
328
359
  params = json.loads(params)
329
360
  return string.Template(self.description).safe_substitute(params)
330
361
 
331
362
 
332
- @python_2_unicode_compatible
333
363
  class Filter(object):
334
- def __init__(self, template, params={}):
335
- # ensure params have been deserialized
336
- if isinstance(params, string_types):
337
- params = json.loads(params)
338
- self._template = template
339
- self._params = params
340
- self._filter_string = string.Template(template).safe_substitute(params)
341
- self._filter_tuple = tuple(self._filter_string.split()) or ("",)
364
+ def __init__(self, filter_str, params=None):
365
+ """
366
+ :param filter_str: The partition filter string, which may have multiple separated by newlines
367
+ :type filter_str: str
368
+ :param params: DEPRECATED: USE Filter.from_template() INSTEAD
369
+ :type params: dict|str
370
+ """
371
+ if params is not None:
372
+ logging.warning("DEPRECATED: Constructing a filter with a template and params is deprecated. Use Filter.from_template() instead")
373
+ filter_str = str(Filter.from_template(filter_str, params=params))
374
+
375
+ self._filter_tuple = tuple(filter_str.split()) or ("",)
342
376
 
343
377
  def is_subset_of(self, other):
344
- for partition in self._filter_tuple:
345
- if not partition.startswith(other._filter_tuple):
378
+ """
379
+ :param other: The other Filter
380
+ :type other: Filter
381
+ :return: A boolean on whether this Filter is captured within the other Filter
382
+ :rtype: bool
383
+ """
384
+ for partition in self:
385
+ if not other.contains_partition(partition):
346
386
  return False
347
387
  return True
348
388
 
349
389
  def contains_partition(self, partition):
390
+ """Returns True if the partition starts with as least one of the partitions in this Filter"""
350
391
  return partition.startswith(self._filter_tuple)
351
392
 
393
+ def contains_exact_partition(self, partition):
394
+ """Returns True if the partition exactly matches one of the partitions in this Filter"""
395
+ return partition in self._filter_tuple
396
+
397
+ def copy(self):
398
+ return Filter(str(self))
399
+
352
400
  def __le__(self, other):
401
+ """Returns True if this Filter is a subset of the other"""
353
402
  return self.is_subset_of(other)
354
403
 
355
404
  def __eq__(self, other):
405
+ """Returns True if this Filter has exactly the same partitions as the other"""
356
406
  if other is None:
357
407
  return False
358
- for partition in self._filter_tuple:
359
- if partition not in other._filter_tuple:
408
+ for partition in self:
409
+ if not other.contains_exact_partition(partition):
360
410
  return False
361
- for partition in other._filter_tuple:
362
- if partition not in self._filter_tuple:
411
+ for partition in other:
412
+ if not self.contains_exact_partition(partition):
363
413
  return False
364
414
  return True
365
415
 
366
416
  def __contains__(self, partition):
417
+ """
418
+ Performs a 'startswith' comparison on the partition, determining whether it matches or
419
+ is a subset of any partition in this Filter
420
+
421
+ :param partition: str
422
+ :return: A boolean
423
+ :rtype: bool
424
+ """
367
425
  return self.contains_partition(partition)
368
426
 
369
427
  def __add__(self, other):
370
- return Filter(self._filter_string + "\n" + other._filter_string)
428
+ """
429
+ The Filter's addition operator overload
430
+ :param other: Filter or None
431
+ :type other: Filter|None
432
+ :return: The combined Filter
433
+ :rtype: Filter
434
+ """
435
+ if other is None:
436
+ return self
437
+ # create a list of partition filters, deduplicating them between the two filter objects
438
+ partitions = []
439
+ partitions.extend(p for p in self if p)
440
+ partitions.extend(p for p in other if p and p not in partitions)
441
+ return Filter("\n".join(partitions))
371
442
 
372
443
  def __iter__(self):
444
+ """
445
+ :rtype: tuple[str]
446
+ """
373
447
  return iter(self._filter_tuple)
374
448
 
375
449
  def __str__(self):
@@ -378,13 +452,48 @@ class Filter(object):
378
452
  def __len__(self):
379
453
  return len(self._filter_tuple)
380
454
 
455
+ @classmethod
456
+ def add(cls, filter_a, filter_b):
457
+ """
458
+ The Filter's addition operator overload is already defensive against None being the
459
+ right-hand operand, but this method is defensive against None being the left-hand operand
460
+
461
+ :param filter_a: A Filter or None
462
+ :type filter_a: Filter|None
463
+ :param filter_b: A Filter or None
464
+ :type filter_b: Filter|None
465
+ :return: The combined Filter or None
466
+ :rtype: Filter|None
467
+ """
468
+ if filter_a is None:
469
+ return filter_b
470
+ return filter_a + filter_b
471
+
472
+ @classmethod
473
+ def from_template(cls, template, params=None):
474
+ """
475
+ Create a filter from a string template, which may have params that will be replaced with
476
+ values passed to `params`
477
+
478
+ :param template: The partition filter template
479
+ :type template: str
480
+ :param params: The param dictionary or JSON object string
481
+ :type params: dict|str
482
+ :return: The filter with params replaced
483
+ :rtype: Filter
484
+ """
485
+ if isinstance(params, str):
486
+ params = json.loads(params)
487
+ params = params or {}
488
+ return Filter(string.Template(template).safe_substitute(params))
489
+
381
490
 
382
491
  class Scope(object):
383
492
  def __init__(self, definition, params):
384
493
  # turn the scope definition filter templates into Filter objects
385
- rw_filter = Filter(definition.read_write_filter_template, params)
386
- self.read_filter = rw_filter + Filter(definition.read_filter_template, params)
387
- self.write_filter = rw_filter + Filter(definition.write_filter_template, params)
494
+ rw_filter = Filter.from_template(definition.read_write_filter_template, params)
495
+ self.read_filter = rw_filter + Filter.from_template(definition.read_filter_template, params)
496
+ self.write_filter = rw_filter + Filter.from_template(definition.write_filter_template, params)
388
497
 
389
498
  def is_subset_of(self, other):
390
499
  if not self.read_filter.is_subset_of(other.read_filter):