django-bulk-hooks 0.1.217__tar.gz → 0.1.219__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.217 → django_bulk_hooks-0.1.219}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/conditions.py +1 -6
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/engine.py +5 -17
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/queryset.py +21 -43
- django_bulk_hooks-0.1.219/django_bulk_hooks/registry.py +29 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.217/django_bulk_hooks/registry.py +0 -37
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/LICENSE +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/README.md +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/priority.py +0 -0
|
@@ -77,13 +77,8 @@ class HasChanged(HookCondition):
|
|
|
77
77
|
previous = resolve_dotted_attr(original_instance, self.field)
|
|
78
78
|
|
|
79
79
|
# Add more detailed debugging
|
|
80
|
-
print(f"DEBUG: Field '{self.field}' - current value: {current} (type: {type(current)})")
|
|
81
|
-
print(f"DEBUG: Field '{self.field}' - previous value: {previous} (type: {type(previous)})")
|
|
82
|
-
print(f"DEBUG: Values are equal: {current == previous}")
|
|
83
|
-
|
|
84
80
|
result = (current != previous) == self.has_changed
|
|
85
|
-
print(f"DEBUG: HasChanged
|
|
86
|
-
|
|
81
|
+
print(f"DEBUG: HasChanged {self.field} result={result}")
|
|
87
82
|
return result
|
|
88
83
|
|
|
89
84
|
|
|
@@ -23,17 +23,11 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
23
23
|
import traceback
|
|
24
24
|
|
|
25
25
|
stack = traceback.format_stack()
|
|
26
|
-
|
|
27
|
-
f"DEBUG: engine.run called for {model_cls.__name__}.{event} with {len(new_records)} records"
|
|
28
|
-
)
|
|
29
|
-
print(f"DEBUG: Call stack (last 3 frames):")
|
|
30
|
-
for line in stack[-4:-1]: # Show last 3 frames before this one
|
|
31
|
-
print(f" {line.strip()}")
|
|
32
|
-
print(f"DEBUG: Total hooks found: {len(hooks)}")
|
|
26
|
+
logger.debug(f"engine.run {model_cls.__name__}.{event} {len(new_records)} records")
|
|
33
27
|
|
|
34
28
|
# Check if we're in a bypass context
|
|
35
29
|
if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
|
|
36
|
-
|
|
30
|
+
logger.debug("engine.run bypassed")
|
|
37
31
|
return
|
|
38
32
|
|
|
39
33
|
# For BEFORE_* events, run model.clean() first for validation
|
|
@@ -47,7 +41,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
47
41
|
|
|
48
42
|
# Process hooks
|
|
49
43
|
for handler_cls, method_name, condition, priority in hooks:
|
|
50
|
-
|
|
44
|
+
logger.debug(f"Processing {handler_cls.__name__}.{method_name}")
|
|
51
45
|
handler_instance = handler_cls()
|
|
52
46
|
func = getattr(handler_instance, method_name)
|
|
53
47
|
|
|
@@ -60,27 +54,21 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
60
54
|
strict=True,
|
|
61
55
|
):
|
|
62
56
|
if not condition:
|
|
63
|
-
print(f"DEBUG: No condition, adding record {new.pk if hasattr(new, 'pk') else 'No PK'}")
|
|
64
57
|
to_process_new.append(new)
|
|
65
58
|
to_process_old.append(original)
|
|
66
59
|
else:
|
|
67
60
|
condition_result = condition.check(new, original)
|
|
68
|
-
print(f"DEBUG: Condition {condition.__class__.__name__} check result: {condition_result} for record {new.pk if hasattr(new, 'pk') else 'No PK'}")
|
|
69
61
|
if condition_result:
|
|
70
62
|
to_process_new.append(new)
|
|
71
63
|
to_process_old.append(original)
|
|
72
64
|
|
|
73
65
|
if to_process_new:
|
|
74
|
-
|
|
75
|
-
f"DEBUG: Executing hook {handler_cls.__name__}.{method_name} for {len(to_process_new)} records"
|
|
76
|
-
)
|
|
66
|
+
logger.debug(f"Executing {handler_cls.__name__}.{method_name} for {len(to_process_new)} records")
|
|
77
67
|
try:
|
|
78
68
|
func(
|
|
79
69
|
new_records=to_process_new,
|
|
80
70
|
old_records=to_process_old if any(to_process_old) else None,
|
|
81
71
|
)
|
|
82
72
|
except Exception as e:
|
|
83
|
-
|
|
73
|
+
logger.debug(f"Hook execution failed: {e}")
|
|
84
74
|
raise
|
|
85
|
-
else:
|
|
86
|
-
print(f"DEBUG: No records to process for hook {handler_cls.__name__}.{method_name}")
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from django.db import models, transaction
|
|
2
3
|
from django.db.models import AutoField, Case, Field, Value, When
|
|
3
4
|
|
|
4
5
|
from django_bulk_hooks import engine
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
5
8
|
from django_bulk_hooks.constants import (
|
|
6
9
|
AFTER_CREATE,
|
|
7
10
|
AFTER_DELETE,
|
|
@@ -78,12 +81,14 @@ class HookQuerySetMixin:
|
|
|
78
81
|
|
|
79
82
|
# If we're in a bulk operation context, skip hooks to prevent double execution
|
|
80
83
|
if current_bypass_hooks:
|
|
81
|
-
|
|
84
|
+
logger.debug("update skipping hooks (bulk context)")
|
|
82
85
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
83
86
|
else:
|
|
84
|
-
|
|
87
|
+
logger.debug("update running hooks (standalone)")
|
|
85
88
|
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
86
|
-
# Run
|
|
89
|
+
# Run validation hooks first
|
|
90
|
+
engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
|
|
91
|
+
# Then run BEFORE_UPDATE hooks
|
|
87
92
|
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
88
93
|
|
|
89
94
|
# Use Django's built-in update logic directly
|
|
@@ -113,10 +118,10 @@ class HookQuerySetMixin:
|
|
|
113
118
|
|
|
114
119
|
# Run AFTER_UPDATE hooks only for standalone updates
|
|
115
120
|
if not current_bypass_hooks:
|
|
116
|
-
|
|
121
|
+
logger.debug("update running AFTER_UPDATE")
|
|
117
122
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
118
123
|
else:
|
|
119
|
-
|
|
124
|
+
logger.debug("update skipping AFTER_UPDATE (bulk context)")
|
|
120
125
|
|
|
121
126
|
return update_count
|
|
122
127
|
|
|
@@ -181,7 +186,7 @@ class HookQuerySetMixin:
|
|
|
181
186
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
182
187
|
else:
|
|
183
188
|
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
184
|
-
|
|
189
|
+
logger.debug("bulk_create bypassed hooks")
|
|
185
190
|
|
|
186
191
|
# For MTI models, we need to handle them specially
|
|
187
192
|
if is_mti:
|
|
@@ -236,9 +241,7 @@ class HookQuerySetMixin:
|
|
|
236
241
|
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
237
242
|
)
|
|
238
243
|
|
|
239
|
-
|
|
240
|
-
print(f"DEBUG: bulk_update for model {model_cls.__name__} with {len(objs)} objects")
|
|
241
|
-
print(f"DEBUG: kwargs: {kwargs}")
|
|
244
|
+
logger.debug(f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}")
|
|
242
245
|
|
|
243
246
|
# Check for MTI
|
|
244
247
|
is_mti = False
|
|
@@ -248,31 +251,12 @@ class HookQuerySetMixin:
|
|
|
248
251
|
break
|
|
249
252
|
|
|
250
253
|
if not bypass_hooks:
|
|
251
|
-
|
|
252
|
-
original_map = {
|
|
253
|
-
obj.pk: obj
|
|
254
|
-
for obj in model_cls._base_manager.filter(
|
|
255
|
-
pk__in=[obj.pk for obj in objs]
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
originals = [original_map.get(obj.pk) for obj in objs]
|
|
254
|
+
logger.debug("bulk_update setting bypass_hooks=False (hooks will run in update())")
|
|
259
255
|
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
260
|
-
#
|
|
261
|
-
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
262
|
-
|
|
263
|
-
# Then run business logic hooks
|
|
264
|
-
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
265
|
-
|
|
266
|
-
# Detect modified fields during hooks
|
|
267
|
-
modified_fields = self._detect_modified_fields(objs, originals)
|
|
268
|
-
if modified_fields:
|
|
269
|
-
fields_set = set(fields)
|
|
270
|
-
fields_set.update(modified_fields)
|
|
271
|
-
fields = list(fields_set)
|
|
256
|
+
originals = [None] * len(objs) # Placeholder for after_update call
|
|
272
257
|
else:
|
|
273
|
-
|
|
258
|
+
logger.debug("bulk_update setting bypass_hooks=True (no hooks)")
|
|
274
259
|
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
275
|
-
print(f"DEBUG: Set thread-local bypass_hooks=True to prevent nested update() calls from running hooks")
|
|
276
260
|
originals = [None] * len(objs) # Ensure originals is defined for after_update call
|
|
277
261
|
|
|
278
262
|
# Handle auto_now fields like Django's update_or_create does
|
|
@@ -298,22 +282,16 @@ class HookQuerySetMixin:
|
|
|
298
282
|
for k, v in kwargs.items()
|
|
299
283
|
if k not in ["bypass_hooks", "bypass_validation"]
|
|
300
284
|
}
|
|
301
|
-
|
|
302
|
-
# Call Django's bulk_update with hook suspension to prevent double execution
|
|
303
|
-
# Django's bulk_update internally calls .update() which would trigger our hooks again
|
|
304
|
-
print(f"DEBUG: About to call Django's bulk_update")
|
|
285
|
+
logger.debug("Calling Django bulk_update")
|
|
305
286
|
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
306
|
-
|
|
287
|
+
logger.debug(f"Django bulk_update done: {result}")
|
|
307
288
|
|
|
289
|
+
# Note: We don't run AFTER_UPDATE hooks here to prevent double execution
|
|
290
|
+
# The update() method will handle all hook execution based on thread-local state
|
|
308
291
|
if not bypass_hooks:
|
|
309
|
-
|
|
310
|
-
f"DEBUG: bypass_hooks=False, running AFTER_UPDATE hooks for {len(objs)} objects"
|
|
311
|
-
)
|
|
312
|
-
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
292
|
+
logger.debug("bulk_update skipping AFTER_UPDATE (update() will handle)")
|
|
313
293
|
else:
|
|
314
|
-
|
|
315
|
-
f"DEBUG: bypass_hooks=True, skipping AFTER_UPDATE hooks for {len(objs)} objects"
|
|
316
|
-
)
|
|
294
|
+
logger.debug("bulk_update bypassed hooks")
|
|
317
295
|
|
|
318
296
|
return result
|
|
319
297
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from django_bulk_hooks.priority import Priority
|
|
5
|
+
|
|
6
|
+
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_hook(
|
|
10
|
+
model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
|
|
11
|
+
):
|
|
12
|
+
key = (model, event)
|
|
13
|
+
hooks = _hooks.setdefault(key, [])
|
|
14
|
+
hooks.append((handler_cls, method_name, condition, priority))
|
|
15
|
+
# keep sorted by priority
|
|
16
|
+
hooks.sort(key=lambda x: x[3])
|
|
17
|
+
logger.debug(f"Registered {handler_cls.__name__}.{method_name} for {model.__name__}.{event}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_hooks(model, event):
|
|
21
|
+
key = (model, event)
|
|
22
|
+
hooks = _hooks.get(key, [])
|
|
23
|
+
logger.debug(f"get_hooks {model.__name__}.{event} found {len(hooks)} hooks")
|
|
24
|
+
return hooks
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def list_all_hooks():
|
|
28
|
+
"""Debug function to list all registered hooks"""
|
|
29
|
+
return _hooks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.219"
|
|
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"
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
from typing import Union
|
|
3
|
-
|
|
4
|
-
from django_bulk_hooks.priority import Priority
|
|
5
|
-
|
|
6
|
-
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def register_hook(
|
|
10
|
-
model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
|
|
11
|
-
):
|
|
12
|
-
key = (model, event)
|
|
13
|
-
hooks = _hooks.setdefault(key, [])
|
|
14
|
-
hooks.append((handler_cls, method_name, condition, priority))
|
|
15
|
-
# keep sorted by priority
|
|
16
|
-
hooks.sort(key=lambda x: x[3])
|
|
17
|
-
print(f"DEBUG: Registered hook {handler_cls.__name__}.{method_name} for {model.__name__}.{event} with condition: {condition}")
|
|
18
|
-
print(f"DEBUG: Model class: {model} (id: {id(model)})")
|
|
19
|
-
print(f"DEBUG: Total hooks for {model.__name__}.{event}: {len(hooks)}")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_hooks(model, event):
|
|
23
|
-
key = (model, event)
|
|
24
|
-
hooks = _hooks.get(key, [])
|
|
25
|
-
print(f"DEBUG: get_hooks called for {model.__name__}.{event}, found {len(hooks)} hooks")
|
|
26
|
-
print(f"DEBUG: Hook key: {key}")
|
|
27
|
-
print(f"DEBUG: Model class: {model} (id: {id(model)})")
|
|
28
|
-
print(f"DEBUG: All registered hook keys: {list(_hooks.keys())}")
|
|
29
|
-
for hook in hooks:
|
|
30
|
-
handler_cls, method_name, condition, priority = hook
|
|
31
|
-
print(f"DEBUG: Hook: {handler_cls.__name__}.{method_name} with condition: {condition}")
|
|
32
|
-
return hooks
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def list_all_hooks():
|
|
36
|
-
"""Debug function to list all registered hooks"""
|
|
37
|
-
return _hooks
|
|
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
|