django-bulk-hooks 0.1.111__py3-none-any.whl → 0.1.113__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.

Potentially problematic release.


This version of django-bulk-hooks might be problematic. Click here for more details.

@@ -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 for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
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
- result = []
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
- for i in range(0, len(objs), self.CHUNK_SIZE):
132
- chunk = objs[i : i + self.CHUNK_SIZE]
133
- result.extend(super(models.Manager, self).bulk_create(chunk, **kwargs))
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 BulkHookManager
16
+ from django_bulk_hooks.manager import BulkManager
17
17
 
18
18
 
19
19
  class HookModelMixin(models.Model):
20
- objects = BulkHookManager()
20
+ objects = BulkManager()
21
21
 
22
22
  class Meta:
23
23
  abstract = True
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.111
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 BulkHookManager
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 `BulkHookManager` to support formula fields or property querying.
207
+ You can extend from `BulkManager` to support formula fields or property querying.
208
208
 
209
209
  ```python
210
- class MyManager(BulkHookManager, QueryablePropertiesManager):
210
+ class MyManager(BulkManager, QueryablePropertiesManager):
211
211
  pass
212
212
  ```
213
213
 
@@ -6,12 +6,12 @@ django_bulk_hooks/decorators.py,sha256=tckDcxtOzKCbgvS9QydgeIAWTFDEl-ch3_Q--ruEG
6
6
  django_bulk_hooks/engine.py,sha256=3HbgV12JRYIy9IlygHPxZiHnFXj7EwzLyTuJNQeVIoI,1402
7
7
  django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
8
8
  django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
9
- django_bulk_hooks/manager.py,sha256=JkhOw9XbEUt8VpunhUSP0z-NlazIFUWmAesj7H4ge8w,7148
10
- django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
9
+ django_bulk_hooks/manager.py,sha256=XJzWQmkJB-gkAn3YGDnOdkNG-cZlsw8FdNljlH4LMYo,12510
10
+ django_bulk_hooks/models.py,sha256=zb5DGqVezzQe3wCHdtwmwcqjRE8RBZ9Vy8nU5e6zrTw,3323
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
12
  django_bulk_hooks/queryset.py,sha256=iet4z-9SKhnresA4FBQbxx9rdYnoaOWbw9LUlGftlP0,1466
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.111.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.111.dist-info/METADATA,sha256=eljgMdpOu6xv8oWYbEMeP0GM7oXy4NrVDHDlrAfpHP0,6951
16
- django_bulk_hooks-0.1.111.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.111.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.113.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.113.dist-info/METADATA,sha256=_cTiMJbj7lRAS6aZOG_trK8yfYm0l5kF5eScc4_pim8,6927
16
+ django_bulk_hooks-0.1.113.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
+ django_bulk_hooks-0.1.113.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any