django-bulk-hooks 0.1.83__py3-none-any.whl → 0.2.100__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/__init__.py +53 -50
- django_bulk_hooks/changeset.py +214 -0
- django_bulk_hooks/conditions.py +230 -351
- django_bulk_hooks/constants.py +4 -0
- django_bulk_hooks/context.py +49 -9
- django_bulk_hooks/decorators.py +219 -96
- django_bulk_hooks/dispatcher.py +588 -0
- django_bulk_hooks/factory.py +541 -0
- django_bulk_hooks/handler.py +106 -167
- django_bulk_hooks/helpers.py +258 -0
- django_bulk_hooks/manager.py +134 -208
- django_bulk_hooks/models.py +89 -101
- django_bulk_hooks/operations/__init__.py +18 -0
- django_bulk_hooks/operations/analyzer.py +466 -0
- django_bulk_hooks/operations/bulk_executor.py +757 -0
- django_bulk_hooks/operations/coordinator.py +928 -0
- django_bulk_hooks/operations/field_utils.py +341 -0
- django_bulk_hooks/operations/mti_handler.py +696 -0
- django_bulk_hooks/operations/mti_plans.py +103 -0
- django_bulk_hooks/operations/record_classifier.py +196 -0
- django_bulk_hooks/queryset.py +233 -43
- django_bulk_hooks/registry.py +276 -25
- django_bulk_hooks-0.2.100.dist-info/METADATA +320 -0
- django_bulk_hooks-0.2.100.dist-info/RECORD +27 -0
- django_bulk_hooks/engine.py +0 -53
- django_bulk_hooks-0.1.83.dist-info/METADATA +0 -228
- django_bulk_hooks-0.1.83.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/WHEEL +0 -0
django_bulk_hooks/conditions.py
CHANGED
|
@@ -1,351 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
""
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
instance
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
return
|
|
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
|
-
def
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
self.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
class HasChanged(HookCondition):
|
|
234
|
-
def __init__(self, field, has_changed=True):
|
|
235
|
-
self.field = field
|
|
236
|
-
self.has_changed = has_changed
|
|
237
|
-
|
|
238
|
-
def check(self, instance, original_instance=None):
|
|
239
|
-
if not original_instance:
|
|
240
|
-
return False
|
|
241
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
242
|
-
previous = resolve_dotted_attr(original_instance, self.field)
|
|
243
|
-
return (current != previous) == self.has_changed
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class WasEqual(HookCondition):
|
|
247
|
-
def __init__(self, field, value, only_on_change=False):
|
|
248
|
-
"""
|
|
249
|
-
Check if a field's original value was `value`.
|
|
250
|
-
If only_on_change is True, only return True when the field has changed away from that value.
|
|
251
|
-
"""
|
|
252
|
-
self.field = field
|
|
253
|
-
self.value = value
|
|
254
|
-
self.only_on_change = only_on_change
|
|
255
|
-
|
|
256
|
-
def check(self, instance, original_instance=None):
|
|
257
|
-
if original_instance is None:
|
|
258
|
-
return False
|
|
259
|
-
previous = resolve_dotted_attr(original_instance, self.field)
|
|
260
|
-
if self.only_on_change:
|
|
261
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
262
|
-
return previous == self.value and current != self.value
|
|
263
|
-
else:
|
|
264
|
-
return previous == self.value
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class ChangesTo(HookCondition):
|
|
268
|
-
def __init__(self, field, value):
|
|
269
|
-
"""
|
|
270
|
-
Check if a field's value has changed to `value`.
|
|
271
|
-
Only returns True when original value != value and current value == value.
|
|
272
|
-
"""
|
|
273
|
-
self.field = field
|
|
274
|
-
self.value = value
|
|
275
|
-
|
|
276
|
-
def check(self, instance, original_instance=None):
|
|
277
|
-
if original_instance is None:
|
|
278
|
-
return False
|
|
279
|
-
previous = resolve_dotted_attr(original_instance, self.field)
|
|
280
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
281
|
-
return previous != self.value and current == self.value
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
class IsGreaterThan(HookCondition):
|
|
285
|
-
def __init__(self, field, value):
|
|
286
|
-
self.field = field
|
|
287
|
-
self.value = value
|
|
288
|
-
|
|
289
|
-
def check(self, instance, original_instance=None):
|
|
290
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
291
|
-
return current is not None and current > self.value
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
class IsGreaterThanOrEqual(HookCondition):
|
|
295
|
-
def __init__(self, field, value):
|
|
296
|
-
self.field = field
|
|
297
|
-
self.value = value
|
|
298
|
-
|
|
299
|
-
def check(self, instance, original_instance=None):
|
|
300
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
301
|
-
return current is not None and current >= self.value
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
class IsLessThan(HookCondition):
|
|
305
|
-
def __init__(self, field, value):
|
|
306
|
-
self.field = field
|
|
307
|
-
self.value = value
|
|
308
|
-
|
|
309
|
-
def check(self, instance, original_instance=None):
|
|
310
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
311
|
-
return current is not None and current < self.value
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
class IsLessThanOrEqual(HookCondition):
|
|
315
|
-
def __init__(self, field, value):
|
|
316
|
-
self.field = field
|
|
317
|
-
self.value = value
|
|
318
|
-
|
|
319
|
-
def check(self, instance, original_instance=None):
|
|
320
|
-
current = resolve_dotted_attr(instance, self.field)
|
|
321
|
-
return current is not None and current <= self.value
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
class AndCondition(HookCondition):
|
|
325
|
-
def __init__(self, cond1, cond2):
|
|
326
|
-
self.cond1 = cond1
|
|
327
|
-
self.cond2 = cond2
|
|
328
|
-
|
|
329
|
-
def check(self, instance, original_instance=None):
|
|
330
|
-
return self.cond1.check(instance, original_instance) and self.cond2.check(
|
|
331
|
-
instance, original_instance
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
class OrCondition(HookCondition):
|
|
336
|
-
def __init__(self, cond1, cond2):
|
|
337
|
-
self.cond1 = cond1
|
|
338
|
-
self.cond2 = cond2
|
|
339
|
-
|
|
340
|
-
def check(self, instance, original_instance=None):
|
|
341
|
-
return self.cond1.check(instance, original_instance) or self.cond2.check(
|
|
342
|
-
instance, original_instance
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
class NotCondition(HookCondition):
|
|
347
|
-
def __init__(self, cond):
|
|
348
|
-
self.cond = cond
|
|
349
|
-
|
|
350
|
-
def check(self, instance, original_instance=None):
|
|
351
|
-
return not self.cond.check(instance, original_instance)
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolve_field_path(instance, field_path):
|
|
7
|
+
"""
|
|
8
|
+
Recursively resolve a field path using Django's __ notation, e.g., "author__profile__name".
|
|
9
|
+
|
|
10
|
+
CRITICAL: For foreign key fields, uses attname to access the ID directly
|
|
11
|
+
to avoid hooking Django's descriptor protocol which causes N+1 queries.
|
|
12
|
+
"""
|
|
13
|
+
# For simple field access (no __), use optimized field access
|
|
14
|
+
if "__" not in field_path:
|
|
15
|
+
try:
|
|
16
|
+
# Get the field from the model's meta to check if it's a foreign key
|
|
17
|
+
field = instance._meta.get_field(field_path)
|
|
18
|
+
if field.is_relation and not field.many_to_many:
|
|
19
|
+
# For foreign key fields, use attname to get the ID directly
|
|
20
|
+
# This avoids hooking Django's descriptor protocol
|
|
21
|
+
return getattr(instance, field.attname, None)
|
|
22
|
+
# For regular fields, use normal getattr
|
|
23
|
+
return getattr(instance, field_path, None)
|
|
24
|
+
except Exception:
|
|
25
|
+
# If field lookup fails, fall back to normal getattr
|
|
26
|
+
return getattr(instance, field_path, None)
|
|
27
|
+
|
|
28
|
+
# For paths with __, traverse the relationship chain with FK optimization
|
|
29
|
+
current_instance = instance
|
|
30
|
+
for i, attr in enumerate(field_path.split("__")):
|
|
31
|
+
if current_instance is None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# Check if this is the last attribute and if it's a FK field
|
|
36
|
+
is_last_attr = i == len(field_path.split("__")) - 1
|
|
37
|
+
if is_last_attr and hasattr(current_instance, "_meta"):
|
|
38
|
+
try:
|
|
39
|
+
field = current_instance._meta.get_field(attr)
|
|
40
|
+
if field.is_relation and not field.many_to_many:
|
|
41
|
+
# Use attname for the final FK field access
|
|
42
|
+
current_instance = getattr(
|
|
43
|
+
current_instance,
|
|
44
|
+
field.attname,
|
|
45
|
+
None,
|
|
46
|
+
)
|
|
47
|
+
continue
|
|
48
|
+
except:
|
|
49
|
+
pass # Fall through to normal getattr
|
|
50
|
+
|
|
51
|
+
# Normal getattr for non-FK fields or when FK optimization fails
|
|
52
|
+
current_instance = getattr(current_instance, attr, None)
|
|
53
|
+
except Exception:
|
|
54
|
+
current_instance = None
|
|
55
|
+
|
|
56
|
+
return current_instance
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class HookCondition:
|
|
60
|
+
def check(self, instance, original_instance=None):
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def __call__(self, instance, original_instance=None):
|
|
64
|
+
return self.check(instance, original_instance)
|
|
65
|
+
|
|
66
|
+
def __and__(self, other):
|
|
67
|
+
return AndCondition(self, other)
|
|
68
|
+
|
|
69
|
+
def __or__(self, other):
|
|
70
|
+
return OrCondition(self, other)
|
|
71
|
+
|
|
72
|
+
def __invert__(self):
|
|
73
|
+
return NotCondition(self)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IsNotEqual(HookCondition):
|
|
77
|
+
def __init__(self, field, value, only_on_change=False):
|
|
78
|
+
self.field = field
|
|
79
|
+
self.value = value
|
|
80
|
+
self.only_on_change = only_on_change
|
|
81
|
+
|
|
82
|
+
def check(self, instance, original_instance=None):
|
|
83
|
+
current = resolve_field_path(instance, self.field)
|
|
84
|
+
if self.only_on_change:
|
|
85
|
+
if original_instance is None:
|
|
86
|
+
return False
|
|
87
|
+
previous = resolve_field_path(original_instance, self.field)
|
|
88
|
+
return previous == self.value and current != self.value
|
|
89
|
+
return current != self.value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class IsEqual(HookCondition):
|
|
93
|
+
def __init__(self, field, value, only_on_change=False):
|
|
94
|
+
self.field = field
|
|
95
|
+
self.value = value
|
|
96
|
+
self.only_on_change = only_on_change
|
|
97
|
+
|
|
98
|
+
def check(self, instance, original_instance=None):
|
|
99
|
+
current = resolve_field_path(instance, self.field)
|
|
100
|
+
|
|
101
|
+
if self.only_on_change:
|
|
102
|
+
if original_instance is None:
|
|
103
|
+
return False
|
|
104
|
+
previous = resolve_field_path(original_instance, self.field)
|
|
105
|
+
return previous != self.value and current == self.value
|
|
106
|
+
return current == self.value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class HasChanged(HookCondition):
|
|
110
|
+
def __init__(self, field, has_changed=True):
|
|
111
|
+
self.field = field
|
|
112
|
+
self.has_changed = has_changed
|
|
113
|
+
|
|
114
|
+
def check(self, instance, original_instance=None):
|
|
115
|
+
if not original_instance:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
current = resolve_field_path(instance, self.field)
|
|
119
|
+
previous = resolve_field_path(original_instance, self.field)
|
|
120
|
+
|
|
121
|
+
return (current != previous) == self.has_changed
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class WasEqual(HookCondition):
|
|
125
|
+
def __init__(self, field, value, only_on_change=False):
|
|
126
|
+
"""
|
|
127
|
+
Check if a field's original value was `value`.
|
|
128
|
+
If only_on_change is True, only return True when the field has changed away from that value.
|
|
129
|
+
"""
|
|
130
|
+
self.field = field
|
|
131
|
+
self.value = value
|
|
132
|
+
self.only_on_change = only_on_change
|
|
133
|
+
|
|
134
|
+
def check(self, instance, original_instance=None):
|
|
135
|
+
if original_instance is None:
|
|
136
|
+
return False
|
|
137
|
+
previous = resolve_field_path(original_instance, self.field)
|
|
138
|
+
if self.only_on_change:
|
|
139
|
+
current = resolve_field_path(instance, self.field)
|
|
140
|
+
return previous == self.value and current != self.value
|
|
141
|
+
return previous == self.value
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ChangesTo(HookCondition):
|
|
145
|
+
def __init__(self, field, value):
|
|
146
|
+
"""
|
|
147
|
+
Check if a field's value has changed to `value`.
|
|
148
|
+
Only returns True when original value != value and current value == value.
|
|
149
|
+
"""
|
|
150
|
+
self.field = field
|
|
151
|
+
self.value = value
|
|
152
|
+
|
|
153
|
+
def check(self, instance, original_instance=None):
|
|
154
|
+
if original_instance is None:
|
|
155
|
+
return False
|
|
156
|
+
previous = resolve_field_path(original_instance, self.field)
|
|
157
|
+
current = resolve_field_path(instance, self.field)
|
|
158
|
+
return previous != self.value and current == self.value
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class IsGreaterThan(HookCondition):
|
|
162
|
+
def __init__(self, field, value):
|
|
163
|
+
self.field = field
|
|
164
|
+
self.value = value
|
|
165
|
+
|
|
166
|
+
def check(self, instance, original_instance=None):
|
|
167
|
+
current = resolve_field_path(instance, self.field)
|
|
168
|
+
return current is not None and current > self.value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class IsGreaterThanOrEqual(HookCondition):
|
|
172
|
+
def __init__(self, field, value):
|
|
173
|
+
self.field = field
|
|
174
|
+
self.value = value
|
|
175
|
+
|
|
176
|
+
def check(self, instance, original_instance=None):
|
|
177
|
+
current = resolve_field_path(instance, self.field)
|
|
178
|
+
return current is not None and current >= self.value
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class IsLessThan(HookCondition):
|
|
182
|
+
def __init__(self, field, value):
|
|
183
|
+
self.field = field
|
|
184
|
+
self.value = value
|
|
185
|
+
|
|
186
|
+
def check(self, instance, original_instance=None):
|
|
187
|
+
current = resolve_field_path(instance, self.field)
|
|
188
|
+
return current is not None and current < self.value
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class IsLessThanOrEqual(HookCondition):
|
|
192
|
+
def __init__(self, field, value):
|
|
193
|
+
self.field = field
|
|
194
|
+
self.value = value
|
|
195
|
+
|
|
196
|
+
def check(self, instance, original_instance=None):
|
|
197
|
+
current = resolve_field_path(instance, self.field)
|
|
198
|
+
return current is not None and current <= self.value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class AndCondition(HookCondition):
|
|
202
|
+
def __init__(self, cond1, cond2):
|
|
203
|
+
self.cond1 = cond1
|
|
204
|
+
self.cond2 = cond2
|
|
205
|
+
|
|
206
|
+
def check(self, instance, original_instance=None):
|
|
207
|
+
return self.cond1.check(instance, original_instance) and self.cond2.check(
|
|
208
|
+
instance,
|
|
209
|
+
original_instance,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class OrCondition(HookCondition):
|
|
214
|
+
def __init__(self, cond1, cond2):
|
|
215
|
+
self.cond1 = cond1
|
|
216
|
+
self.cond2 = cond2
|
|
217
|
+
|
|
218
|
+
def check(self, instance, original_instance=None):
|
|
219
|
+
return self.cond1.check(instance, original_instance) or self.cond2.check(
|
|
220
|
+
instance,
|
|
221
|
+
original_instance,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class NotCondition(HookCondition):
|
|
226
|
+
def __init__(self, cond):
|
|
227
|
+
self.cond = cond
|
|
228
|
+
|
|
229
|
+
def check(self, instance, original_instance=None):
|
|
230
|
+
return not self.cond.check(instance, original_instance)
|
django_bulk_hooks/constants.py
CHANGED
|
@@ -7,3 +7,7 @@ AFTER_DELETE = "after_delete"
|
|
|
7
7
|
VALIDATE_CREATE = "validate_create"
|
|
8
8
|
VALIDATE_UPDATE = "validate_update"
|
|
9
9
|
VALIDATE_DELETE = "validate_delete"
|
|
10
|
+
|
|
11
|
+
# Default batch size for bulk_update operations to prevent massive SQL statements
|
|
12
|
+
# This prevents PostgreSQL from crashing when updating large datasets with hooks
|
|
13
|
+
DEFAULT_BULK_UPDATE_BATCH_SIZE = 1000
|
django_bulk_hooks/context.py
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thread-local context management for bulk operations.
|
|
3
|
+
|
|
4
|
+
This module provides thread-safe storage for operation state like
|
|
5
|
+
bypass_hooks flags and bulk update metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import threading
|
|
2
|
-
from collections import deque
|
|
3
9
|
|
|
4
10
|
_hook_context = threading.local()
|
|
5
11
|
|
|
6
12
|
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
def set_bypass_hooks(bypass_hooks):
|
|
14
|
+
"""Set the current bypass_hooks state for the current thread."""
|
|
15
|
+
_hook_context.bypass_hooks = bypass_hooks
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_bypass_hooks():
|
|
19
|
+
"""Get the current bypass_hooks state for the current thread."""
|
|
20
|
+
return getattr(_hook_context, "bypass_hooks", False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Thread-local storage for passing per-object field values from bulk_update -> update
|
|
24
|
+
def set_bulk_update_value_map(value_map):
|
|
25
|
+
"""Store a mapping of {pk: {field_name: value}} for the current thread.
|
|
26
|
+
|
|
27
|
+
This allows the internal update() call (hooked by Django's bulk_update)
|
|
28
|
+
to populate in-memory instances with the concrete values that will be
|
|
29
|
+
written to the database, instead of Django expression objects like Case/Cast.
|
|
30
|
+
"""
|
|
31
|
+
_hook_context.bulk_update_value_map = value_map
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_bulk_update_value_map():
|
|
35
|
+
"""Retrieve the mapping {pk: {field_name: value}} for the current thread, if any."""
|
|
36
|
+
return getattr(_hook_context, "bulk_update_value_map", None)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def set_bulk_update_active(active):
|
|
40
|
+
"""Set whether we're currently in a bulk_update operation."""
|
|
41
|
+
_hook_context.bulk_update_active = active
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_bulk_update_active():
|
|
45
|
+
"""Get whether we're currently in a bulk_update operation."""
|
|
46
|
+
return getattr(_hook_context, "bulk_update_active", False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_bulk_update_batch_size(batch_size):
|
|
50
|
+
"""Store the batch_size for the current bulk_update operation."""
|
|
51
|
+
_hook_context.bulk_update_batch_size = batch_size
|
|
11
52
|
|
|
12
53
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.metadata = metadata or {}
|
|
54
|
+
def get_bulk_update_batch_size():
|
|
55
|
+
"""Get the batch_size for the current bulk_update operation."""
|
|
56
|
+
return getattr(_hook_context, "bulk_update_batch_size", None)
|