django-bulk-hooks 0.1.238__py3-none-any.whl → 0.1.239__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 +4 -4
- django_bulk_hooks/decorators.py +34 -10
- django_bulk_hooks/handler.py +188 -167
- django_bulk_hooks/queryset.py +63 -3
- django_bulk_hooks/registry.py +8 -1
- {django_bulk_hooks-0.1.238.dist-info → django_bulk_hooks-0.1.239.dist-info}/METADATA +3 -3
- django_bulk_hooks-0.1.239.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.238.dist-info → django_bulk_hooks-0.1.239.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.238.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.238.dist-info → django_bulk_hooks-0.1.239.dist-info}/LICENSE +0 -0
django_bulk_hooks/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from django_bulk_hooks.handler import Hook
|
|
2
|
-
from django_bulk_hooks.manager import BulkHookManager
|
|
3
|
-
|
|
4
|
-
__all__ = ["BulkHookManager", "
|
|
1
|
+
from django_bulk_hooks.handler import Hook as HookClass
|
|
2
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
3
|
+
|
|
4
|
+
__all__ = ["BulkHookManager", "HookClass"]
|
django_bulk_hooks/decorators.py
CHANGED
|
@@ -2,6 +2,7 @@ import inspect
|
|
|
2
2
|
from functools import wraps
|
|
3
3
|
|
|
4
4
|
from django.core.exceptions import FieldDoesNotExist
|
|
5
|
+
|
|
5
6
|
from django_bulk_hooks.enums import DEFAULT_PRIORITY
|
|
6
7
|
from django_bulk_hooks.registry import register_hook
|
|
7
8
|
|
|
@@ -61,13 +62,35 @@ def select_related(*related_fields):
|
|
|
61
62
|
continue
|
|
62
63
|
# if any related field is not already cached on the instance,
|
|
63
64
|
# mark it for fetching
|
|
64
|
-
if any(
|
|
65
|
+
if any(
|
|
66
|
+
field not in obj._state.fields_cache for field in related_fields
|
|
67
|
+
):
|
|
65
68
|
ids_to_fetch.append(obj.pk)
|
|
66
69
|
|
|
67
70
|
fetched = {}
|
|
68
71
|
if ids_to_fetch:
|
|
69
|
-
#
|
|
70
|
-
|
|
72
|
+
# Validate fields before passing to select_related
|
|
73
|
+
validated_fields = []
|
|
74
|
+
for field in related_fields:
|
|
75
|
+
if "." in field:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"@select_related does not support nested fields like '{field}'"
|
|
78
|
+
)
|
|
79
|
+
try:
|
|
80
|
+
f = model_cls._meta.get_field(field)
|
|
81
|
+
if not (
|
|
82
|
+
f.is_relation and not f.many_to_many and not f.one_to_many
|
|
83
|
+
):
|
|
84
|
+
continue
|
|
85
|
+
validated_fields.append(field)
|
|
86
|
+
except FieldDoesNotExist:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if validated_fields:
|
|
90
|
+
# Use the base manager to avoid recursion
|
|
91
|
+
fetched = model_cls._base_manager.select_related(
|
|
92
|
+
*validated_fields
|
|
93
|
+
).in_bulk(ids_to_fetch)
|
|
71
94
|
|
|
72
95
|
for obj in new_records:
|
|
73
96
|
preloaded = fetched.get(obj.pk)
|
|
@@ -78,9 +101,8 @@ def select_related(*related_fields):
|
|
|
78
101
|
# don't override values that were explicitly set or already loaded
|
|
79
102
|
continue
|
|
80
103
|
if "." in field:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
104
|
+
# This should have been caught earlier, but just in case
|
|
105
|
+
continue
|
|
84
106
|
|
|
85
107
|
try:
|
|
86
108
|
f = model_cls._meta.get_field(field)
|
|
@@ -108,30 +130,32 @@ def select_related(*related_fields):
|
|
|
108
130
|
def bulk_hook(model_cls, event, when=None, priority=None):
|
|
109
131
|
"""
|
|
110
132
|
Decorator to register a bulk hook for a model.
|
|
111
|
-
|
|
133
|
+
|
|
112
134
|
Args:
|
|
113
135
|
model_cls: The model class to hook into
|
|
114
136
|
event: The event to hook into (e.g., BEFORE_UPDATE, AFTER_UPDATE)
|
|
115
137
|
when: Optional condition for when the hook should run
|
|
116
138
|
priority: Optional priority for hook execution order
|
|
117
139
|
"""
|
|
140
|
+
|
|
118
141
|
def decorator(func):
|
|
119
142
|
# Create a simple handler class for the function
|
|
120
143
|
class FunctionHandler:
|
|
121
144
|
def __init__(self):
|
|
122
145
|
self.func = func
|
|
123
|
-
|
|
146
|
+
|
|
124
147
|
def handle(self, new_instances, original_instances):
|
|
125
148
|
return self.func(new_instances, original_instances)
|
|
126
|
-
|
|
149
|
+
|
|
127
150
|
# Register the hook using the registry
|
|
128
151
|
register_hook(
|
|
129
152
|
model=model_cls,
|
|
130
153
|
event=event,
|
|
131
154
|
handler_cls=FunctionHandler,
|
|
132
|
-
method_name=
|
|
155
|
+
method_name="handle",
|
|
133
156
|
condition=when,
|
|
134
157
|
priority=priority or DEFAULT_PRIORITY,
|
|
135
158
|
)
|
|
136
159
|
return func
|
|
160
|
+
|
|
137
161
|
return decorator
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -1,167 +1,188 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import threading
|
|
3
|
-
from collections import deque
|
|
4
|
-
|
|
5
|
-
from django.db import transaction
|
|
6
|
-
|
|
7
|
-
from django_bulk_hooks.registry import get_hooks, register_hook
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Thread-local hook context and hook state
|
|
13
|
-
class HookVars(threading.local):
|
|
14
|
-
def __init__(self):
|
|
15
|
-
self.new = None
|
|
16
|
-
self.old = None
|
|
17
|
-
self.event = None
|
|
18
|
-
self.model = None
|
|
19
|
-
self.depth = 0
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
hook_vars = HookVars()
|
|
23
|
-
|
|
24
|
-
# Hook queue per thread
|
|
25
|
-
_hook_context = threading.local()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_hook_queue():
|
|
29
|
-
if not hasattr(_hook_context, "queue"):
|
|
30
|
-
_hook_context.queue = deque()
|
|
31
|
-
return _hook_context.queue
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class HookContextState:
|
|
35
|
-
@property
|
|
36
|
-
def is_before(self):
|
|
37
|
-
return hook_vars.event.startswith("before_") if hook_vars.event else False
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def is_after(self):
|
|
41
|
-
return hook_vars.event.startswith("after_") if hook_vars.event else False
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def is_create(self):
|
|
45
|
-
return "create" in hook_vars.event if hook_vars.event else False
|
|
46
|
-
|
|
47
|
-
@property
|
|
48
|
-
def is_update(self):
|
|
49
|
-
return "update" in hook_vars.event if hook_vars.event else False
|
|
50
|
-
|
|
51
|
-
@property
|
|
52
|
-
def new(self):
|
|
53
|
-
return hook_vars.new
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def old(self):
|
|
57
|
-
return hook_vars.old
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def model(self):
|
|
61
|
-
return hook_vars.model
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class HookMeta(type):
|
|
68
|
-
_registered = set()
|
|
69
|
-
|
|
70
|
-
def __new__(mcs, name, bases, namespace):
|
|
71
|
-
cls = super().__new__(mcs, name, bases, namespace)
|
|
72
|
-
for method_name, method in namespace.items():
|
|
73
|
-
if hasattr(method, "hooks_hooks"):
|
|
74
|
-
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
75
|
-
key = (model_cls, event, cls, method_name)
|
|
76
|
-
if key not in HookMeta._registered:
|
|
77
|
-
register_hook(
|
|
78
|
-
model=model_cls,
|
|
79
|
-
event=event,
|
|
80
|
-
handler_cls=cls,
|
|
81
|
-
method_name=method_name,
|
|
82
|
-
condition=condition,
|
|
83
|
-
priority=priority,
|
|
84
|
-
)
|
|
85
|
-
HookMeta._registered.add(key)
|
|
86
|
-
return cls
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class Hook(metaclass=HookMeta):
|
|
90
|
-
@classmethod
|
|
91
|
-
def handle(
|
|
92
|
-
cls,
|
|
93
|
-
event: str,
|
|
94
|
-
model: type,
|
|
95
|
-
*,
|
|
96
|
-
new_records: list = None,
|
|
97
|
-
old_records: list = None,
|
|
98
|
-
**kwargs,
|
|
99
|
-
) -> None:
|
|
100
|
-
queue = get_hook_queue()
|
|
101
|
-
queue.append((cls, event, model, new_records, old_records, kwargs))
|
|
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
|
-
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from collections import deque
|
|
4
|
+
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
|
|
7
|
+
from django_bulk_hooks.registry import get_hooks, register_hook
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Thread-local hook context and hook state
|
|
13
|
+
class HookVars(threading.local):
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.new = None
|
|
16
|
+
self.old = None
|
|
17
|
+
self.event = None
|
|
18
|
+
self.model = None
|
|
19
|
+
self.depth = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
hook_vars = HookVars()
|
|
23
|
+
|
|
24
|
+
# Hook queue per thread
|
|
25
|
+
_hook_context = threading.local()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_hook_queue():
|
|
29
|
+
if not hasattr(_hook_context, "queue"):
|
|
30
|
+
_hook_context.queue = deque()
|
|
31
|
+
return _hook_context.queue
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HookContextState:
|
|
35
|
+
@property
|
|
36
|
+
def is_before(self):
|
|
37
|
+
return hook_vars.event.startswith("before_") if hook_vars.event else False
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_after(self):
|
|
41
|
+
return hook_vars.event.startswith("after_") if hook_vars.event else False
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def is_create(self):
|
|
45
|
+
return "create" in hook_vars.event if hook_vars.event else False
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_update(self):
|
|
49
|
+
return "update" in hook_vars.event if hook_vars.event else False
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def new(self):
|
|
53
|
+
return hook_vars.new
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def old(self):
|
|
57
|
+
return hook_vars.old
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def model(self):
|
|
61
|
+
return hook_vars.model
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
HookContext = HookContextState()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HookMeta(type):
|
|
68
|
+
_registered = set()
|
|
69
|
+
|
|
70
|
+
def __new__(mcs, name, bases, namespace):
|
|
71
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
72
|
+
for method_name, method in namespace.items():
|
|
73
|
+
if hasattr(method, "hooks_hooks"):
|
|
74
|
+
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
75
|
+
key = (model_cls, event, cls, method_name)
|
|
76
|
+
if key not in HookMeta._registered:
|
|
77
|
+
register_hook(
|
|
78
|
+
model=model_cls,
|
|
79
|
+
event=event,
|
|
80
|
+
handler_cls=cls,
|
|
81
|
+
method_name=method_name,
|
|
82
|
+
condition=condition,
|
|
83
|
+
priority=priority,
|
|
84
|
+
)
|
|
85
|
+
HookMeta._registered.add(key)
|
|
86
|
+
return cls
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Hook(metaclass=HookMeta):
|
|
90
|
+
@classmethod
|
|
91
|
+
def handle(
|
|
92
|
+
cls,
|
|
93
|
+
event: str,
|
|
94
|
+
model: type,
|
|
95
|
+
*,
|
|
96
|
+
new_records: list = None,
|
|
97
|
+
old_records: list = None,
|
|
98
|
+
**kwargs,
|
|
99
|
+
) -> None:
|
|
100
|
+
queue = get_hook_queue()
|
|
101
|
+
queue.append((cls, event, model, new_records, old_records, kwargs))
|
|
102
|
+
logger.debug(f"Added item to queue: {event}, depth: {hook_vars.depth}")
|
|
103
|
+
|
|
104
|
+
# If we're already processing hooks (depth > 0), don't process the queue
|
|
105
|
+
# The outermost call will process the entire queue
|
|
106
|
+
if hook_vars.depth > 0:
|
|
107
|
+
logger.debug(f"Depth > 0, returning without processing queue")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
# Process the entire queue
|
|
111
|
+
logger.debug(f"Processing queue with {len(queue)} items")
|
|
112
|
+
while queue:
|
|
113
|
+
item = queue.popleft()
|
|
114
|
+
if len(item) == 6:
|
|
115
|
+
cls_, event_, model_, new_, old_, kw_ = item
|
|
116
|
+
logger.debug(f"Processing queue item: {event_}")
|
|
117
|
+
# Call _process on the Hook class, not the calling class
|
|
118
|
+
Hook._process(event_, model_, new_, old_, **kw_)
|
|
119
|
+
else:
|
|
120
|
+
logger.warning(f"Invalid queue item format: {item}")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def _process(
|
|
125
|
+
cls,
|
|
126
|
+
event,
|
|
127
|
+
model,
|
|
128
|
+
new_records,
|
|
129
|
+
old_records,
|
|
130
|
+
**kwargs,
|
|
131
|
+
):
|
|
132
|
+
hook_vars.depth += 1
|
|
133
|
+
hook_vars.new = new_records
|
|
134
|
+
hook_vars.old = old_records
|
|
135
|
+
hook_vars.event = event
|
|
136
|
+
hook_vars.model = model
|
|
137
|
+
|
|
138
|
+
hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
|
|
139
|
+
logger.debug(f"Found {len(hooks)} hooks for {event}")
|
|
140
|
+
|
|
141
|
+
def _execute():
|
|
142
|
+
logger.debug(f"Executing {len(hooks)} hooks for {event}")
|
|
143
|
+
new_local = new_records or []
|
|
144
|
+
old_local = old_records or []
|
|
145
|
+
if len(old_local) < len(new_local):
|
|
146
|
+
old_local += [None] * (len(new_local) - len(old_local))
|
|
147
|
+
|
|
148
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
149
|
+
logger.debug(f"Processing hook {handler_cls.__name__}.{method_name}")
|
|
150
|
+
if condition is not None:
|
|
151
|
+
checks = [
|
|
152
|
+
condition.check(n, o) for n, o in zip(new_local, old_local)
|
|
153
|
+
]
|
|
154
|
+
if not any(checks):
|
|
155
|
+
logger.debug(f"Condition failed for {handler_cls.__name__}.{method_name}")
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
handler = handler_cls()
|
|
159
|
+
method = getattr(handler, method_name)
|
|
160
|
+
logger.debug(f"Executing {handler_cls.__name__}.{method_name}")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
method(
|
|
164
|
+
new_records=new_local,
|
|
165
|
+
old_records=old_local,
|
|
166
|
+
**kwargs,
|
|
167
|
+
)
|
|
168
|
+
logger.debug(f"Successfully executed {handler_cls.__name__}.{method_name}")
|
|
169
|
+
except Exception:
|
|
170
|
+
logger.exception(
|
|
171
|
+
"Error in hook %s.%s", handler_cls.__name__, method_name
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
conn = transaction.get_connection()
|
|
175
|
+
logger.debug(f"Transaction in_atomic_block: {conn.in_atomic_block}, event: {event}")
|
|
176
|
+
try:
|
|
177
|
+
if conn.in_atomic_block and event.startswith("after_"):
|
|
178
|
+
logger.debug(f"Deferring {event} to on_commit")
|
|
179
|
+
transaction.on_commit(_execute)
|
|
180
|
+
else:
|
|
181
|
+
logger.debug(f"Executing {event} immediately")
|
|
182
|
+
_execute()
|
|
183
|
+
finally:
|
|
184
|
+
hook_vars.new = None
|
|
185
|
+
hook_vars.old = None
|
|
186
|
+
hook_vars.event = None
|
|
187
|
+
hook_vars.model = None
|
|
188
|
+
hook_vars.depth -= 1
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -70,10 +70,20 @@ class HookQuerySetMixin:
|
|
|
70
70
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
71
71
|
|
|
72
72
|
# Check if any of the update values are Subquery objects
|
|
73
|
+
from django.db.models import Subquery
|
|
73
74
|
has_subquery = any(
|
|
74
|
-
|
|
75
|
+
isinstance(value, Subquery)
|
|
75
76
|
for value in kwargs.values()
|
|
76
77
|
)
|
|
78
|
+
|
|
79
|
+
# Debug logging for Subquery detection
|
|
80
|
+
if has_subquery:
|
|
81
|
+
logger.debug(f"Detected Subquery in update: {[k for k, v in kwargs.items() if isinstance(v, Subquery)]}")
|
|
82
|
+
else:
|
|
83
|
+
# Check if we missed any Subquery objects
|
|
84
|
+
for k, v in kwargs.items():
|
|
85
|
+
if hasattr(v, 'query') and hasattr(v, 'resolve_expression'):
|
|
86
|
+
logger.warning(f"Potential Subquery-like object detected but not recognized: {k}={type(v).__name__}")
|
|
77
87
|
|
|
78
88
|
# Apply field updates to instances
|
|
79
89
|
# If a per-object value map exists (from bulk_update), prefer it over kwargs
|
|
@@ -175,8 +185,15 @@ class HookQuerySetMixin:
|
|
|
175
185
|
getattr(refreshed_instance, field.name),
|
|
176
186
|
)
|
|
177
187
|
|
|
178
|
-
# Run AFTER_UPDATE hooks
|
|
179
|
-
if
|
|
188
|
+
# Run AFTER_UPDATE hooks for standalone updates or subquery operations
|
|
189
|
+
# For subquery operations, we need to run hooks even if we're in a bulk context
|
|
190
|
+
# because subqueries bypass the normal object-level update flow
|
|
191
|
+
should_run_hooks = (
|
|
192
|
+
not current_bypass_hooks or
|
|
193
|
+
has_subquery # Always run hooks for subquery operations
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if should_run_hooks:
|
|
180
197
|
logger.debug("update: running AFTER_UPDATE")
|
|
181
198
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
182
199
|
else:
|
|
@@ -837,6 +854,49 @@ class HookQuerySetMixin:
|
|
|
837
854
|
|
|
838
855
|
return total_updated
|
|
839
856
|
|
|
857
|
+
@transaction.atomic
|
|
858
|
+
def bulk_delete(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
859
|
+
"""
|
|
860
|
+
Bulk delete objects in the database.
|
|
861
|
+
"""
|
|
862
|
+
model_cls = self.model
|
|
863
|
+
|
|
864
|
+
if not objs:
|
|
865
|
+
return 0
|
|
866
|
+
|
|
867
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
868
|
+
raise TypeError(
|
|
869
|
+
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
logger.debug(
|
|
873
|
+
f"bulk_delete {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}"
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
# Fire hooks before DB ops
|
|
877
|
+
if not bypass_hooks:
|
|
878
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
879
|
+
if not bypass_validation:
|
|
880
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
881
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
882
|
+
else:
|
|
883
|
+
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
884
|
+
logger.debug("bulk_delete bypassed hooks")
|
|
885
|
+
|
|
886
|
+
# Use Django's standard delete() method on the queryset
|
|
887
|
+
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
888
|
+
if pks:
|
|
889
|
+
# Use the base manager to avoid recursion
|
|
890
|
+
result = self.model._base_manager.filter(pk__in=pks).delete()[0]
|
|
891
|
+
else:
|
|
892
|
+
result = 0
|
|
893
|
+
|
|
894
|
+
# Fire AFTER_DELETE hooks
|
|
895
|
+
if not bypass_hooks:
|
|
896
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
897
|
+
|
|
898
|
+
return result
|
|
899
|
+
|
|
840
900
|
|
|
841
901
|
class HookQuerySet(HookQuerySetMixin, models.QuerySet):
|
|
842
902
|
"""
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -15,7 +15,7 @@ def register_hook(
|
|
|
15
15
|
key = (model, event)
|
|
16
16
|
hooks = _hooks.setdefault(key, [])
|
|
17
17
|
hooks.append((handler_cls, method_name, condition, priority))
|
|
18
|
-
#
|
|
18
|
+
# Sort by priority (lower values first)
|
|
19
19
|
hooks.sort(key=lambda x: x[3])
|
|
20
20
|
logger.debug(f"Registered {handler_cls.__name__}.{method_name} for {model.__name__}.{event}")
|
|
21
21
|
|
|
@@ -29,6 +29,13 @@ def get_hooks(model, event):
|
|
|
29
29
|
return hooks
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def clear_hooks():
|
|
33
|
+
"""Clear all registered hooks. Useful for testing."""
|
|
34
|
+
global _hooks
|
|
35
|
+
_hooks.clear()
|
|
36
|
+
logger.debug("Cleared all registered hooks")
|
|
37
|
+
|
|
38
|
+
|
|
32
39
|
def list_all_hooks():
|
|
33
40
|
"""Debug function to list all registered hooks"""
|
|
34
41
|
return _hooks
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.239
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
-
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
5
|
License: MIT
|
|
7
6
|
Keywords: django,bulk,hooks
|
|
8
7
|
Author: Konrad Beck
|
|
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Requires-Dist: Django (>=4.0)
|
|
16
|
+
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=hsbKduccFEcsV4KIw8CbxCUDOtLZwToCc-XP3sqNy-8,154
|
|
2
|
+
django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
|
|
3
|
+
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
+
django_bulk_hooks/context.py,sha256=jlLsqGZbj__J0-iBUp1D6jTrlDEiX3qIo0XlywW4D9I,2244
|
|
5
|
+
django_bulk_hooks/decorators.py,sha256=tBHjegw1qZgpJkKng1q7gMpd2UpSY2nH9f7oD1cWhr0,5735
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=t_kvgex6_iZEFc5LK-srBTZPe-1bdlYdip5LfWOc6lc,2411
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=Bx-W6yyiciKMyy-BRxUt3CmRPCrX9_LhQgU-5LaJTjg,6019
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
+
django_bulk_hooks/models.py,sha256=exnXYVKEVbYAXhChCP8VdWTnKCnm9DiTcokEIBee1I0,4350
|
|
11
|
+
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=38BvCf_FgKMCAOxEoSYMSkNnmpO2bvUqaPcJkf_6gMc,38561
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
|
|
14
|
+
django_bulk_hooks-0.1.239.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.239.dist-info/METADATA,sha256=CDwXkK4tvZenRQrbIHkpYE75r2hZRwI8S8NDzmT5IgI,9061
|
|
16
|
+
django_bulk_hooks-0.1.239.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
django_bulk_hooks-0.1.239.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
-
django_bulk_hooks/context.py,sha256=jlLsqGZbj__J0-iBUp1D6jTrlDEiX3qIo0XlywW4D9I,2244
|
|
5
|
-
django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=t_kvgex6_iZEFc5LK-srBTZPe-1bdlYdip5LfWOc6lc,2411
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
-
django_bulk_hooks/models.py,sha256=exnXYVKEVbYAXhChCP8VdWTnKCnm9DiTcokEIBee1I0,4350
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=OCKx6sXV1LG-aPOJ4gISjnn4zZzWE-R_NMGbzZF91HY,36156
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=8UuhniiH5ChSeOKV1UUbqTEiIu25bZXvcHmkaRbxmME,1131
|
|
14
|
-
django_bulk_hooks-0.1.238.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.238.dist-info/METADATA,sha256=DHtPWOMH6HjrqO3jx7Ez1JegBVENAxPJISuexntKE-U,9049
|
|
16
|
-
django_bulk_hooks-0.1.238.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
-
django_bulk_hooks-0.1.238.dist-info/RECORD,,
|
|
File without changes
|