django-bulk-hooks 0.1.241__py3-none-any.whl → 0.1.243__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 +65 -24
- django_bulk_hooks/engine.py +8 -4
- django_bulk_hooks/handler.py +188 -188
- django_bulk_hooks/models.py +2 -2
- django_bulk_hooks/queryset.py +164 -62
- {django_bulk_hooks-0.1.241.dist-info → django_bulk_hooks-0.1.243.dist-info}/METADATA +3 -3
- django_bulk_hooks-0.1.243.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.241.dist-info → django_bulk_hooks-0.1.243.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.241.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.241.dist-info → django_bulk_hooks-0.1.243.dist-info}/LICENSE +0 -0
django_bulk_hooks/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from django_bulk_hooks.handler import Hook as HookClass
|
|
2
|
-
from django_bulk_hooks.manager import BulkHookManager
|
|
3
|
-
|
|
4
|
-
__all__ = ["BulkHookManager", "HookClass"]
|
|
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
|
@@ -55,35 +55,59 @@ def select_related(*related_fields):
|
|
|
55
55
|
return func(*args, **kwargs)
|
|
56
56
|
|
|
57
57
|
# Determine which instances actually need preloading
|
|
58
|
-
model_cls
|
|
58
|
+
# Allow model_cls to be passed as a keyword argument for testing
|
|
59
|
+
if "model_cls" in bound.arguments:
|
|
60
|
+
model_cls = bound.arguments["model_cls"]
|
|
61
|
+
else:
|
|
62
|
+
model_cls = new_records[0].__class__
|
|
59
63
|
ids_to_fetch = []
|
|
60
64
|
for obj in new_records:
|
|
61
65
|
if obj.pk is None:
|
|
62
66
|
continue
|
|
63
67
|
# if any related field is not already cached on the instance,
|
|
64
68
|
# mark it for fetching
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
# Handle Mock objects that don't have _state.fields_cache
|
|
70
|
+
if hasattr(obj, "_state") and hasattr(obj._state, "fields_cache"):
|
|
71
|
+
try:
|
|
72
|
+
if any(
|
|
73
|
+
field not in obj._state.fields_cache
|
|
74
|
+
for field in related_fields
|
|
75
|
+
):
|
|
76
|
+
ids_to_fetch.append(obj.pk)
|
|
77
|
+
except (TypeError, AttributeError):
|
|
78
|
+
# If _state.fields_cache is not iterable or accessible, always fetch
|
|
79
|
+
ids_to_fetch.append(obj.pk)
|
|
80
|
+
else:
|
|
81
|
+
# For Mock objects or objects without _state.fields_cache, always fetch
|
|
68
82
|
ids_to_fetch.append(obj.pk)
|
|
69
83
|
|
|
84
|
+
# Always validate fields for nested field errors, regardless of whether we need to fetch
|
|
85
|
+
for field in related_fields:
|
|
86
|
+
if "." in field or "__" in field:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"@select_related does not support nested fields like '{field}'"
|
|
89
|
+
)
|
|
90
|
+
|
|
70
91
|
fetched = {}
|
|
71
92
|
if ids_to_fetch:
|
|
72
93
|
# Validate fields before passing to select_related
|
|
73
94
|
validated_fields = []
|
|
74
95
|
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
96
|
try:
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
f
|
|
83
|
-
|
|
97
|
+
# Handle Mock objects that don't have _meta
|
|
98
|
+
if hasattr(model_cls, "_meta"):
|
|
99
|
+
f = model_cls._meta.get_field(field)
|
|
100
|
+
if not (
|
|
101
|
+
f.is_relation
|
|
102
|
+
and not f.many_to_many
|
|
103
|
+
and not f.one_to_many
|
|
104
|
+
):
|
|
105
|
+
continue
|
|
106
|
+
validated_fields.append(field)
|
|
107
|
+
else:
|
|
108
|
+
# For Mock objects, skip validation
|
|
84
109
|
continue
|
|
85
|
-
|
|
86
|
-
except FieldDoesNotExist:
|
|
110
|
+
except (FieldDoesNotExist, AttributeError):
|
|
87
111
|
continue
|
|
88
112
|
|
|
89
113
|
if validated_fields:
|
|
@@ -97,26 +121,39 @@ def select_related(*related_fields):
|
|
|
97
121
|
if not preloaded:
|
|
98
122
|
continue
|
|
99
123
|
for field in related_fields:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
# Handle Mock objects that don't have _state.fields_cache
|
|
125
|
+
if hasattr(obj, "_state") and hasattr(obj._state, "fields_cache"):
|
|
126
|
+
if field in obj._state.fields_cache:
|
|
127
|
+
# don't override values that were explicitly set or already loaded
|
|
128
|
+
continue
|
|
129
|
+
if "." in field or "__" in field:
|
|
104
130
|
# This should have been caught earlier, but just in case
|
|
105
131
|
continue
|
|
106
132
|
|
|
107
133
|
try:
|
|
108
|
-
|
|
109
|
-
if
|
|
110
|
-
f
|
|
111
|
-
|
|
134
|
+
# Handle Mock objects that don't have _meta
|
|
135
|
+
if hasattr(model_cls, "_meta"):
|
|
136
|
+
f = model_cls._meta.get_field(field)
|
|
137
|
+
if not (
|
|
138
|
+
f.is_relation
|
|
139
|
+
and not f.many_to_many
|
|
140
|
+
and not f.one_to_many
|
|
141
|
+
):
|
|
142
|
+
continue
|
|
143
|
+
else:
|
|
144
|
+
# For Mock objects, skip validation
|
|
112
145
|
continue
|
|
113
|
-
except FieldDoesNotExist:
|
|
146
|
+
except (FieldDoesNotExist, AttributeError):
|
|
114
147
|
continue
|
|
115
148
|
|
|
116
149
|
try:
|
|
117
150
|
rel_obj = getattr(preloaded, field)
|
|
118
151
|
setattr(obj, field, rel_obj)
|
|
119
|
-
|
|
152
|
+
# Only set _state.fields_cache if it exists
|
|
153
|
+
if hasattr(obj, "_state") and hasattr(
|
|
154
|
+
obj._state, "fields_cache"
|
|
155
|
+
):
|
|
156
|
+
obj._state.fields_cache[field] = rel_obj
|
|
120
157
|
except AttributeError:
|
|
121
158
|
pass
|
|
122
159
|
|
|
@@ -156,6 +193,10 @@ def bulk_hook(model_cls, event, when=None, priority=None):
|
|
|
156
193
|
condition=when,
|
|
157
194
|
priority=priority or DEFAULT_PRIORITY,
|
|
158
195
|
)
|
|
196
|
+
|
|
197
|
+
# Set attribute to indicate the function has been registered as a bulk hook
|
|
198
|
+
func._bulk_hook_registered = True
|
|
199
|
+
|
|
159
200
|
return func
|
|
160
201
|
|
|
161
202
|
return decorator
|
django_bulk_hooks/engine.py
CHANGED
|
@@ -23,7 +23,9 @@ 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
|
-
|
|
26
|
+
# Safely get model name, fallback to str representation if __name__ not available
|
|
27
|
+
model_name = getattr(model_cls, '__name__', str(model_cls))
|
|
28
|
+
logger.debug(f"engine.run {model_name}.{event} {len(new_records)} records")
|
|
27
29
|
|
|
28
30
|
# Check if we're in a bypass context
|
|
29
31
|
if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
|
|
@@ -31,7 +33,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
31
33
|
return
|
|
32
34
|
|
|
33
35
|
# For BEFORE_* events, run model.clean() first for validation
|
|
34
|
-
if event.startswith("before_"):
|
|
36
|
+
if event.lower().startswith("before_"):
|
|
35
37
|
for instance in new_records:
|
|
36
38
|
try:
|
|
37
39
|
instance.clean()
|
|
@@ -41,7 +43,9 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
41
43
|
|
|
42
44
|
# Process hooks
|
|
43
45
|
for handler_cls, method_name, condition, priority in hooks:
|
|
44
|
-
|
|
46
|
+
# Safely get handler class name
|
|
47
|
+
handler_name = getattr(handler_cls, '__name__', str(handler_cls))
|
|
48
|
+
logger.debug(f"Processing {handler_name}.{method_name}")
|
|
45
49
|
handler_instance = handler_cls()
|
|
46
50
|
func = getattr(handler_instance, method_name)
|
|
47
51
|
|
|
@@ -63,7 +67,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
63
67
|
to_process_old.append(original)
|
|
64
68
|
|
|
65
69
|
if to_process_new:
|
|
66
|
-
logger.debug(f"Executing {
|
|
70
|
+
logger.debug(f"Executing {handler_name}.{method_name} for {len(to_process_new)} records")
|
|
67
71
|
try:
|
|
68
72
|
func(
|
|
69
73
|
new_records=to_process_new,
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -1,188 +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
|
-
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
|
|
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/models.py
CHANGED
|
@@ -60,7 +60,7 @@ class HookModelMixin(models.Model):
|
|
|
60
60
|
# If bypass_hooks is True, use base manager to avoid triggering hooks
|
|
61
61
|
if bypass_hooks:
|
|
62
62
|
logger.debug(f"save() called with bypass_hooks=True for {self.__class__.__name__} pk={self.pk}")
|
|
63
|
-
return self._base_manager.save(self, *args, **kwargs)
|
|
63
|
+
return self.__class__._base_manager.save(self, *args, **kwargs)
|
|
64
64
|
|
|
65
65
|
is_create = self.pk is None
|
|
66
66
|
|
|
@@ -99,7 +99,7 @@ class HookModelMixin(models.Model):
|
|
|
99
99
|
def delete(self, *args, bypass_hooks=False, **kwargs):
|
|
100
100
|
# If bypass_hooks is True, use base manager to avoid triggering hooks
|
|
101
101
|
if bypass_hooks:
|
|
102
|
-
return self._base_manager.delete(self, *args, **kwargs)
|
|
102
|
+
return self.__class__._base_manager.delete(self, *args, **kwargs)
|
|
103
103
|
|
|
104
104
|
ctx = HookContext(self.__class__)
|
|
105
105
|
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -73,47 +73,90 @@ class HookQuerySetMixin:
|
|
|
73
73
|
# Check if any of the update values are Subquery objects
|
|
74
74
|
try:
|
|
75
75
|
from django.db.models import Subquery
|
|
76
|
+
|
|
76
77
|
logger.debug(f"Successfully imported Subquery from django.db.models")
|
|
77
78
|
except ImportError as e:
|
|
78
79
|
logger.error(f"Failed to import Subquery: {e}")
|
|
79
80
|
raise
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
logger.debug(f"Checking for Subquery objects in {len(kwargs)} kwargs")
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
subquery_detected = []
|
|
84
85
|
for key, value in kwargs.items():
|
|
85
86
|
is_subquery = isinstance(value, Subquery)
|
|
86
|
-
logger.debug(
|
|
87
|
+
logger.debug(
|
|
88
|
+
f"Key '{key}': type={type(value).__name__}, is_subquery={is_subquery}"
|
|
89
|
+
)
|
|
87
90
|
if is_subquery:
|
|
88
91
|
subquery_detected.append(key)
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
has_subquery = len(subquery_detected) > 0
|
|
91
|
-
logger.debug(
|
|
92
|
-
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"Subquery detection result: {has_subquery}, detected keys: {subquery_detected}"
|
|
96
|
+
)
|
|
97
|
+
|
|
93
98
|
# Debug logging for Subquery detection
|
|
94
99
|
logger.debug(f"Update kwargs: {list(kwargs.keys())}")
|
|
95
|
-
logger.debug(
|
|
96
|
-
|
|
100
|
+
logger.debug(
|
|
101
|
+
f"Update kwargs types: {[(k, type(v).__name__) for k, v in kwargs.items()]}"
|
|
102
|
+
)
|
|
103
|
+
|
|
97
104
|
if has_subquery:
|
|
98
|
-
logger.debug(
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"Detected Subquery in update: {[k for k, v in kwargs.items() if isinstance(v, Subquery)]}"
|
|
107
|
+
)
|
|
99
108
|
else:
|
|
100
109
|
# Check if we missed any Subquery objects
|
|
101
110
|
for k, v in kwargs.items():
|
|
102
|
-
if hasattr(v,
|
|
103
|
-
logger.warning(
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
if hasattr(v, "query") and hasattr(v, "resolve_expression"):
|
|
112
|
+
logger.warning(
|
|
113
|
+
f"Potential Subquery-like object detected but not recognized: {k}={type(v).__name__}"
|
|
114
|
+
)
|
|
115
|
+
logger.warning(
|
|
116
|
+
f"Object attributes: query={hasattr(v, 'query')}, resolve_expression={hasattr(v, 'resolve_expression')}"
|
|
117
|
+
)
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"Object dir: {[attr for attr in dir(v) if not attr.startswith('_')][:10]}"
|
|
120
|
+
)
|
|
106
121
|
|
|
107
122
|
# Apply field updates to instances
|
|
108
123
|
# If a per-object value map exists (from bulk_update), prefer it over kwargs
|
|
124
|
+
# IMPORTANT: Do not assign Django expression objects (e.g., Subquery/Case/F)
|
|
125
|
+
# to in-memory instances before running BEFORE_UPDATE hooks. Hooks must not
|
|
126
|
+
# receive unresolved expression objects.
|
|
109
127
|
per_object_values = get_bulk_update_value_map()
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
|
|
129
|
+
# For Subquery updates, skip all in-memory field assignments to prevent
|
|
130
|
+
# expression objects from reaching hooks
|
|
131
|
+
if has_subquery:
|
|
132
|
+
logger.debug(
|
|
133
|
+
"Skipping in-memory field assignments due to Subquery detection"
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
for obj in instances:
|
|
137
|
+
if per_object_values and obj.pk in per_object_values:
|
|
138
|
+
for field, value in per_object_values[obj.pk].items():
|
|
139
|
+
setattr(obj, field, value)
|
|
140
|
+
else:
|
|
141
|
+
for field, value in kwargs.items():
|
|
142
|
+
# Skip assigning expression-like objects (they will be handled at DB level)
|
|
143
|
+
is_expression_like = hasattr(value, "resolve_expression")
|
|
144
|
+
if is_expression_like:
|
|
145
|
+
# Special-case Value() which can be unwrapped safely
|
|
146
|
+
if isinstance(value, Value):
|
|
147
|
+
try:
|
|
148
|
+
setattr(obj, field, value.value)
|
|
149
|
+
except Exception:
|
|
150
|
+
# If Value cannot be unwrapped for any reason, skip assignment
|
|
151
|
+
continue
|
|
152
|
+
else:
|
|
153
|
+
# Do not assign unresolved expressions to in-memory objects
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"Skipping assignment of expression {type(value).__name__} to field {field}"
|
|
156
|
+
)
|
|
157
|
+
continue
|
|
158
|
+
else:
|
|
159
|
+
setattr(obj, field, value)
|
|
117
160
|
|
|
118
161
|
# Check if we're in a bulk operation context to prevent double hook execution
|
|
119
162
|
from django_bulk_hooks.context import get_bypass_hooks
|
|
@@ -164,22 +207,32 @@ class HookQuerySetMixin:
|
|
|
164
207
|
output_field = field_obj
|
|
165
208
|
target_name = field_name
|
|
166
209
|
|
|
167
|
-
# Special handling for Subquery values in CASE statements
|
|
210
|
+
# Special handling for Subquery and other expression values in CASE statements
|
|
168
211
|
if isinstance(value, Subquery):
|
|
169
|
-
logger.debug(
|
|
212
|
+
logger.debug(
|
|
213
|
+
f"Creating When statement with Subquery for {field_name}"
|
|
214
|
+
)
|
|
170
215
|
# Ensure the Subquery has proper output_field
|
|
171
|
-
if
|
|
216
|
+
if (
|
|
217
|
+
not hasattr(value, "output_field")
|
|
218
|
+
or value.output_field is None
|
|
219
|
+
):
|
|
172
220
|
value.output_field = output_field
|
|
173
|
-
logger.debug(
|
|
174
|
-
|
|
175
|
-
When(
|
|
176
|
-
pk=obj_pk, then=value
|
|
221
|
+
logger.debug(
|
|
222
|
+
f"Set output_field for Subquery in When statement to {output_field}"
|
|
177
223
|
)
|
|
224
|
+
when_statements.append(When(pk=obj_pk, then=value))
|
|
225
|
+
elif hasattr(value, "resolve_expression"):
|
|
226
|
+
# Handle other expression objects (Case, F, etc.)
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"Creating When statement with expression for {field_name}: {type(value).__name__}"
|
|
178
229
|
)
|
|
230
|
+
when_statements.append(When(pk=obj_pk, then=value))
|
|
179
231
|
else:
|
|
180
232
|
when_statements.append(
|
|
181
233
|
When(
|
|
182
|
-
pk=obj_pk,
|
|
234
|
+
pk=obj_pk,
|
|
235
|
+
then=Value(value, output_field=output_field),
|
|
183
236
|
)
|
|
184
237
|
)
|
|
185
238
|
|
|
@@ -190,40 +243,52 @@ class HookQuerySetMixin:
|
|
|
190
243
|
|
|
191
244
|
# Merge extra CASE updates into kwargs for DB update
|
|
192
245
|
if case_statements:
|
|
193
|
-
logger.debug(
|
|
246
|
+
logger.debug(
|
|
247
|
+
f"Adding case statements to kwargs: {list(case_statements.keys())}"
|
|
248
|
+
)
|
|
194
249
|
for field_name, case_stmt in case_statements.items():
|
|
195
|
-
logger.debug(
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"Case statement for {field_name}: {type(case_stmt).__name__}"
|
|
252
|
+
)
|
|
196
253
|
# Check if the case statement contains Subquery objects
|
|
197
|
-
if hasattr(case_stmt,
|
|
254
|
+
if hasattr(case_stmt, "get_source_expressions"):
|
|
198
255
|
source_exprs = case_stmt.get_source_expressions()
|
|
199
256
|
for expr in source_exprs:
|
|
200
257
|
if isinstance(expr, Subquery):
|
|
201
|
-
logger.debug(
|
|
202
|
-
|
|
258
|
+
logger.debug(
|
|
259
|
+
f"Case statement for {field_name} contains Subquery"
|
|
260
|
+
)
|
|
261
|
+
elif hasattr(expr, "get_source_expressions"):
|
|
203
262
|
# Check nested expressions (like Value objects)
|
|
204
263
|
nested_exprs = expr.get_source_expressions()
|
|
205
264
|
for nested_expr in nested_exprs:
|
|
206
265
|
if isinstance(nested_expr, Subquery):
|
|
207
|
-
logger.debug(
|
|
208
|
-
|
|
266
|
+
logger.debug(
|
|
267
|
+
f"Case statement for {field_name} contains nested Subquery"
|
|
268
|
+
)
|
|
269
|
+
|
|
209
270
|
kwargs = {**kwargs, **case_statements}
|
|
210
271
|
|
|
211
272
|
# Use Django's built-in update logic directly
|
|
212
273
|
# Call the base QuerySet implementation to avoid recursion
|
|
213
|
-
|
|
274
|
+
|
|
214
275
|
# Additional safety check: ensure Subquery objects are properly handled
|
|
215
276
|
# This prevents the "cannot adapt type 'Subquery'" error
|
|
216
277
|
safe_kwargs = {}
|
|
217
278
|
logger.debug(f"Processing {len(kwargs)} kwargs for safety check")
|
|
218
|
-
|
|
279
|
+
|
|
219
280
|
for key, value in kwargs.items():
|
|
220
|
-
logger.debug(
|
|
221
|
-
|
|
281
|
+
logger.debug(
|
|
282
|
+
f"Processing key '{key}' with value type {type(value).__name__}"
|
|
283
|
+
)
|
|
284
|
+
|
|
222
285
|
if isinstance(value, Subquery):
|
|
223
286
|
logger.debug(f"Found Subquery for field {key}")
|
|
224
287
|
# Ensure Subquery has proper output_field
|
|
225
|
-
if not hasattr(value,
|
|
226
|
-
logger.warning(
|
|
288
|
+
if not hasattr(value, "output_field") or value.output_field is None:
|
|
289
|
+
logger.warning(
|
|
290
|
+
f"Subquery for field {key} missing output_field, attempting to infer"
|
|
291
|
+
)
|
|
227
292
|
# Try to infer from the model field
|
|
228
293
|
try:
|
|
229
294
|
field = model_cls._meta.get_field(key)
|
|
@@ -232,35 +297,52 @@ class HookQuerySetMixin:
|
|
|
232
297
|
value.output_field = field
|
|
233
298
|
logger.debug(f"Set output_field to {field}")
|
|
234
299
|
except Exception as e:
|
|
235
|
-
logger.error(
|
|
300
|
+
logger.error(
|
|
301
|
+
f"Failed to infer output_field for Subquery on {key}: {e}"
|
|
302
|
+
)
|
|
236
303
|
raise
|
|
237
304
|
else:
|
|
238
|
-
logger.debug(
|
|
305
|
+
logger.debug(
|
|
306
|
+
f"Subquery for field {key} already has output_field: {value.output_field}"
|
|
307
|
+
)
|
|
239
308
|
safe_kwargs[key] = value
|
|
240
|
-
elif hasattr(value,
|
|
309
|
+
elif hasattr(value, "get_source_expressions") and hasattr(
|
|
310
|
+
value, "resolve_expression"
|
|
311
|
+
):
|
|
241
312
|
# Handle Case statements and other complex expressions
|
|
242
|
-
logger.debug(
|
|
243
|
-
|
|
313
|
+
logger.debug(
|
|
314
|
+
f"Found complex expression for field {key}: {type(value).__name__}"
|
|
315
|
+
)
|
|
316
|
+
|
|
244
317
|
# Check if this expression contains any Subquery objects
|
|
245
318
|
source_expressions = value.get_source_expressions()
|
|
246
319
|
has_nested_subquery = False
|
|
247
|
-
|
|
320
|
+
|
|
248
321
|
for expr in source_expressions:
|
|
249
322
|
if isinstance(expr, Subquery):
|
|
250
323
|
has_nested_subquery = True
|
|
251
324
|
logger.debug(f"Found nested Subquery in {type(value).__name__}")
|
|
252
325
|
# Ensure the nested Subquery has proper output_field
|
|
253
|
-
if
|
|
326
|
+
if (
|
|
327
|
+
not hasattr(expr, "output_field")
|
|
328
|
+
or expr.output_field is None
|
|
329
|
+
):
|
|
254
330
|
try:
|
|
255
331
|
field = model_cls._meta.get_field(key)
|
|
256
332
|
expr.output_field = field
|
|
257
|
-
logger.debug(
|
|
333
|
+
logger.debug(
|
|
334
|
+
f"Set output_field for nested Subquery to {field}"
|
|
335
|
+
)
|
|
258
336
|
except Exception as e:
|
|
259
|
-
logger.error(
|
|
337
|
+
logger.error(
|
|
338
|
+
f"Failed to set output_field for nested Subquery: {e}"
|
|
339
|
+
)
|
|
260
340
|
raise
|
|
261
|
-
|
|
341
|
+
|
|
262
342
|
if has_nested_subquery:
|
|
263
|
-
logger.debug(
|
|
343
|
+
logger.debug(
|
|
344
|
+
f"Expression contains Subquery, ensuring proper output_field"
|
|
345
|
+
)
|
|
264
346
|
# Try to resolve the expression to ensure it's properly formatted
|
|
265
347
|
try:
|
|
266
348
|
resolved_value = value.resolve_expression(None, None)
|
|
@@ -272,12 +354,16 @@ class HookQuerySetMixin:
|
|
|
272
354
|
else:
|
|
273
355
|
safe_kwargs[key] = value
|
|
274
356
|
else:
|
|
275
|
-
logger.debug(
|
|
357
|
+
logger.debug(
|
|
358
|
+
f"Non-Subquery value for field {key}: {type(value).__name__}"
|
|
359
|
+
)
|
|
276
360
|
safe_kwargs[key] = value
|
|
277
|
-
|
|
361
|
+
|
|
278
362
|
logger.debug(f"Safe kwargs keys: {list(safe_kwargs.keys())}")
|
|
279
|
-
logger.debug(
|
|
280
|
-
|
|
363
|
+
logger.debug(
|
|
364
|
+
f"Safe kwargs types: {[(k, type(v).__name__) for k, v in safe_kwargs.items()]}"
|
|
365
|
+
)
|
|
366
|
+
|
|
281
367
|
logger.debug(f"Calling super().update() with {len(safe_kwargs)} kwargs")
|
|
282
368
|
try:
|
|
283
369
|
update_count = super().update(**safe_kwargs)
|
|
@@ -313,10 +399,10 @@ class HookQuerySetMixin:
|
|
|
313
399
|
# For subquery operations, we need to run hooks even if we're in a bulk context
|
|
314
400
|
# because subqueries bypass the normal object-level update flow
|
|
315
401
|
should_run_hooks = (
|
|
316
|
-
not current_bypass_hooks
|
|
317
|
-
has_subquery # Always run hooks for subquery operations
|
|
402
|
+
not current_bypass_hooks
|
|
403
|
+
or has_subquery # Always run hooks for subquery operations
|
|
318
404
|
)
|
|
319
|
-
|
|
405
|
+
|
|
320
406
|
if should_run_hooks:
|
|
321
407
|
logger.debug("update: running AFTER_UPDATE")
|
|
322
408
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
@@ -523,6 +609,9 @@ class HookQuerySetMixin:
|
|
|
523
609
|
"""
|
|
524
610
|
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
525
611
|
new instances with their original values.
|
|
612
|
+
|
|
613
|
+
IMPORTANT: Skip fields that contain Django expression objects (Subquery, Case, etc.)
|
|
614
|
+
as these should not be treated as in-memory modifications.
|
|
526
615
|
"""
|
|
527
616
|
if not original_instances:
|
|
528
617
|
return set()
|
|
@@ -539,15 +628,28 @@ class HookQuerySetMixin:
|
|
|
539
628
|
if field.name == "id":
|
|
540
629
|
continue
|
|
541
630
|
|
|
631
|
+
# Get the new value to check if it's an expression object
|
|
632
|
+
new_value = getattr(new_instance, field.name)
|
|
633
|
+
|
|
634
|
+
# Skip fields that contain expression objects - these are not in-memory modifications
|
|
635
|
+
# but rather database-level expressions that should not be applied to instances
|
|
636
|
+
from django.db.models import Subquery
|
|
637
|
+
|
|
638
|
+
if isinstance(new_value, Subquery) or hasattr(
|
|
639
|
+
new_value, "resolve_expression"
|
|
640
|
+
):
|
|
641
|
+
logger.debug(
|
|
642
|
+
f"Skipping field {field.name} with expression value: {type(new_value).__name__}"
|
|
643
|
+
)
|
|
644
|
+
continue
|
|
645
|
+
|
|
542
646
|
# Handle different field types appropriately
|
|
543
647
|
if field.is_relation:
|
|
544
648
|
# Compare by raw id values to catch cases where only <fk>_id was set
|
|
545
|
-
new_pk = getattr(new_instance, field.attname, None)
|
|
546
649
|
original_pk = getattr(original, field.attname, None)
|
|
547
|
-
if
|
|
650
|
+
if new_value != original_pk:
|
|
548
651
|
modified_fields.add(field.name)
|
|
549
652
|
else:
|
|
550
|
-
new_value = getattr(new_instance, field.name)
|
|
551
653
|
original_value = getattr(original, field.name)
|
|
552
654
|
if new_value != original_value:
|
|
553
655
|
modified_fields.add(field.name)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.243
|
|
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
|
|
5
6
|
License: MIT
|
|
6
7
|
Keywords: django,bulk,hooks
|
|
7
8
|
Author: Konrad Beck
|
|
@@ -13,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
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=6afmyFwRwC4E9CSWyQdONFJUPl5PeXp3ZuTebd7Ic0Y,158
|
|
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=32ffydS9tARaG_WJoiVri7zJnfS2iMd7SuZ8L_sRAGM,7985
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=M3b7Rcb65PYAZTLfWrIRi99BUBPgSLCryL3MSjMVlfQ,2663
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=e_GACTQT-pFF-zL7POeo232MgOikUoCLcxDVInAUiBw,6207
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
+
django_bulk_hooks/models.py,sha256=WtSfc4GBOG_oOt8n37cVvid0MtFIGze9JYKSixil2y0,4370
|
|
11
|
+
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=73l5YZXea1vhQr2H3JagVwYPAtuw9FVzp3Qb2khEwIY,50215
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
|
|
14
|
+
django_bulk_hooks-0.1.243.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.243.dist-info/METADATA,sha256=29LtTjeGKvX-piAZAVOJqthlW_9IzTMTLI6L0GBh-4A,9049
|
|
16
|
+
django_bulk_hooks-0.1.243.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.243.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
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=kUmV4izYquYsvtcR3PH8TkY3PBm-Kt8c8B4HO4ck0uo,46280
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
|
|
14
|
-
django_bulk_hooks-0.1.241.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.241.dist-info/METADATA,sha256=DUraAu1YHa04565ngESG8QnmmUwrcvrD1-4oqoOnBhY,9061
|
|
16
|
-
django_bulk_hooks-0.1.241.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
-
django_bulk_hooks-0.1.241.dist-info/RECORD,,
|
|
File without changes
|