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.
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/queryset.py +53 -37
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/LICENSE +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/README.md +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.233 → django_bulk_hooks-0.1.234}/django_bulk_hooks/registry.py +0 -0
|
@@ -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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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"
|
|
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
|