django-bulk-hooks 0.1.116__tar.gz → 0.1.118__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.

Files changed (17) hide show
  1. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/PKG-INFO +3 -3
  2. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/manager.py +90 -42
  3. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/README.md +0 -0
  6. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/models.py +0 -0
  15. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/priority.py +0 -0
  16. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/queryset.py +0 -0
  17. {django_bulk_hooks-0.1.116 → django_bulk_hooks-0.1.118}/django_bulk_hooks/registry.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.116
3
+ Version: 0.1.118
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
 
@@ -1,6 +1,4 @@
1
-
2
-
3
- from django.db import models, transaction
1
+ from django.db import models, transaction, connections
4
2
  from django.db.models import AutoField
5
3
 
6
4
  from django_bulk_hooks import engine
@@ -22,12 +20,9 @@ from django_bulk_hooks.queryset import HookQuerySet
22
20
  class BulkHookManager(models.Manager):
23
21
  CHUNK_SIZE = 200
24
22
 
25
-
26
23
  def get_queryset(self):
27
24
  return HookQuerySet(self.model, using=self._db)
28
25
 
29
-
30
-
31
26
  @transaction.atomic
32
27
  def bulk_update(
33
28
  self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
@@ -77,8 +72,6 @@ class BulkHookManager(models.Manager):
77
72
 
78
73
  return objs
79
74
 
80
-
81
-
82
75
  @transaction.atomic
83
76
  def bulk_create(
84
77
  self,
@@ -102,9 +95,10 @@ class BulkHookManager(models.Manager):
102
95
  raise ValueError("Batch size must be a positive integer.")
103
96
 
104
97
  # Check that the parents share the same concrete model with our model to detect inheritance pattern
98
+ # (Do NOT raise for MTI, just skip the exception)
105
99
  for parent in model_cls._meta.all_parents:
106
100
  if parent._meta.concrete_model is not model_cls._meta.concrete_model:
107
- # We allow this for MTI, but not for single-table
101
+ # Do not raise, just continue
108
102
  break
109
103
 
110
104
  if not objs:
@@ -125,39 +119,68 @@ class BulkHookManager(models.Manager):
125
119
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
126
120
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
127
121
 
128
- # MTI detection: if inheritance chain > 1, use MTI logic
129
- inheritance_chain = self._get_inheritance_chain()
130
- if len(inheritance_chain) <= 1:
131
- # Single-table: use Django's standard bulk_create
132
- # Pass through all supported arguments
133
- result = super(models.Manager, self).bulk_create(
134
- objs,
135
- batch_size=batch_size,
136
- ignore_conflicts=ignore_conflicts,
137
- update_conflicts=update_conflicts,
138
- update_fields=update_fields,
139
- unique_fields=unique_fields,
140
- )
141
- else:
142
- # Multi-table: use workaround (parent saves, child bulk)
143
- # Only batch_size is supported for MTI; others will raise NotImplementedError
144
- if ignore_conflicts or update_conflicts or update_fields or unique_fields:
145
- raise NotImplementedError(
146
- "bulk_create with ignore_conflicts, update_conflicts, update_fields, or unique_fields is not supported for multi-table inheritance models."
122
+ opts = model_cls._meta
123
+ if unique_fields:
124
+ unique_fields = [
125
+ model_cls._meta.get_field(opts.pk.name if name == "pk" else name)
126
+ for name in unique_fields
127
+ ]
128
+ if update_fields:
129
+ update_fields = [model_cls._meta.get_field(name) for name in update_fields]
130
+ on_conflict = self._check_bulk_create_options(
131
+ ignore_conflicts,
132
+ update_conflicts,
133
+ update_fields,
134
+ unique_fields,
135
+ )
136
+ self._for_write = True
137
+ fields = [f for f in opts.concrete_fields if not f.generated]
138
+ objs = list(objs)
139
+ objs_with_pk, objs_without_pk = self._prepare_for_bulk_create(objs)
140
+ with transaction.atomic(using=self.db, savepoint=False):
141
+ self._handle_order_with_respect_to(objs)
142
+ if objs_with_pk:
143
+ returned_columns = self._batched_insert(
144
+ objs_with_pk,
145
+ fields,
146
+ batch_size,
147
+ on_conflict=on_conflict,
148
+ update_fields=update_fields,
149
+ unique_fields=unique_fields,
147
150
  )
148
- result = self._mti_bulk_create(
149
- objs, inheritance_chain, batch_size=batch_size
150
- )
151
+ for obj_with_pk, results in zip(objs_with_pk, returned_columns):
152
+ for result, field in zip(results, opts.db_returning_fields):
153
+ if field != opts.pk:
154
+ setattr(obj_with_pk, field.attname, result)
155
+ for obj_with_pk in objs_with_pk:
156
+ obj_with_pk._state.adding = False
157
+ obj_with_pk._state.db = self.db
158
+ if objs_without_pk:
159
+ fields_wo_pk = [f for f in fields if not isinstance(f, AutoField)]
160
+ returned_columns = self._batched_insert(
161
+ objs_without_pk,
162
+ fields_wo_pk,
163
+ batch_size,
164
+ on_conflict=on_conflict,
165
+ update_fields=update_fields,
166
+ unique_fields=unique_fields,
167
+ )
168
+ connection = connections[self.db]
169
+ if (
170
+ connection.features.can_return_rows_from_bulk_insert
171
+ and on_conflict is None
172
+ ):
173
+ assert len(returned_columns) == len(objs_without_pk)
174
+ for obj_without_pk, results in zip(objs_without_pk, returned_columns):
175
+ for result, field in zip(results, opts.db_returning_fields):
176
+ setattr(obj_without_pk, field.attname, result)
177
+ obj_without_pk._state.adding = False
178
+ obj_without_pk._state.db = self.db
151
179
 
152
180
  if not bypass_hooks:
153
- engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
154
-
155
- return result
156
-
157
-
158
-
159
-
181
+ engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
160
182
 
183
+ return objs
161
184
 
162
185
  # --- Private helper methods (moved to bottom for clarity) ---
163
186
 
@@ -261,6 +284,8 @@ class BulkHookManager(models.Manager):
261
284
  obj, child_model, parent_objects_map.get(id(obj), {})
262
285
  )
263
286
  child_objects.append(child_obj)
287
+ # Handle order_with_respect_to like Django's bulk_create
288
+ self._handle_order_with_respect_to(child_objects)
264
289
  # If the child model is still MTI, call our own logic recursively
265
290
  if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
266
291
  # Build inheritance chain for the child model
@@ -329,12 +354,16 @@ class BulkHookManager(models.Manager):
329
354
  Set auto_now_add and auto_now fields on objects before bulk_create.
330
355
  """
331
356
  from django.utils import timezone
357
+
332
358
  now = timezone.now()
333
359
  for obj in objs:
334
360
  for field in model._meta.local_fields:
335
- if getattr(field, 'auto_now_add', False) and getattr(obj, field.name, None) is None:
361
+ if (
362
+ getattr(field, "auto_now_add", False)
363
+ and getattr(obj, field.name, None) is None
364
+ ):
336
365
  setattr(obj, field.name, now)
337
- if getattr(field, 'auto_now', False):
366
+ if getattr(field, "auto_now", False):
338
367
  setattr(obj, field.name, now)
339
368
 
340
369
  @transaction.atomic
@@ -370,8 +399,6 @@ class BulkHookManager(models.Manager):
370
399
  if not bypass_hooks:
371
400
  engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
372
401
 
373
- return objs
374
-
375
402
  @transaction.atomic
376
403
  def update(self, **kwargs):
377
404
  objs = list(self.all())
@@ -401,3 +428,24 @@ class BulkHookManager(models.Manager):
401
428
  else:
402
429
  self.bulk_create([obj])
403
430
  return obj
431
+
432
+ def _handle_order_with_respect_to(self, objs):
433
+ """
434
+ Set _order fields for models with order_with_respect_to.
435
+ """
436
+ for obj in objs:
437
+ order_with_respect_to = obj.__class__._meta.order_with_respect_to
438
+ if order_with_respect_to:
439
+ key = getattr(obj, order_with_respect_to.attname)
440
+ obj._order = key
441
+ # Group by the value of order_with_respect_to
442
+ groups = defaultdict(list)
443
+ for obj in objs:
444
+ order_with_respect_to = obj.__class__._meta.order_with_respect_to
445
+ if order_with_respect_to:
446
+ key = getattr(obj, order_with_respect_to.attname)
447
+ groups[key].append(obj)
448
+ # Enumerate within each group
449
+ for group_objs in groups.values():
450
+ for i, obj in enumerate(group_objs):
451
+ obj._order = i
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.116"
3
+ version = "0.1.118"
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"