django-bulk-hooks 0.1.235__py3-none-any.whl → 0.1.237__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/context.py +16 -0
- django_bulk_hooks/queryset.py +88 -16
- {django_bulk_hooks-0.1.235.dist-info → django_bulk_hooks-0.1.237.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.235.dist-info → django_bulk_hooks-0.1.237.dist-info}/RECORD +6 -6
- {django_bulk_hooks-0.1.235.dist-info → django_bulk_hooks-0.1.237.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.235.dist-info → django_bulk_hooks-0.1.237.dist-info}/WHEEL +0 -0
django_bulk_hooks/context.py
CHANGED
|
@@ -22,6 +22,22 @@ def get_bypass_hooks():
|
|
|
22
22
|
return getattr(_hook_context, 'bypass_hooks', False)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# Thread-local storage for passing per-object field values from bulk_update -> update
|
|
26
|
+
def set_bulk_update_value_map(value_map):
|
|
27
|
+
"""Store a mapping of {pk: {field_name: value}} for the current thread.
|
|
28
|
+
|
|
29
|
+
This allows the internal update() call (triggered by Django's bulk_update)
|
|
30
|
+
to populate in-memory instances with the concrete values that will be
|
|
31
|
+
written to the database, instead of Django expression objects like Case/Cast.
|
|
32
|
+
"""
|
|
33
|
+
_hook_context.bulk_update_value_map = value_map
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_bulk_update_value_map():
|
|
37
|
+
"""Retrieve the mapping {pk: {field_name: value}} for the current thread, if any."""
|
|
38
|
+
return getattr(_hook_context, 'bulk_update_value_map', None)
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
class HookContext:
|
|
26
42
|
def __init__(self, model, bypass_hooks=False):
|
|
27
43
|
self.model = model
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
|
|
2
3
|
from django.db import models, transaction
|
|
3
4
|
from django.db.models import AutoField, Case, Field, Value, When
|
|
4
5
|
|
|
@@ -16,7 +17,11 @@ from django_bulk_hooks.constants import (
|
|
|
16
17
|
VALIDATE_DELETE,
|
|
17
18
|
VALIDATE_UPDATE,
|
|
18
19
|
)
|
|
19
|
-
from django_bulk_hooks.context import
|
|
20
|
+
from django_bulk_hooks.context import (
|
|
21
|
+
HookContext,
|
|
22
|
+
get_bulk_update_value_map,
|
|
23
|
+
set_bulk_update_value_map,
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
class HookQuerySetMixin:
|
|
@@ -71,14 +76,21 @@ class HookQuerySetMixin:
|
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
# Apply field updates to instances
|
|
79
|
+
# If a per-object value map exists (from bulk_update), prefer it over kwargs
|
|
80
|
+
per_object_values = get_bulk_update_value_map()
|
|
74
81
|
for obj in instances:
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
if per_object_values and obj.pk in per_object_values:
|
|
83
|
+
for field, value in per_object_values[obj.pk].items():
|
|
84
|
+
setattr(obj, field, value)
|
|
85
|
+
else:
|
|
86
|
+
for field, value in kwargs.items():
|
|
87
|
+
setattr(obj, field, value)
|
|
77
88
|
|
|
78
89
|
# Check if we're in a bulk operation context to prevent double hook execution
|
|
79
90
|
from django_bulk_hooks.context import get_bypass_hooks
|
|
91
|
+
|
|
80
92
|
current_bypass_hooks = get_bypass_hooks()
|
|
81
|
-
|
|
93
|
+
|
|
82
94
|
# If we're in a bulk operation context, skip hooks to prevent double execution
|
|
83
95
|
if current_bypass_hooks:
|
|
84
96
|
logger.debug("update: skipping hooks (bulk context)")
|
|
@@ -91,6 +103,44 @@ class HookQuerySetMixin:
|
|
|
91
103
|
# Then run BEFORE_UPDATE hooks
|
|
92
104
|
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
93
105
|
|
|
106
|
+
# Persist any additional field mutations made by BEFORE_UPDATE hooks.
|
|
107
|
+
# Build CASE statements per modified field not already present in kwargs.
|
|
108
|
+
modified_fields = self._detect_modified_fields(instances, originals)
|
|
109
|
+
extra_fields = [f for f in modified_fields if f not in kwargs]
|
|
110
|
+
if extra_fields:
|
|
111
|
+
case_statements = {}
|
|
112
|
+
for field_name in extra_fields:
|
|
113
|
+
try:
|
|
114
|
+
field_obj = model_cls._meta.get_field(field_name)
|
|
115
|
+
except Exception:
|
|
116
|
+
# Skip unknown fields
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
when_statements = []
|
|
120
|
+
for obj in instances:
|
|
121
|
+
obj_pk = getattr(obj, "pk", None)
|
|
122
|
+
if obj_pk is None:
|
|
123
|
+
continue
|
|
124
|
+
value = getattr(obj, field_name)
|
|
125
|
+
# Normalize relation values to instance or pk consistently
|
|
126
|
+
if getattr(field_obj, "is_relation", False):
|
|
127
|
+
# If a model instance is provided, use it; else use as-is
|
|
128
|
+
# Value() with output_field handles conversion
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
when_statements.append(
|
|
132
|
+
When(pk=obj_pk, then=Value(value, output_field=field_obj))
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if when_statements:
|
|
136
|
+
case_statements[field_name] = Case(
|
|
137
|
+
*when_statements, output_field=field_obj
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Merge extra CASE updates into kwargs for DB update
|
|
141
|
+
if case_statements:
|
|
142
|
+
kwargs = {**kwargs, **case_statements}
|
|
143
|
+
|
|
94
144
|
# Use Django's built-in update logic directly
|
|
95
145
|
# Call the base QuerySet implementation to avoid recursion
|
|
96
146
|
update_count = super().update(**kwargs)
|
|
@@ -180,12 +230,12 @@ class HookQuerySetMixin:
|
|
|
180
230
|
|
|
181
231
|
# Fire hooks before DB ops
|
|
182
232
|
if not bypass_hooks:
|
|
183
|
-
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
233
|
+
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
184
234
|
if not bypass_validation:
|
|
185
235
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
186
236
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
187
237
|
else:
|
|
188
|
-
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
238
|
+
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
189
239
|
logger.debug("bulk_create bypassed hooks")
|
|
190
240
|
|
|
191
241
|
# For MTI models, we need to handle them specially
|
|
@@ -241,7 +291,9 @@ class HookQuerySetMixin:
|
|
|
241
291
|
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
242
292
|
)
|
|
243
293
|
|
|
244
|
-
logger.debug(
|
|
294
|
+
logger.debug(
|
|
295
|
+
f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
|
|
296
|
+
)
|
|
245
297
|
|
|
246
298
|
# Check for MTI
|
|
247
299
|
is_mti = False
|
|
@@ -257,7 +309,9 @@ class HookQuerySetMixin:
|
|
|
257
309
|
else:
|
|
258
310
|
logger.debug("bulk_update: hooks bypassed")
|
|
259
311
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
260
|
-
originals = [None] * len(
|
|
312
|
+
originals = [None] * len(
|
|
313
|
+
objs
|
|
314
|
+
) # Ensure originals is defined for after_update call
|
|
261
315
|
|
|
262
316
|
# Handle auto_now fields like Django's update_or_create does
|
|
263
317
|
fields_set = set(fields)
|
|
@@ -283,7 +337,27 @@ class HookQuerySetMixin:
|
|
|
283
337
|
if k not in ["bypass_hooks", "bypass_validation"]
|
|
284
338
|
}
|
|
285
339
|
logger.debug("Calling Django bulk_update")
|
|
286
|
-
|
|
340
|
+
# Build a per-object concrete value map to avoid leaking expressions into hooks
|
|
341
|
+
value_map = {}
|
|
342
|
+
for obj in objs:
|
|
343
|
+
if obj.pk is None:
|
|
344
|
+
continue
|
|
345
|
+
field_values = {}
|
|
346
|
+
for field_name in fields:
|
|
347
|
+
# Capture raw values assigned on the object (not expressions)
|
|
348
|
+
field_values[field_name] = getattr(obj, field_name)
|
|
349
|
+
if field_values:
|
|
350
|
+
value_map[obj.pk] = field_values
|
|
351
|
+
|
|
352
|
+
# Make the value map available to the subsequent update() call
|
|
353
|
+
if value_map:
|
|
354
|
+
set_bulk_update_value_map(value_map)
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
358
|
+
finally:
|
|
359
|
+
# Always clear after the internal update() path finishes
|
|
360
|
+
set_bulk_update_value_map(None)
|
|
287
361
|
logger.debug(f"Django bulk_update done: {result}")
|
|
288
362
|
|
|
289
363
|
# Note: We don't run AFTER_UPDATE hooks here to prevent double execution
|
|
@@ -315,18 +389,16 @@ class HookQuerySetMixin:
|
|
|
315
389
|
if field.name == "id":
|
|
316
390
|
continue
|
|
317
391
|
|
|
318
|
-
new_value = getattr(new_instance, field.name)
|
|
319
|
-
original_value = getattr(original, field.name)
|
|
320
|
-
|
|
321
392
|
# Handle different field types appropriately
|
|
322
393
|
if field.is_relation:
|
|
323
|
-
#
|
|
324
|
-
new_pk =
|
|
325
|
-
original_pk =
|
|
394
|
+
# Compare by raw id values to catch cases where only <fk>_id was set
|
|
395
|
+
new_pk = getattr(new_instance, field.attname, None)
|
|
396
|
+
original_pk = getattr(original, field.attname, None)
|
|
326
397
|
if new_pk != original_pk:
|
|
327
398
|
modified_fields.add(field.name)
|
|
328
399
|
else:
|
|
329
|
-
|
|
400
|
+
new_value = getattr(new_instance, field.name)
|
|
401
|
+
original_value = getattr(original, field.name)
|
|
330
402
|
if new_value != original_value:
|
|
331
403
|
modified_fields.add(field.name)
|
|
332
404
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
2
|
django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
|
|
3
3
|
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
-
django_bulk_hooks/context.py,sha256=
|
|
4
|
+
django_bulk_hooks/context.py,sha256=jlLsqGZbj__J0-iBUp1D6jTrlDEiX3qIo0XlywW4D9I,2244
|
|
5
5
|
django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
|
|
6
6
|
django_bulk_hooks/engine.py,sha256=t_kvgex6_iZEFc5LK-srBTZPe-1bdlYdip5LfWOc6lc,2411
|
|
7
7
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
@@ -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=5utEjCnrmJ7tbE2OrDgWf0LU6SCGYoJ1OecmaQW9HVM,35767
|
|
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.237.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.237.dist-info/METADATA,sha256=_jfrUKLLRuYMbka2ojsBYkINVC4kg8jbb5zVvwOhS8M,9049
|
|
16
|
+
django_bulk_hooks-0.1.237.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.237.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|