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
morango/models/core.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from __future__ import unicode_literals
|
|
2
|
-
|
|
3
1
|
import functools
|
|
4
2
|
import json
|
|
5
3
|
import logging
|
|
@@ -17,13 +15,13 @@ from django.db.models import F
|
|
|
17
15
|
from django.db.models import Func
|
|
18
16
|
from django.db.models import Max
|
|
19
17
|
from django.db.models import Q
|
|
18
|
+
from django.db.models import signals
|
|
20
19
|
from django.db.models import TextField
|
|
21
20
|
from django.db.models import Value
|
|
22
21
|
from django.db.models.deletion import Collector
|
|
23
22
|
from django.db.models.expressions import CombinedExpression
|
|
24
23
|
from django.db.models.fields.related import ForeignKey
|
|
25
24
|
from django.db.models.functions import Cast
|
|
26
|
-
from django.utils import six
|
|
27
25
|
from django.utils import timezone
|
|
28
26
|
from django.utils.functional import cached_property
|
|
29
27
|
|
|
@@ -38,7 +36,6 @@ from morango.models.fields.uuids import UUIDField
|
|
|
38
36
|
from morango.models.fields.uuids import UUIDModelMixin
|
|
39
37
|
from morango.models.fsic_utils import remove_redundant_instance_counters
|
|
40
38
|
from morango.models.manager import SyncableModelManager
|
|
41
|
-
from morango.models.morango_mptt import MorangoMPTTModel
|
|
42
39
|
from morango.models.utils import get_0_4_system_parameters
|
|
43
40
|
from morango.models.utils import get_0_5_mac_address
|
|
44
41
|
from morango.models.utils import get_0_5_system_id
|
|
@@ -122,7 +119,7 @@ class InstanceIDModel(models.Model):
|
|
|
122
119
|
hostname = models.TextField()
|
|
123
120
|
sysversion = models.TextField()
|
|
124
121
|
node_id = models.CharField(max_length=20, blank=True)
|
|
125
|
-
database = models.ForeignKey(DatabaseIDModel)
|
|
122
|
+
database = models.ForeignKey(DatabaseIDModel, on_delete=models.CASCADE)
|
|
126
123
|
counter = models.IntegerField(default=0)
|
|
127
124
|
current = models.BooleanField(default=True)
|
|
128
125
|
db_path = models.CharField(max_length=1000)
|
|
@@ -219,10 +216,10 @@ class SyncSession(models.Model):
|
|
|
219
216
|
|
|
220
217
|
# track the certificates being used by each side for this session
|
|
221
218
|
client_certificate = models.ForeignKey(
|
|
222
|
-
Certificate, blank=True, null=True, related_name="syncsessions_client"
|
|
219
|
+
Certificate, blank=True, null=True, related_name="syncsessions_client", on_delete=models.CASCADE
|
|
223
220
|
)
|
|
224
221
|
server_certificate = models.ForeignKey(
|
|
225
|
-
Certificate, blank=True, null=True, related_name="syncsessions_server"
|
|
222
|
+
Certificate, blank=True, null=True, related_name="syncsessions_server", on_delete=models.CASCADE
|
|
226
223
|
)
|
|
227
224
|
|
|
228
225
|
# track the morango profile this sync session is happening for
|
|
@@ -240,9 +237,12 @@ class SyncSession(models.Model):
|
|
|
240
237
|
client_ip = models.CharField(max_length=100, blank=True)
|
|
241
238
|
server_ip = models.CharField(max_length=100, blank=True)
|
|
242
239
|
|
|
243
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
# track the instance IDs for convenient querying, as well as serialized copies of
|
|
241
|
+
# the client and server instance model fields for debugging/tracking purposes
|
|
242
|
+
client_instance_id = models.UUIDField(null=True, blank=True)
|
|
243
|
+
client_instance_json = models.TextField(default="{}")
|
|
244
|
+
server_instance_id = models.UUIDField(null=True, blank=True)
|
|
245
|
+
server_instance_json = models.TextField(default="{}")
|
|
246
246
|
|
|
247
247
|
# used to store other data we may need to know about this sync session
|
|
248
248
|
extra_fields = models.TextField(default="{}")
|
|
@@ -252,11 +252,11 @@ class SyncSession(models.Model):
|
|
|
252
252
|
|
|
253
253
|
@cached_property
|
|
254
254
|
def client_instance_data(self):
|
|
255
|
-
return json.loads(self.
|
|
255
|
+
return json.loads(self.client_instance_json)
|
|
256
256
|
|
|
257
257
|
@cached_property
|
|
258
258
|
def server_instance_data(self):
|
|
259
|
-
return json.loads(self.
|
|
259
|
+
return json.loads(self.server_instance_json)
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
class TransferSession(models.Model):
|
|
@@ -280,7 +280,7 @@ class TransferSession(models.Model):
|
|
|
280
280
|
bytes_sent = models.BigIntegerField(default=0, null=True, blank=True)
|
|
281
281
|
bytes_received = models.BigIntegerField(default=0, null=True, blank=True)
|
|
282
282
|
|
|
283
|
-
sync_session = models.ForeignKey(SyncSession)
|
|
283
|
+
sync_session = models.ForeignKey(SyncSession, on_delete=models.CASCADE)
|
|
284
284
|
|
|
285
285
|
# track when the transfer session started and the last time there was activity on it
|
|
286
286
|
start_timestamp = models.DateTimeField(default=timezone.now)
|
|
@@ -351,10 +351,10 @@ class TransferSession(models.Model):
|
|
|
351
351
|
|
|
352
352
|
def get_touched_record_ids_for_model(self, model):
|
|
353
353
|
if isinstance(model, SyncableModel) or (
|
|
354
|
-
isinstance(model,
|
|
354
|
+
isinstance(model, type) and issubclass(model, SyncableModel)
|
|
355
355
|
):
|
|
356
356
|
model = model.morango_model_name
|
|
357
|
-
_assert(isinstance(model,
|
|
357
|
+
_assert(isinstance(model, str), "Model must resolve to string")
|
|
358
358
|
return Store.objects.filter(
|
|
359
359
|
model_name=model, last_transfer_session_id=self.id
|
|
360
360
|
).values_list("id", flat=True)
|
|
@@ -418,7 +418,7 @@ class StoreQueryset(models.QuerySet):
|
|
|
418
418
|
self.annotate(id_cast=Cast("id", TextField()))
|
|
419
419
|
# remove dashes from char uuid
|
|
420
420
|
.annotate(
|
|
421
|
-
fixed_id=Func(F("id_cast"), Value("-"), Value(""), function="replace")
|
|
421
|
+
fixed_id=Func(F("id_cast"), Value("-"), Value(""), function="replace", output_field=TextField())
|
|
422
422
|
)
|
|
423
423
|
# return as list
|
|
424
424
|
.values_list("fixed_id", flat=True)
|
|
@@ -449,7 +449,8 @@ class Store(AbstractStore):
|
|
|
449
449
|
|
|
450
450
|
class Meta:
|
|
451
451
|
indexes = [
|
|
452
|
-
models.Index(fields=[
|
|
452
|
+
models.Index(fields=["partition"], name="idx_morango_store_partition"),
|
|
453
|
+
models.Index(fields=["profile", "model_name", "partition", "dirty_bit"], condition=models.Q(dirty_bit=True), name="idx_morango_deserialize"),
|
|
453
454
|
]
|
|
454
455
|
|
|
455
456
|
def _deserialize_store_model(self, fk_cache, defer_fks=False): # noqa: C901
|
|
@@ -465,14 +466,14 @@ class Store(AbstractStore):
|
|
|
465
466
|
klass_model = syncable_models.get_model(self.profile, self.model_name)
|
|
466
467
|
# if store model marked as deleted, attempt to delete in app layer
|
|
467
468
|
if self.deleted:
|
|
468
|
-
#
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
klass_model.
|
|
469
|
+
# Don't differentiate between deletion and hard deletion here,
|
|
470
|
+
# as we don't want to add additional tracking for models in either case,
|
|
471
|
+
# just to actually delete them.
|
|
472
|
+
# Import here to avoid circular import, as the utils module
|
|
473
|
+
# imports core models.
|
|
474
|
+
from morango.sync.utils import mute_signals
|
|
475
|
+
with mute_signals(signals.post_delete):
|
|
476
|
+
klass_model.syncing_objects.filter(id=self.id).delete()
|
|
476
477
|
return None, deferred_fks
|
|
477
478
|
else:
|
|
478
479
|
# load model into memory
|
|
@@ -527,7 +528,7 @@ class Buffer(AbstractStore):
|
|
|
527
528
|
dequeuing into the local store.
|
|
528
529
|
"""
|
|
529
530
|
|
|
530
|
-
transfer_session = models.ForeignKey(TransferSession)
|
|
531
|
+
transfer_session = models.ForeignKey(TransferSession, on_delete=models.CASCADE)
|
|
531
532
|
model_uuid = UUIDField()
|
|
532
533
|
|
|
533
534
|
class Meta:
|
|
@@ -608,7 +609,7 @@ class DatabaseMaxCounter(AbstractCounter):
|
|
|
608
609
|
)
|
|
609
610
|
else:
|
|
610
611
|
updated_fsic = {}
|
|
611
|
-
for key, value in
|
|
612
|
+
for key, value in fsics.items():
|
|
612
613
|
if key in internal_fsic:
|
|
613
614
|
# if same instance id, update fsic with larger value
|
|
614
615
|
if fsics[key] > internal_fsic[key]:
|
|
@@ -618,7 +619,7 @@ class DatabaseMaxCounter(AbstractCounter):
|
|
|
618
619
|
updated_fsic[key] = fsics[key]
|
|
619
620
|
|
|
620
621
|
# load database max counters
|
|
621
|
-
for (key, value) in
|
|
622
|
+
for (key, value) in updated_fsic.items():
|
|
622
623
|
for f in sync_filter:
|
|
623
624
|
DatabaseMaxCounter.objects.update_or_create(
|
|
624
625
|
instance_id=key, partition=f, defaults={"counter": value}
|
|
@@ -761,7 +762,7 @@ class RecordMaxCounter(AbstractCounter):
|
|
|
761
762
|
during the sync process.
|
|
762
763
|
"""
|
|
763
764
|
|
|
764
|
-
store_model = models.ForeignKey(Store)
|
|
765
|
+
store_model = models.ForeignKey(Store, on_delete=models.CASCADE)
|
|
765
766
|
|
|
766
767
|
class Meta:
|
|
767
768
|
unique_together = ("store_model", "instance_id")
|
|
@@ -773,11 +774,13 @@ class RecordMaxCounterBuffer(AbstractCounter):
|
|
|
773
774
|
until they are sent or received by another morango instance.
|
|
774
775
|
"""
|
|
775
776
|
|
|
776
|
-
transfer_session = models.ForeignKey(TransferSession)
|
|
777
|
+
transfer_session = models.ForeignKey(TransferSession, on_delete=models.CASCADE)
|
|
777
778
|
model_uuid = UUIDField(db_index=True)
|
|
778
779
|
|
|
779
780
|
|
|
780
|
-
ForeignKeyReference = namedtuple(
|
|
781
|
+
ForeignKeyReference = namedtuple(
|
|
782
|
+
"ForeignKeyReference", ["from_field", "from_pk", "to_pk"]
|
|
783
|
+
)
|
|
781
784
|
|
|
782
785
|
|
|
783
786
|
class SyncableModel(UUIDModelMixin):
|
|
@@ -803,6 +806,11 @@ class SyncableModel(UUIDModelMixin):
|
|
|
803
806
|
|
|
804
807
|
objects = SyncableModelManager()
|
|
805
808
|
|
|
809
|
+
# Add a special syncing_objects queryset to every SyncableModel for use in syncing operations.
|
|
810
|
+
# This means that we still deal with the entire set of objects when syncing, even if the default
|
|
811
|
+
# model manager has been overridden to filter the queryset.
|
|
812
|
+
syncing_objects = SyncableModelManager()
|
|
813
|
+
|
|
806
814
|
class Meta:
|
|
807
815
|
abstract = True
|
|
808
816
|
|
|
@@ -839,10 +847,8 @@ class SyncableModel(UUIDModelMixin):
|
|
|
839
847
|
with transaction.atomic():
|
|
840
848
|
if hard_delete:
|
|
841
849
|
# set hard deletion for all related models
|
|
842
|
-
for model, instances in
|
|
843
|
-
if issubclass(model, SyncableModel)
|
|
844
|
-
model, MorangoMPTTModel
|
|
845
|
-
):
|
|
850
|
+
for model, instances in collector.data.items():
|
|
851
|
+
if issubclass(model, SyncableModel):
|
|
846
852
|
for obj in instances:
|
|
847
853
|
obj._update_hard_deleted_models()
|
|
848
854
|
return collector.delete()
|
|
@@ -908,7 +914,7 @@ class SyncableModel(UUIDModelMixin):
|
|
|
908
914
|
ForeignKeyReference(
|
|
909
915
|
from_field=field.attname,
|
|
910
916
|
from_pk=self.pk,
|
|
911
|
-
to_pk=getattr(self, field.attname)
|
|
917
|
+
to_pk=getattr(self, field.attname),
|
|
912
918
|
)
|
|
913
919
|
)
|
|
914
920
|
|
|
@@ -926,15 +932,8 @@ class SyncableModel(UUIDModelMixin):
|
|
|
926
932
|
continue
|
|
927
933
|
if f.attname in self._morango_internal_fields_not_to_serialize:
|
|
928
934
|
continue
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
self,
|
|
932
|
-
"_internal_mptt_fields_not_to_serialize",
|
|
933
|
-
"_internal_fields_not_to_serialize",
|
|
934
|
-
):
|
|
935
|
-
continue
|
|
936
|
-
if hasattr(f, "value_from_object_json_compatible"):
|
|
937
|
-
data[f.attname] = f.value_from_object_json_compatible(self)
|
|
935
|
+
if getattr(f, "morango_serialize_to_string", False):
|
|
936
|
+
data[f.attname] = f.value_to_string(self)
|
|
938
937
|
else:
|
|
939
938
|
data[f.attname] = f.value_from_object(self)
|
|
940
939
|
return data
|
|
@@ -945,7 +944,10 @@ class SyncableModel(UUIDModelMixin):
|
|
|
945
944
|
kwargs = {}
|
|
946
945
|
for f in cls._meta.concrete_fields:
|
|
947
946
|
if f.attname in dict_model:
|
|
948
|
-
|
|
947
|
+
if getattr(f, "morango_serialize_to_string", False):
|
|
948
|
+
kwargs[f.attname] = f.to_python(dict_model[f.attname])
|
|
949
|
+
else:
|
|
950
|
+
kwargs[f.attname] = dict_model[f.attname]
|
|
949
951
|
return cls(**kwargs)
|
|
950
952
|
|
|
951
953
|
@classmethod
|
morango/models/fields/crypto.py
CHANGED
|
@@ -21,6 +21,16 @@ except ImportError:
|
|
|
21
21
|
M2CRYPTO_EXISTS = False
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
|
+
# Pre-empt the PanicException that importing cryptography can cause
|
|
25
|
+
# when we are using a non-compatible version of cffi on Python 3.13
|
|
26
|
+
# this happens because of static depdendency bundling in Kolibri
|
|
27
|
+
import cffi
|
|
28
|
+
|
|
29
|
+
if sys.version_info > (3, 13):
|
|
30
|
+
if hasattr(cffi, "__version_info__"):
|
|
31
|
+
if cffi.__version_info__ < (1, 17, 1):
|
|
32
|
+
raise ImportError
|
|
33
|
+
|
|
24
34
|
from cryptography.hazmat.backends import default_backend
|
|
25
35
|
from cryptography import exceptions as crypto_exceptions
|
|
26
36
|
|
|
@@ -40,11 +50,15 @@ try:
|
|
|
40
50
|
CRYPTOGRAPHY_EXISTS = True
|
|
41
51
|
except ImportError:
|
|
42
52
|
CRYPTOGRAPHY_EXISTS = False
|
|
53
|
+
except BaseException as e:
|
|
54
|
+
# Still catch PanicExceptions just in case.
|
|
55
|
+
if "Python API call failed" in str(e):
|
|
56
|
+
CRYPTOGRAPHY_EXISTS = False
|
|
57
|
+
else:
|
|
58
|
+
# Otherwise raise the error again to avoid silently catching other errors
|
|
59
|
+
raise
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
from base64 import encodestring as b64encode, decodestring as b64decode
|
|
46
|
-
else:
|
|
47
|
-
from base64 import encodebytes as b64encode, decodebytes as b64decode
|
|
61
|
+
from base64 import encodebytes as b64encode, decodebytes as b64decode
|
|
48
62
|
|
|
49
63
|
|
|
50
64
|
PKCS8_HEADER = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A"
|
|
@@ -347,7 +361,7 @@ class RSAKeyBaseField(models.TextField):
|
|
|
347
361
|
|
|
348
362
|
|
|
349
363
|
class PublicKeyField(RSAKeyBaseField):
|
|
350
|
-
def from_db_value(self, value, expression, connection
|
|
364
|
+
def from_db_value(self, value, expression, connection):
|
|
351
365
|
if not value:
|
|
352
366
|
return None
|
|
353
367
|
return Key(public_key_string=value)
|
|
@@ -366,7 +380,7 @@ class PublicKeyField(RSAKeyBaseField):
|
|
|
366
380
|
|
|
367
381
|
|
|
368
382
|
class PrivateKeyField(RSAKeyBaseField):
|
|
369
|
-
def from_db_value(self, value, expression, connection
|
|
383
|
+
def from_db_value(self, value, expression, connection):
|
|
370
384
|
if not value:
|
|
371
385
|
return None
|
|
372
386
|
return Key(private_key_string=value)
|
morango/models/fields/uuids.py
CHANGED
|
@@ -2,6 +2,7 @@ import hashlib
|
|
|
2
2
|
import uuid
|
|
3
3
|
|
|
4
4
|
from django.db import models
|
|
5
|
+
|
|
5
6
|
from morango.utils import _assert
|
|
6
7
|
|
|
7
8
|
|
|
@@ -41,7 +42,7 @@ class UUIDField(models.CharField):
|
|
|
41
42
|
raise TypeError(self.error_messages["invalid"] % {"value": value})
|
|
42
43
|
return value.hex
|
|
43
44
|
|
|
44
|
-
def from_db_value(self, value, expression, connection
|
|
45
|
+
def from_db_value(self, value, expression, connection):
|
|
45
46
|
return self.to_python(value)
|
|
46
47
|
|
|
47
48
|
def to_python(self, value):
|
morango/models/utils.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
import ifcfg
|
|
3
2
|
import os
|
|
4
3
|
import platform
|
|
5
4
|
import socket
|
|
@@ -7,10 +6,10 @@ import subprocess
|
|
|
7
6
|
import sys
|
|
8
7
|
import uuid
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import ifcfg
|
|
12
10
|
from django.conf import settings
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
from .fields.uuids import sha2_uuid
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _get_database_path():
|
|
@@ -109,7 +108,7 @@ def _get_android_uuid():
|
|
|
109
108
|
def _do_salted_hash(value):
|
|
110
109
|
if not value:
|
|
111
110
|
return ""
|
|
112
|
-
if not isinstance(value,
|
|
111
|
+
if not isinstance(value, str):
|
|
113
112
|
value = str(value)
|
|
114
113
|
try:
|
|
115
114
|
value = value.encode()
|
|
@@ -185,7 +184,7 @@ def _get_mac_address_flags(mac):
|
|
|
185
184
|
"""
|
|
186
185
|
See: https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local
|
|
187
186
|
"""
|
|
188
|
-
if isinstance(mac,
|
|
187
|
+
if isinstance(mac, int):
|
|
189
188
|
mac = _mac_int_to_ether(mac)
|
|
190
189
|
|
|
191
190
|
first_octet = int(mac[:2], base=16)
|
morango/proquint.py
CHANGED
|
@@ -4,7 +4,6 @@ The simplest ways to use this module are the :func:`humanize` and :func:`uuid`
|
|
|
4
4
|
functions. For tighter control over the output, see :class:`HumanHasher`.
|
|
5
5
|
"""
|
|
6
6
|
import uuid
|
|
7
|
-
from django.utils import six
|
|
8
7
|
|
|
9
8
|
# Copyright (c) 2014 SUNET. All rights reserved.
|
|
10
9
|
#
|
|
@@ -59,7 +58,7 @@ def from_int(data):
|
|
|
59
58
|
:type data: int
|
|
60
59
|
:rtype: string
|
|
61
60
|
"""
|
|
62
|
-
if not isinstance(data,
|
|
61
|
+
if not isinstance(data, int):
|
|
63
62
|
raise TypeError("Input must be integer")
|
|
64
63
|
|
|
65
64
|
res = []
|
|
@@ -84,7 +83,7 @@ def to_int(data):
|
|
|
84
83
|
:type data: string
|
|
85
84
|
:rtype: int
|
|
86
85
|
"""
|
|
87
|
-
if not isinstance(data,
|
|
86
|
+
if not isinstance(data, str):
|
|
88
87
|
raise TypeError("Input must be string")
|
|
89
88
|
|
|
90
89
|
res = 0
|
morango/registry.py
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
`SyncableModelRegistry` holds all syncable models for a project, on a per profile basis.
|
|
3
3
|
This class is registered at app load time for morango in `apps.py`.
|
|
4
4
|
"""
|
|
5
|
-
import sys
|
|
6
5
|
import inspect
|
|
6
|
+
import sys
|
|
7
7
|
from collections import OrderedDict
|
|
8
8
|
|
|
9
9
|
from django.db.models.fields.related import ForeignKey
|
|
10
|
-
from django.utils import six
|
|
11
10
|
|
|
12
11
|
from morango.constants import transfer_stages
|
|
13
12
|
from morango.errors import InvalidMorangoModelConfiguration
|
|
@@ -19,7 +18,7 @@ from morango.utils import SETTINGS
|
|
|
19
18
|
|
|
20
19
|
def _get_foreign_key_classes(m):
|
|
21
20
|
return set(
|
|
22
|
-
[field.
|
|
21
|
+
[field.related_model for field in m._meta.fields if isinstance(field, ForeignKey)]
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
|
|
@@ -36,6 +35,24 @@ def _multiple_self_ref_fk_check(class_model):
|
|
|
36
35
|
return False
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
def _check_manager(name, objects):
|
|
39
|
+
from morango.models.manager import SyncableModelManager
|
|
40
|
+
from morango.models.query import SyncableModelQuerySet
|
|
41
|
+
# syncable model checks
|
|
42
|
+
if not isinstance(objects, SyncableModelManager):
|
|
43
|
+
raise InvalidMorangoModelConfiguration(
|
|
44
|
+
"Manager for {} must inherit from SyncableModelManager.".format(
|
|
45
|
+
name
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
if not isinstance(objects.none(), SyncableModelQuerySet):
|
|
49
|
+
raise InvalidMorangoModelConfiguration(
|
|
50
|
+
"Queryset for {} model must inherit from SyncableModelQuerySet.".format(
|
|
51
|
+
name
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
39
56
|
class SyncableModelRegistry(object):
|
|
40
57
|
def __init__(self):
|
|
41
58
|
self.profile_models = {}
|
|
@@ -99,8 +116,6 @@ class SyncableModelRegistry(object):
|
|
|
99
116
|
|
|
100
117
|
import django.apps
|
|
101
118
|
from morango.models.core import SyncableModel
|
|
102
|
-
from morango.models.manager import SyncableModelManager
|
|
103
|
-
from morango.models.query import SyncableModelQuerySet
|
|
104
119
|
|
|
105
120
|
model_list = []
|
|
106
121
|
for model in django.apps.apps.get_models():
|
|
@@ -111,49 +126,10 @@ class SyncableModelRegistry(object):
|
|
|
111
126
|
raise InvalidMorangoModelConfiguration(
|
|
112
127
|
"Syncing models with more than 1 self referential ForeignKey is not supported."
|
|
113
128
|
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
MorangoMPTTModel,
|
|
118
|
-
MorangoMPTTTreeManager,
|
|
119
|
-
MorangoTreeQuerySet,
|
|
120
|
-
)
|
|
129
|
+
# Check both the objects and the syncing_objects querysets.
|
|
130
|
+
_check_manager(name, model.objects)
|
|
131
|
+
_check_manager(name, model.syncing_objects)
|
|
121
132
|
|
|
122
|
-
# mptt syncable model checks
|
|
123
|
-
if issubclass(model, models.MPTTModel):
|
|
124
|
-
if not issubclass(model, MorangoMPTTModel):
|
|
125
|
-
raise InvalidMorangoModelConfiguration(
|
|
126
|
-
"{} that inherits from MPTTModel, should instead inherit from MorangoMPTTModel.".format(
|
|
127
|
-
name
|
|
128
|
-
)
|
|
129
|
-
)
|
|
130
|
-
if not isinstance(model.objects, MorangoMPTTTreeManager):
|
|
131
|
-
raise InvalidMorangoModelConfiguration(
|
|
132
|
-
"Manager for {} must inherit from MorangoMPTTTreeManager.".format(
|
|
133
|
-
name
|
|
134
|
-
)
|
|
135
|
-
)
|
|
136
|
-
if not isinstance(model.objects.none(), MorangoTreeQuerySet):
|
|
137
|
-
raise InvalidMorangoModelConfiguration(
|
|
138
|
-
"Queryset for {} model must inherit from MorangoTreeQuerySet.".format(
|
|
139
|
-
name
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
except ImportError:
|
|
143
|
-
pass
|
|
144
|
-
# syncable model checks
|
|
145
|
-
if not isinstance(model.objects, SyncableModelManager):
|
|
146
|
-
raise InvalidMorangoModelConfiguration(
|
|
147
|
-
"Manager for {} must inherit from SyncableModelManager.".format(
|
|
148
|
-
name
|
|
149
|
-
)
|
|
150
|
-
)
|
|
151
|
-
if not isinstance(model.objects.none(), SyncableModelQuerySet):
|
|
152
|
-
raise InvalidMorangoModelConfiguration(
|
|
153
|
-
"Queryset for {} model must inherit from SyncableModelQuerySet.".format(
|
|
154
|
-
name
|
|
155
|
-
)
|
|
156
|
-
)
|
|
157
133
|
if model._meta.many_to_many:
|
|
158
134
|
raise UnsupportedFieldType(
|
|
159
135
|
"{} model with a ManyToManyField is not supported in morango."
|
|
@@ -178,7 +154,7 @@ class SyncableModelRegistry(object):
|
|
|
178
154
|
self._insert_model_in_dependency_order(model, profile)
|
|
179
155
|
|
|
180
156
|
# for each profile, create a dict mapping from morango_model_name to model class
|
|
181
|
-
for profile, model_list in
|
|
157
|
+
for profile, model_list in self.profile_models.items():
|
|
182
158
|
mapping = OrderedDict()
|
|
183
159
|
for model in model_list:
|
|
184
160
|
mapping[model.morango_model_name] = model
|
|
@@ -196,6 +172,7 @@ class SessionMiddlewareOperations(list):
|
|
|
196
172
|
Iterable list class that holds and initializes a list of transfer operations as configured
|
|
197
173
|
through Morango settings, and associate the group with a transfer stage by `related_stage`
|
|
198
174
|
"""
|
|
175
|
+
|
|
199
176
|
__slots__ = ("related_stage",)
|
|
200
177
|
|
|
201
178
|
def __init__(self, related_stage):
|
|
@@ -230,7 +207,9 @@ class SessionMiddlewareOperations(list):
|
|
|
230
207
|
return result
|
|
231
208
|
else:
|
|
232
209
|
raise NotImplementedError(
|
|
233
|
-
"Operation for {} stage has no middleware".format(
|
|
210
|
+
"Operation for {} stage has no applicable middleware for context '{}'".format(
|
|
211
|
+
self.related_stage, context.__class__.__name__
|
|
212
|
+
)
|
|
234
213
|
)
|
|
235
214
|
|
|
236
215
|
|
morango/sync/backends/base.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
1
3
|
from morango.models.core import Buffer
|
|
2
4
|
from morango.models.core import RecordMaxCounter
|
|
3
5
|
from morango.models.core import RecordMaxCounterBuffer
|
|
@@ -10,6 +12,19 @@ class BaseSQLWrapper(object):
|
|
|
10
12
|
def __init__(self, connection):
|
|
11
13
|
self.connection = connection
|
|
12
14
|
|
|
15
|
+
def _is_transaction_isolation_error(self, error):
|
|
16
|
+
"""
|
|
17
|
+
Determine if an error is related to transaction isolation
|
|
18
|
+
:param error: An exception
|
|
19
|
+
:return: A bool whether the error is a transaction isolation error
|
|
20
|
+
"""
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def _set_transaction_repeatable_read(self):
|
|
25
|
+
"""Set the current transaction isolation level"""
|
|
26
|
+
yield
|
|
27
|
+
|
|
13
28
|
def _create_placeholder_list(self, fields, db_values):
|
|
14
29
|
# number of rows to update
|
|
15
30
|
num_of_rows = len(db_values) // len(fields)
|
|
@@ -176,3 +191,22 @@ class BaseSQLWrapper(object):
|
|
|
176
191
|
name=name, fields=", ".join(field_sqls)
|
|
177
192
|
)
|
|
178
193
|
cursor.execute(sql, fields_params)
|
|
194
|
+
|
|
195
|
+
def _lock_all_partitions(self, shared=False):
|
|
196
|
+
"""
|
|
197
|
+
Execute a lock within the database for all partitions, if the backend supports it. The lock
|
|
198
|
+
should block until acquired
|
|
199
|
+
|
|
200
|
+
:param shared: Whether or not the lock is exclusive or shared
|
|
201
|
+
"""
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def _lock_partition(self, partition, shared=False):
|
|
205
|
+
"""
|
|
206
|
+
Execute a lock within the database for a specific partition, if the database supports it.
|
|
207
|
+
The lock should block until acquired
|
|
208
|
+
|
|
209
|
+
:param partition: The partition prefix string to lock
|
|
210
|
+
:param shared: Whether or not the lock is exclusive or shared
|
|
211
|
+
"""
|
|
212
|
+
pass
|