django-bulk-hooks 0.1.122__tar.gz → 0.1.124__tar.gz
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 django-bulk-hooks might be problematic. Click here for more details.
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/PKG-INFO +1 -1
- django_bulk_hooks-0.1.124/django_bulk_hooks/manager.py +83 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/queryset.py +35 -24
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.122/django_bulk_hooks/manager.py +0 -201
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/LICENSE +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/README.md +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.122 → django_bulk_hooks-0.1.124}/django_bulk_hooks/registry.py +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from django_bulk_hooks.queryset import HookQuerySet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BulkHookManager(models.Manager):
|
|
7
|
+
def get_queryset(self):
|
|
8
|
+
return HookQuerySet(self.model, using=self._db)
|
|
9
|
+
|
|
10
|
+
def bulk_update(
|
|
11
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Delegate to QuerySet's bulk_update implementation.
|
|
15
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
16
|
+
"""
|
|
17
|
+
return self.get_queryset().bulk_update(
|
|
18
|
+
objs, fields, bypass_hooks=bypass_hooks, bypass_validation=bypass_validation, **kwargs
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def bulk_create(
|
|
22
|
+
self,
|
|
23
|
+
objs,
|
|
24
|
+
batch_size=None,
|
|
25
|
+
ignore_conflicts=False,
|
|
26
|
+
update_conflicts=False,
|
|
27
|
+
update_fields=None,
|
|
28
|
+
unique_fields=None,
|
|
29
|
+
bypass_hooks=False,
|
|
30
|
+
bypass_validation=False,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Delegate to QuerySet's bulk_create implementation.
|
|
34
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
35
|
+
"""
|
|
36
|
+
return self.get_queryset().bulk_create(
|
|
37
|
+
objs,
|
|
38
|
+
batch_size=batch_size,
|
|
39
|
+
ignore_conflicts=ignore_conflicts,
|
|
40
|
+
update_conflicts=update_conflicts,
|
|
41
|
+
update_fields=update_fields,
|
|
42
|
+
unique_fields=unique_fields,
|
|
43
|
+
bypass_hooks=bypass_hooks,
|
|
44
|
+
bypass_validation=bypass_validation,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def bulk_delete(
|
|
48
|
+
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Delegate to QuerySet's bulk_delete implementation.
|
|
52
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
53
|
+
"""
|
|
54
|
+
return self.get_queryset().bulk_delete(
|
|
55
|
+
objs, batch_size=batch_size, bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def update(self, **kwargs):
|
|
59
|
+
"""
|
|
60
|
+
Delegate to QuerySet's update implementation.
|
|
61
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
62
|
+
"""
|
|
63
|
+
return self.get_queryset().update(**kwargs)
|
|
64
|
+
|
|
65
|
+
def delete(self):
|
|
66
|
+
"""
|
|
67
|
+
Delegate to QuerySet's delete implementation.
|
|
68
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
69
|
+
"""
|
|
70
|
+
return self.get_queryset().delete()
|
|
71
|
+
|
|
72
|
+
def save(self, obj):
|
|
73
|
+
"""
|
|
74
|
+
Save a single object using the appropriate bulk operation.
|
|
75
|
+
"""
|
|
76
|
+
if obj.pk:
|
|
77
|
+
self.bulk_update(
|
|
78
|
+
[obj],
|
|
79
|
+
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
self.bulk_create([obj])
|
|
83
|
+
return obj
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
from django.db
|
|
3
|
-
from django.db import
|
|
4
|
-
from django.db.models.constants import OnConflict
|
|
5
|
-
from django.db.models.expressions import DatabaseDefault
|
|
6
|
-
import operator
|
|
7
|
-
from functools import reduce
|
|
1
|
+
|
|
2
|
+
from django.db import models, transaction
|
|
3
|
+
from django.db.models import AutoField
|
|
8
4
|
|
|
9
5
|
from django_bulk_hooks import engine
|
|
10
6
|
from django_bulk_hooks.constants import (
|
|
@@ -303,8 +299,11 @@ class HookQuerySet(models.QuerySet):
|
|
|
303
299
|
"""
|
|
304
300
|
Process a single batch of objects through the inheritance chain.
|
|
305
301
|
"""
|
|
306
|
-
# Step 1: Handle parent tables with
|
|
302
|
+
# Step 1: Handle parent tables with bulk operations where possible
|
|
307
303
|
parent_objects_map = {}
|
|
304
|
+
|
|
305
|
+
# Group parent objects by model class to enable bulk operations
|
|
306
|
+
parent_objects_by_model = {}
|
|
308
307
|
for obj in batch:
|
|
309
308
|
parent_instances = {}
|
|
310
309
|
current_parent = None
|
|
@@ -312,10 +311,24 @@ class HookQuerySet(models.QuerySet):
|
|
|
312
311
|
parent_obj = self._create_parent_instance(
|
|
313
312
|
obj, model_class, current_parent
|
|
314
313
|
)
|
|
315
|
-
parent_obj.save()
|
|
316
314
|
parent_instances[model_class] = parent_obj
|
|
317
315
|
current_parent = parent_obj
|
|
316
|
+
|
|
317
|
+
# Group by model class for potential bulk operations
|
|
318
|
+
if model_class not in parent_objects_by_model:
|
|
319
|
+
parent_objects_by_model[model_class] = []
|
|
320
|
+
parent_objects_by_model[model_class].append(parent_obj)
|
|
321
|
+
|
|
318
322
|
parent_objects_map[id(obj)] = parent_instances
|
|
323
|
+
|
|
324
|
+
# Save parent objects in bulk where possible
|
|
325
|
+
for model_class, parent_objs in parent_objects_by_model.items():
|
|
326
|
+
if len(parent_objs) > 1:
|
|
327
|
+
# Use bulk_create for multiple objects of the same model
|
|
328
|
+
model_class._base_manager.bulk_create(parent_objs)
|
|
329
|
+
else:
|
|
330
|
+
# Individual save for single objects
|
|
331
|
+
super(parent_objs[0].__class__, parent_objs[0]).save()
|
|
319
332
|
# Step 2: Bulk insert for child objects
|
|
320
333
|
child_model = inheritance_chain[-1]
|
|
321
334
|
child_objects = []
|
|
@@ -324,22 +337,8 @@ class HookQuerySet(models.QuerySet):
|
|
|
324
337
|
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
325
338
|
)
|
|
326
339
|
child_objects.append(child_obj)
|
|
327
|
-
# If the child model is still MTI,
|
|
340
|
+
# If the child model is still MTI, we need to handle it specially
|
|
328
341
|
if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
|
|
329
|
-
# Build inheritance chain for the child model
|
|
330
|
-
child_inheritance_chain = []
|
|
331
|
-
current_model = child_model
|
|
332
|
-
while current_model:
|
|
333
|
-
if not current_model._meta.proxy:
|
|
334
|
-
child_inheritance_chain.append(current_model)
|
|
335
|
-
parents = [
|
|
336
|
-
parent
|
|
337
|
-
for parent in current_model._meta.parents.keys()
|
|
338
|
-
if not parent._meta.proxy
|
|
339
|
-
]
|
|
340
|
-
current_model = parents[0] if parents else None
|
|
341
|
-
child_inheritance_chain.reverse()
|
|
342
|
-
|
|
343
342
|
# For nested MTI, we can't use bulk operations recursively
|
|
344
343
|
# because it would create infinite recursion. Instead, we save each child individually.
|
|
345
344
|
# We use super().save() to avoid triggering hooks that would cause recursion.
|
|
@@ -388,6 +387,12 @@ class HookQuerySet(models.QuerySet):
|
|
|
388
387
|
elif hasattr(field, 'auto_now') and field.auto_now:
|
|
389
388
|
field.pre_save(parent_obj, add=True)
|
|
390
389
|
|
|
390
|
+
# Ensure auto_now_add fields are explicitly set to prevent null constraint violations
|
|
391
|
+
for field in parent_model._meta.local_fields:
|
|
392
|
+
if hasattr(field, 'auto_now_add') and field.auto_now_add:
|
|
393
|
+
if getattr(parent_obj, field.name) is None:
|
|
394
|
+
field.pre_save(parent_obj, add=True)
|
|
395
|
+
|
|
391
396
|
return parent_obj
|
|
392
397
|
|
|
393
398
|
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
@@ -411,4 +416,10 @@ class HookQuerySet(models.QuerySet):
|
|
|
411
416
|
elif hasattr(field, 'auto_now') and field.auto_now:
|
|
412
417
|
field.pre_save(child_obj, add=True)
|
|
413
418
|
|
|
419
|
+
# Ensure auto_now_add fields are explicitly set to prevent null constraint violations
|
|
420
|
+
for field in child_model._meta.local_fields:
|
|
421
|
+
if hasattr(field, 'auto_now_add') and field.auto_now_add:
|
|
422
|
+
if getattr(child_obj, field.name) is None:
|
|
423
|
+
field.pre_save(child_obj, add=True)
|
|
424
|
+
|
|
414
425
|
return child_obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.124"
|
|
4
4
|
description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
|
|
5
5
|
authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
|
|
3
|
-
from django_bulk_hooks import engine
|
|
4
|
-
from django_bulk_hooks.constants import (
|
|
5
|
-
AFTER_CREATE,
|
|
6
|
-
AFTER_DELETE,
|
|
7
|
-
AFTER_UPDATE,
|
|
8
|
-
BEFORE_CREATE,
|
|
9
|
-
BEFORE_DELETE,
|
|
10
|
-
BEFORE_UPDATE,
|
|
11
|
-
VALIDATE_CREATE,
|
|
12
|
-
VALIDATE_DELETE,
|
|
13
|
-
VALIDATE_UPDATE,
|
|
14
|
-
)
|
|
15
|
-
from django_bulk_hooks.context import HookContext
|
|
16
|
-
from django_bulk_hooks.queryset import HookQuerySet
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class BulkHookManager(models.Manager):
|
|
20
|
-
CHUNK_SIZE = 200
|
|
21
|
-
|
|
22
|
-
def get_queryset(self):
|
|
23
|
-
return HookQuerySet(self.model, using=self._db)
|
|
24
|
-
|
|
25
|
-
@transaction.atomic
|
|
26
|
-
def bulk_update(
|
|
27
|
-
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
28
|
-
):
|
|
29
|
-
if not objs:
|
|
30
|
-
return []
|
|
31
|
-
|
|
32
|
-
model_cls = self.model
|
|
33
|
-
|
|
34
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
35
|
-
raise TypeError(
|
|
36
|
-
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
if not bypass_hooks:
|
|
40
|
-
# Load originals for hook comparison and ensure they match the order of new instances
|
|
41
|
-
original_map = {
|
|
42
|
-
obj.pk: obj
|
|
43
|
-
for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
44
|
-
}
|
|
45
|
-
originals = [original_map.get(obj.pk) for obj in objs]
|
|
46
|
-
|
|
47
|
-
ctx = HookContext(model_cls)
|
|
48
|
-
|
|
49
|
-
# Run validation hooks first
|
|
50
|
-
if not bypass_validation:
|
|
51
|
-
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
52
|
-
|
|
53
|
-
# Then run business logic hooks
|
|
54
|
-
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
55
|
-
|
|
56
|
-
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
57
|
-
modified_fields = self._detect_modified_fields(objs, originals)
|
|
58
|
-
if modified_fields:
|
|
59
|
-
# Convert to set for efficient union operation
|
|
60
|
-
fields_set = set(fields)
|
|
61
|
-
fields_set.update(modified_fields)
|
|
62
|
-
fields = list(fields_set)
|
|
63
|
-
|
|
64
|
-
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
65
|
-
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
66
|
-
# Call the base implementation to avoid re-triggering this method
|
|
67
|
-
super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
|
|
68
|
-
|
|
69
|
-
if not bypass_hooks:
|
|
70
|
-
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
71
|
-
|
|
72
|
-
return objs
|
|
73
|
-
|
|
74
|
-
@transaction.atomic
|
|
75
|
-
def bulk_create(
|
|
76
|
-
self,
|
|
77
|
-
objs,
|
|
78
|
-
batch_size=None,
|
|
79
|
-
ignore_conflicts=False,
|
|
80
|
-
update_conflicts=False,
|
|
81
|
-
update_fields=None,
|
|
82
|
-
unique_fields=None,
|
|
83
|
-
bypass_hooks=False,
|
|
84
|
-
bypass_validation=False,
|
|
85
|
-
):
|
|
86
|
-
"""
|
|
87
|
-
Delegate to QuerySet's bulk_create implementation.
|
|
88
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
89
|
-
"""
|
|
90
|
-
return self.get_queryset().bulk_create(
|
|
91
|
-
objs,
|
|
92
|
-
batch_size=batch_size,
|
|
93
|
-
ignore_conflicts=ignore_conflicts,
|
|
94
|
-
update_conflicts=update_conflicts,
|
|
95
|
-
update_fields=update_fields,
|
|
96
|
-
unique_fields=unique_fields,
|
|
97
|
-
bypass_hooks=bypass_hooks,
|
|
98
|
-
bypass_validation=bypass_validation,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
@transaction.atomic
|
|
102
|
-
def bulk_delete(
|
|
103
|
-
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
104
|
-
):
|
|
105
|
-
if not objs:
|
|
106
|
-
return []
|
|
107
|
-
|
|
108
|
-
model_cls = self.model
|
|
109
|
-
|
|
110
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
111
|
-
raise TypeError(
|
|
112
|
-
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
ctx = HookContext(model_cls)
|
|
116
|
-
|
|
117
|
-
if not bypass_hooks:
|
|
118
|
-
# Run validation hooks first
|
|
119
|
-
if not bypass_validation:
|
|
120
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
121
|
-
|
|
122
|
-
# Then run business logic hooks
|
|
123
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
124
|
-
|
|
125
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
126
|
-
|
|
127
|
-
# Use base manager for the actual deletion to prevent recursion
|
|
128
|
-
# The hooks have already been fired above, so we don't need them again
|
|
129
|
-
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
130
|
-
|
|
131
|
-
if not bypass_hooks:
|
|
132
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
133
|
-
|
|
134
|
-
return objs
|
|
135
|
-
|
|
136
|
-
@transaction.atomic
|
|
137
|
-
def update(self, **kwargs):
|
|
138
|
-
objs = list(self.all())
|
|
139
|
-
if not objs:
|
|
140
|
-
return 0
|
|
141
|
-
for key, value in kwargs.items():
|
|
142
|
-
for obj in objs:
|
|
143
|
-
setattr(obj, key, value)
|
|
144
|
-
self.bulk_update(objs, fields=list(kwargs.keys()))
|
|
145
|
-
return len(objs)
|
|
146
|
-
|
|
147
|
-
@transaction.atomic
|
|
148
|
-
def delete(self):
|
|
149
|
-
objs = list(self.all())
|
|
150
|
-
if not objs:
|
|
151
|
-
return 0
|
|
152
|
-
self.bulk_delete(objs)
|
|
153
|
-
return len(objs)
|
|
154
|
-
|
|
155
|
-
@transaction.atomic
|
|
156
|
-
def save(self, obj):
|
|
157
|
-
if obj.pk:
|
|
158
|
-
self.bulk_update(
|
|
159
|
-
[obj],
|
|
160
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
161
|
-
)
|
|
162
|
-
else:
|
|
163
|
-
self.bulk_create([obj])
|
|
164
|
-
return obj
|
|
165
|
-
|
|
166
|
-
def _detect_modified_fields(self, new_instances, original_instances):
|
|
167
|
-
"""
|
|
168
|
-
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
169
|
-
new instances with their original values.
|
|
170
|
-
"""
|
|
171
|
-
if not original_instances:
|
|
172
|
-
return set()
|
|
173
|
-
|
|
174
|
-
modified_fields = set()
|
|
175
|
-
|
|
176
|
-
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
177
|
-
for new_instance, original in zip(new_instances, original_instances):
|
|
178
|
-
if new_instance.pk is None or original is None:
|
|
179
|
-
continue
|
|
180
|
-
|
|
181
|
-
# Compare all fields to detect changes
|
|
182
|
-
for field in new_instance._meta.fields:
|
|
183
|
-
if field.name == "id":
|
|
184
|
-
continue
|
|
185
|
-
|
|
186
|
-
new_value = getattr(new_instance, field.name)
|
|
187
|
-
original_value = getattr(original, field.name)
|
|
188
|
-
|
|
189
|
-
# Handle different field types appropriately
|
|
190
|
-
if field.is_relation:
|
|
191
|
-
# For foreign keys, compare the pk values
|
|
192
|
-
new_pk = new_value.pk if new_value else None
|
|
193
|
-
original_pk = original_value.pk if original_value else None
|
|
194
|
-
if new_pk != original_pk:
|
|
195
|
-
modified_fields.add(field.name)
|
|
196
|
-
else:
|
|
197
|
-
# For regular fields, use direct comparison
|
|
198
|
-
if new_value != original_value:
|
|
199
|
-
modified_fields.add(field.name)
|
|
200
|
-
|
|
201
|
-
return modified_fields
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|