django-bulk-hooks 0.1.205__py3-none-any.whl → 0.1.207__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,26 @@ 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
+ # Single bulk query to get all updated values
86
+ # Use values() to get only the fields we need for maximum performance
87
+ field_names = [f.name for f in model_cls._meta.fields if f.name != "id"]
88
+ refreshed_data = {
89
+ obj["pk"]: obj
90
+ for obj in model_cls._base_manager.filter(pk__in=pks).values(
91
+ "pk", *field_names
92
+ )
93
+ }
94
+
95
+ # Bulk update all instances in memory
96
+ for instance in instances:
97
+ if instance.pk in refreshed_data:
98
+ data = refreshed_data[instance.pk]
99
+ # Bulk copy all field values except primary key
100
+ for field_name in field_names:
101
+ setattr(instance, field_name, data[field_name])
102
+
78
103
  # Run AFTER_UPDATE hooks
79
104
  engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
80
105
 
@@ -303,14 +328,14 @@ class HookQuerySetMixin:
303
328
  while current_model:
304
329
  if not current_model._meta.proxy:
305
330
  chain.append(current_model)
306
-
331
+
307
332
  parents = [
308
333
  parent
309
334
  for parent in current_model._meta.parents.keys()
310
335
  if not parent._meta.proxy
311
336
  ]
312
337
  current_model = parents[0] if parents else None
313
-
338
+
314
339
  chain.reverse()
315
340
  return chain
316
341
 
@@ -584,10 +609,10 @@ class HookQuerySetMixin:
584
609
  for field in model._meta.local_fields:
585
610
  if hasattr(field, "auto_now") and field.auto_now:
586
611
  auto_now_fields.add(field.name)
587
-
612
+
588
613
  # Combine original fields with auto_now fields
589
614
  all_fields = list(fields) + list(auto_now_fields)
590
-
615
+
591
616
  # Group fields by model in the inheritance chain
592
617
  field_groups = {}
593
618
  for field_name in all_fields:
@@ -603,7 +628,7 @@ class HookQuerySetMixin:
603
628
  # Process in batches
604
629
  batch_size = django_kwargs.get("batch_size") or len(objs)
605
630
  total_updated = 0
606
-
631
+
607
632
  with transaction.atomic(using=self.db, savepoint=False):
608
633
  for i in range(0, len(objs), batch_size):
609
634
  batch = objs[i : i + batch_size]
@@ -614,35 +639,37 @@ class HookQuerySetMixin:
614
639
 
615
640
  return total_updated
616
641
 
617
- def _process_mti_bulk_update_batch(self, batch, field_groups, inheritance_chain, **kwargs):
642
+ def _process_mti_bulk_update_batch(
643
+ self, batch, field_groups, inheritance_chain, **kwargs
644
+ ):
618
645
  """
619
646
  Process a single batch of objects for MTI bulk update.
620
647
  Updates each table in the inheritance chain for the batch.
621
648
  """
622
649
  total_updated = 0
623
-
650
+
624
651
  # For MTI, we need to handle parent links correctly
625
652
  # The root model (first in chain) has its own PK
626
653
  # Child models use the parent link to reference the root PK
627
654
  root_model = inheritance_chain[0]
628
-
655
+
629
656
  # Get the primary keys from the objects
630
657
  # If objects have pk set but are not loaded from DB, use those PKs
631
658
  root_pks = []
632
659
  for obj in batch:
633
660
  # Check both pk and id attributes
634
- pk_value = getattr(obj, 'pk', None)
661
+ pk_value = getattr(obj, "pk", None)
635
662
  if pk_value is None:
636
- pk_value = getattr(obj, 'id', None)
637
-
663
+ pk_value = getattr(obj, "id", None)
664
+
638
665
  if pk_value is not None:
639
666
  root_pks.append(pk_value)
640
667
  else:
641
668
  continue
642
-
669
+
643
670
  if not root_pks:
644
671
  return 0
645
-
672
+
646
673
  # Update each table in the inheritance chain
647
674
  for model, model_fields in field_groups.items():
648
675
  if not model_fields:
@@ -651,7 +678,7 @@ class HookQuerySetMixin:
651
678
  if model == inheritance_chain[0]:
652
679
  # Root model - use primary keys directly
653
680
  pks = root_pks
654
- filter_field = 'pk'
681
+ filter_field = "pk"
655
682
  else:
656
683
  # Child model - use parent link field
657
684
  parent_link = None
@@ -659,48 +686,58 @@ class HookQuerySetMixin:
659
686
  if parent_model in model._meta.parents:
660
687
  parent_link = model._meta.parents[parent_model]
661
688
  break
662
-
689
+
663
690
  if parent_link is None:
664
691
  continue
665
-
692
+
666
693
  # For child models, the parent link values should be the same as root PKs
667
694
  pks = root_pks
668
695
  filter_field = parent_link.attname
669
-
696
+
670
697
  if pks:
671
698
  base_qs = model._base_manager.using(self.db)
672
-
699
+
673
700
  # Check if records exist
674
701
  existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
675
-
702
+
676
703
  if existing_count == 0:
677
704
  continue
678
-
705
+
679
706
  # Build CASE statements for each field to perform a single bulk update
680
707
  case_statements = {}
681
708
  for field_name in model_fields:
682
709
  field = model._meta.get_field(field_name)
683
710
  when_statements = []
684
-
711
+
685
712
  for pk, obj in zip(pks, batch):
686
713
  # Check both pk and id attributes for the object
687
- obj_pk = getattr(obj, 'pk', None)
714
+ obj_pk = getattr(obj, "pk", None)
688
715
  if obj_pk is None:
689
- obj_pk = getattr(obj, 'id', None)
690
-
716
+ obj_pk = getattr(obj, "id", None)
717
+
691
718
  if obj_pk is None:
692
719
  continue
693
720
  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
-
721
+ when_statements.append(
722
+ When(
723
+ **{filter_field: pk},
724
+ then=Value(value, output_field=field),
725
+ )
726
+ )
727
+
728
+ case_statements[field_name] = Case(
729
+ *when_statements, output_field=field
730
+ )
731
+
698
732
  # Execute a single bulk update for all objects in this model
699
733
  try:
700
- updated_count = base_qs.filter(**{f"{filter_field}__in": pks}).update(**case_statements)
734
+ updated_count = base_qs.filter(
735
+ **{f"{filter_field}__in": pks}
736
+ ).update(**case_statements)
701
737
  total_updated += updated_count
702
738
  except Exception as e:
703
739
  import traceback
740
+
704
741
  traceback.print_exc()
705
742
 
706
743
  return total_updated
@@ -711,4 +748,5 @@ class HookQuerySet(HookQuerySetMixin, models.QuerySet):
711
748
  A QuerySet that provides bulk hook functionality.
712
749
  This is the traditional approach for backward compatibility.
713
750
  """
751
+
714
752
  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.205
3
+ Version: 0.1.207
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=YMw8Za_1aqLkCmTqhYKnznq95IVlie3Wx9NBDND7KcM,31387
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.205.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.205.dist-info/METADATA,sha256=8CqllhoMI6B-20rLs6m2IfqUOhWhkSCHO7nsfwAzAeA,7418
16
- django_bulk_hooks-0.1.205.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.205.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.207.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.207.dist-info/METADATA,sha256=6XzEu4cdHtO1kVPzw-vuUmoFjbEUaVrTdEEKAFqEP60,9049
16
+ django_bulk_hooks-0.1.207.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
+ django_bulk_hooks-0.1.207.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