django-bulk-hooks 0.1.121__py3-none-any.whl → 0.1.123__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,77 +1,23 @@
1
- from django.db import models, transaction
1
+ from django.db import models
2
2
 
3
- from django_bulk_hooks import engine
4
- from django_bulk_hooks.constants import (
5
- AFTER_CREATE,
6
- AFTER_DELETE,
7
- AFTER_UPDATE,
8
- BEFORE_CREATE,
9
- BEFORE_DELETE,
10
- BEFORE_UPDATE,
11
- VALIDATE_CREATE,
12
- VALIDATE_DELETE,
13
- VALIDATE_UPDATE,
14
- )
15
- from django_bulk_hooks.context import HookContext
16
3
  from django_bulk_hooks.queryset import HookQuerySet
17
4
 
18
5
 
19
6
  class BulkHookManager(models.Manager):
20
- CHUNK_SIZE = 200
21
-
22
7
  def get_queryset(self):
23
8
  return HookQuerySet(self.model, using=self._db)
24
9
 
25
- @transaction.atomic
26
10
  def bulk_update(
27
11
  self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
28
12
  ):
29
- if not objs:
30
- return []
31
-
32
- model_cls = self.model
33
-
34
- if any(not isinstance(obj, model_cls) for obj in objs):
35
- raise TypeError(
36
- f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
37
- )
38
-
39
- if not bypass_hooks:
40
- # Load originals for hook comparison and ensure they match the order of new instances
41
- original_map = {
42
- obj.pk: obj
43
- for obj in model_cls.objects.filter(pk__in=[obj.pk for obj in objs])
44
- }
45
- originals = [original_map.get(obj.pk) for obj in objs]
46
-
47
- ctx = HookContext(model_cls)
48
-
49
- # Run validation hooks first
50
- if not bypass_validation:
51
- engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
52
-
53
- # Then run business logic hooks
54
- engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
55
-
56
- # Automatically detect fields that were modified during BEFORE_UPDATE hooks
57
- modified_fields = self._detect_modified_fields(objs, originals)
58
- if modified_fields:
59
- # Convert to set for efficient union operation
60
- fields_set = set(fields)
61
- fields_set.update(modified_fields)
62
- fields = list(fields_set)
63
-
64
- for i in range(0, len(objs), self.CHUNK_SIZE):
65
- chunk = objs[i : i + self.CHUNK_SIZE]
66
- # Call the base implementation to avoid re-triggering this method
67
- super(models.Manager, self).bulk_update(chunk, fields, **kwargs)
68
-
69
- if not bypass_hooks:
70
- engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
71
-
72
- return objs
13
+ """
14
+ Delegate to QuerySet's bulk_update implementation.
15
+ This follows Django's pattern where Manager methods call QuerySet methods.
16
+ """
17
+ return self.get_queryset().bulk_update(
18
+ objs, fields, bypass_hooks=bypass_hooks, bypass_validation=bypass_validation, **kwargs
19
+ )
73
20
 
74
- @transaction.atomic
75
21
  def bulk_create(
76
22
  self,
77
23
  objs,
@@ -98,62 +44,35 @@ class BulkHookManager(models.Manager):
98
44
  bypass_validation=bypass_validation,
99
45
  )
100
46
 
101
- @transaction.atomic
102
47
  def bulk_delete(
103
48
  self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
104
49
  ):
105
- if not objs:
106
- return []
107
-
108
- model_cls = self.model
109
-
110
- if any(not isinstance(obj, model_cls) for obj in objs):
111
- raise TypeError(
112
- f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
113
- )
114
-
115
- ctx = HookContext(model_cls)
116
-
117
- if not bypass_hooks:
118
- # Run validation hooks first
119
- if not bypass_validation:
120
- engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
121
-
122
- # Then run business logic hooks
123
- engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
124
-
125
- pks = [obj.pk for obj in objs if obj.pk is not None]
126
-
127
- # Use base manager for the actual deletion to prevent recursion
128
- # The hooks have already been fired above, so we don't need them again
129
- model_cls._base_manager.filter(pk__in=pks).delete()
130
-
131
- if not bypass_hooks:
132
- engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
133
-
134
- return objs
50
+ """
51
+ Delegate to QuerySet's bulk_delete implementation.
52
+ This follows Django's pattern where Manager methods call QuerySet methods.
53
+ """
54
+ return self.get_queryset().bulk_delete(
55
+ objs, batch_size=batch_size, bypass_hooks=bypass_hooks, bypass_validation=bypass_validation
56
+ )
135
57
 
136
- @transaction.atomic
137
58
  def update(self, **kwargs):
138
- objs = list(self.all())
139
- if not objs:
140
- return 0
141
- for key, value in kwargs.items():
142
- for obj in objs:
143
- setattr(obj, key, value)
144
- self.bulk_update(objs, fields=list(kwargs.keys()))
145
- return len(objs)
59
+ """
60
+ Delegate to QuerySet's update implementation.
61
+ This follows Django's pattern where Manager methods call QuerySet methods.
62
+ """
63
+ return self.get_queryset().update(**kwargs)
146
64
 
147
- @transaction.atomic
148
65
  def delete(self):
149
- objs = list(self.all())
150
- if not objs:
151
- return 0
152
- self.bulk_delete(objs)
153
- return len(objs)
66
+ """
67
+ Delegate to QuerySet's delete implementation.
68
+ This follows Django's pattern where Manager methods call QuerySet methods.
69
+ """
70
+ return self.get_queryset().delete()
154
71
 
155
- @transaction.atomic
156
72
  def save(self, obj):
73
+ """
74
+ Save a single object using the appropriate bulk operation.
75
+ """
157
76
  if obj.pk:
158
77
  self.bulk_update(
159
78
  [obj],
@@ -162,40 +81,3 @@ class BulkHookManager(models.Manager):
162
81
  else:
163
82
  self.bulk_create([obj])
164
83
  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
@@ -284,6 +284,10 @@ class HookQuerySet(models.QuerySet):
284
284
  if inheritance_chain is None:
285
285
  inheritance_chain = self._get_inheritance_chain()
286
286
 
287
+ # Safety check to prevent infinite recursion
288
+ if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
289
+ raise ValueError("Inheritance chain too deep - possible infinite recursion detected")
290
+
287
291
  batch_size = kwargs.get("batch_size") or len(objs)
288
292
  created_objects = []
289
293
  with transaction.atomic(using=self.db, savepoint=False):
@@ -323,19 +327,28 @@ class HookQuerySet(models.QuerySet):
323
327
  # If the child model is still MTI, call our own logic recursively
324
328
  if len([p for p in child_model._meta.parents.keys() if not p._meta.proxy]) > 0:
325
329
  # Build inheritance chain for the child model
326
- inheritance_chain = []
330
+ child_inheritance_chain = []
327
331
  current_model = child_model
328
332
  while current_model:
329
333
  if not current_model._meta.proxy:
330
- inheritance_chain.append(current_model)
334
+ child_inheritance_chain.append(current_model)
331
335
  parents = [
332
336
  parent
333
337
  for parent in current_model._meta.parents.keys()
334
338
  if not parent._meta.proxy
335
339
  ]
336
340
  current_model = parents[0] if parents else None
337
- inheritance_chain.reverse()
338
- created = self._mti_bulk_create(child_objects, inheritance_chain, **kwargs)
341
+ child_inheritance_chain.reverse()
342
+
343
+ # For nested MTI, we can't use bulk operations recursively
344
+ # because it would create infinite recursion. Instead, we save each child individually.
345
+ # We use super().save() to avoid triggering hooks that would cause recursion.
346
+ created = []
347
+ for child_obj in child_objects:
348
+ # Use the base model's save method to avoid triggering hooks
349
+ # This prevents infinite recursion when hooks try to query the database
350
+ super(child_obj.__class__, child_obj).save()
351
+ created.append(child_obj)
339
352
  else:
340
353
  # Single-table, safe to use bulk_create
341
354
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.121
3
+ Version: 0.1.123
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -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=DjEW-nZjhlBW6cp8GRPl6xOSsAmmquP0Y-QyCZMoSHo,6946
9
+ django_bulk_hooks/manager.py,sha256=r54ct3S6AcqME2OsX-jPF944CEKcoSIW3qiAx_NwUaw,2801
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=GPjyhzko8CsZoqj85oqzXjbRMSUaIn7xi6_OdJfyjm4,15914
12
+ django_bulk_hooks/queryset.py,sha256=19uG70BxNAJwLqu3j3ZmT4DStX8sPRwbV9079m0yn84,16717
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.121.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.121.dist-info/METADATA,sha256=4_kx2vZEonPYANDAA0PDupUr4r9-QHz63224XPu6t0o,6951
16
- django_bulk_hooks-0.1.121.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.121.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.123.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.123.dist-info/METADATA,sha256=1irToq1BSI9i4dP80sqHhRDq3BhwGVcEK0B-Xf69xXo,6951
16
+ django_bulk_hooks-0.1.123.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.123.dist-info/RECORD,,