django-bulk-hooks 0.1.117__py3-none-any.whl → 0.1.119__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,5 +1,4 @@
1
1
  from django.db import models, transaction
2
- from django.db.models import AutoField
3
2
 
4
3
  from django_bulk_hooks import engine
5
4
  from django_bulk_hooks.constants import (
@@ -85,249 +84,19 @@ class BulkHookManager(models.Manager):
85
84
  bypass_validation=False,
86
85
  ):
87
86
  """
88
- Insert each of the instances into the database. Behaves like Django's bulk_create,
89
- but supports multi-table inheritance (MTI) models. All arguments are supported and
90
- passed through to the correct logic. For MTI, only a subset of options may be supported.
87
+ Delegate to QuerySet's bulk_create implementation.
88
+ This follows Django's pattern where Manager methods call QuerySet methods.
91
89
  """
92
- model_cls = self.model
93
-
94
- if batch_size is not None and batch_size <= 0:
95
- raise ValueError("Batch size must be a positive integer.")
96
-
97
- # Check that the parents share the same concrete model with our model to detect inheritance pattern
98
- for parent in model_cls._meta.all_parents:
99
- if parent._meta.concrete_model is not model_cls._meta.concrete_model:
100
- # We allow this for MTI, but not for single-table
101
- break
102
-
103
- if not objs:
104
- return objs
105
-
106
- if any(not isinstance(obj, model_cls) for obj in objs):
107
- raise TypeError(
108
- f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
109
- )
110
-
111
- # Set auto_now_add/auto_now fields before DB ops
112
- self._set_auto_now_fields(objs, model_cls)
113
-
114
- # Fire hooks before DB ops
115
- if not bypass_hooks:
116
- ctx = HookContext(model_cls)
117
- if not bypass_validation:
118
- engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
119
- engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
120
-
121
- # MTI detection: if inheritance chain > 1, use MTI logic
122
- inheritance_chain = self._get_inheritance_chain()
123
- if len(inheritance_chain) <= 1:
124
- # Single-table: use Django's standard bulk_create
125
- # Pass through all supported arguments
126
- result = super(models.Manager, self).bulk_create(
127
- objs,
128
- batch_size=batch_size,
129
- ignore_conflicts=ignore_conflicts,
130
- update_conflicts=update_conflicts,
131
- update_fields=update_fields,
132
- unique_fields=unique_fields,
133
- )
134
- else:
135
- # Multi-table: use workaround (parent saves, child bulk)
136
- # Only batch_size is supported for MTI; others will raise NotImplementedError
137
- if ignore_conflicts or update_conflicts or update_fields or unique_fields:
138
- raise NotImplementedError(
139
- "bulk_create with ignore_conflicts, update_conflicts, update_fields, or unique_fields is not supported for multi-table inheritance models."
140
- )
141
- result = self._mti_bulk_create(
142
- objs, inheritance_chain, batch_size=batch_size
143
- )
144
-
145
- if not bypass_hooks:
146
- engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
147
-
148
- return result
149
-
150
- # --- Private helper methods (moved to bottom for clarity) ---
151
-
152
- def _detect_modified_fields(self, new_instances, original_instances):
153
- """
154
- Detect fields that were modified during BEFORE_UPDATE hooks by comparing
155
- new instances with their original values.
156
- """
157
- if not original_instances:
158
- return set()
159
-
160
- modified_fields = set()
161
-
162
- # Since original_instances is now ordered to match new_instances, we can zip them directly
163
- for new_instance, original in zip(new_instances, original_instances):
164
- if new_instance.pk is None or original is None:
165
- continue
166
-
167
- # Compare all fields to detect changes
168
- for field in new_instance._meta.fields:
169
- if field.name == "id":
170
- continue
171
-
172
- new_value = getattr(new_instance, field.name)
173
- original_value = getattr(original, field.name)
174
-
175
- # Handle different field types appropriately
176
- if field.is_relation:
177
- # For foreign keys, compare the pk values
178
- new_pk = new_value.pk if new_value else None
179
- original_pk = original_value.pk if original_value else None
180
- if new_pk != original_pk:
181
- modified_fields.add(field.name)
182
- else:
183
- # For regular fields, use direct comparison
184
- if new_value != original_value:
185
- modified_fields.add(field.name)
186
-
187
- return modified_fields
188
-
189
- def _get_inheritance_chain(self):
190
- """
191
- Get the complete inheritance chain from root parent to current model.
192
- Returns list of model classes in order: [RootParent, Parent, Child]
193
- """
194
- chain = []
195
- current_model = self.model
196
- while current_model:
197
- if not current_model._meta.proxy:
198
- chain.append(current_model)
199
- parents = [
200
- parent
201
- for parent in current_model._meta.parents.keys()
202
- if not parent._meta.proxy
203
- ]
204
- current_model = parents[0] if parents else None
205
- chain.reverse()
206
- return chain
207
-
208
- def _mti_bulk_create(self, objs, inheritance_chain, **kwargs):
209
- """
210
- Implements workaround: individual saves for parents, bulk create for child.
211
- Sets auto_now_add/auto_now fields for each model in the chain.
212
- """
213
- batch_size = kwargs.get("batch_size") or len(objs)
214
- created_objects = []
215
- with transaction.atomic(using=self.db, savepoint=False):
216
- for i in range(0, len(objs), batch_size):
217
- batch = objs[i : i + batch_size]
218
- # Set auto_now fields for each model in the chain
219
- for model in inheritance_chain:
220
- self._set_auto_now_fields(batch, model)
221
- batch_result = self._process_mti_batch(
222
- batch, inheritance_chain, **kwargs
223
- )
224
- created_objects.extend(batch_result)
225
- return created_objects
226
-
227
- def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
228
- """
229
- Process a single batch of objects through the inheritance chain.
230
- """
231
- # Step 1: Handle parent tables with individual saves (needed for PKs)
232
- parent_objects_map = {}
233
- for obj in batch:
234
- parent_instances = {}
235
- current_parent = None
236
- for model_class in inheritance_chain[:-1]:
237
- parent_obj = self._create_parent_instance(
238
- obj, model_class, current_parent
239
- )
240
- parent_obj.save()
241
- parent_instances[model_class] = parent_obj
242
- current_parent = parent_obj
243
- parent_objects_map[id(obj)] = parent_instances
244
- # Step 2: Bulk insert for child objects
245
- child_model = inheritance_chain[-1]
246
- child_objects = []
247
- for obj in batch:
248
- child_obj = self._create_child_instance(
249
- obj, child_model, parent_objects_map.get(id(obj), {})
250
- )
251
- child_objects.append(child_obj)
252
- # If the child model is still MTI, call our own logic recursively
253
- if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
254
- # Build inheritance chain for the child model
255
- inheritance_chain = []
256
- current_model = child_model
257
- while current_model:
258
- if not current_model._meta.proxy:
259
- inheritance_chain.append(current_model)
260
- parents = [
261
- parent
262
- for parent in current_model._meta.parents.keys()
263
- if not parent._meta.proxy
264
- ]
265
- current_model = parents[0] if parents else None
266
- inheritance_chain.reverse()
267
- created = self._mti_bulk_create(child_objects, inheritance_chain, **kwargs)
268
- else:
269
- # Single-table, safe to use bulk_create
270
- child_manager = child_model._base_manager
271
- child_manager._for_write = True
272
- created = child_manager.bulk_create(child_objects, **kwargs)
273
- # Step 3: Update original objects with generated PKs and state
274
- pk_field_name = child_model._meta.pk.name
275
- for orig_obj, child_obj in zip(batch, created):
276
- setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
277
- orig_obj._state.adding = False
278
- orig_obj._state.db = self.db
279
- return batch
280
-
281
- def _create_parent_instance(self, source_obj, parent_model, current_parent):
282
- parent_obj = parent_model()
283
- for field in parent_model._meta.local_fields:
284
- # Only copy if the field exists on the source and is not None
285
- if hasattr(source_obj, field.name):
286
- value = getattr(source_obj, field.name, None)
287
- if value is not None:
288
- setattr(parent_obj, field.name, value)
289
- if current_parent is not None:
290
- for field in parent_model._meta.local_fields:
291
- if (
292
- hasattr(field, "remote_field")
293
- and field.remote_field
294
- and field.remote_field.model == current_parent.__class__
295
- ):
296
- setattr(parent_obj, field.name, current_parent)
297
- break
298
- return parent_obj
299
-
300
- def _create_child_instance(self, source_obj, child_model, parent_instances):
301
- child_obj = child_model()
302
- for field in child_model._meta.local_fields:
303
- if isinstance(field, AutoField):
304
- continue
305
- if hasattr(source_obj, field.name):
306
- value = getattr(source_obj, field.name, None)
307
- if value is not None:
308
- setattr(child_obj, field.name, value)
309
- for parent_model, parent_instance in parent_instances.items():
310
- parent_link = child_model._meta.get_ancestor_link(parent_model)
311
- if parent_link:
312
- setattr(child_obj, parent_link.name, parent_instance)
313
- return child_obj
314
-
315
- def _set_auto_now_fields(self, objs, model):
316
- """
317
- Set auto_now_add and auto_now fields on objects before bulk_create.
318
- """
319
- from django.utils import timezone
320
-
321
- now = timezone.now()
322
- for obj in objs:
323
- for field in model._meta.local_fields:
324
- if (
325
- getattr(field, "auto_now_add", False)
326
- and getattr(obj, field.name, None) is None
327
- ):
328
- setattr(obj, field.name, now)
329
- if getattr(field, "auto_now", False):
330
- setattr(obj, field.name, now)
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
+ )
331
100
 
332
101
  @transaction.atomic
333
102
  def bulk_delete(
@@ -393,3 +162,40 @@ class BulkHookManager(models.Manager):
393
162
  else:
394
163
  self.bulk_create([obj])
395
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
@@ -1,7 +1,29 @@
1
- from django.db import models, transaction
1
+ from django.db import models, transaction, connections
2
+ from django.db.models import AutoField, Q, Max
3
+ from django.db import NotSupportedError
4
+ from django.db.models.constants import OnConflict
5
+ from django.db.models.expressions import DatabaseDefault
6
+ import operator
7
+ from functools import reduce
8
+
9
+ from django_bulk_hooks import engine
10
+ from django_bulk_hooks.constants import (
11
+ AFTER_CREATE,
12
+ AFTER_DELETE,
13
+ AFTER_UPDATE,
14
+ BEFORE_CREATE,
15
+ BEFORE_DELETE,
16
+ BEFORE_UPDATE,
17
+ VALIDATE_CREATE,
18
+ VALIDATE_DELETE,
19
+ VALIDATE_UPDATE,
20
+ )
21
+ from django_bulk_hooks.context import HookContext
2
22
 
3
23
 
4
24
  class HookQuerySet(models.QuerySet):
25
+ CHUNK_SIZE = 200
26
+
5
27
  @transaction.atomic
6
28
  def delete(self):
7
29
  objs = list(self)
@@ -28,17 +50,333 @@ class HookQuerySet(models.QuerySet):
28
50
  setattr(obj, field, value)
29
51
 
30
52
  # Run BEFORE_UPDATE hooks
31
- from django_bulk_hooks import engine
32
- from django_bulk_hooks.context import HookContext
33
-
34
53
  ctx = HookContext(model_cls)
35
- engine.run(model_cls, "before_update", instances, originals, ctx=ctx)
54
+ engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
36
55
 
37
56
  # Use Django's built-in update logic directly
38
57
  queryset = self.model.objects.filter(pk__in=pks)
39
58
  update_count = queryset.update(**kwargs)
40
59
 
41
60
  # Run AFTER_UPDATE hooks
42
- engine.run(model_cls, "after_update", instances, originals, ctx=ctx)
61
+ engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
43
62
 
44
63
  return update_count
64
+
65
+ @transaction.atomic
66
+ def bulk_create(
67
+ self,
68
+ objs,
69
+ batch_size=None,
70
+ ignore_conflicts=False,
71
+ update_conflicts=False,
72
+ update_fields=None,
73
+ unique_fields=None,
74
+ bypass_hooks=False,
75
+ bypass_validation=False,
76
+ ):
77
+ """
78
+ Insert each of the instances into the database. Behaves like Django's bulk_create,
79
+ but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
80
+ passed through to the correct logic. For MTI, only a subset of options may be supported.
81
+ """
82
+ model_cls = self.model
83
+
84
+ if batch_size is not None and batch_size <= 0:
85
+ raise ValueError("Batch size must be a positive integer.")
86
+
87
+ # Check for MTI - if we detect multi-table inheritance, we need special handling
88
+ is_mti = False
89
+ for parent in model_cls._meta.all_parents:
90
+ if parent._meta.concrete_model is not model_cls._meta.concrete_model:
91
+ is_mti = True
92
+ break
93
+
94
+ if not objs:
95
+ return objs
96
+
97
+ if any(not isinstance(obj, model_cls) for obj in objs):
98
+ raise TypeError(
99
+ f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
100
+ )
101
+
102
+ # Fire hooks before DB ops
103
+ if not bypass_hooks:
104
+ ctx = HookContext(model_cls)
105
+ if not bypass_validation:
106
+ engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
107
+ engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
108
+
109
+ # For MTI models, we need to handle them specially
110
+ if is_mti:
111
+ # Use our MTI-specific logic
112
+ result = self._mti_bulk_create(
113
+ objs,
114
+ batch_size=batch_size,
115
+ ignore_conflicts=ignore_conflicts,
116
+ update_conflicts=update_conflicts,
117
+ update_fields=update_fields,
118
+ unique_fields=unique_fields,
119
+ )
120
+ else:
121
+ # For single-table models, use Django's built-in bulk_create
122
+ # but we need to call it on the base manager to avoid recursion
123
+ result = model_cls._base_manager.bulk_create(
124
+ objs,
125
+ batch_size=batch_size,
126
+ ignore_conflicts=ignore_conflicts,
127
+ update_conflicts=update_conflicts,
128
+ update_fields=update_fields,
129
+ unique_fields=unique_fields,
130
+ )
131
+
132
+ if not bypass_hooks:
133
+ engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
134
+
135
+ return result
136
+
137
+ @transaction.atomic
138
+ def bulk_update(
139
+ self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
140
+ ):
141
+ if not objs:
142
+ return []
143
+
144
+ model_cls = self.model
145
+
146
+ if any(not isinstance(obj, model_cls) for obj in objs):
147
+ raise TypeError(
148
+ f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
149
+ )
150
+
151
+ if not bypass_hooks:
152
+ # Load originals for hook comparison and ensure they match the order of new instances
153
+ original_map = {
154
+ obj.pk: obj
155
+ for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
156
+ }
157
+ originals = [original_map.get(obj.pk) for obj in objs]
158
+
159
+ ctx = HookContext(model_cls)
160
+
161
+ # Run validation hooks first
162
+ if not bypass_validation:
163
+ engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
164
+
165
+ # Then run business logic hooks
166
+ engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
167
+
168
+ # Automatically detect fields that were modified during BEFORE_UPDATE hooks
169
+ modified_fields = self._detect_modified_fields(objs, originals)
170
+ if modified_fields:
171
+ # Convert to set for efficient union operation
172
+ fields_set = set(fields)
173
+ fields_set.update(modified_fields)
174
+ fields = list(fields_set)
175
+
176
+ for i in range(0, len(objs), self.CHUNK_SIZE):
177
+ chunk = objs[i : i + self.CHUNK_SIZE]
178
+ # Call the base implementation to avoid re-triggering this method
179
+ super().bulk_update(chunk, fields, **kwargs)
180
+
181
+ if not bypass_hooks:
182
+ engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
183
+
184
+ return objs
185
+
186
+ @transaction.atomic
187
+ def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
188
+ if not objs:
189
+ return []
190
+
191
+ model_cls = self.model
192
+
193
+ if any(not isinstance(obj, model_cls) for obj in objs):
194
+ raise TypeError(
195
+ f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
196
+ )
197
+
198
+ ctx = HookContext(model_cls)
199
+
200
+ if not bypass_hooks:
201
+ # Run validation hooks first
202
+ if not bypass_validation:
203
+ engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
204
+
205
+ # Then run business logic hooks
206
+ engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
207
+
208
+ pks = [obj.pk for obj in objs if obj.pk is not None]
209
+
210
+ # Use base manager for the actual deletion to prevent recursion
211
+ # The hooks have already been fired above, so we don't need them again
212
+ model_cls._base_manager.filter(pk__in=pks).delete()
213
+
214
+ if not bypass_hooks:
215
+ engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
216
+
217
+ return objs
218
+
219
+ # --- Private helper methods ---
220
+
221
+ def _detect_modified_fields(self, new_instances, original_instances):
222
+ """
223
+ Detect fields that were modified during BEFORE_UPDATE hooks by comparing
224
+ new instances with their original values.
225
+ """
226
+ if not original_instances:
227
+ return set()
228
+
229
+ modified_fields = set()
230
+
231
+ # Since original_instances is now ordered to match new_instances, we can zip them directly
232
+ for new_instance, original in zip(new_instances, original_instances):
233
+ if new_instance.pk is None or original is None:
234
+ continue
235
+
236
+ # Compare all fields to detect changes
237
+ for field in new_instance._meta.fields:
238
+ if field.name == "id":
239
+ continue
240
+
241
+ new_value = getattr(new_instance, field.name)
242
+ original_value = getattr(original, field.name)
243
+
244
+ # Handle different field types appropriately
245
+ if field.is_relation:
246
+ # For foreign keys, compare the pk values
247
+ new_pk = new_value.pk if new_value else None
248
+ original_pk = original_value.pk if original_value else None
249
+ if new_pk != original_pk:
250
+ modified_fields.add(field.name)
251
+ else:
252
+ # For regular fields, use direct comparison
253
+ if new_value != original_value:
254
+ modified_fields.add(field.name)
255
+
256
+ return modified_fields
257
+
258
+ def _get_inheritance_chain(self):
259
+ """
260
+ Get the complete inheritance chain from root parent to current model.
261
+ Returns list of model classes in order: [RootParent, Parent, Child]
262
+ """
263
+ chain = []
264
+ current_model = self.model
265
+ while current_model:
266
+ if not current_model._meta.proxy:
267
+ chain.append(current_model)
268
+ parents = [
269
+ parent
270
+ for parent in current_model._meta.parents.keys()
271
+ if not parent._meta.proxy
272
+ ]
273
+ current_model = parents[0] if parents else None
274
+ chain.reverse()
275
+ return chain
276
+
277
+ def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
278
+ """
279
+ Implements workaround: individual saves for parents, bulk create for child.
280
+ Sets auto_now_add/auto_now fields for each model in the chain.
281
+ """
282
+ if inheritance_chain is None:
283
+ inheritance_chain = self._get_inheritance_chain()
284
+
285
+ batch_size = kwargs.get("batch_size") or len(objs)
286
+ created_objects = []
287
+ with transaction.atomic(using=self.db, savepoint=False):
288
+ for i in range(0, len(objs), batch_size):
289
+ batch = objs[i : i + batch_size]
290
+ batch_result = self._process_mti_batch(
291
+ batch, inheritance_chain, **kwargs
292
+ )
293
+ created_objects.extend(batch_result)
294
+ return created_objects
295
+
296
+ def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
297
+ """
298
+ Process a single batch of objects through the inheritance chain.
299
+ """
300
+ # Step 1: Handle parent tables with individual saves (needed for PKs)
301
+ parent_objects_map = {}
302
+ for obj in batch:
303
+ parent_instances = {}
304
+ current_parent = None
305
+ for model_class in inheritance_chain[:-1]:
306
+ parent_obj = self._create_parent_instance(
307
+ obj, model_class, current_parent
308
+ )
309
+ parent_obj.save()
310
+ parent_instances[model_class] = parent_obj
311
+ current_parent = parent_obj
312
+ parent_objects_map[id(obj)] = parent_instances
313
+ # Step 2: Bulk insert for child objects
314
+ child_model = inheritance_chain[-1]
315
+ child_objects = []
316
+ for obj in batch:
317
+ child_obj = self._create_child_instance(
318
+ obj, child_model, parent_objects_map.get(id(obj), {})
319
+ )
320
+ child_objects.append(child_obj)
321
+ # If the child model is still MTI, call our own logic recursively
322
+ if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
323
+ # Build inheritance chain for the child model
324
+ inheritance_chain = []
325
+ current_model = child_model
326
+ while current_model:
327
+ if not current_model._meta.proxy:
328
+ inheritance_chain.append(current_model)
329
+ parents = [
330
+ parent
331
+ for parent in current_model._meta.parents.keys()
332
+ if not parent._meta.proxy
333
+ ]
334
+ current_model = parents[0] if parents else None
335
+ inheritance_chain.reverse()
336
+ created = self._mti_bulk_create(child_objects, inheritance_chain, **kwargs)
337
+ else:
338
+ # Single-table, safe to use bulk_create
339
+ child_manager = child_model._base_manager
340
+ child_manager._for_write = True
341
+ created = child_manager.bulk_create(child_objects, **kwargs)
342
+ # Step 3: Update original objects with generated PKs and state
343
+ pk_field_name = child_model._meta.pk.name
344
+ for orig_obj, child_obj in zip(batch, created):
345
+ setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
346
+ orig_obj._state.adding = False
347
+ orig_obj._state.db = self.db
348
+ return batch
349
+
350
+ def _create_parent_instance(self, source_obj, parent_model, current_parent):
351
+ parent_obj = parent_model()
352
+ for field in parent_model._meta.local_fields:
353
+ # Only copy if the field exists on the source and is not None
354
+ if hasattr(source_obj, field.name):
355
+ value = getattr(source_obj, field.name, None)
356
+ if value is not None:
357
+ setattr(parent_obj, field.name, value)
358
+ if current_parent is not None:
359
+ for field in parent_model._meta.local_fields:
360
+ if (
361
+ hasattr(field, "remote_field")
362
+ and field.remote_field
363
+ and field.remote_field.model == current_parent.__class__
364
+ ):
365
+ setattr(parent_obj, field.name, current_parent)
366
+ break
367
+ return parent_obj
368
+
369
+ def _create_child_instance(self, source_obj, child_model, parent_instances):
370
+ child_obj = child_model()
371
+ for field in child_model._meta.local_fields:
372
+ if isinstance(field, AutoField):
373
+ continue
374
+ if hasattr(source_obj, field.name):
375
+ value = getattr(source_obj, field.name, None)
376
+ if value is not None:
377
+ setattr(child_obj, field.name, value)
378
+ for parent_model, parent_instance in parent_instances.items():
379
+ parent_link = child_model._meta.get_ancestor_link(parent_model)
380
+ if parent_link:
381
+ setattr(child_obj, parent_link.name, parent_instance)
382
+ return child_obj
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.117
3
+ Version: 0.1.119
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
6
5
  License: MIT
7
6
  Keywords: django,bulk,hooks
8
7
  Author: Konrad Beck
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
16
15
  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
 
@@ -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=3WtqgKBqL8uWkOeGnw0xPp3AGpaxJSbT63ltgDxb2fU,15747
9
+ django_bulk_hooks/manager.py,sha256=DjEW-nZjhlBW6cp8GRPl6xOSsAmmquP0Y-QyCZMoSHo,6946
10
10
  django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=iet4z-9SKhnresA4FBQbxx9rdYnoaOWbw9LUlGftlP0,1466
12
+ django_bulk_hooks/queryset.py,sha256=8hNS49L049_h3GsNZ30UOdrCyW30I_T9ulq8e0UOFps,15089
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.117.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.117.dist-info/METADATA,sha256=b1mTw0rJZLbei3sm2bN480iVoU5KzXOs6D-_qyeVcrQ,6939
16
- django_bulk_hooks-0.1.117.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
- django_bulk_hooks-0.1.117.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.119.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.119.dist-info/METADATA,sha256=CAGAieM9Jj9ap5NxEVpjXmUbXfYxX2JMe_A-Iy9Fqm4,6951
16
+ django_bulk_hooks-0.1.119.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.119.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any