django-bulk-hooks 0.1.53__py3-none-any.whl → 0.1.55__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/context.py +12 -0
- django_bulk_hooks/handler.py +105 -156
- django_bulk_hooks/queryset.py +0 -1
- {django_bulk_hooks-0.1.53.dist-info → django_bulk_hooks-0.1.55.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.53.dist-info → django_bulk_hooks-0.1.55.dist-info}/RECORD +7 -7
- {django_bulk_hooks-0.1.53.dist-info → django_bulk_hooks-0.1.55.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.53.dist-info → django_bulk_hooks-0.1.55.dist-info}/WHEEL +0 -0
django_bulk_hooks/context.py
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from collections import deque
|
|
3
|
+
|
|
4
|
+
_hook_context = threading.local()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_hook_queue():
|
|
8
|
+
if not hasattr(_hook_context, "queue"):
|
|
9
|
+
_hook_context.queue = deque()
|
|
10
|
+
return _hook_context.queue
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
class TriggerContext:
|
|
2
14
|
def __init__(self, model_cls, metadata=None):
|
|
3
15
|
self.model_cls = model_cls
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -1,11 +1,61 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from collections import deque
|
|
2
4
|
|
|
3
5
|
from django.db import transaction
|
|
4
|
-
from django_bulk_hooks.conditions import HookCondition
|
|
5
6
|
from django_bulk_hooks.registry import get_hooks, register_hook
|
|
6
7
|
|
|
7
8
|
logger = logging.getLogger(__name__)
|
|
8
9
|
|
|
10
|
+
# Thread-local hook context and trigger state
|
|
11
|
+
class TriggerVars(threading.local):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.new = None
|
|
14
|
+
self.old = None
|
|
15
|
+
self.event = None
|
|
16
|
+
self.model = None
|
|
17
|
+
self.depth = 0
|
|
18
|
+
|
|
19
|
+
trigger = TriggerVars()
|
|
20
|
+
|
|
21
|
+
# Hook queue per thread
|
|
22
|
+
_hook_context = threading.local()
|
|
23
|
+
|
|
24
|
+
def get_hook_queue():
|
|
25
|
+
if not hasattr(_hook_context, "queue"):
|
|
26
|
+
_hook_context.queue = deque()
|
|
27
|
+
return _hook_context.queue
|
|
28
|
+
|
|
29
|
+
class TriggerContextState:
|
|
30
|
+
@property
|
|
31
|
+
def is_before(self):
|
|
32
|
+
return trigger.event.startswith("before_") if trigger.event else False
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_after(self):
|
|
36
|
+
return trigger.event.startswith("after_") if trigger.event else False
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_create(self):
|
|
40
|
+
return "create" in trigger.event if trigger.event else False
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def is_update(self):
|
|
44
|
+
return "update" in trigger.event if trigger.event else False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def new(self):
|
|
48
|
+
return trigger.new
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def old(self):
|
|
52
|
+
return trigger.old
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def model(self):
|
|
56
|
+
return trigger.model
|
|
57
|
+
|
|
58
|
+
Trigger = TriggerContextState()
|
|
9
59
|
|
|
10
60
|
class TriggerHandlerMeta(type):
|
|
11
61
|
_registered = set()
|
|
@@ -16,15 +66,7 @@ class TriggerHandlerMeta(type):
|
|
|
16
66
|
if hasattr(method, "hooks_hooks"):
|
|
17
67
|
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
18
68
|
key = (model_cls, event, cls, method_name)
|
|
19
|
-
if key in TriggerHandlerMeta._registered:
|
|
20
|
-
logger.debug(
|
|
21
|
-
"Skipping duplicate registration for %s.%s on %s.%s",
|
|
22
|
-
cls.__name__,
|
|
23
|
-
method_name,
|
|
24
|
-
model_cls.__name__,
|
|
25
|
-
event,
|
|
26
|
-
)
|
|
27
|
-
else:
|
|
69
|
+
if key not in TriggerHandlerMeta._registered:
|
|
28
70
|
register_hook(
|
|
29
71
|
model=model_cls,
|
|
30
72
|
event=event,
|
|
@@ -34,18 +76,8 @@ class TriggerHandlerMeta(type):
|
|
|
34
76
|
priority=priority,
|
|
35
77
|
)
|
|
36
78
|
TriggerHandlerMeta._registered.add(key)
|
|
37
|
-
logger.debug(
|
|
38
|
-
"Registered hook %s.%s → %s.%s (cond=%r, prio=%s)",
|
|
39
|
-
model_cls.__name__,
|
|
40
|
-
event,
|
|
41
|
-
cls.__name__,
|
|
42
|
-
method_name,
|
|
43
|
-
condition,
|
|
44
|
-
priority,
|
|
45
|
-
)
|
|
46
79
|
return cls
|
|
47
80
|
|
|
48
|
-
|
|
49
81
|
class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
50
82
|
@classmethod
|
|
51
83
|
def handle(
|
|
@@ -57,152 +89,69 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
57
89
|
old_records: list = None,
|
|
58
90
|
**kwargs,
|
|
59
91
|
) -> None:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# Sort hooks by priority (ascending: lower number = higher priority)
|
|
64
|
-
hooks = sorted(hooks, key=lambda x: x[3])
|
|
65
|
-
|
|
66
|
-
hook_names = [f"{h.__name__}.{m}" for h, m, _, _ in hooks]
|
|
67
|
-
logger.debug(
|
|
68
|
-
"Found %d hooks for %s.%s: %s",
|
|
69
|
-
len(hooks),
|
|
70
|
-
model.__name__,
|
|
71
|
-
event,
|
|
72
|
-
hook_names,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
def _process():
|
|
76
|
-
# Ensure new_records is a list
|
|
77
|
-
new_records_local = new_records or []
|
|
78
|
-
|
|
79
|
-
# Normalize old_records: ensure list and pad with None
|
|
80
|
-
old_records_local = list(old_records) if old_records else []
|
|
81
|
-
if len(old_records_local) < len(new_records_local):
|
|
82
|
-
old_records_local += [None] * (
|
|
83
|
-
len(new_records_local) - len(old_records_local)
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
logger.debug(
|
|
87
|
-
"ℹ️ bulk_hooks.handle() start: model=%s event=%s new_count=%d old_count=%d",
|
|
88
|
-
model.__name__,
|
|
89
|
-
event,
|
|
90
|
-
len(new_records_local),
|
|
91
|
-
len(old_records_local),
|
|
92
|
-
)
|
|
92
|
+
queue = get_hook_queue()
|
|
93
|
+
queue.append((cls, event, model, new_records, old_records, kwargs))
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"→ evaluating hook %s.%s (cond=%r, prio=%s)",
|
|
97
|
-
handler_cls.__name__,
|
|
98
|
-
method_name,
|
|
99
|
-
condition,
|
|
100
|
-
priority,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# Evaluate condition
|
|
104
|
-
passed = True
|
|
105
|
-
if condition is not None:
|
|
106
|
-
if isinstance(condition, HookCondition):
|
|
107
|
-
cond_info = getattr(condition, "__dict__", str(condition))
|
|
108
|
-
logger.debug(
|
|
109
|
-
" [cond-info] %s.%s → %r",
|
|
110
|
-
handler_cls.__name__,
|
|
111
|
-
method_name,
|
|
112
|
-
cond_info,
|
|
113
|
-
)
|
|
95
|
+
if len(queue) > 1:
|
|
96
|
+
return # nested call, will be processed by outermost
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
if field_name:
|
|
121
|
-
actual_val = getattr(new, field_name, None)
|
|
122
|
-
expected = getattr(condition, "value", None) or getattr(
|
|
123
|
-
condition, "value", None
|
|
124
|
-
)
|
|
125
|
-
logger.debug(
|
|
126
|
-
" [field-lookup] %s.%s → field=%r actual=%r expected=%r",
|
|
127
|
-
handler_cls.__name__,
|
|
128
|
-
method_name,
|
|
129
|
-
field_name,
|
|
130
|
-
actual_val,
|
|
131
|
-
expected,
|
|
132
|
-
)
|
|
133
|
-
result = condition.check(new, old)
|
|
134
|
-
checks.append(result)
|
|
135
|
-
logger.debug(
|
|
136
|
-
" [cond-check] %s.%s → new=%r old=%r => %s",
|
|
137
|
-
handler_cls.__name__,
|
|
138
|
-
method_name,
|
|
139
|
-
new,
|
|
140
|
-
old,
|
|
141
|
-
result,
|
|
142
|
-
)
|
|
143
|
-
passed = any(checks)
|
|
144
|
-
logger.debug(
|
|
145
|
-
" [cond-summary] %s.%s any-passed=%s",
|
|
146
|
-
handler_cls.__name__,
|
|
147
|
-
method_name,
|
|
148
|
-
passed,
|
|
149
|
-
)
|
|
150
|
-
else:
|
|
151
|
-
# Legacy callable conditions
|
|
152
|
-
passed = condition(
|
|
153
|
-
new_records=new_records_local,
|
|
154
|
-
old_records=old_records_local,
|
|
155
|
-
)
|
|
156
|
-
logger.debug(
|
|
157
|
-
" [legacy-cond] %s.%s → full-list => %s",
|
|
158
|
-
handler_cls.__name__,
|
|
159
|
-
method_name,
|
|
160
|
-
passed,
|
|
161
|
-
)
|
|
98
|
+
# only outermost handle will process the queue
|
|
99
|
+
while queue:
|
|
100
|
+
cls_, event_, model_, new_, old_, kw_ = queue.popleft()
|
|
101
|
+
cls_._process(event_, model_, new_, old_, **kw_)
|
|
162
102
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
103
|
+
@classmethod
|
|
104
|
+
def _process(
|
|
105
|
+
cls,
|
|
106
|
+
event,
|
|
107
|
+
model,
|
|
108
|
+
new_records,
|
|
109
|
+
old_records,
|
|
110
|
+
**kwargs,
|
|
111
|
+
):
|
|
112
|
+
trigger.depth += 1
|
|
113
|
+
trigger.new = new_records
|
|
114
|
+
trigger.old = old_records
|
|
115
|
+
trigger.event = event
|
|
116
|
+
trigger.model = model
|
|
117
|
+
|
|
118
|
+
hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
|
|
119
|
+
logger.debug("Processing %d hooks for %s.%s", len(hooks), model.__name__, event)
|
|
120
|
+
|
|
121
|
+
def _execute():
|
|
122
|
+
new_local = new_records or []
|
|
123
|
+
old_local = old_records or []
|
|
124
|
+
if len(old_local) < len(new_local):
|
|
125
|
+
old_local += [None] * (len(new_local) - len(old_local))
|
|
126
|
+
|
|
127
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
128
|
+
if condition is not None:
|
|
129
|
+
checks = [condition.check(n, o) for n, o in zip(new_local, old_local)]
|
|
130
|
+
if not any(checks):
|
|
131
|
+
continue
|
|
170
132
|
|
|
171
|
-
# Instantiate & invoke handler method
|
|
172
133
|
handler = handler_cls()
|
|
173
134
|
method = getattr(handler, method_name)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
handler_cls.__name__,
|
|
177
|
-
method_name,
|
|
178
|
-
len(new_records_local),
|
|
179
|
-
)
|
|
135
|
+
|
|
136
|
+
logger.info("Running hook %s.%s on %d items", handler_cls.__name__, method_name, len(new_local))
|
|
180
137
|
try:
|
|
181
138
|
method(
|
|
182
|
-
new_records=
|
|
183
|
-
old_records=
|
|
139
|
+
new_records=new_local,
|
|
140
|
+
old_records=old_local,
|
|
184
141
|
**kwargs,
|
|
185
142
|
)
|
|
186
143
|
except Exception:
|
|
187
|
-
logger.exception(
|
|
188
|
-
"❌ exception in %s.%s",
|
|
189
|
-
handler_cls.__name__,
|
|
190
|
-
method_name,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
logger.debug(
|
|
194
|
-
"✔️ bulk_hooks.handle() complete for %s.%s",
|
|
195
|
-
model.__name__,
|
|
196
|
-
event,
|
|
197
|
-
)
|
|
144
|
+
logger.exception("Error in hook %s.%s", handler_cls.__name__, method_name)
|
|
198
145
|
|
|
199
|
-
# Defer if in atomic block and event is after_*
|
|
200
146
|
conn = transaction.get_connection()
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
147
|
+
try:
|
|
148
|
+
if conn.in_atomic_block and event.startswith("after_"):
|
|
149
|
+
transaction.on_commit(_execute)
|
|
150
|
+
else:
|
|
151
|
+
_execute()
|
|
152
|
+
finally:
|
|
153
|
+
trigger.new = None
|
|
154
|
+
trigger.old = None
|
|
155
|
+
trigger.event = None
|
|
156
|
+
trigger.model = None
|
|
157
|
+
trigger.depth -= 1
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
django_bulk_hooks/__init__.py,sha256=6VFU52Kz5Yjhjr_RhID41nMaXUMZ4na_MVDXEhIMXc4,98
|
|
2
2
|
django_bulk_hooks/conditions.py,sha256=UQP5Y7yyJXqDdUcUukRPxCbvrsV6HqNmIxERFZqBh58,5317
|
|
3
3
|
django_bulk_hooks/constants.py,sha256=Jks1BIADYbap2fpq3Ry0e7w-CiXBCsR9b5h1yan1qoc,192
|
|
4
|
-
django_bulk_hooks/context.py,sha256=
|
|
4
|
+
django_bulk_hooks/context.py,sha256=VyjgaubjJTTXQHgyoGY4OLBbdCpBwo-RX4bvxRfZ1c4,383
|
|
5
5
|
django_bulk_hooks/decorators.py,sha256=wbT9lbpKZqDbG5vPvtTC6MHYWvjLaVccCwFM9U2nHkA,3159
|
|
6
6
|
django_bulk_hooks/engine.py,sha256=l7BzU3lfYhZHZazks8FgHhZB4iLVKuzmcnVcyEQkGpQ,1978
|
|
7
7
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=qUulFSPhi__gWHISC4GogeVQ9aDo45bz0Dj421Y6skE,4968
|
|
9
9
|
django_bulk_hooks/manager.py,sha256=jsTgbjmRe5SeakVJxV8wYxqH6f3fRX5zm3q0Wy-M9Nc,4271
|
|
10
10
|
django_bulk_hooks/models.py,sha256=_cqIVHKhXb1EOnXxhsuJUlLnPjTRM_sCP5OKbq4q-I8,542
|
|
11
11
|
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=wrK--iZBsBNuf25HpaaZE9__EN9y6KlVmfXqioS8SmY,681
|
|
13
13
|
django_bulk_hooks/registry.py,sha256=yeTi0IhodL61J86ohb5OyITufE28T3ecMbt6RWvkzTs,585
|
|
14
|
-
django_bulk_hooks-0.1.
|
|
15
|
-
django_bulk_hooks-0.1.
|
|
16
|
-
django_bulk_hooks-0.1.
|
|
17
|
-
django_bulk_hooks-0.1.
|
|
14
|
+
django_bulk_hooks-0.1.55.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.55.dist-info/METADATA,sha256=jge-9Tq4hx6ISQV49qKbu6kUrl1DrQfHxZ8-7PwKT5g,3101
|
|
16
|
+
django_bulk_hooks-0.1.55.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.55.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|