django-bulk-hooks 0.1.233__tar.gz → 0.1.234__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.233 → django_bulk_hooks-0.1.234}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/queryset.py +53 -37
  3. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/README.md +0 -0
  6. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.233
3
+ Version: 0.1.234
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  Home-page: https://github.com/AugendLimited/django-bulk-hooks
6
6
  License: MIT
@@ -1,11 +1,11 @@
1
1
  import logging
2
+ from typing import Any
2
3
 
3
4
  from django.db import models, transaction
4
5
  from django.db.models import AutoField, Case, Value, When
6
+ from django.db.models.functions import Cast
5
7
 
6
8
  from django_bulk_hooks import engine
7
-
8
- logger = logging.getLogger(__name__)
9
9
  from django_bulk_hooks.constants import (
10
10
  AFTER_CREATE,
11
11
  AFTER_DELETE,
@@ -19,6 +19,8 @@ from django_bulk_hooks.constants import (
19
19
  )
20
20
  from django_bulk_hooks.context import HookContext
21
21
 
22
+ logger = logging.getLogger(__name__)
23
+
22
24
 
23
25
  class HookQuerySetMixin:
24
26
  """
@@ -27,7 +29,7 @@ class HookQuerySetMixin:
27
29
  """
28
30
 
29
31
  @transaction.atomic
30
- def delete(self):
32
+ def delete(self) -> int:
31
33
  objs = list(self)
32
34
  if not objs:
33
35
  return 0
@@ -35,22 +37,18 @@ class HookQuerySetMixin:
35
37
  model_cls = self.model
36
38
  ctx = HookContext(model_cls)
37
39
 
38
- # Run validation hooks first
39
40
  engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
40
41
 
41
- # Then run business logic hooks
42
42
  engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
43
43
 
44
- # Use Django's standard delete() method
45
44
  result = super().delete()
46
45
 
47
- # Run AFTER_DELETE hooks
48
46
  engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
49
47
 
50
48
  return result
51
49
 
52
50
  @transaction.atomic
53
- def update(self, **kwargs):
51
+ def update(self, **kwargs) -> int:
54
52
  instances = list(self)
55
53
  if not instances:
56
54
  return 0
@@ -95,8 +93,6 @@ class HookQuerySetMixin:
95
93
  # Use original kwargs so Django can handle subqueries at database level
96
94
  update_count = super().update(**kwargs)
97
95
 
98
- # Since we resolved subqueries upfront, we don't need the post-refresh logic
99
-
100
96
  # Run AFTER_UPDATE hooks only for standalone updates
101
97
  if not current_bypass_hooks:
102
98
  logger.debug("update: running AFTER_UPDATE")
@@ -106,34 +102,57 @@ class HookQuerySetMixin:
106
102
 
107
103
  return update_count
108
104
 
109
- def _resolve_subquery_values(self, kwargs):
105
+ def _resolve_subquery_values(self, kwargs) -> dict[str, Any]:
110
106
  """
111
107
  Resolve Subquery objects to their actual values by evaluating them
112
108
  against the database. This ensures hooks receive resolved values
113
109
  instead of raw Subquery objects.
114
110
  """
115
111
  resolved_kwargs = {}
112
+
113
+ # Check if we have any data to evaluate against
114
+ has_data = self.model._default_manager.exists()
115
+
116
116
  for field, value in kwargs.items():
117
- if hasattr(value, "query") and hasattr(value, "resolve_expression"):
118
- # This is a subquery - we need to resolve it
117
+ # Handle Cast expressions
118
+ if isinstance(value, Cast):
119
+ if has_data:
120
+ # Try to resolve Cast expression
121
+ try:
122
+ temp_qs = self.model._default_manager.all()
123
+ temp_qs = temp_qs.annotate(_temp_field=value)
124
+ temp_qs = temp_qs.values("_temp_field")
125
+ result = temp_qs.first()
126
+ if result is not None:
127
+ resolved_kwargs[field] = result["_temp_field"]
128
+ else:
129
+ resolved_kwargs[field] = value
130
+ except Exception:
131
+ logger.warning(
132
+ f"Failed to resolve Cast expression for field {field}"
133
+ )
134
+ resolved_kwargs[field] = value
135
+ else:
136
+ # No data to evaluate against, use original
137
+ resolved_kwargs[field] = value
138
+
139
+ # Handle Subquery expressions
140
+ elif hasattr(value, "query") and hasattr(value, "resolve_expression"):
119
141
  try:
120
- # Create a temporary queryset to evaluate the subquery
121
142
  temp_qs = self.model._default_manager.all()
122
143
  temp_qs = temp_qs.annotate(_temp_field=value)
123
144
  temp_qs = temp_qs.values("_temp_field")
124
-
125
- # Get the resolved value (assuming single result for update context)
126
- resolved_value = temp_qs.first()["_temp_field"]
127
- resolved_kwargs[field] = resolved_value
145
+ result = temp_qs.first()
146
+ if result is not None:
147
+ resolved_kwargs[field] = result["_temp_field"]
148
+ else:
149
+ resolved_kwargs[field] = value
128
150
  except Exception:
129
- # If resolution fails, use the original subquery
130
- # Django's update() will handle it at the database level
131
- logger.warning(
132
- f"Failed to resolve subquery for field {field}, using original"
133
- )
151
+ logger.warning(f"Failed to resolve subquery for field {field}")
134
152
  resolved_kwargs[field] = value
153
+
154
+ # Handle regular values
135
155
  else:
136
- # Not a subquery, use as-is
137
156
  resolved_kwargs[field] = value
138
157
 
139
158
  return resolved_kwargs
@@ -149,7 +168,7 @@ class HookQuerySetMixin:
149
168
  unique_fields=None,
150
169
  bypass_hooks=False,
151
170
  bypass_validation=False,
152
- ):
171
+ ) -> list[Any]:
153
172
  """
154
173
  Insert each of the instances into the database. Behaves like Django's bulk_create,
155
174
  but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
@@ -240,7 +259,7 @@ class HookQuerySetMixin:
240
259
  @transaction.atomic
241
260
  def bulk_update(
242
261
  self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
243
- ):
262
+ ) -> int:
244
263
  """
245
264
  Bulk update objects in the database with MTI support.
246
265
  """
@@ -312,7 +331,7 @@ class HookQuerySetMixin:
312
331
 
313
332
  return result
314
333
 
315
- def _detect_modified_fields(self, new_instances, original_instances):
334
+ def _detect_modified_fields(self, new_instances, original_instances) -> set[str]:
316
335
  """
317
336
  Detect fields that were modified during BEFORE_UPDATE hooks by comparing
318
337
  new instances with their original values.
@@ -349,7 +368,7 @@ class HookQuerySetMixin:
349
368
 
350
369
  return modified_fields
351
370
 
352
- def _get_inheritance_chain(self):
371
+ def _get_inheritance_chain(self) -> list[type[models.Model]]:
353
372
  """
354
373
  Get the complete inheritance chain from root parent to current model.
355
374
  Returns list of model classes in order: [RootParent, Parent, Child]
@@ -370,7 +389,7 @@ class HookQuerySetMixin:
370
389
  chain.reverse()
371
390
  return chain
372
391
 
373
- def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
392
+ def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs) -> list[Any]:
374
393
  """
375
394
  Implements Django's suggested workaround #2 for MTI bulk_create:
376
395
  O(n) normal inserts into parent tables to get primary keys back,
@@ -403,7 +422,9 @@ class HookQuerySetMixin:
403
422
  created_objects.extend(batch_result)
404
423
  return created_objects
405
424
 
406
- def _process_mti_bulk_create_batch(self, batch, inheritance_chain, **kwargs):
425
+ def _process_mti_bulk_create_batch(
426
+ self, batch, inheritance_chain, **kwargs
427
+ ) -> list[Any]:
407
428
  """
408
429
  Process a single batch of objects through the inheritance chain.
409
430
  Implements Django's suggested workaround #2: O(n) normal inserts into parent
@@ -605,7 +626,7 @@ class HookQuerySetMixin:
605
626
 
606
627
  return child_obj
607
628
 
608
- def _mti_bulk_update(self, objs, fields, **kwargs):
629
+ def _mti_bulk_update(self, objs, fields, **kwargs) -> int:
609
630
  """
610
631
  Custom bulk update implementation for MTI models.
611
632
  Updates each table in the inheritance chain efficiently using Django's batch_size.
@@ -672,18 +693,13 @@ class HookQuerySetMixin:
672
693
 
673
694
  def _process_mti_bulk_update_batch(
674
695
  self, batch, field_groups, inheritance_chain, **kwargs
675
- ):
696
+ ) -> int:
676
697
  """
677
698
  Process a single batch of objects for MTI bulk update.
678
699
  Updates each table in the inheritance chain for the batch.
679
700
  """
680
701
  total_updated = 0
681
702
 
682
- # For MTI, we need to handle parent links correctly
683
- # The root model (first in chain) has its own PK
684
- # Child models use the parent link to reference the root PK
685
- root_model = inheritance_chain[0]
686
-
687
703
  # Get the primary keys from the objects
688
704
  # If objects have pk set but are not loaded from DB, use those PKs
689
705
  root_pks = []
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.233"
3
+ version = "0.1.234"
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"