django-bulk-hooks 0.2.68__py3-none-any.whl → 0.2.70__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.

@@ -21,29 +21,19 @@ class BulkHookManager(models.Manager):
21
21
  """
22
22
  Manager that provides hook-aware bulk operations.
23
23
 
24
- This is a simple facade that returns HookQuerySet,
25
- delegating all bulk operations to it.
24
+ This manager automatically applies hook functionality to its querysets.
25
+ It can be used as a base class or composed with other managers using
26
+ the queryset-based approach.
26
27
  """
27
28
 
28
29
  def get_queryset(self):
29
30
  """
30
31
  Return a HookQuerySet for this manager.
31
32
 
32
- This ensures all bulk operations go through the coordinator.
33
+ Uses the new with_hooks() method for better composition with other managers.
33
34
  """
34
35
  base_queryset = super().get_queryset()
35
-
36
- # If the base queryset is already a HookQuerySet, return it as-is
37
- if isinstance(base_queryset, HookQuerySet):
38
- return base_queryset
39
-
40
- # Otherwise, create a new HookQuerySet with the same parameters
41
- return HookQuerySet(
42
- model=base_queryset.model,
43
- query=base_queryset.query,
44
- using=base_queryset._db,
45
- hints=base_queryset._hints,
46
- )
36
+ return HookQuerySet.with_hooks(base_queryset)
47
37
 
48
38
  def bulk_create(
49
39
  self,
@@ -938,6 +938,18 @@ class BulkOperationCoordinator:
938
938
  except Exception as e:
939
939
  logger.warning(f" ❌ Failed to extract @select_related from {handler_cls.__name__}.{method_name}: {e}")
940
940
 
941
+ # AGGRESSIVE APPROACH: Also preload ALL relationship fields on the model
942
+ # This prevents N+1 queries from any relationship access during hook execution
943
+ try:
944
+ for field in self.model_cls._meta.get_fields():
945
+ if field.is_relation and not field.many_to_many and not field.one_to_many:
946
+ # This is a forward foreign key relationship
947
+ field_name = field.name
948
+ logger.info(f" 🔗 AUTO: Adding all relationship fields including {field_name}")
949
+ relationships.add(field_name)
950
+ except Exception as e:
951
+ logger.warning(f" ❌ Failed to extract all relationship fields: {e}")
952
+
941
953
  logger.info(f"🔗 BULK PRELOAD: Total extracted relationships for {self.model_cls.__name__}: {list(relationships)}")
942
954
  return relationships
943
955
 
@@ -35,6 +35,57 @@ class HookQuerySet(models.QuerySet):
35
35
  super().__init__(*args, **kwargs)
36
36
  self._coordinator = None
37
37
 
38
+ @classmethod
39
+ def with_hooks(cls, queryset):
40
+ """
41
+ Apply hook functionality to any queryset.
42
+
43
+ This enables hooks to work with any manager by applying hook
44
+ capabilities at the queryset level rather than through inheritance.
45
+
46
+ Args:
47
+ queryset: Any Django QuerySet instance
48
+
49
+ Returns:
50
+ HookQuerySet instance with the same query parameters
51
+ """
52
+ if isinstance(queryset, cls):
53
+ return queryset # Already has hooks
54
+
55
+ # Create a new HookQuerySet with the same parameters as the original queryset
56
+ hook_qs = cls(
57
+ model=queryset.model,
58
+ query=queryset.query,
59
+ using=queryset._db,
60
+ hints=getattr(queryset, '_hints', {}),
61
+ )
62
+
63
+ # Preserve any additional attributes from the original queryset
64
+ # This allows composition with other queryset enhancements
65
+ cls._preserve_queryset_attributes(hook_qs, queryset)
66
+
67
+ return hook_qs
68
+
69
+ @classmethod
70
+ def _preserve_queryset_attributes(cls, hook_qs, original_qs):
71
+ """
72
+ Preserve attributes from the original queryset.
73
+
74
+ This enables composition with other queryset enhancements like
75
+ queryable properties, annotations, etc.
76
+ """
77
+ # Copy non-method attributes that might be set by other managers
78
+ for attr_name in dir(original_qs):
79
+ if (not attr_name.startswith('_') and
80
+ not hasattr(cls, attr_name) and
81
+ not callable(getattr(original_qs, attr_name, None))):
82
+ try:
83
+ value = getattr(original_qs, attr_name)
84
+ setattr(hook_qs, attr_name, value)
85
+ except (AttributeError, TypeError):
86
+ # Skip attributes that can't be copied
87
+ continue
88
+
38
89
  @property
39
90
  def coordinator(self):
40
91
  """Lazy initialization of coordinator"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.68
3
+ Version: 0.2.70
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
@@ -244,17 +244,68 @@ LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
244
244
 
245
245
  ## 🧩 Integration with Other Managers
246
246
 
247
- You can extend from `BulkHookManager` to work with other manager classes. The manager uses a cooperative approach that dynamically injects bulk hook functionality into any queryset, ensuring compatibility with other managers.
247
+ ### Recommended: QuerySet-based Composition (New Approach)
248
+
249
+ For the best compatibility and to avoid inheritance conflicts, use the queryset-based composition approach:
250
+
251
+ ```python
252
+ from django_bulk_hooks.queryset import HookQuerySet
253
+ from queryable_properties.managers import QueryablePropertiesManager
254
+
255
+ class MyManager(QueryablePropertiesManager):
256
+ """Manager that combines queryable properties with hooks"""
257
+
258
+ def get_queryset(self):
259
+ # Get the QueryableProperties QuerySet
260
+ qs = super().get_queryset()
261
+ # Apply hooks on top of it
262
+ return HookQuerySet.with_hooks(qs)
263
+
264
+ class Article(models.Model):
265
+ title = models.CharField(max_length=100)
266
+ published = models.BooleanField(default=False)
267
+
268
+ objects = MyManager()
269
+
270
+ # This gives you both queryable properties AND hooks
271
+ # No inheritance conflicts, no MRO issues!
272
+ ```
273
+
274
+ ### Alternative: Explicit Hook Application
275
+
276
+ For more control, you can apply hooks explicitly:
277
+
278
+ ```python
279
+ class MyManager(QueryablePropertiesManager):
280
+ def get_queryset(self):
281
+ return super().get_queryset()
282
+
283
+ def with_hooks(self):
284
+ """Apply hooks to this queryset"""
285
+ return HookQuerySet.with_hooks(self.get_queryset())
286
+
287
+ # Usage:
288
+ Article.objects.with_hooks().filter(published=True).update(title="Updated")
289
+ ```
290
+
291
+ ### Legacy: Manager Inheritance (Not Recommended)
292
+
293
+ The old inheritance approach still works but is not recommended due to potential MRO conflicts:
248
294
 
249
295
  ```python
250
296
  from django_bulk_hooks.manager import BulkHookManager
251
297
  from queryable_properties.managers import QueryablePropertiesManager
252
298
 
253
299
  class MyManager(BulkHookManager, QueryablePropertiesManager):
254
- pass
300
+ pass # ⚠️ Can cause inheritance conflicts
255
301
  ```
256
302
 
257
- This approach uses the industry-standard injection pattern, similar to how `QueryablePropertiesManager` works, ensuring both functionalities work seamlessly together without any framework-specific knowledge.
303
+ **Why the new approach is better:**
304
+ - ✅ No inheritance conflicts
305
+ - ✅ No MRO (Method Resolution Order) issues
306
+ - ✅ Works with any manager combination
307
+ - ✅ Cleaner and more maintainable
308
+ - ✅ Follows Django's queryset enhancement patterns
258
309
 
259
310
  Framework needs to:
260
311
  Register these methods
@@ -9,19 +9,19 @@ django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,38
9
9
  django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
10
10
  django_bulk_hooks/handler.py,sha256=SRCrMzgolrruTkvMnYBFmXLR-ABiw0JiH3605PEdCZM,4207
11
11
  django_bulk_hooks/helpers.py,sha256=3rH9TJkdCPF7Vu--0tDaZzJg9Yxcv7yoSF1K1_-0psQ,8048
12
- django_bulk_hooks/manager.py,sha256=aDuP87DZLWWbDK2qeA7usl3pxoIjHFIWnQNi_jEq6z0,4446
12
+ django_bulk_hooks/manager.py,sha256=k-uhKAzNqkxQzzH775v06g21L-LnAderBywWeJHJy38,4174
13
13
  django_bulk_hooks/models.py,sha256=TWN_F-SsLGPx9jrkNT9pmJFR5VsZ0Z_QaVOZOmt7bpw,2434
14
14
  django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
15
  django_bulk_hooks/operations/analyzer.py,sha256=Pz8mc-EL8KDOfLQFYiRuN-r0OmINW3nIBhRJJCma-yo,10360
16
16
  django_bulk_hooks/operations/bulk_executor.py,sha256=po8V_2H3ULiE0RYJ-wbaRIz52SKhss81UHwuQjlz3H8,26214
17
- django_bulk_hooks/operations/coordinator.py,sha256=GJKP60To6A7_E3Ge_F-cGSPTx2-BHMBBtE8GK6rO-Pc,37842
17
+ django_bulk_hooks/operations/coordinator.py,sha256=NHQWg36IvWAsbQnAppsExOhFQsvlk_VD-aMwWsoo1ao,38572
18
18
  django_bulk_hooks/operations/field_utils.py,sha256=cQ9w4xdk-z3PrMLFvRzVV07Wc0D2qbpSepwoupqwQH8,7888
19
19
  django_bulk_hooks/operations/mti_handler.py,sha256=Vmz0C0gtYDvbybmb4cDzIaGglSaQK4DQVkaBK-WuQeE,25855
20
20
  django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZPV2SzwQHLRzYc,3667
21
21
  django_bulk_hooks/operations/record_classifier.py,sha256=It85hJC2K-UsEOLbTR-QBdY5UPV-acQIJ91TSGa7pYo,7053
22
- django_bulk_hooks/queryset.py,sha256=g_9OtOTC8FXY0hBwYr2FCqQ3mYXbfJTFPLlFV3SHmWQ,5600
22
+ django_bulk_hooks/queryset.py,sha256=tHt1U2O9Hvozg9sdn5MjzAk_I6wDU-LupuKFTfv1SiQ,7449
23
23
  django_bulk_hooks/registry.py,sha256=4HxP1mVK2z4VzvlohbEw2359wM21UJZJYagJJ1komM0,7947
24
- django_bulk_hooks-0.2.68.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
- django_bulk_hooks-0.2.68.dist-info/METADATA,sha256=ny5caEpFwD2Dw2sJtJHVHBL-I1pTBTdU5YDNOysuNRM,9265
26
- django_bulk_hooks-0.2.68.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- django_bulk_hooks-0.2.68.dist-info/RECORD,,
24
+ django_bulk_hooks-0.2.70.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.70.dist-info/METADATA,sha256=cgeAfI0qSgIJJDl7WXpsqErM3YzqmuGnIeqPeSUpVOk,10555
26
+ django_bulk_hooks-0.2.70.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.70.dist-info/RECORD,,