django-bulk-hooks 0.2.41__py3-none-any.whl → 0.2.42__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/dispatcher.py +242 -255
- django_bulk_hooks/factory.py +541 -563
- django_bulk_hooks/handler.py +106 -114
- django_bulk_hooks/operations/bulk_executor.py +512 -576
- django_bulk_hooks/operations/mti_handler.py +5 -4
- django_bulk_hooks/queryset.py +188 -191
- django_bulk_hooks/registry.py +277 -298
- {django_bulk_hooks-0.2.41.dist-info → django_bulk_hooks-0.2.42.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.41.dist-info → django_bulk_hooks-0.2.42.dist-info}/RECORD +11 -11
- {django_bulk_hooks-0.2.41.dist-info → django_bulk_hooks-0.2.42.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.41.dist-info → django_bulk_hooks-0.2.42.dist-info}/WHEEL +0 -0
|
@@ -259,10 +259,11 @@ class MTIHandler:
|
|
|
259
259
|
uf for uf in (update_fields or []) if uf in model_fields_by_name
|
|
260
260
|
]
|
|
261
261
|
|
|
262
|
-
if
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
# Enable upsert even if no fields to update at this level
|
|
263
|
+
# This prevents unique constraint violations on parent tables
|
|
264
|
+
level_update_conflicts = True
|
|
265
|
+
level_unique_fields = normalized_unique
|
|
266
|
+
level_update_fields = filtered_updates # Can be empty list
|
|
266
267
|
|
|
267
268
|
# Create parent level
|
|
268
269
|
parent_level = ParentLevel(
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,191 +1,188 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HookQuerySet - Django QuerySet with hook support.
|
|
3
|
-
|
|
4
|
-
This is a thin coordinator that delegates all complex logic to services.
|
|
5
|
-
It follows the Facade pattern, providing a simple interface over the
|
|
6
|
-
complex coordination required for bulk operations with hooks.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
|
|
11
|
-
from django.db import models
|
|
12
|
-
from django.db import transaction
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class HookQuerySet(models.QuerySet):
|
|
18
|
-
"""
|
|
19
|
-
QuerySet with hook support.
|
|
20
|
-
|
|
21
|
-
This is a thin facade over BulkOperationCoordinator. It provides
|
|
22
|
-
backward-compatible API for Django's QuerySet while integrating
|
|
23
|
-
the full hook lifecycle.
|
|
24
|
-
|
|
25
|
-
Key design principles:
|
|
26
|
-
- Minimal logic (< 10 lines per method)
|
|
27
|
-
- No business logic (delegate to coordinator)
|
|
28
|
-
- No conditionals (let services handle it)
|
|
29
|
-
- Transaction boundaries only
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(self, *args, **kwargs):
|
|
33
|
-
super().__init__(*args, **kwargs)
|
|
34
|
-
self._coordinator = None
|
|
35
|
-
|
|
36
|
-
@property
|
|
37
|
-
def coordinator(self):
|
|
38
|
-
"""Lazy initialization of coordinator"""
|
|
39
|
-
if self._coordinator is None:
|
|
40
|
-
from django_bulk_hooks.operations import BulkOperationCoordinator
|
|
41
|
-
|
|
42
|
-
self._coordinator = BulkOperationCoordinator(self)
|
|
43
|
-
return self._coordinator
|
|
44
|
-
|
|
45
|
-
@transaction.atomic
|
|
46
|
-
def bulk_create(
|
|
47
|
-
self,
|
|
48
|
-
objs,
|
|
49
|
-
batch_size=None,
|
|
50
|
-
ignore_conflicts=False,
|
|
51
|
-
update_conflicts=False,
|
|
52
|
-
update_fields=None,
|
|
53
|
-
unique_fields=None,
|
|
54
|
-
bypass_hooks=False,
|
|
55
|
-
bypass_validation=False,
|
|
56
|
-
):
|
|
57
|
-
"""
|
|
58
|
-
Create multiple objects with hook support.
|
|
59
|
-
|
|
60
|
-
This is the public API - delegates to coordinator.
|
|
61
|
-
"""
|
|
62
|
-
return self.coordinator.create(
|
|
63
|
-
objs=objs,
|
|
64
|
-
batch_size=batch_size,
|
|
65
|
-
ignore_conflicts=ignore_conflicts,
|
|
66
|
-
update_conflicts=update_conflicts,
|
|
67
|
-
update_fields=update_fields,
|
|
68
|
-
unique_fields=unique_fields,
|
|
69
|
-
bypass_hooks=bypass_hooks,
|
|
70
|
-
bypass_validation=bypass_validation,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
@transaction.atomic
|
|
74
|
-
def bulk_update(
|
|
75
|
-
self,
|
|
76
|
-
objs,
|
|
77
|
-
fields=None,
|
|
78
|
-
batch_size=None,
|
|
79
|
-
bypass_hooks=False,
|
|
80
|
-
bypass_validation=False,
|
|
81
|
-
**kwargs,
|
|
82
|
-
):
|
|
83
|
-
"""
|
|
84
|
-
Update multiple objects with hook support.
|
|
85
|
-
|
|
86
|
-
This is the public API - delegates to coordinator.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
objs: List of model instances to update
|
|
90
|
-
fields: List of field names to update (optional, will auto-detect if None)
|
|
91
|
-
batch_size: Number of objects per batch
|
|
92
|
-
bypass_hooks: Skip all hooks if True
|
|
93
|
-
bypass_validation: Skip validation hooks if True
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
Number of objects updated
|
|
97
|
-
"""
|
|
98
|
-
# If fields is None, auto-detect changed fields using analyzer
|
|
99
|
-
if fields is None:
|
|
100
|
-
fields = self.coordinator.analyzer.detect_changed_fields(objs)
|
|
101
|
-
if not fields:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
#
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
bypass_hooks=bypass_hooks,
|
|
190
|
-
bypass_validation=bypass_validation,
|
|
191
|
-
)
|
|
1
|
+
"""
|
|
2
|
+
HookQuerySet - Django QuerySet with hook support.
|
|
3
|
+
|
|
4
|
+
This is a thin coordinator that delegates all complex logic to services.
|
|
5
|
+
It follows the Facade pattern, providing a simple interface over the
|
|
6
|
+
complex coordination required for bulk operations with hooks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.db import transaction
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HookQuerySet(models.QuerySet):
|
|
18
|
+
"""
|
|
19
|
+
QuerySet with hook support.
|
|
20
|
+
|
|
21
|
+
This is a thin facade over BulkOperationCoordinator. It provides
|
|
22
|
+
backward-compatible API for Django's QuerySet while integrating
|
|
23
|
+
the full hook lifecycle.
|
|
24
|
+
|
|
25
|
+
Key design principles:
|
|
26
|
+
- Minimal logic (< 10 lines per method)
|
|
27
|
+
- No business logic (delegate to coordinator)
|
|
28
|
+
- No conditionals (let services handle it)
|
|
29
|
+
- Transaction boundaries only
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, *args, **kwargs):
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
self._coordinator = None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def coordinator(self):
|
|
38
|
+
"""Lazy initialization of coordinator"""
|
|
39
|
+
if self._coordinator is None:
|
|
40
|
+
from django_bulk_hooks.operations import BulkOperationCoordinator
|
|
41
|
+
|
|
42
|
+
self._coordinator = BulkOperationCoordinator(self)
|
|
43
|
+
return self._coordinator
|
|
44
|
+
|
|
45
|
+
@transaction.atomic
|
|
46
|
+
def bulk_create(
|
|
47
|
+
self,
|
|
48
|
+
objs,
|
|
49
|
+
batch_size=None,
|
|
50
|
+
ignore_conflicts=False,
|
|
51
|
+
update_conflicts=False,
|
|
52
|
+
update_fields=None,
|
|
53
|
+
unique_fields=None,
|
|
54
|
+
bypass_hooks=False,
|
|
55
|
+
bypass_validation=False,
|
|
56
|
+
):
|
|
57
|
+
"""
|
|
58
|
+
Create multiple objects with hook support.
|
|
59
|
+
|
|
60
|
+
This is the public API - delegates to coordinator.
|
|
61
|
+
"""
|
|
62
|
+
return self.coordinator.create(
|
|
63
|
+
objs=objs,
|
|
64
|
+
batch_size=batch_size,
|
|
65
|
+
ignore_conflicts=ignore_conflicts,
|
|
66
|
+
update_conflicts=update_conflicts,
|
|
67
|
+
update_fields=update_fields,
|
|
68
|
+
unique_fields=unique_fields,
|
|
69
|
+
bypass_hooks=bypass_hooks,
|
|
70
|
+
bypass_validation=bypass_validation,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@transaction.atomic
|
|
74
|
+
def bulk_update(
|
|
75
|
+
self,
|
|
76
|
+
objs,
|
|
77
|
+
fields=None,
|
|
78
|
+
batch_size=None,
|
|
79
|
+
bypass_hooks=False,
|
|
80
|
+
bypass_validation=False,
|
|
81
|
+
**kwargs,
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Update multiple objects with hook support.
|
|
85
|
+
|
|
86
|
+
This is the public API - delegates to coordinator.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
objs: List of model instances to update
|
|
90
|
+
fields: List of field names to update (optional, will auto-detect if None)
|
|
91
|
+
batch_size: Number of objects per batch
|
|
92
|
+
bypass_hooks: Skip all hooks if True
|
|
93
|
+
bypass_validation: Skip validation hooks if True
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Number of objects updated
|
|
97
|
+
"""
|
|
98
|
+
# If fields is None, auto-detect changed fields using analyzer
|
|
99
|
+
if fields is None:
|
|
100
|
+
fields = self.coordinator.analyzer.detect_changed_fields(objs)
|
|
101
|
+
if not fields:
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
return self.coordinator.update(
|
|
105
|
+
objs=objs,
|
|
106
|
+
fields=fields,
|
|
107
|
+
batch_size=batch_size,
|
|
108
|
+
bypass_hooks=bypass_hooks,
|
|
109
|
+
bypass_validation=bypass_validation,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@transaction.atomic
|
|
113
|
+
def update(self, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
114
|
+
"""
|
|
115
|
+
Update QuerySet with hook support.
|
|
116
|
+
|
|
117
|
+
This is the public API - delegates to coordinator.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
bypass_hooks: Skip all hooks if True
|
|
121
|
+
bypass_validation: Skip validation hooks if True
|
|
122
|
+
**kwargs: Fields to update
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Number of objects updated
|
|
126
|
+
"""
|
|
127
|
+
return self.coordinator.update_queryset(
|
|
128
|
+
update_kwargs=kwargs,
|
|
129
|
+
bypass_hooks=bypass_hooks,
|
|
130
|
+
bypass_validation=bypass_validation,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@transaction.atomic
|
|
134
|
+
def bulk_delete(
|
|
135
|
+
self, objs, bypass_hooks=False, bypass_validation=False, **kwargs,
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Delete multiple objects with hook support.
|
|
139
|
+
|
|
140
|
+
This is the public API - delegates to coordinator.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
objs: List of objects to delete
|
|
144
|
+
bypass_hooks: Skip all hooks if True
|
|
145
|
+
bypass_validation: Skip validation hooks if True
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Tuple of (count, details dict)
|
|
149
|
+
"""
|
|
150
|
+
# Filter queryset to only these objects
|
|
151
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
152
|
+
if not pks:
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
# Create a filtered queryset
|
|
156
|
+
filtered_qs = self.filter(pk__in=pks)
|
|
157
|
+
|
|
158
|
+
# Use coordinator with the filtered queryset
|
|
159
|
+
from django_bulk_hooks.operations import BulkOperationCoordinator
|
|
160
|
+
|
|
161
|
+
coordinator = BulkOperationCoordinator(filtered_qs)
|
|
162
|
+
|
|
163
|
+
count, details = coordinator.delete(
|
|
164
|
+
bypass_hooks=bypass_hooks,
|
|
165
|
+
bypass_validation=bypass_validation,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# For bulk_delete, return just the count to match Django's behavior
|
|
169
|
+
return count
|
|
170
|
+
|
|
171
|
+
@transaction.atomic
|
|
172
|
+
def delete(self, bypass_hooks=False, bypass_validation=False):
|
|
173
|
+
"""
|
|
174
|
+
Delete QuerySet with hook support.
|
|
175
|
+
|
|
176
|
+
This is the public API - delegates to coordinator.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
bypass_hooks: Skip all hooks if True
|
|
180
|
+
bypass_validation: Skip validation hooks if True
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Tuple of (count, details dict)
|
|
184
|
+
"""
|
|
185
|
+
return self.coordinator.delete(
|
|
186
|
+
bypass_hooks=bypass_hooks,
|
|
187
|
+
bypass_validation=bypass_validation,
|
|
188
|
+
)
|