django-bulk-hooks 0.1.231__py3-none-any.whl → 0.1.233__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 +1 -12
- django_bulk_hooks/conditions.py +30 -33
- django_bulk_hooks/context.py +15 -43
- django_bulk_hooks/decorators.py +8 -111
- django_bulk_hooks/engine.py +41 -127
- django_bulk_hooks/enums.py +13 -10
- django_bulk_hooks/handler.py +73 -40
- django_bulk_hooks/manager.py +101 -123
- django_bulk_hooks/models.py +15 -51
- django_bulk_hooks/priority.py +6 -6
- django_bulk_hooks/queryset.py +181 -306
- django_bulk_hooks/registry.py +24 -191
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/METADATA +16 -32
- django_bulk_hooks-0.1.233.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.231.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/LICENSE +0 -0
django_bulk_hooks/registry.py
CHANGED
|
@@ -1,201 +1,34 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import threading
|
|
3
2
|
from collections.abc import Callable
|
|
4
|
-
from
|
|
5
|
-
from typing import Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
3
|
+
from typing import Union
|
|
6
4
|
|
|
7
5
|
from django_bulk_hooks.priority import Priority
|
|
8
6
|
|
|
9
7
|
logger = logging.getLogger(__name__)
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
# Value: list of tuples (handler_cls, method_name, condition_callable, priority)
|
|
13
|
-
_hooks: Dict[Tuple[type, str], List[Tuple[type, str, Callable, int]]] = {}
|
|
14
|
-
_registration_counter = 0 # secondary stable ordering for tie-breaks
|
|
15
|
-
|
|
16
|
-
# Registry lock for thread-safety during registration and clearing
|
|
17
|
-
_lock = threading.RLock()
|
|
9
|
+
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
18
10
|
|
|
19
11
|
|
|
20
12
|
def register_hook(
|
|
21
|
-
model:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
condition
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Normalize event to str just in case enums are used upstream
|
|
44
|
-
event = str(event)
|
|
45
|
-
|
|
46
|
-
with _lock:
|
|
47
|
-
global _registration_counter
|
|
48
|
-
_registration_counter += 1
|
|
49
|
-
key = (model, event)
|
|
50
|
-
hooks = _hooks.setdefault(key, [])
|
|
51
|
-
|
|
52
|
-
# Check for duplicate registrations
|
|
53
|
-
duplicate = any(h[0] == handler_cls and h[1] == method_name for h in hooks)
|
|
54
|
-
if duplicate:
|
|
55
|
-
logger.warning(
|
|
56
|
-
"Hook %s.%s already registered for %s.%s",
|
|
57
|
-
handler_cls.__name__,
|
|
58
|
-
method_name,
|
|
59
|
-
model.__name__,
|
|
60
|
-
event,
|
|
61
|
-
)
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
# Add the hook (append preserves registration order)
|
|
65
|
-
hooks.append((handler_cls, method_name, condition, priority))
|
|
66
|
-
|
|
67
|
-
# Sort by priority (highest numbers execute first)
|
|
68
|
-
# Stable sort by priority (desc) and then by original registration order (stable append)
|
|
69
|
-
def sort_key(hook_info: Tuple[type, str, Callable, int]) -> int:
|
|
70
|
-
p = hook_info[3]
|
|
71
|
-
return p.value if hasattr(p, "value") else int(p)
|
|
72
|
-
|
|
73
|
-
hooks.sort(key=sort_key, reverse=True)
|
|
74
|
-
|
|
75
|
-
logger.debug(
|
|
76
|
-
"Registered %s.%s for %s.%s with priority %s",
|
|
77
|
-
handler_cls.__name__,
|
|
78
|
-
method_name,
|
|
79
|
-
model.__name__,
|
|
80
|
-
event,
|
|
81
|
-
priority,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def get_hooks(model: type, event: str):
|
|
86
|
-
"""
|
|
87
|
-
Get all registered hooks for a specific model and event.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
model: The Django model class
|
|
91
|
-
event: The hook event
|
|
92
|
-
|
|
93
|
-
Returns:
|
|
94
|
-
List of (handler_cls, method_name, condition, priority) tuples
|
|
95
|
-
"""
|
|
96
|
-
if not model or not event:
|
|
97
|
-
return []
|
|
98
|
-
|
|
99
|
-
event = str(event)
|
|
100
|
-
|
|
101
|
-
with _lock:
|
|
102
|
-
key = (model, event)
|
|
103
|
-
hooks = _hooks.get(key, [])
|
|
104
|
-
|
|
105
|
-
# Log hook discovery for debugging
|
|
106
|
-
if hooks:
|
|
107
|
-
logger.debug("Found %d hooks for %s.%s", len(hooks), model.__name__, event)
|
|
108
|
-
for handler_cls, method_name, condition, priority in hooks:
|
|
109
|
-
logger.debug(" - %s.%s (priority: %s)", handler_cls.__name__, method_name, priority)
|
|
110
|
-
else:
|
|
111
|
-
logger.debug("No hooks found for %s.%s", model.__name__, event)
|
|
112
|
-
|
|
113
|
-
# Return a shallow copy to prevent external mutation of registry state
|
|
114
|
-
return list(hooks)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def list_all_hooks() -> Dict[Tuple[type, str], List[Tuple[type, str, Callable, int]]]:
|
|
118
|
-
"""Debug function to list all registered hooks (shallow copy)."""
|
|
119
|
-
with _lock:
|
|
120
|
-
return {k: list(v) for k, v in _hooks.items()}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def clear_hooks() -> None:
|
|
124
|
-
"""Clear all registered hooks (mainly for testing)."""
|
|
125
|
-
with _lock:
|
|
126
|
-
_hooks.clear()
|
|
127
|
-
logger.debug("All hooks cleared")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def unregister_hook(model: type, event: str, handler_cls: type, method_name: str) -> None:
|
|
131
|
-
"""Unregister a previously registered hook (safe no-op if not present)."""
|
|
132
|
-
event = str(event)
|
|
133
|
-
with _lock:
|
|
134
|
-
key = (model, event)
|
|
135
|
-
if key not in _hooks:
|
|
136
|
-
return
|
|
137
|
-
_hooks[key] = [
|
|
138
|
-
h for h in _hooks[key] if not (h[0] == handler_cls and h[1] == method_name)
|
|
139
|
-
]
|
|
140
|
-
if not _hooks[key]:
|
|
141
|
-
del _hooks[key]
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@contextmanager
|
|
145
|
-
def isolated_registry() -> Iterator[None]:
|
|
146
|
-
"""
|
|
147
|
-
Context manager that snapshots the hook registry and restores it on exit.
|
|
148
|
-
|
|
149
|
-
Useful for tests to avoid global cross-test interference without relying on
|
|
150
|
-
private state mutation from the outside.
|
|
151
|
-
"""
|
|
152
|
-
with _lock:
|
|
153
|
-
snapshot = {k: list(v) for k, v in _hooks.items()}
|
|
154
|
-
try:
|
|
155
|
-
yield
|
|
156
|
-
finally:
|
|
157
|
-
with _lock:
|
|
158
|
-
_hooks.clear()
|
|
159
|
-
_hooks.update({k: list(v) for k, v in snapshot.items()})
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@contextmanager
|
|
163
|
-
def temporary_hook(
|
|
164
|
-
model: type,
|
|
165
|
-
event: str,
|
|
166
|
-
handler_cls: type,
|
|
167
|
-
method_name: str,
|
|
168
|
-
condition: Optional[Callable] = None,
|
|
169
|
-
priority: Union[int, Priority] = Priority.NORMAL,
|
|
170
|
-
) -> Iterator[None]:
|
|
171
|
-
"""
|
|
172
|
-
Temporarily register a single hook for the duration of the context.
|
|
173
|
-
|
|
174
|
-
Ensures the hook is unregistered even if an exception occurs.
|
|
175
|
-
"""
|
|
176
|
-
register_hook(model, event, handler_cls, method_name, condition, priority)
|
|
177
|
-
try:
|
|
178
|
-
yield
|
|
179
|
-
finally:
|
|
180
|
-
unregister_hook(model, event, handler_cls, method_name)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@contextmanager
|
|
184
|
-
def temporary_hooks(
|
|
185
|
-
registrations: Iterable[Tuple[type, str, type, str, Optional[Callable], Union[int, Priority]]]
|
|
186
|
-
) -> Iterator[None]:
|
|
187
|
-
"""
|
|
188
|
-
Temporarily register multiple hooks for the duration of the context.
|
|
189
|
-
|
|
190
|
-
Args:
|
|
191
|
-
registrations: Iterable of (model, event, handler_cls, method_name, condition, priority)
|
|
192
|
-
"""
|
|
193
|
-
# Register all
|
|
194
|
-
for model, event, handler_cls, method_name, condition, priority in registrations:
|
|
195
|
-
register_hook(model, event, handler_cls, method_name, condition, priority)
|
|
196
|
-
try:
|
|
197
|
-
yield
|
|
198
|
-
finally:
|
|
199
|
-
# Best-effort unregister all in reverse order
|
|
200
|
-
for model, event, handler_cls, method_name, _, _ in reversed(list(registrations)):
|
|
201
|
-
unregister_hook(model, event, handler_cls, method_name)
|
|
13
|
+
model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
|
|
14
|
+
):
|
|
15
|
+
key = (model, event)
|
|
16
|
+
hooks = _hooks.setdefault(key, [])
|
|
17
|
+
hooks.append((handler_cls, method_name, condition, priority))
|
|
18
|
+
# keep sorted by priority
|
|
19
|
+
hooks.sort(key=lambda x: x[3])
|
|
20
|
+
logger.debug(f"Registered {handler_cls.__name__}.{method_name} for {model.__name__}.{event}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_hooks(model, event):
|
|
24
|
+
key = (model, event)
|
|
25
|
+
hooks = _hooks.get(key, [])
|
|
26
|
+
# Only log when hooks are found or for specific events to reduce noise
|
|
27
|
+
if hooks or event in ['after_update', 'before_update', 'after_create', 'before_create']:
|
|
28
|
+
logger.debug(f"get_hooks {model.__name__}.{event} found {len(hooks)} hooks")
|
|
29
|
+
return hooks
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def list_all_hooks():
|
|
33
|
+
"""Debug function to list all registered hooks"""
|
|
34
|
+
return _hooks
|
|
@@ -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.233
|
|
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
|
|
|
@@ -59,37 +59,21 @@ from django_bulk_hooks.conditions import WhenFieldHasChanged
|
|
|
59
59
|
from .models import Account
|
|
60
60
|
|
|
61
61
|
class AccountHooks(Hook):
|
|
62
|
-
@hook(AFTER_UPDATE, condition=WhenFieldHasChanged(
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# For bulk operations with hooks
|
|
77
|
-
accounts = Account.objects.filter(active=True)
|
|
78
|
-
instances = list(accounts)
|
|
79
|
-
|
|
80
|
-
# bulk_update now runs complete hook cycle by default
|
|
81
|
-
accounts.bulk_update(instances, ['balance']) # Runs VALIDATE → BEFORE → DB update → AFTER
|
|
82
|
-
|
|
83
|
-
# To skip hooks (for performance or when called from update())
|
|
84
|
-
accounts.bulk_update(instances, ['balance'], bypass_hooks=True)
|
|
62
|
+
@hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
63
|
+
def log_balance_change(self, new_records, old_records):
|
|
64
|
+
print("Accounts updated:", [a.pk for a in new_records])
|
|
65
|
+
|
|
66
|
+
@hook(BEFORE_CREATE, model=Account)
|
|
67
|
+
def before_create(self, new_records, old_records):
|
|
68
|
+
for account in new_records:
|
|
69
|
+
if account.balance < 0:
|
|
70
|
+
raise ValueError("Account cannot have negative balance")
|
|
71
|
+
|
|
72
|
+
@hook(AFTER_DELETE, model=Account)
|
|
73
|
+
def after_delete(self, new_records, old_records):
|
|
74
|
+
print("Accounts deleted:", [a.pk for a in old_records])
|
|
85
75
|
```
|
|
86
76
|
|
|
87
|
-
### Understanding Hook Execution
|
|
88
|
-
|
|
89
|
-
- **`update()` method**: Runs complete hook cycle (VALIDATE → BEFORE → DB update → AFTER)
|
|
90
|
-
- **`bulk_update()` method**: Runs complete hook cycle (VALIDATE → BEFORE → DB update → AFTER)
|
|
91
|
-
- **`bypass_hooks=True`**: Skips all hooks for performance or to prevent double execution
|
|
92
|
-
|
|
93
77
|
## 🛠 Supported Hook Events
|
|
94
78
|
|
|
95
79
|
- `BEFORE_CREATE`, `AFTER_CREATE`
|
|
@@ -0,0 +1,17 @@
|
|
|
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=_NbGWTq9s66g0vbFIaqN4GlIHWQmFg3EQ44qY8YvvEg,1537
|
|
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=7ug24q6M457TQwqpwqJHnxct1Hhhp9xnl8zR0cgIzWY,33237
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=8UuhniiH5ChSeOKV1UUbqTEiIu25bZXvcHmkaRbxmME,1131
|
|
14
|
+
django_bulk_hooks-0.1.233.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.233.dist-info/METADATA,sha256=yLbe8bU5rPxGaovxTa9_nKPujSRW-XoXjZ1Ok2GSidg,9049
|
|
16
|
+
django_bulk_hooks-0.1.233.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.233.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=-HwaG02h62oqcjMxlVil6hNJviUaOwjVOerRLPlcNn4,427
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=7C0enCYMgmIP0WO9Sf_rBbmdHC1yORMELAFoyuBcGgs,7169
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
-
django_bulk_hooks/context.py,sha256=L95s1n9N5oBdfdrIA3_zoi0tf7GnDVrQxYaSXPwBkvg,2626
|
|
5
|
-
django_bulk_hooks/decorators.py,sha256=Uxey4ED4x2ky0CVP3yGhn5BUltT1SdDAchU0BcMUgMg,8919
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=5CNvvnfeKxlh0sv55a9_fAFozlID7LEFxel6w0DtwPs,6152
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=ZSYPwHcjlAMrISOHb9sqNjEfxyv4XupoDoe1hn87VJg,499
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=zR1H1qwID4y1sxOZvBD2Vo7RvuclzzuWwA-_tjEMeu0,3955
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=uqmvGlskkzKMERf3iexxXCAaQl0v3Wc45BeN-YmfTnE,4060
|
|
10
|
-
django_bulk_hooks/models.py,sha256=saCZGk-i-ixUq7ds59tl-SdzQ18slFbQKHnOtruxzmc,6428
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=EGFBbRmX_LhwRYFCKzM8I5m8NGCsUEVJp2pfNTcoHe4,378
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=u8Abj_W5cABM9pi-vQG_6zGsekbPI6It3tA_zDHUDC4,37480
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=VyniRXVgeWq-3RmveXzoG3FjvOhXwwm89Z5CzAgkkU4,6648
|
|
14
|
-
django_bulk_hooks-0.1.231.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.231.dist-info/METADATA,sha256=whqmfiC35Q3I9-epd68ca85QQsJlNyVWqpi-_5DY1TM,9743
|
|
16
|
-
django_bulk_hooks-0.1.231.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
-
django_bulk_hooks-0.1.231.dist-info/RECORD,,
|
|
File without changes
|