django-bulk-hooks 0.1.52__py3-none-any.whl → 0.1.54__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 +44 -150
- django_bulk_hooks/queryset.py +1 -3
- {django_bulk_hooks-0.1.52.dist-info → django_bulk_hooks-0.1.54.dist-info}/METADATA +3 -3
- {django_bulk_hooks-0.1.52.dist-info → django_bulk_hooks-0.1.54.dist-info}/RECORD +7 -7
- {django_bulk_hooks-0.1.52.dist-info → django_bulk_hooks-0.1.54.dist-info}/WHEEL +1 -1
- {django_bulk_hooks-0.1.52.dist-info → django_bulk_hooks-0.1.54.dist-info}/LICENSE +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,19 @@
|
|
|
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 queue context
|
|
11
|
+
_hook_context = threading.local()
|
|
12
|
+
|
|
13
|
+
def get_hook_queue():
|
|
14
|
+
if not hasattr(_hook_context, "queue"):
|
|
15
|
+
_hook_context.queue = deque()
|
|
16
|
+
return _hook_context.queue
|
|
9
17
|
|
|
10
18
|
class TriggerHandlerMeta(type):
|
|
11
19
|
_registered = set()
|
|
@@ -16,15 +24,7 @@ class TriggerHandlerMeta(type):
|
|
|
16
24
|
if hasattr(method, "hooks_hooks"):
|
|
17
25
|
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
18
26
|
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:
|
|
27
|
+
if key not in TriggerHandlerMeta._registered:
|
|
28
28
|
register_hook(
|
|
29
29
|
model=model_cls,
|
|
30
30
|
event=event,
|
|
@@ -34,18 +34,8 @@ class TriggerHandlerMeta(type):
|
|
|
34
34
|
priority=priority,
|
|
35
35
|
)
|
|
36
36
|
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
37
|
return cls
|
|
47
38
|
|
|
48
|
-
|
|
49
39
|
class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
50
40
|
@classmethod
|
|
51
41
|
def handle(
|
|
@@ -57,152 +47,56 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
57
47
|
old_records: list = None,
|
|
58
48
|
**kwargs,
|
|
59
49
|
) -> None:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# Sort hooks by priority (ascending: lower number = higher priority)
|
|
64
|
-
hooks = sorted(hooks, key=lambda x: x[3])
|
|
50
|
+
queue = get_hook_queue()
|
|
51
|
+
queue.append((cls, event, model, new_records, old_records, kwargs))
|
|
65
52
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"Found %d hooks for %s.%s: %s",
|
|
69
|
-
len(hooks),
|
|
70
|
-
model.__name__,
|
|
71
|
-
event,
|
|
72
|
-
hook_names,
|
|
73
|
-
)
|
|
53
|
+
if len(queue) > 1:
|
|
54
|
+
return # nested call, will be processed by outermost
|
|
74
55
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
# only outermost handle will process the queue
|
|
57
|
+
while queue:
|
|
58
|
+
cls_, event_, model_, new_, old_, kw_ = queue.popleft()
|
|
59
|
+
cls_._process(event_, model_, new_, old_, **kw_)
|
|
78
60
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
61
|
+
@classmethod
|
|
62
|
+
def _process(
|
|
63
|
+
cls,
|
|
64
|
+
event,
|
|
65
|
+
model,
|
|
66
|
+
new_records,
|
|
67
|
+
old_records,
|
|
68
|
+
**kwargs,
|
|
69
|
+
):
|
|
70
|
+
hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
|
|
71
|
+
logger.debug("Processing %d hooks for %s.%s", len(hooks), model.__name__, event)
|
|
85
72
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
len(
|
|
91
|
-
len(old_records_local),
|
|
92
|
-
)
|
|
73
|
+
def _execute():
|
|
74
|
+
new_local = new_records or []
|
|
75
|
+
old_local = old_records or []
|
|
76
|
+
if len(old_local) < len(new_local):
|
|
77
|
+
old_local += [None] * (len(new_local) - len(old_local))
|
|
93
78
|
|
|
94
79
|
for handler_cls, method_name, condition, priority in hooks:
|
|
95
|
-
logger.debug(
|
|
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
80
|
if condition is not None:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
" [cond-info] %s.%s → %r",
|
|
110
|
-
handler_cls.__name__,
|
|
111
|
-
method_name,
|
|
112
|
-
cond_info,
|
|
113
|
-
)
|
|
81
|
+
checks = [condition.check(n, o) for n, o in zip(new_local, old_local)]
|
|
82
|
+
if not any(checks):
|
|
83
|
+
continue
|
|
114
84
|
|
|
115
|
-
checks = []
|
|
116
|
-
for new, old in zip(new_records_local, old_records_local):
|
|
117
|
-
field_name = getattr(condition, "field", None) or getattr(
|
|
118
|
-
condition, "field_name", None
|
|
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
|
-
)
|
|
162
|
-
|
|
163
|
-
if not passed:
|
|
164
|
-
logger.debug(
|
|
165
|
-
"↳ skipping %s.%s (condition not met)",
|
|
166
|
-
handler_cls.__name__,
|
|
167
|
-
method_name,
|
|
168
|
-
)
|
|
169
|
-
continue
|
|
170
|
-
|
|
171
|
-
# Instantiate & invoke handler method
|
|
172
85
|
handler = handler_cls()
|
|
173
86
|
method = getattr(handler, method_name)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
handler_cls.__name__,
|
|
177
|
-
method_name,
|
|
178
|
-
len(new_records_local),
|
|
179
|
-
)
|
|
87
|
+
|
|
88
|
+
logger.info("Running hook %s.%s on %d items", handler_cls.__name__, method_name, len(new_local))
|
|
180
89
|
try:
|
|
181
90
|
method(
|
|
182
|
-
new_records=
|
|
183
|
-
old_records=
|
|
91
|
+
new_records=new_local,
|
|
92
|
+
old_records=old_local,
|
|
184
93
|
**kwargs,
|
|
185
94
|
)
|
|
186
95
|
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
|
-
)
|
|
96
|
+
logger.exception("Error in hook %s.%s", handler_cls.__name__, method_name)
|
|
198
97
|
|
|
199
|
-
# Defer if in atomic block and event is after_*
|
|
200
98
|
conn = transaction.get_connection()
|
|
201
99
|
if conn.in_atomic_block and event.startswith("after_"):
|
|
202
|
-
|
|
203
|
-
"Deferring hook execution until after transaction commit for event '%s'",
|
|
204
|
-
event,
|
|
205
|
-
)
|
|
206
|
-
transaction.on_commit(_process)
|
|
100
|
+
transaction.on_commit(_execute)
|
|
207
101
|
else:
|
|
208
|
-
|
|
102
|
+
_execute()
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
|
-
from django.db.models.manager import BaseManager
|
|
3
2
|
|
|
4
3
|
|
|
5
4
|
class LifecycleQuerySet(models.QuerySet):
|
|
@@ -20,8 +19,7 @@ class LifecycleQuerySet(models.QuerySet):
|
|
|
20
19
|
for field, value in kwargs.items():
|
|
21
20
|
setattr(obj, field, value)
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
self.model._base_manager,
|
|
22
|
+
self.model.objects.bulk_update(
|
|
25
23
|
instances,
|
|
26
24
|
fields=list(kwargs.keys()),
|
|
27
25
|
)
|
|
@@ -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.54
|
|
4
4
|
Summary: Lifecycle-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
|
|
|
@@ -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=P1HuX9pQpLmMMmrKfk30HHpfz-TaKzB-MMIOGlIYIR8,3608
|
|
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.54.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.54.dist-info/METADATA,sha256=Rg8LRCTFZmtVZFl1q0xfOV4Yq8UqFUKXTPrGO5OS8ak,3101
|
|
16
|
+
django_bulk_hooks-0.1.54.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.54.dist-info/RECORD,,
|
|
File without changes
|