django-bulk-hooks 0.1.231__py3-none-any.whl → 0.1.232__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.

@@ -1,201 +1,34 @@
1
1
  import logging
2
- import threading
3
2
  from collections.abc import Callable
4
- from contextlib import contextmanager
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
- # Key: (ModelClass, event)
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: type,
22
- event: str,
23
- handler_cls: type,
24
- method_name: str,
25
- condition: Optional[Callable],
26
- priority: Union[int, Priority],
27
- ) -> None:
28
- """
29
- Register a hook for a specific model and event.
30
-
31
- Args:
32
- model: The Django model class
33
- event: The hook event (e.g., 'before_create', 'after_update')
34
- handler_cls: The hook handler class
35
- method_name: The method name in the handler class
36
- condition: Optional condition for when the hook should run
37
- priority: Hook execution priority (higher numbers execute first)
38
- """
39
- if not model or not event or not handler_cls or not method_name:
40
- logger.warning("Invalid hook registration parameters")
41
- return
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.231
3
+ Version: 0.1.232
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -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('balance'))
63
- def _notify_balance_change(self, new_records, old_records, **kwargs):
64
- for new_record, old_record in zip(new_records, old_records):
65
- if old_record and new_record.balance != old_record.balance:
66
- print(f"Balance changed from {old_record.balance} to {new_record.balance}")
67
- ```
68
-
69
- ### Bulk Operations with Hooks
70
-
71
- ```python
72
- # For complete hook execution, use the update() method
73
- accounts = Account.objects.filter(active=True)
74
- accounts.update(balance=1000) # Runs all hooks automatically
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=YSDCMAf24YLeLJrRKCDYwbZInP2mK_Tcuw3EHLDkv_w,32605
13
+ django_bulk_hooks/registry.py,sha256=8UuhniiH5ChSeOKV1UUbqTEiIu25bZXvcHmkaRbxmME,1131
14
+ django_bulk_hooks-0.1.232.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.232.dist-info/METADATA,sha256=f8DOScqSoznpOeAOoIYha3lTiWcDp0aEk8lRihqKjiM,9061
16
+ django_bulk_hooks-0.1.232.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.232.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,,