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.
- morango/__init__.py +1 -6
- morango/api/serializers.py +3 -0
- morango/api/viewsets.py +38 -23
- morango/apps.py +1 -2
- morango/constants/settings.py +3 -0
- morango/constants/transfer_stages.py +1 -1
- morango/constants/transfer_statuses.py +1 -1
- morango/errors.py +4 -0
- morango/management/commands/cleanupsyncs.py +64 -14
- morango/migrations/0001_initial.py +0 -2
- morango/migrations/0001_squashed_0024_auto_20240129_1757.py +583 -0
- morango/migrations/0002_auto_20170511_0400.py +0 -2
- morango/migrations/0002_store_idx_morango_deserialize.py +21 -0
- morango/migrations/0003_auto_20170519_0543.py +0 -2
- morango/migrations/0004_auto_20170520_2112.py +0 -2
- morango/migrations/0005_auto_20170629_2139.py +0 -2
- morango/migrations/0006_instanceidmodel_system_id.py +0 -2
- morango/migrations/0007_auto_20171018_1615.py +0 -2
- morango/migrations/0008_auto_20171114_2217.py +0 -2
- morango/migrations/0009_auto_20171205_0252.py +0 -2
- morango/migrations/0010_auto_20171206_1615.py +0 -2
- morango/migrations/0011_sharedkey.py +0 -2
- morango/migrations/0012_auto_20180927_1658.py +0 -2
- morango/migrations/0013_auto_20190627_1513.py +0 -2
- morango/migrations/0014_syncsession_extra_fields.py +0 -2
- morango/migrations/0015_auto_20200508_2104.py +2 -3
- morango/migrations/0016_store_deserialization_error.py +2 -3
- morango/migrations/0017_store_last_transfer_session_id.py +1 -2
- morango/migrations/0018_auto_20210714_2216.py +2 -3
- morango/migrations/0019_auto_20220113_1807.py +2 -3
- morango/migrations/0020_postgres_fix_nullable.py +0 -2
- morango/migrations/0021_store_partition_index_create.py +0 -2
- morango/migrations/0022_rename_instance_fields.py +23 -0
- morango/migrations/0023_add_instance_id_fields.py +24 -0
- morango/migrations/0024_auto_20240129_1757.py +28 -0
- morango/models/__init__.py +0 -6
- morango/models/certificates.py +137 -28
- morango/models/core.py +48 -46
- morango/models/fields/crypto.py +20 -6
- morango/models/fields/uuids.py +2 -1
- morango/models/utils.py +5 -6
- morango/proquint.py +2 -3
- morango/registry.py +28 -49
- morango/sync/backends/base.py +34 -0
- morango/sync/backends/postgres.py +129 -0
- morango/sync/backends/utils.py +10 -11
- morango/sync/context.py +198 -13
- morango/sync/controller.py +33 -11
- morango/sync/operations.py +324 -251
- morango/sync/session.py +11 -0
- morango/sync/syncsession.py +78 -85
- morango/sync/utils.py +18 -0
- morango/urls.py +3 -3
- morango/utils.py +2 -3
- {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/METADATA +29 -14
- morango-0.8.6.dist-info/RECORD +79 -0
- {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/WHEEL +1 -1
- morango/models/morango_mptt.py +0 -33
- morango/settings.py +0 -115
- morango/wsgi.py +0 -33
- morango-0.6.11.dist-info/RECORD +0 -77
- {morango-0.6.11.dist-info → morango-0.8.6.dist-info/licenses}/AUTHORS.md +0 -0
- {morango-0.6.11.dist-info → morango-0.8.6.dist-info/licenses}/LICENSE +0 -0
- {morango-0.6.11.dist-info → morango-0.8.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
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 2021-07-14 22:16
|
|
3
|
-
from
|
|
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
|
|
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):
|
|
@@ -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
|
+
]
|
morango/models/__init__.py
CHANGED
|
@@ -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",
|
morango/models/certificates.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
|
359
|
-
if
|
|
408
|
+
for partition in self:
|
|
409
|
+
if not other.contains_exact_partition(partition):
|
|
360
410
|
return False
|
|
361
|
-
for partition in other
|
|
362
|
-
if
|
|
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
|
-
|
|
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):
|