django-bulk-hooks 0.1.103__tar.gz → 0.1.105__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.103 → django_bulk_hooks-0.1.105}/PKG-INFO +3 -3
  2. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/manager.py +150 -1
  3. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/README.md +0 -0
  6. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/models.py +0 -0
  15. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/priority.py +0 -0
  16. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/django_bulk_hooks/queryset.py +0 -0
  17. {django_bulk_hooks-0.1.103 → django_bulk_hooks-0.1.105}/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.103
3
+ Version: 0.1.105
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
 
@@ -22,6 +22,143 @@ class BulkHookManager(models.Manager):
22
22
  def get_queryset(self):
23
23
  return HookQuerySet(self.model, using=self._db)
24
24
 
25
+ def _has_multi_table_inheritance(self, model_cls):
26
+ """
27
+ Check if this model uses multi-table inheritance.
28
+ """
29
+ return (
30
+ model_cls._meta.parents and
31
+ not all(parent._meta.abstract for parent in model_cls._meta.parents.values())
32
+ )
33
+
34
+ def _get_base_model(self, model_cls):
35
+ """
36
+ Get the base model (first non-abstract parent or self).
37
+ """
38
+ base_model = model_cls
39
+ while base_model._meta.parents:
40
+ parent = next(iter(base_model._meta.parents.values()))
41
+ if not parent._meta.abstract:
42
+ base_model = parent
43
+ else:
44
+ break
45
+ return base_model
46
+
47
+ def _extract_base_objects(self, objs, model_cls):
48
+ """
49
+ Extract base model objects from inherited objects.
50
+ """
51
+ base_model = self._get_base_model(model_cls)
52
+ base_objects = []
53
+
54
+ for obj in objs:
55
+ base_obj = base_model()
56
+ for field in base_model._meta.fields:
57
+ # Skip ID field
58
+ if field.name == 'id':
59
+ continue
60
+
61
+ # Safely copy field values
62
+ try:
63
+ if hasattr(obj, field.name):
64
+ setattr(base_obj, field.name, getattr(obj, field.name))
65
+ except (AttributeError, ValueError):
66
+ # Skip fields that can't be copied
67
+ continue
68
+
69
+ base_objects.append(base_obj)
70
+
71
+ return base_objects
72
+
73
+ def _extract_child_objects(self, objs, model_cls):
74
+ """
75
+ Extract child model objects from inherited objects.
76
+ """
77
+ child_objects = []
78
+
79
+ for obj in objs:
80
+ child_obj = model_cls()
81
+ child_obj.pk = obj.pk # Set the same PK as base
82
+
83
+ # Copy only fields specific to this model
84
+ for field in model_cls._meta.fields:
85
+ # Skip ID field and fields that don't belong to this model
86
+ if field.name == 'id':
87
+ continue
88
+
89
+ # Check if this field belongs to the current model
90
+ # Use a safer way to check field ownership
91
+ try:
92
+ if hasattr(field, 'model') and field.model == model_cls:
93
+ # This field belongs to the current model
94
+ if hasattr(obj, field.name):
95
+ setattr(child_obj, field.name, getattr(obj, field.name))
96
+ except AttributeError:
97
+ # Skip fields that don't have proper model reference
98
+ continue
99
+
100
+ child_objects.append(child_obj)
101
+
102
+ return child_objects
103
+
104
+ def _bulk_create_inherited(self, objs, **kwargs):
105
+ """
106
+ Handle bulk create for inherited models by handling each table separately.
107
+ """
108
+ if not objs:
109
+ return []
110
+
111
+ model_cls = self.model
112
+ result = []
113
+
114
+ # Group objects by their actual class
115
+ objects_by_class = {}
116
+ for obj in objs:
117
+ obj_class = obj.__class__
118
+ if obj_class not in objects_by_class:
119
+ objects_by_class[obj_class] = []
120
+ objects_by_class[obj_class].append(obj)
121
+
122
+ for obj_class, class_objects in objects_by_class.items():
123
+ try:
124
+ # Check if this class has multi-table inheritance
125
+ parent_models = [p for p in obj_class._meta.get_parent_list()
126
+ if not p._meta.abstract]
127
+
128
+ if not parent_models:
129
+ # No inheritance, use standard bulk_create
130
+ chunk_result = super(models.Manager, self).bulk_create(class_objects, **kwargs)
131
+ result.extend(chunk_result)
132
+ continue
133
+
134
+ # Handle multi-table inheritance
135
+ # Step 1: Bulk create base objects without hooks
136
+ base_objects = self._extract_base_objects(class_objects, obj_class)
137
+ created_base = super(models.Manager, self).bulk_create(base_objects, **kwargs)
138
+
139
+ # Step 2: Update original objects with base IDs
140
+ for obj, base_obj in zip(class_objects, created_base):
141
+ obj.pk = base_obj.pk
142
+ obj._state.adding = False
143
+
144
+ # Step 3: Bulk create child objects without hooks
145
+ child_objects = self._extract_child_objects(class_objects, obj_class)
146
+ if child_objects:
147
+ # Use _base_manager to avoid recursion
148
+ obj_class._base_manager.bulk_create(child_objects, **kwargs)
149
+
150
+ result.extend(class_objects)
151
+
152
+ except Exception as e:
153
+ # Add debugging information
154
+ import logging
155
+ logger = logging.getLogger(__name__)
156
+ logger.error(f"Error in _bulk_create_inherited for {obj_class}: {e}")
157
+ logger.error(f"Model fields: {[f.name for f in obj_class._meta.fields]}")
158
+ raise
159
+
160
+ return result
161
+
25
162
  @transaction.atomic
26
163
  def bulk_update(
27
164
  self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
@@ -116,6 +253,9 @@ class BulkHookManager(models.Manager):
116
253
  f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
117
254
  )
118
255
 
256
+ # Check if this model uses multi-table inheritance
257
+ has_multi_table_inheritance = self._has_multi_table_inheritance(model_cls)
258
+
119
259
  result = []
120
260
 
121
261
  if not bypass_hooks:
@@ -128,9 +268,18 @@ class BulkHookManager(models.Manager):
128
268
  # Then run business logic hooks
129
269
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
130
270
 
271
+ # Perform bulk create in chunks
131
272
  for i in range(0, len(objs), self.CHUNK_SIZE):
132
273
  chunk = objs[i : i + self.CHUNK_SIZE]
133
- result.extend(super(models.Manager, self).bulk_create(chunk, **kwargs))
274
+
275
+ if has_multi_table_inheritance:
276
+ # Use our multi-table bulk create
277
+ created_chunk = self._bulk_create_inherited(chunk, **kwargs)
278
+ else:
279
+ # Use Django's standard bulk create
280
+ created_chunk = super(models.Manager, self).bulk_create(chunk, **kwargs)
281
+
282
+ result.extend(created_chunk)
134
283
 
135
284
  if not bypass_hooks:
136
285
  engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.103"
3
+ version = "0.1.105"
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"