django-bulk-hooks 0.1.111__tar.gz → 0.1.113__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.111 → django_bulk_hooks-0.1.113}/PKG-INFO +6 -6
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/README.md +3 -3
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/manager.py +129 -11
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/models.py +2 -2
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/LICENSE +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.1.111 → django_bulk_hooks-0.1.113}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.113
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
+
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
5
6
|
License: MIT
|
|
6
7
|
Keywords: django,bulk,hooks
|
|
7
8
|
Author: Konrad Beck
|
|
@@ -13,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Requires-Dist: Django (>=4.0)
|
|
16
|
-
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -48,7 +48,7 @@ from django_bulk_hooks.models import HookModelMixin
|
|
|
48
48
|
|
|
49
49
|
class Account(HookModelMixin):
|
|
50
50
|
balance = models.DecimalField(max_digits=10, decimal_places=2)
|
|
51
|
-
# The HookModelMixin automatically provides
|
|
51
|
+
# The HookModelMixin automatically provides BulkManager
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
### Create a Hook Handler
|
|
@@ -204,10 +204,10 @@ LoanAccount.objects.bulk_update(reordered, ['balance'])
|
|
|
204
204
|
|
|
205
205
|
## 🧩 Integration with Queryable Properties
|
|
206
206
|
|
|
207
|
-
You can extend from `
|
|
207
|
+
You can extend from `BulkManager` to support formula fields or property querying.
|
|
208
208
|
|
|
209
209
|
```python
|
|
210
|
-
class MyManager(
|
|
210
|
+
class MyManager(BulkManager, QueryablePropertiesManager):
|
|
211
211
|
pass
|
|
212
212
|
```
|
|
213
213
|
|
|
@@ -29,7 +29,7 @@ from django_bulk_hooks.models import HookModelMixin
|
|
|
29
29
|
|
|
30
30
|
class Account(HookModelMixin):
|
|
31
31
|
balance = models.DecimalField(max_digits=10, decimal_places=2)
|
|
32
|
-
# The HookModelMixin automatically provides
|
|
32
|
+
# The HookModelMixin automatically provides BulkManager
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
### Create a Hook Handler
|
|
@@ -185,10 +185,10 @@ LoanAccount.objects.bulk_update(reordered, ['balance'])
|
|
|
185
185
|
|
|
186
186
|
## 🧩 Integration with Queryable Properties
|
|
187
187
|
|
|
188
|
-
You can extend from `
|
|
188
|
+
You can extend from `BulkManager` to support formula fields or property querying.
|
|
189
189
|
|
|
190
190
|
```python
|
|
191
|
-
class MyManager(
|
|
191
|
+
class MyManager(BulkManager, QueryablePropertiesManager):
|
|
192
192
|
pass
|
|
193
193
|
```
|
|
194
194
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
|
+
from django.db.models import AutoField
|
|
2
3
|
|
|
3
4
|
from django_bulk_hooks import engine
|
|
4
5
|
from django_bulk_hooks.constants import (
|
|
@@ -39,7 +40,8 @@ class BulkHookManager(models.Manager):
|
|
|
39
40
|
if not bypass_hooks:
|
|
40
41
|
# Load originals for hook comparison and ensure they match the order of new instances
|
|
41
42
|
original_map = {
|
|
42
|
-
obj.pk: obj
|
|
43
|
+
obj.pk: obj
|
|
44
|
+
for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
|
|
43
45
|
}
|
|
44
46
|
originals = [original_map.get(obj.pk) for obj in objs]
|
|
45
47
|
|
|
@@ -109,34 +111,150 @@ class BulkHookManager(models.Manager):
|
|
|
109
111
|
|
|
110
112
|
@transaction.atomic
|
|
111
113
|
def bulk_create(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
114
|
+
"""
|
|
115
|
+
Enhanced bulk_create that handles multi-table inheritance (MTI) and single-table models.
|
|
116
|
+
Falls back to Django's standard bulk_create for single-table models.
|
|
117
|
+
Fires hooks as usual.
|
|
118
|
+
"""
|
|
112
119
|
model_cls = self.model
|
|
113
120
|
|
|
121
|
+
if not objs:
|
|
122
|
+
return []
|
|
123
|
+
|
|
114
124
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
115
125
|
raise TypeError(
|
|
116
126
|
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
117
127
|
)
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
# Fire hooks before DB ops
|
|
121
130
|
if not bypass_hooks:
|
|
122
131
|
ctx = HookContext(model_cls)
|
|
123
|
-
|
|
124
|
-
# Run validation hooks first
|
|
125
132
|
if not bypass_validation:
|
|
126
133
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
127
|
-
|
|
128
|
-
# Then run business logic hooks
|
|
129
134
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
# MTI detection: if inheritance chain > 1, use MTI logic
|
|
137
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
138
|
+
if len(inheritance_chain) <= 1:
|
|
139
|
+
# Single-table: use Django's standard bulk_create
|
|
140
|
+
result = []
|
|
141
|
+
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
142
|
+
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
143
|
+
result.extend(super(models.Manager, self).bulk_create(chunk, **kwargs))
|
|
144
|
+
else:
|
|
145
|
+
# Multi-table: use workaround (parent saves, child bulk)
|
|
146
|
+
result = self._mti_bulk_create(objs, inheritance_chain, **kwargs)
|
|
134
147
|
|
|
135
148
|
if not bypass_hooks:
|
|
136
149
|
engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
|
|
137
150
|
|
|
138
151
|
return result
|
|
139
152
|
|
|
153
|
+
def _get_inheritance_chain(self):
|
|
154
|
+
"""
|
|
155
|
+
Get the complete inheritance chain from root parent to current model.
|
|
156
|
+
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
157
|
+
"""
|
|
158
|
+
chain = []
|
|
159
|
+
current_model = self.model
|
|
160
|
+
while current_model:
|
|
161
|
+
if not current_model._meta.proxy:
|
|
162
|
+
chain.append(current_model)
|
|
163
|
+
parents = [
|
|
164
|
+
parent
|
|
165
|
+
for parent in current_model._meta.parents.keys()
|
|
166
|
+
if not parent._meta.proxy
|
|
167
|
+
]
|
|
168
|
+
current_model = parents[0] if parents else None
|
|
169
|
+
chain.reverse()
|
|
170
|
+
return chain
|
|
171
|
+
|
|
172
|
+
def _mti_bulk_create(self, objs, inheritance_chain, **kwargs):
|
|
173
|
+
"""
|
|
174
|
+
Implements workaround: individual saves for parents, bulk create for child.
|
|
175
|
+
"""
|
|
176
|
+
batch_size = kwargs.get("batch_size") or len(objs)
|
|
177
|
+
created_objects = []
|
|
178
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
179
|
+
for i in range(0, len(objs), batch_size):
|
|
180
|
+
batch = objs[i : i + batch_size]
|
|
181
|
+
batch_result = self._process_mti_batch(
|
|
182
|
+
batch, inheritance_chain, **kwargs
|
|
183
|
+
)
|
|
184
|
+
created_objects.extend(batch_result)
|
|
185
|
+
return created_objects
|
|
186
|
+
|
|
187
|
+
def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
|
|
188
|
+
"""
|
|
189
|
+
Process a single batch of objects through the inheritance chain.
|
|
190
|
+
"""
|
|
191
|
+
# Step 1: Handle parent tables with individual saves (needed for PKs)
|
|
192
|
+
parent_objects_map = {}
|
|
193
|
+
for obj in batch:
|
|
194
|
+
parent_instances = {}
|
|
195
|
+
current_parent = None
|
|
196
|
+
for model_class in inheritance_chain[:-1]:
|
|
197
|
+
parent_obj = self._create_parent_instance(
|
|
198
|
+
obj, model_class, current_parent
|
|
199
|
+
)
|
|
200
|
+
parent_obj.save()
|
|
201
|
+
parent_instances[model_class] = parent_obj
|
|
202
|
+
current_parent = parent_obj
|
|
203
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
204
|
+
# Step 2: Bulk insert for child objects
|
|
205
|
+
child_model = inheritance_chain[-1]
|
|
206
|
+
child_objects = []
|
|
207
|
+
for obj in batch:
|
|
208
|
+
child_obj = self._create_child_instance(
|
|
209
|
+
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
210
|
+
)
|
|
211
|
+
child_objects.append(child_obj)
|
|
212
|
+
# Use Django's _base_manager for child table to avoid recursion
|
|
213
|
+
child_manager = child_model._base_manager
|
|
214
|
+
child_manager._for_write = True
|
|
215
|
+
created = child_manager.bulk_create(child_objects, **kwargs)
|
|
216
|
+
# Step 3: Update original objects with generated PKs and state
|
|
217
|
+
pk_field_name = child_model._meta.pk.name
|
|
218
|
+
for orig_obj, child_obj in zip(batch, created):
|
|
219
|
+
setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
|
|
220
|
+
orig_obj._state.adding = False
|
|
221
|
+
orig_obj._state.db = self.db
|
|
222
|
+
return batch
|
|
223
|
+
|
|
224
|
+
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
225
|
+
parent_obj = parent_model()
|
|
226
|
+
for field in parent_model._meta.local_fields:
|
|
227
|
+
# Only copy if the field exists on the source and is not None
|
|
228
|
+
if hasattr(source_obj, field.name):
|
|
229
|
+
value = getattr(source_obj, field.name, None)
|
|
230
|
+
if value is not None:
|
|
231
|
+
setattr(parent_obj, field.name, value)
|
|
232
|
+
if current_parent is not None:
|
|
233
|
+
for field in parent_model._meta.local_fields:
|
|
234
|
+
if (
|
|
235
|
+
hasattr(field, "remote_field")
|
|
236
|
+
and field.remote_field
|
|
237
|
+
and field.remote_field.model == current_parent.__class__
|
|
238
|
+
):
|
|
239
|
+
setattr(parent_obj, field.name, current_parent)
|
|
240
|
+
break
|
|
241
|
+
return parent_obj
|
|
242
|
+
|
|
243
|
+
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
244
|
+
child_obj = child_model()
|
|
245
|
+
for field in child_model._meta.local_fields:
|
|
246
|
+
if isinstance(field, AutoField):
|
|
247
|
+
continue
|
|
248
|
+
if hasattr(source_obj, field.name):
|
|
249
|
+
value = getattr(source_obj, field.name, None)
|
|
250
|
+
if value is not None:
|
|
251
|
+
setattr(child_obj, field.name, value)
|
|
252
|
+
for parent_model, parent_instance in parent_instances.items():
|
|
253
|
+
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
254
|
+
if parent_link:
|
|
255
|
+
setattr(child_obj, parent_link.name, parent_instance)
|
|
256
|
+
return child_obj
|
|
257
|
+
|
|
140
258
|
@transaction.atomic
|
|
141
259
|
def bulk_delete(
|
|
142
260
|
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
@@ -162,7 +280,7 @@ class BulkHookManager(models.Manager):
|
|
|
162
280
|
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
163
281
|
|
|
164
282
|
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
165
|
-
|
|
283
|
+
|
|
166
284
|
# Use base manager for the actual deletion to prevent recursion
|
|
167
285
|
# The hooks have already been fired above, so we don't need them again
|
|
168
286
|
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
@@ -13,11 +13,11 @@ from django_bulk_hooks.constants import (
|
|
|
13
13
|
)
|
|
14
14
|
from django_bulk_hooks.context import HookContext
|
|
15
15
|
from django_bulk_hooks.engine import run
|
|
16
|
-
from django_bulk_hooks.manager import
|
|
16
|
+
from django_bulk_hooks.manager import BulkManager
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class HookModelMixin(models.Model):
|
|
20
|
-
objects =
|
|
20
|
+
objects = BulkManager()
|
|
21
21
|
|
|
22
22
|
class Meta:
|
|
23
23
|
abstract = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.113"
|
|
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"
|
|
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
|