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