django-bulk-hooks 0.1.232__py3-none-any.whl → 0.1.234__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.
- django_bulk_hooks/queryset.py +88 -55
- {django_bulk_hooks-0.1.232.dist-info → django_bulk_hooks-0.1.234.dist-info}/METADATA +3 -3
- {django_bulk_hooks-0.1.232.dist-info → django_bulk_hooks-0.1.234.dist-info}/RECORD +5 -5
- {django_bulk_hooks-0.1.232.dist-info → django_bulk_hooks-0.1.234.dist-info}/WHEEL +1 -1
- {django_bulk_hooks-0.1.232.dist-info → django_bulk_hooks-0.1.234.dist-info}/LICENSE +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
2
4
|
from django.db import models, transaction
|
|
3
|
-
from django.db.models import AutoField, Case,
|
|
5
|
+
from django.db.models import AutoField, Case, Value, When
|
|
6
|
+
from django.db.models.functions import Cast
|
|
4
7
|
|
|
5
8
|
from django_bulk_hooks import engine
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
9
|
from django_bulk_hooks.constants import (
|
|
9
10
|
AFTER_CREATE,
|
|
10
11
|
AFTER_DELETE,
|
|
@@ -18,6 +19,8 @@ from django_bulk_hooks.constants import (
|
|
|
18
19
|
)
|
|
19
20
|
from django_bulk_hooks.context import HookContext
|
|
20
21
|
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
class HookQuerySetMixin:
|
|
23
26
|
"""
|
|
@@ -26,7 +29,7 @@ class HookQuerySetMixin:
|
|
|
26
29
|
"""
|
|
27
30
|
|
|
28
31
|
@transaction.atomic
|
|
29
|
-
def delete(self):
|
|
32
|
+
def delete(self) -> int:
|
|
30
33
|
objs = list(self)
|
|
31
34
|
if not objs:
|
|
32
35
|
return 0
|
|
@@ -34,22 +37,18 @@ class HookQuerySetMixin:
|
|
|
34
37
|
model_cls = self.model
|
|
35
38
|
ctx = HookContext(model_cls)
|
|
36
39
|
|
|
37
|
-
# Run validation hooks first
|
|
38
40
|
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
39
41
|
|
|
40
|
-
# Then run business logic hooks
|
|
41
42
|
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
42
43
|
|
|
43
|
-
# Use Django's standard delete() method
|
|
44
44
|
result = super().delete()
|
|
45
45
|
|
|
46
|
-
# Run AFTER_DELETE hooks
|
|
47
46
|
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
48
47
|
|
|
49
48
|
return result
|
|
50
49
|
|
|
51
50
|
@transaction.atomic
|
|
52
|
-
def update(self, **kwargs):
|
|
51
|
+
def update(self, **kwargs) -> int:
|
|
53
52
|
instances = list(self)
|
|
54
53
|
if not instances:
|
|
55
54
|
return 0
|
|
@@ -64,21 +63,19 @@ class HookQuerySetMixin:
|
|
|
64
63
|
}
|
|
65
64
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
66
65
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
hasattr(value, "query") and hasattr(value, "resolve_expression")
|
|
70
|
-
for value in kwargs.values()
|
|
71
|
-
)
|
|
66
|
+
# Resolve subqueries to actual values before applying to instances
|
|
67
|
+
resolved_kwargs = self._resolve_subquery_values(kwargs)
|
|
72
68
|
|
|
73
|
-
# Apply field updates to instances
|
|
69
|
+
# Apply resolved field updates to instances
|
|
74
70
|
for obj in instances:
|
|
75
|
-
for field, value in
|
|
71
|
+
for field, value in resolved_kwargs.items():
|
|
76
72
|
setattr(obj, field, value)
|
|
77
73
|
|
|
78
74
|
# Check if we're in a bulk operation context to prevent double hook execution
|
|
79
75
|
from django_bulk_hooks.context import get_bypass_hooks
|
|
76
|
+
|
|
80
77
|
current_bypass_hooks = get_bypass_hooks()
|
|
81
|
-
|
|
78
|
+
|
|
82
79
|
# If we're in a bulk operation context, skip hooks to prevent double execution
|
|
83
80
|
if current_bypass_hooks:
|
|
84
81
|
logger.debug("update: skipping hooks (bulk context)")
|
|
@@ -93,29 +90,9 @@ class HookQuerySetMixin:
|
|
|
93
90
|
|
|
94
91
|
# Use Django's built-in update logic directly
|
|
95
92
|
# Call the base QuerySet implementation to avoid recursion
|
|
93
|
+
# Use original kwargs so Django can handle subqueries at database level
|
|
96
94
|
update_count = super().update(**kwargs)
|
|
97
95
|
|
|
98
|
-
# If we used Subquery objects, refresh the instances to get computed values
|
|
99
|
-
if has_subquery and instances:
|
|
100
|
-
# Simple refresh of model fields without fetching related objects
|
|
101
|
-
# Subquery updates only affect the model's own fields, not relationships
|
|
102
|
-
refreshed_instances = {
|
|
103
|
-
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
# Bulk update all instances in memory
|
|
107
|
-
for instance in instances:
|
|
108
|
-
if instance.pk in refreshed_instances:
|
|
109
|
-
refreshed_instance = refreshed_instances[instance.pk]
|
|
110
|
-
# Update all fields except primary key
|
|
111
|
-
for field in model_cls._meta.fields:
|
|
112
|
-
if field.name != "id":
|
|
113
|
-
setattr(
|
|
114
|
-
instance,
|
|
115
|
-
field.name,
|
|
116
|
-
getattr(refreshed_instance, field.name),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
96
|
# Run AFTER_UPDATE hooks only for standalone updates
|
|
120
97
|
if not current_bypass_hooks:
|
|
121
98
|
logger.debug("update: running AFTER_UPDATE")
|
|
@@ -125,6 +102,61 @@ class HookQuerySetMixin:
|
|
|
125
102
|
|
|
126
103
|
return update_count
|
|
127
104
|
|
|
105
|
+
def _resolve_subquery_values(self, kwargs) -> dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Resolve Subquery objects to their actual values by evaluating them
|
|
108
|
+
against the database. This ensures hooks receive resolved values
|
|
109
|
+
instead of raw Subquery objects.
|
|
110
|
+
"""
|
|
111
|
+
resolved_kwargs = {}
|
|
112
|
+
|
|
113
|
+
# Check if we have any data to evaluate against
|
|
114
|
+
has_data = self.model._default_manager.exists()
|
|
115
|
+
|
|
116
|
+
for field, value in kwargs.items():
|
|
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"):
|
|
141
|
+
try:
|
|
142
|
+
temp_qs = self.model._default_manager.all()
|
|
143
|
+
temp_qs = temp_qs.annotate(_temp_field=value)
|
|
144
|
+
temp_qs = temp_qs.values("_temp_field")
|
|
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
|
|
150
|
+
except Exception:
|
|
151
|
+
logger.warning(f"Failed to resolve subquery for field {field}")
|
|
152
|
+
resolved_kwargs[field] = value
|
|
153
|
+
|
|
154
|
+
# Handle regular values
|
|
155
|
+
else:
|
|
156
|
+
resolved_kwargs[field] = value
|
|
157
|
+
|
|
158
|
+
return resolved_kwargs
|
|
159
|
+
|
|
128
160
|
@transaction.atomic
|
|
129
161
|
def bulk_create(
|
|
130
162
|
self,
|
|
@@ -136,7 +168,7 @@ class HookQuerySetMixin:
|
|
|
136
168
|
unique_fields=None,
|
|
137
169
|
bypass_hooks=False,
|
|
138
170
|
bypass_validation=False,
|
|
139
|
-
):
|
|
171
|
+
) -> list[Any]:
|
|
140
172
|
"""
|
|
141
173
|
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
142
174
|
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
@@ -180,12 +212,12 @@ class HookQuerySetMixin:
|
|
|
180
212
|
|
|
181
213
|
# Fire hooks before DB ops
|
|
182
214
|
if not bypass_hooks:
|
|
183
|
-
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
215
|
+
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
184
216
|
if not bypass_validation:
|
|
185
217
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
186
218
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
187
219
|
else:
|
|
188
|
-
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
220
|
+
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
189
221
|
logger.debug("bulk_create bypassed hooks")
|
|
190
222
|
|
|
191
223
|
# For MTI models, we need to handle them specially
|
|
@@ -227,7 +259,7 @@ class HookQuerySetMixin:
|
|
|
227
259
|
@transaction.atomic
|
|
228
260
|
def bulk_update(
|
|
229
261
|
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
230
|
-
):
|
|
262
|
+
) -> int:
|
|
231
263
|
"""
|
|
232
264
|
Bulk update objects in the database with MTI support.
|
|
233
265
|
"""
|
|
@@ -241,7 +273,9 @@ class HookQuerySetMixin:
|
|
|
241
273
|
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
242
274
|
)
|
|
243
275
|
|
|
244
|
-
logger.debug(
|
|
276
|
+
logger.debug(
|
|
277
|
+
f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
|
|
278
|
+
)
|
|
245
279
|
|
|
246
280
|
# Check for MTI
|
|
247
281
|
is_mti = False
|
|
@@ -257,7 +291,9 @@ class HookQuerySetMixin:
|
|
|
257
291
|
else:
|
|
258
292
|
logger.debug("bulk_update: hooks bypassed")
|
|
259
293
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
260
|
-
originals = [None] * len(
|
|
294
|
+
originals = [None] * len(
|
|
295
|
+
objs
|
|
296
|
+
) # Ensure originals is defined for after_update call
|
|
261
297
|
|
|
262
298
|
# Handle auto_now fields like Django's update_or_create does
|
|
263
299
|
fields_set = set(fields)
|
|
@@ -295,7 +331,7 @@ class HookQuerySetMixin:
|
|
|
295
331
|
|
|
296
332
|
return result
|
|
297
333
|
|
|
298
|
-
def _detect_modified_fields(self, new_instances, original_instances):
|
|
334
|
+
def _detect_modified_fields(self, new_instances, original_instances) -> set[str]:
|
|
299
335
|
"""
|
|
300
336
|
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
301
337
|
new instances with their original values.
|
|
@@ -332,7 +368,7 @@ class HookQuerySetMixin:
|
|
|
332
368
|
|
|
333
369
|
return modified_fields
|
|
334
370
|
|
|
335
|
-
def _get_inheritance_chain(self):
|
|
371
|
+
def _get_inheritance_chain(self) -> list[type[models.Model]]:
|
|
336
372
|
"""
|
|
337
373
|
Get the complete inheritance chain from root parent to current model.
|
|
338
374
|
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
@@ -353,7 +389,7 @@ class HookQuerySetMixin:
|
|
|
353
389
|
chain.reverse()
|
|
354
390
|
return chain
|
|
355
391
|
|
|
356
|
-
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
392
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs) -> list[Any]:
|
|
357
393
|
"""
|
|
358
394
|
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
359
395
|
O(n) normal inserts into parent tables to get primary keys back,
|
|
@@ -386,7 +422,9 @@ class HookQuerySetMixin:
|
|
|
386
422
|
created_objects.extend(batch_result)
|
|
387
423
|
return created_objects
|
|
388
424
|
|
|
389
|
-
def _process_mti_bulk_create_batch(
|
|
425
|
+
def _process_mti_bulk_create_batch(
|
|
426
|
+
self, batch, inheritance_chain, **kwargs
|
|
427
|
+
) -> list[Any]:
|
|
390
428
|
"""
|
|
391
429
|
Process a single batch of objects through the inheritance chain.
|
|
392
430
|
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
@@ -588,7 +626,7 @@ class HookQuerySetMixin:
|
|
|
588
626
|
|
|
589
627
|
return child_obj
|
|
590
628
|
|
|
591
|
-
def _mti_bulk_update(self, objs, fields, **kwargs):
|
|
629
|
+
def _mti_bulk_update(self, objs, fields, **kwargs) -> int:
|
|
592
630
|
"""
|
|
593
631
|
Custom bulk update implementation for MTI models.
|
|
594
632
|
Updates each table in the inheritance chain efficiently using Django's batch_size.
|
|
@@ -655,18 +693,13 @@ class HookQuerySetMixin:
|
|
|
655
693
|
|
|
656
694
|
def _process_mti_bulk_update_batch(
|
|
657
695
|
self, batch, field_groups, inheritance_chain, **kwargs
|
|
658
|
-
):
|
|
696
|
+
) -> int:
|
|
659
697
|
"""
|
|
660
698
|
Process a single batch of objects for MTI bulk update.
|
|
661
699
|
Updates each table in the inheritance chain for the batch.
|
|
662
700
|
"""
|
|
663
701
|
total_updated = 0
|
|
664
702
|
|
|
665
|
-
# For MTI, we need to handle parent links correctly
|
|
666
|
-
# The root model (first in chain) has its own PK
|
|
667
|
-
# Child models use the parent link to reference the root PK
|
|
668
|
-
root_model = inheritance_chain[0]
|
|
669
|
-
|
|
670
703
|
# Get the primary keys from the objects
|
|
671
704
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
672
705
|
root_pks = []
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.234
|
|
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
|
|
|
@@ -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=exnXYVKEVbYAXhChCP8VdWTnKCnm9DiTcokEIBee1I0,4350
|
|
11
11
|
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=28MtLT3xKEruAEDVNtXVQRrusMtya12yznMAdmIojEs,33850
|
|
13
13
|
django_bulk_hooks/registry.py,sha256=8UuhniiH5ChSeOKV1UUbqTEiIu25bZXvcHmkaRbxmME,1131
|
|
14
|
-
django_bulk_hooks-0.1.
|
|
15
|
-
django_bulk_hooks-0.1.
|
|
16
|
-
django_bulk_hooks-0.1.
|
|
17
|
-
django_bulk_hooks-0.1.
|
|
14
|
+
django_bulk_hooks-0.1.234.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.234.dist-info/METADATA,sha256=bzEaNj562keAtfN9p-z1_KmC8CWNjH6A4M8E1HMu--c,9049
|
|
16
|
+
django_bulk_hooks-0.1.234.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.234.dist-info/RECORD,,
|
|
File without changes
|