django-bulk-hooks 0.1.206__py3-none-any.whl → 0.1.208__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,5 +1,5 @@
1
1
  from django.db import models, transaction
2
- from django.db.models import AutoField
2
+ from django.db.models import AutoField, Case, Field, Value, When
3
3
 
4
4
  from django_bulk_hooks import engine
5
5
  from django_bulk_hooks.constants import (
@@ -14,7 +14,6 @@ from django_bulk_hooks.constants import (
14
14
  VALIDATE_UPDATE,
15
15
  )
16
16
  from django_bulk_hooks.context import HookContext
17
- from django.db.models import When, Value, Case, Field
18
17
 
19
18
 
20
19
  class HookQuerySetMixin:
@@ -22,7 +21,7 @@ class HookQuerySetMixin:
22
21
  A mixin that provides bulk hook functionality to any QuerySet.
23
22
  This can be dynamically injected into querysets from other managers.
24
23
  """
25
-
24
+
26
25
  @transaction.atomic
27
26
  def delete(self):
28
27
  objs = list(self)
@@ -62,6 +61,12 @@ class HookQuerySetMixin:
62
61
  }
63
62
  originals = [original_map.get(obj.pk) for obj in instances]
64
63
 
64
+ # Check if any of the update values are Subquery objects
65
+ has_subquery = any(
66
+ hasattr(value, "query") and hasattr(value, "resolve_expression")
67
+ for value in kwargs.values()
68
+ )
69
+
65
70
  # Apply field updates to instances
66
71
  for obj in instances:
67
72
  for field, value in kwargs.items():
@@ -75,6 +80,27 @@ class HookQuerySetMixin:
75
80
  # Call the base QuerySet implementation to avoid recursion
76
81
  update_count = super().update(**kwargs)
77
82
 
83
+ # If we used Subquery objects, refresh the instances to get computed values
84
+ if has_subquery and instances:
85
+ # Simple refresh of model fields without fetching related objects
86
+ # Subquery updates only affect the model's own fields, not relationships
87
+ refreshed_instances = {
88
+ obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
89
+ }
90
+
91
+ # Bulk update all instances in memory
92
+ for instance in instances:
93
+ if instance.pk in refreshed_instances:
94
+ refreshed_instance = refreshed_instances[instance.pk]
95
+ # Update all fields except primary key
96
+ for field in model_cls._meta.fields:
97
+ if field.name != "id":
98
+ setattr(
99
+ instance,
100
+ field.name,
101
+ getattr(refreshed_instance, field.name),
102
+ )
103
+
78
104
  # Run AFTER_UPDATE hooks
79
105
  engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
80
106
 
@@ -303,14 +329,14 @@ class HookQuerySetMixin:
303
329
  while current_model:
304
330
  if not current_model._meta.proxy:
305
331
  chain.append(current_model)
306
-
332
+
307
333
  parents = [
308
334
  parent
309
335
  for parent in current_model._meta.parents.keys()
310
336
  if not parent._meta.proxy
311
337
  ]
312
338
  current_model = parents[0] if parents else None
313
-
339
+
314
340
  chain.reverse()
315
341
  return chain
316
342
 
@@ -584,10 +610,10 @@ class HookQuerySetMixin:
584
610
  for field in model._meta.local_fields:
585
611
  if hasattr(field, "auto_now") and field.auto_now:
586
612
  auto_now_fields.add(field.name)
587
-
613
+
588
614
  # Combine original fields with auto_now fields
589
615
  all_fields = list(fields) + list(auto_now_fields)
590
-
616
+
591
617
  # Group fields by model in the inheritance chain
592
618
  field_groups = {}
593
619
  for field_name in all_fields:
@@ -603,7 +629,7 @@ class HookQuerySetMixin:
603
629
  # Process in batches
604
630
  batch_size = django_kwargs.get("batch_size") or len(objs)
605
631
  total_updated = 0
606
-
632
+
607
633
  with transaction.atomic(using=self.db, savepoint=False):
608
634
  for i in range(0, len(objs), batch_size):
609
635
  batch = objs[i : i + batch_size]
@@ -614,35 +640,37 @@ class HookQuerySetMixin:
614
640
 
615
641
  return total_updated
616
642
 
617
- def _process_mti_bulk_update_batch(self, batch, field_groups, inheritance_chain, **kwargs):
643
+ def _process_mti_bulk_update_batch(
644
+ self, batch, field_groups, inheritance_chain, **kwargs
645
+ ):
618
646
  """
619
647
  Process a single batch of objects for MTI bulk update.
620
648
  Updates each table in the inheritance chain for the batch.
621
649
  """
622
650
  total_updated = 0
623
-
651
+
624
652
  # For MTI, we need to handle parent links correctly
625
653
  # The root model (first in chain) has its own PK
626
654
  # Child models use the parent link to reference the root PK
627
655
  root_model = inheritance_chain[0]
628
-
656
+
629
657
  # Get the primary keys from the objects
630
658
  # If objects have pk set but are not loaded from DB, use those PKs
631
659
  root_pks = []
632
660
  for obj in batch:
633
661
  # Check both pk and id attributes
634
- pk_value = getattr(obj, 'pk', None)
662
+ pk_value = getattr(obj, "pk", None)
635
663
  if pk_value is None:
636
- pk_value = getattr(obj, 'id', None)
637
-
664
+ pk_value = getattr(obj, "id", None)
665
+
638
666
  if pk_value is not None:
639
667
  root_pks.append(pk_value)
640
668
  else:
641
669
  continue
642
-
670
+
643
671
  if not root_pks:
644
672
  return 0
645
-
673
+
646
674
  # Update each table in the inheritance chain
647
675
  for model, model_fields in field_groups.items():
648
676
  if not model_fields:
@@ -651,7 +679,7 @@ class HookQuerySetMixin:
651
679
  if model == inheritance_chain[0]:
652
680
  # Root model - use primary keys directly
653
681
  pks = root_pks
654
- filter_field = 'pk'
682
+ filter_field = "pk"
655
683
  else:
656
684
  # Child model - use parent link field
657
685
  parent_link = None
@@ -659,48 +687,58 @@ class HookQuerySetMixin:
659
687
  if parent_model in model._meta.parents:
660
688
  parent_link = model._meta.parents[parent_model]
661
689
  break
662
-
690
+
663
691
  if parent_link is None:
664
692
  continue
665
-
693
+
666
694
  # For child models, the parent link values should be the same as root PKs
667
695
  pks = root_pks
668
696
  filter_field = parent_link.attname
669
-
697
+
670
698
  if pks:
671
699
  base_qs = model._base_manager.using(self.db)
672
-
700
+
673
701
  # Check if records exist
674
702
  existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
675
-
703
+
676
704
  if existing_count == 0:
677
705
  continue
678
-
706
+
679
707
  # Build CASE statements for each field to perform a single bulk update
680
708
  case_statements = {}
681
709
  for field_name in model_fields:
682
710
  field = model._meta.get_field(field_name)
683
711
  when_statements = []
684
-
712
+
685
713
  for pk, obj in zip(pks, batch):
686
714
  # Check both pk and id attributes for the object
687
- obj_pk = getattr(obj, 'pk', None)
715
+ obj_pk = getattr(obj, "pk", None)
688
716
  if obj_pk is None:
689
- obj_pk = getattr(obj, 'id', None)
690
-
717
+ obj_pk = getattr(obj, "id", None)
718
+
691
719
  if obj_pk is None:
692
720
  continue
693
721
  value = getattr(obj, field_name)
694
- when_statements.append(When(**{filter_field: pk}, then=Value(value, output_field=field)))
695
-
696
- case_statements[field_name] = Case(*when_statements, output_field=field)
697
-
722
+ when_statements.append(
723
+ When(
724
+ **{filter_field: pk},
725
+ then=Value(value, output_field=field),
726
+ )
727
+ )
728
+
729
+ case_statements[field_name] = Case(
730
+ *when_statements, output_field=field
731
+ )
732
+
698
733
  # Execute a single bulk update for all objects in this model
699
734
  try:
700
- updated_count = base_qs.filter(**{f"{filter_field}__in": pks}).update(**case_statements)
735
+ updated_count = base_qs.filter(
736
+ **{f"{filter_field}__in": pks}
737
+ ).update(**case_statements)
701
738
  total_updated += updated_count
702
739
  except Exception as e:
703
740
  import traceback
741
+
704
742
  traceback.print_exc()
705
743
 
706
744
  return total_updated
@@ -711,4 +749,5 @@ class HookQuerySet(HookQuerySetMixin, models.QuerySet):
711
749
  A QuerySet that provides bulk hook functionality.
712
750
  This is the traditional approach for backward compatibility.
713
751
  """
752
+
714
753
  pass
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.206
3
+ Version: 0.1.208
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
5
6
  License: MIT
6
7
  Keywords: django,bulk,hooks
7
8
  Author: Konrad Beck
@@ -13,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
15
16
  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
 
@@ -132,6 +132,46 @@ Account.objects.update(balance=0.00)
132
132
  Account.objects.delete()
133
133
  ```
134
134
 
135
+ ### Subquery Support in Updates
136
+
137
+ When using `Subquery` objects in update operations, the computed values are automatically available in hooks. The system efficiently refreshes all instances in bulk for optimal performance:
138
+
139
+ ```python
140
+ from django.db.models import Subquery, OuterRef, Sum
141
+
142
+ def aggregate_revenue_by_ids(self, ids: Iterable[int]) -> int:
143
+ return self.find_by_ids(ids).update(
144
+ revenue=Subquery(
145
+ FinancialTransaction.objects.filter(daily_financial_aggregate_id=OuterRef("pk"))
146
+ .filter(is_revenue=True)
147
+ .values("daily_financial_aggregate_id")
148
+ .annotate(revenue_sum=Sum("amount"))
149
+ .values("revenue_sum")[:1],
150
+ ),
151
+ )
152
+
153
+ # In your hooks, you can now access the computed revenue value:
154
+ class FinancialAggregateHooks(Hook):
155
+ @hook(AFTER_UPDATE, model=DailyFinancialAggregate)
156
+ def log_revenue_update(self, new_records, old_records):
157
+ for new_record in new_records:
158
+ # This will now contain the computed value, not the Subquery object
159
+ print(f"Updated revenue: {new_record.revenue}")
160
+
161
+ # Bulk operations are optimized for performance:
162
+ def bulk_aggregate_revenue(self, ids: Iterable[int]) -> int:
163
+ # This will efficiently refresh all instances in a single query
164
+ return self.filter(id__in=ids).update(
165
+ revenue=Subquery(
166
+ FinancialTransaction.objects.filter(daily_financial_aggregate_id=OuterRef("pk"))
167
+ .filter(is_revenue=True)
168
+ .values("daily_financial_aggregate_id")
169
+ .annotate(revenue_sum=Sum("amount"))
170
+ .values("revenue_sum")[:1],
171
+ ),
172
+ )
173
+ ```
174
+
135
175
  ## 🧠 Why?
136
176
 
137
177
  Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,
9
9
  django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
10
10
  django_bulk_hooks/models.py,sha256=7fnx5xd4HWXfLVlFhhiRzR92JRWFEuxgk6aSWLEsyJg,3996
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=e4rh5rwxIMp4Q1Z49XF5JSVGctulxyG7lkk0OAPxP40,30216
12
+ django_bulk_hooks/queryset.py,sha256=G55Bd0f4Rb_S1Z06lyXvZ4zEYc18R0uor17kfHjdrog,31469
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.206.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.206.dist-info/METADATA,sha256=m77nQzuAEgQLe9V27X-QmGlf8lmF5hxncCzkMhjq_pc,7418
16
- django_bulk_hooks-0.1.206.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.206.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.208.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.208.dist-info/METADATA,sha256=NnIm2Ew0GRwBt3iecx71-RxSZ_x9KJCgFnZklSwPTsU,9049
16
+ django_bulk_hooks-0.1.208.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
+ django_bulk_hooks-0.1.208.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any