django-bulk-hooks 0.1.121__tar.gz → 0.1.123__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.121 → django_bulk_hooks-0.1.123}/PKG-INFO +1 -1
- django_bulk_hooks-0.1.123/django_bulk_hooks/manager.py +83 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/queryset.py +17 -4
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.121/django_bulk_hooks/manager.py +0 -201
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/LICENSE +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/README.md +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.121 → django_bulk_hooks-0.1.123}/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
|
|
@@ -284,6 +284,10 @@ class HookQuerySet(models.QuerySet):
|
|
|
284
284
|
if inheritance_chain is None:
|
|
285
285
|
inheritance_chain = self._get_inheritance_chain()
|
|
286
286
|
|
|
287
|
+
# Safety check to prevent infinite recursion
|
|
288
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
289
|
+
raise ValueError("Inheritance chain too deep - possible infinite recursion detected")
|
|
290
|
+
|
|
287
291
|
batch_size = kwargs.get("batch_size") or len(objs)
|
|
288
292
|
created_objects = []
|
|
289
293
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
@@ -323,19 +327,28 @@ class HookQuerySet(models.QuerySet):
|
|
|
323
327
|
# If the child model is still MTI, call our own logic recursively
|
|
324
328
|
if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
|
|
325
329
|
# Build inheritance chain for the child model
|
|
326
|
-
|
|
330
|
+
child_inheritance_chain = []
|
|
327
331
|
current_model = child_model
|
|
328
332
|
while current_model:
|
|
329
333
|
if not current_model._meta.proxy:
|
|
330
|
-
|
|
334
|
+
child_inheritance_chain.append(current_model)
|
|
331
335
|
parents = [
|
|
332
336
|
parent
|
|
333
337
|
for parent in current_model._meta.parents.keys()
|
|
334
338
|
if not parent._meta.proxy
|
|
335
339
|
]
|
|
336
340
|
current_model = parents[0] if parents else None
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
child_inheritance_chain.reverse()
|
|
342
|
+
|
|
343
|
+
# For nested MTI, we can't use bulk operations recursively
|
|
344
|
+
# because it would create infinite recursion. Instead, we save each child individually.
|
|
345
|
+
# We use super().save() to avoid triggering hooks that would cause recursion.
|
|
346
|
+
created = []
|
|
347
|
+
for child_obj in child_objects:
|
|
348
|
+
# Use the base model's save method to avoid triggering hooks
|
|
349
|
+
# This prevents infinite recursion when hooks try to query the database
|
|
350
|
+
super(child_obj.__class__, child_obj).save()
|
|
351
|
+
created.append(child_obj)
|
|
339
352
|
else:
|
|
340
353
|
# Single-table, safe to use bulk_create
|
|
341
354
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.123"
|
|
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
|