django-bulk-hooks 0.1.117__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.117 → django_bulk_hooks-0.1.118}/PKG-INFO +3 -3
  2. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/manager.py +84 -28
  3. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/README.md +0 -0
  6. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/models.py +0 -0
  15. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/priority.py +0 -0
  16. {django_bulk_hooks-0.1.117 → django_bulk_hooks-0.1.118}/django_bulk_hooks/queryset.py +0 -0
  17. {django_bulk_hooks-0.1.117 → 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.117
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,4 +1,4 @@
1
- from django.db import models, transaction
1
+ from django.db import models, transaction, connections
2
2
  from django.db.models import AutoField
3
3
 
4
4
  from django_bulk_hooks import engine
@@ -95,9 +95,10 @@ class BulkHookManager(models.Manager):
95
95
  raise ValueError("Batch size must be a positive integer.")
96
96
 
97
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)
98
99
  for parent in model_cls._meta.all_parents:
99
100
  if parent._meta.concrete_model is not model_cls._meta.concrete_model:
100
- # We allow this for MTI, but not for single-table
101
+ # Do not raise, just continue
101
102
  break
102
103
 
103
104
  if not objs:
@@ -118,34 +119,68 @@ class BulkHookManager(models.Manager):
118
119
  engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
119
120
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
120
121
 
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."
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,
140
150
  )
141
- result = self._mti_bulk_create(
142
- objs, inheritance_chain, batch_size=batch_size
143
- )
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
144
179
 
145
180
  if not bypass_hooks:
146
- engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
181
+ engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
147
182
 
148
- return result
183
+ return objs
149
184
 
150
185
  # --- Private helper methods (moved to bottom for clarity) ---
151
186
 
@@ -249,6 +284,8 @@ class BulkHookManager(models.Manager):
249
284
  obj, child_model, parent_objects_map.get(id(obj), {})
250
285
  )
251
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)
252
289
  # If the child model is still MTI, call our own logic recursively
253
290
  if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
254
291
  # Build inheritance chain for the child model
@@ -362,8 +399,6 @@ class BulkHookManager(models.Manager):
362
399
  if not bypass_hooks:
363
400
  engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
364
401
 
365
- return objs
366
-
367
402
  @transaction.atomic
368
403
  def update(self, **kwargs):
369
404
  objs = list(self.all())
@@ -393,3 +428,24 @@ class BulkHookManager(models.Manager):
393
428
  else:
394
429
  self.bulk_create([obj])
395
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.117"
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"