django-bulk-hooks 0.2.12__tar.gz → 0.2.13__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.
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/coordinator.py +83 -24
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/LICENSE +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/README.md +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/bulk_executor.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_handler.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -214,29 +214,28 @@ class BulkOperationCoordinator:
|
|
|
214
214
|
"""
|
|
215
215
|
Execute queryset update with hooks - optimized for performance.
|
|
216
216
|
|
|
217
|
-
ARCHITECTURE: Database-Level Update with
|
|
218
|
-
|
|
217
|
+
ARCHITECTURE: Database-Level Update with Hook Support
|
|
218
|
+
=======================================================
|
|
219
219
|
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
For queryset.update() operations:
|
|
221
|
+
1. Fetch old state (before DB update)
|
|
222
|
+
2. Execute native Django UPDATE (fast, direct SQL with Subquery/F() support)
|
|
223
|
+
3. Fetch new state (after DB update, with computed values)
|
|
224
|
+
4. Run BEFORE_UPDATE hooks with old/new state
|
|
225
|
+
- Hooks can see Subquery-computed values via new_records
|
|
226
|
+
- Hooks CAN modify instances (e.g., set derived fields)
|
|
227
|
+
- Modifications are auto-persisted with bulk_update
|
|
228
|
+
5. Run AFTER_UPDATE hooks (for read-only side effects)
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
- ✅ Uses native SQL UPDATE (fastest for aggregations, F(), Subquery)
|
|
225
|
-
- ✅ Maintains framework patterns (MTI support, changeset building)
|
|
226
|
-
- ✅ Runs AFTER_UPDATE hooks with old/new state
|
|
227
|
-
- ❌ BEFORE_UPDATE hooks cannot modify values (use bulk_update() instead)
|
|
230
|
+
Total DML: 1 (queryset.update) + 1 (bulk_update if hooks modify anything)
|
|
228
231
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
bypass_validation: Skip validation hooks if True
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
Number of objects updated
|
|
232
|
+
Note: BEFORE_UPDATE runs AFTER the primary database update.
|
|
233
|
+
This enables:
|
|
234
|
+
- HasChanged conditions to work with Subquery-computed values
|
|
235
|
+
- Cascading updates from hook modifications
|
|
236
|
+
- Optimal performance (Subquery stays in SQL)
|
|
237
|
+
|
|
238
|
+
For true BEFORE semantics (prevent/modify before DB write), use bulk_update().
|
|
240
239
|
"""
|
|
241
240
|
# Check bypass early
|
|
242
241
|
from django_bulk_hooks.context import get_bypass_hooks
|
|
@@ -258,8 +257,8 @@ class BulkOperationCoordinator:
|
|
|
258
257
|
"""
|
|
259
258
|
Execute queryset update with hooks - fast path using native Django update.
|
|
260
259
|
|
|
261
|
-
This method
|
|
262
|
-
|
|
260
|
+
This method provides full hook lifecycle support for queryset.update()
|
|
261
|
+
including BEFORE_UPDATE hooks with automatic persistence of modifications.
|
|
263
262
|
|
|
264
263
|
Args:
|
|
265
264
|
update_kwargs: Dict of fields to update
|
|
@@ -293,14 +292,74 @@ class BulkOperationCoordinator:
|
|
|
293
292
|
|
|
294
293
|
# Mark that this is a queryset update (for potential hook inspection)
|
|
295
294
|
changeset.operation_meta['is_queryset_update'] = True
|
|
296
|
-
changeset.operation_meta['allows_before_modifications'] =
|
|
295
|
+
changeset.operation_meta['allows_before_modifications'] = True
|
|
297
296
|
|
|
298
297
|
# 5. Get MTI chain (follow framework pattern)
|
|
299
298
|
models_in_chain = [self.model_cls]
|
|
300
299
|
if self.mti_handler.is_mti_model():
|
|
301
300
|
models_in_chain.extend(self.mti_handler.get_parent_models())
|
|
302
301
|
|
|
303
|
-
# 6.
|
|
302
|
+
# 6. BEFORE_UPDATE hooks (with auto-persistence)
|
|
303
|
+
# Snapshot state before hooks
|
|
304
|
+
pre_hook_state = {}
|
|
305
|
+
for instance in new_instances:
|
|
306
|
+
if instance.pk is not None:
|
|
307
|
+
pre_hook_values = {}
|
|
308
|
+
for field in self.model_cls._meta.fields:
|
|
309
|
+
try:
|
|
310
|
+
pre_hook_values[field.name] = getattr(instance, field.name, None)
|
|
311
|
+
except Exception:
|
|
312
|
+
pre_hook_values[field.name] = None
|
|
313
|
+
pre_hook_state[instance.pk] = pre_hook_values
|
|
314
|
+
|
|
315
|
+
# Dispatch BEFORE_UPDATE hooks
|
|
316
|
+
for model_cls in models_in_chain:
|
|
317
|
+
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
318
|
+
self.dispatcher.dispatch(model_changeset, "before_update", bypass_hooks=False)
|
|
319
|
+
|
|
320
|
+
# Detect modifications made by BEFORE_UPDATE hooks
|
|
321
|
+
hook_modified_fields = set()
|
|
322
|
+
for instance in new_instances:
|
|
323
|
+
if instance.pk in pre_hook_state:
|
|
324
|
+
for field_name, pre_value in pre_hook_state[instance.pk].items():
|
|
325
|
+
try:
|
|
326
|
+
current_value = getattr(instance, field_name, None)
|
|
327
|
+
except Exception:
|
|
328
|
+
current_value = None
|
|
329
|
+
|
|
330
|
+
if current_value != pre_value:
|
|
331
|
+
hook_modified_fields.add(field_name)
|
|
332
|
+
|
|
333
|
+
# Auto-persist hook modifications
|
|
334
|
+
if hook_modified_fields:
|
|
335
|
+
logger.info(
|
|
336
|
+
f"BEFORE_UPDATE hooks modified {len(hook_modified_fields)} fields: {hook_modified_fields}"
|
|
337
|
+
)
|
|
338
|
+
logger.info("Auto-persisting modifications with bulk_update")
|
|
339
|
+
|
|
340
|
+
# Use bulk_update to persist changes
|
|
341
|
+
# This will trigger another hook cycle (Salesforce-style cascading)
|
|
342
|
+
from django.db.models import QuerySet as BaseQuerySet
|
|
343
|
+
base_qs = BaseQuerySet(model=self.model_cls, using=self.queryset.db)
|
|
344
|
+
base_qs.bulk_update(new_instances, list(hook_modified_fields))
|
|
345
|
+
|
|
346
|
+
# Refresh instances after bulk_update to reflect any changes
|
|
347
|
+
refreshed_map = {
|
|
348
|
+
inst.pk: inst
|
|
349
|
+
for inst in self.model_cls.objects.filter(
|
|
350
|
+
pk__in=[obj.pk for obj in new_instances]
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
for instance in new_instances:
|
|
354
|
+
if instance.pk in refreshed_map:
|
|
355
|
+
refreshed = refreshed_map[instance.pk]
|
|
356
|
+
for field in self.model_cls._meta.fields:
|
|
357
|
+
try:
|
|
358
|
+
setattr(instance, field.name, getattr(refreshed, field.name))
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
# 7. AFTER_UPDATE hooks (read-only side effects)
|
|
304
363
|
for model_cls in models_in_chain:
|
|
305
364
|
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
306
365
|
self.dispatcher.dispatch(model_changeset, "after_update", bypass_hooks=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|