django-bulk-hooks 0.2.41__py3-none-any.whl → 0.2.42__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,298 +1,277 @@
1
- """
2
- Central registry for hook handlers.
3
-
4
- Provides thread-safe registration and lookup of hooks with
5
- deterministic priority ordering.
6
- """
7
-
8
- import logging
9
- import threading
10
- from collections.abc import Callable
11
-
12
- from django_bulk_hooks.enums import Priority
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- # Type alias for hook info tuple
17
- HookInfo = tuple[type, str, Callable | None, int]
18
-
19
-
20
- class HookRegistry:
21
- """
22
- Central registry for all hook handlers.
23
-
24
- Manages registration, lookup, and lifecycle of hooks with
25
- thread-safe operations and deterministic ordering by priority.
26
-
27
- This is a singleton - use get_registry() to access the instance.
28
- """
29
-
30
- def __init__(self):
31
- """Initialize an empty registry with thread-safe storage."""
32
- self._hooks: dict[tuple[type, str], list[HookInfo]] = {}
33
- self._lock = threading.RLock()
34
-
35
- def register(
36
- self,
37
- model: type,
38
- event: str,
39
- handler_cls: type,
40
- method_name: str,
41
- condition: Callable | None,
42
- priority: int | Priority,
43
- ) -> None:
44
- """
45
- Register a hook handler for a model and event.
46
-
47
- Args:
48
- model: Django model class
49
- event: Event name (e.g., 'after_update', 'before_create')
50
- handler_cls: Hook handler class
51
- method_name: Name of the method to call on handler
52
- condition: Optional condition to filter records
53
- priority: Execution priority (lower values execute first)
54
- """
55
- with self._lock:
56
- key = (model, event)
57
- hooks = self._hooks.setdefault(key, [])
58
-
59
- # Check for duplicates before adding
60
- hook_info = (handler_cls, method_name, condition, priority)
61
- if hook_info not in hooks:
62
- hooks.append(hook_info)
63
- # Sort by priority (lower values first)
64
- hooks.sort(key=lambda x: x[3])
65
- logger.debug(
66
- f"Registered {handler_cls.__name__}.{method_name} for {model.__name__}.{event} (priority={priority})",
67
- )
68
- else:
69
- logger.debug(
70
- f"Hook {handler_cls.__name__}.{method_name} already registered for {model.__name__}.{event}",
71
- )
72
-
73
- def get_hooks(self, model: type, event: str) -> list[HookInfo]:
74
- """
75
- Get all hooks for a model and event.
76
-
77
- Args:
78
- model: Django model class
79
- event: Event name
80
-
81
- Returns:
82
- List of hook info tuples (handler_cls, method_name, condition, priority)
83
- sorted by priority (lower values first)
84
- """
85
- with self._lock:
86
- key = (model, event)
87
- hooks = self._hooks.get(key, [])
88
-
89
- # Only log when hooks are found or for specific events to reduce noise
90
- if hooks or event in [
91
- "after_update",
92
- "before_update",
93
- "after_create",
94
- "before_create",
95
- ]:
96
- logger.debug(
97
- f"get_hooks {model.__name__}.{event} found {len(hooks)} hooks",
98
- )
99
-
100
- return hooks
101
-
102
- def unregister(
103
- self,
104
- model: type,
105
- event: str,
106
- handler_cls: type,
107
- method_name: str,
108
- ) -> None:
109
- """
110
- Unregister a specific hook handler.
111
-
112
- Used when child classes override parent hook methods.
113
-
114
- Args:
115
- model: Django model class
116
- event: Event name
117
- handler_cls: Hook handler class to remove
118
- method_name: Method name to remove
119
- """
120
- with self._lock:
121
- key = (model, event)
122
- if key not in self._hooks:
123
- return
124
-
125
- hooks = self._hooks[key]
126
- # Filter out the specific hook
127
- self._hooks[key] = [
128
- (h_cls, m_name, cond, pri) for h_cls, m_name, cond, pri in hooks if not (h_cls == handler_cls and m_name == method_name)
129
- ]
130
-
131
- # Clean up empty hook lists
132
- if not self._hooks[key]:
133
- del self._hooks[key]
134
-
135
- logger.debug(
136
- f"Unregistered {handler_cls.__name__}.{method_name} for {model.__name__}.{event}",
137
- )
138
-
139
- def clear(self) -> None:
140
- """
141
- Clear all registered hooks.
142
-
143
- Useful for testing to ensure clean state between tests.
144
- """
145
- with self._lock:
146
- self._hooks.clear()
147
-
148
- # Also clear HookMeta state to ensure complete reset
149
- from django_bulk_hooks.handler import HookMeta
150
-
151
- HookMeta._registered.clear()
152
- HookMeta._class_hook_map.clear()
153
-
154
- logger.debug("Cleared all registered hooks")
155
-
156
- def list_all(self) -> dict[tuple[type, str], list[HookInfo]]:
157
- """
158
- Get all registered hooks for debugging.
159
-
160
- Returns:
161
- Dictionary mapping (model, event) tuples to lists of hook info
162
- """
163
- with self._lock:
164
- return dict(self._hooks)
165
-
166
- @property
167
- def hooks(self) -> dict[tuple[type, str], list[HookInfo]]:
168
- """
169
- Expose internal hooks dictionary for testing purposes.
170
-
171
- This property provides direct access to the internal hooks storage
172
- to allow tests to clear the registry state between test runs.
173
- """
174
- return self._hooks
175
-
176
- def count_hooks(
177
- self,
178
- model: type | None = None,
179
- event: str | None = None,
180
- ) -> int:
181
- """
182
- Count registered hooks, optionally filtered by model and/or event.
183
-
184
- Args:
185
- model: Optional model class to filter by
186
- event: Optional event name to filter by
187
-
188
- Returns:
189
- Number of matching hooks
190
- """
191
- with self._lock:
192
- if model is None and event is None:
193
- # Count all hooks
194
- return sum(len(hooks) for hooks in self._hooks.values())
195
- if model is not None and event is not None:
196
- # Count hooks for specific model and event
197
- return len(self._hooks.get((model, event), []))
198
- if model is not None:
199
- # Count all hooks for a model
200
- return sum(len(hooks) for (m, _), hooks in self._hooks.items() if m == model)
201
- # event is not None
202
- # Count all hooks for an event
203
- return sum(len(hooks) for (_, e), hooks in self._hooks.items() if e == event)
204
-
205
-
206
- # Global singleton registry
207
- _registry: HookRegistry | None = None
208
- _registry_lock = threading.Lock()
209
-
210
-
211
- def get_registry() -> HookRegistry:
212
- """
213
- Get the global hook registry instance.
214
-
215
- Creates the registry on first access (singleton pattern).
216
- Thread-safe initialization.
217
-
218
- Returns:
219
- HookRegistry singleton instance
220
- """
221
- global _registry
222
-
223
- if _registry is None:
224
- with _registry_lock:
225
- # Double-checked locking
226
- if _registry is None:
227
- _registry = HookRegistry()
228
-
229
- return _registry
230
-
231
-
232
- # Backward-compatible module-level functions
233
- def register_hook(
234
- model: type,
235
- event: str,
236
- handler_cls: type,
237
- method_name: str,
238
- condition: Callable | None,
239
- priority: int | Priority,
240
- ) -> None:
241
- """
242
- Register a hook handler (backward-compatible function).
243
-
244
- Delegates to the global registry instance.
245
- """
246
- registry = get_registry()
247
- registry.register(model, event, handler_cls, method_name, condition, priority)
248
-
249
-
250
- def get_hooks(model: type, event: str) -> list[HookInfo]:
251
- """
252
- Get hooks for a model and event (backward-compatible function).
253
-
254
- Delegates to the global registry instance.
255
- """
256
- registry = get_registry()
257
- return registry.get_hooks(model, event)
258
-
259
-
260
- def unregister_hook(
261
- model: type,
262
- event: str,
263
- handler_cls: type,
264
- method_name: str,
265
- ) -> None:
266
- """
267
- Unregister a hook handler (backward-compatible function).
268
-
269
- Delegates to the global registry instance.
270
- """
271
- registry = get_registry()
272
- registry.unregister(model, event, handler_cls, method_name)
273
-
274
-
275
- def clear_hooks() -> None:
276
- """
277
- Clear all registered hooks (backward-compatible function).
278
-
279
- Delegates to the global registry instance.
280
- Useful for testing.
281
- """
282
- registry = get_registry()
283
- registry.clear()
284
-
285
-
286
- def list_all_hooks() -> dict[tuple[type, str], list[HookInfo]]:
287
- """
288
- List all registered hooks (backward-compatible function).
289
-
290
- Delegates to the global registry instance.
291
- """
292
- registry = get_registry()
293
- return registry.list_all()
294
-
295
-
296
- # Expose hooks dictionary for testing purposes
297
- # This provides backward compatibility with tests that expect to access _hooks directly
298
- _hooks = get_registry().hooks
1
+ """
2
+ Central registry for hook handlers.
3
+
4
+ Provides thread-safe registration and lookup of hooks with
5
+ deterministic priority ordering.
6
+ """
7
+
8
+ import logging
9
+ import threading
10
+ from collections.abc import Callable
11
+
12
+ from django_bulk_hooks.enums import Priority
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Type alias for hook info tuple
17
+ HookInfo = tuple[type, str, Callable | None, int]
18
+
19
+
20
+ class HookRegistry:
21
+ """
22
+ Central registry for all hook handlers.
23
+
24
+ Manages registration, lookup, and lifecycle of hooks with
25
+ thread-safe operations and deterministic ordering by priority.
26
+
27
+ This is a singleton - use get_registry() to access the instance.
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize an empty registry with thread-safe storage."""
32
+ self._hooks: dict[tuple[type, str], list[HookInfo]] = {}
33
+ self._lock = threading.RLock()
34
+
35
+ def register(
36
+ self,
37
+ model: type,
38
+ event: str,
39
+ handler_cls: type,
40
+ method_name: str,
41
+ condition: Callable | None,
42
+ priority: int | Priority,
43
+ ) -> None:
44
+ """
45
+ Register a hook handler for a model and event.
46
+
47
+ Args:
48
+ model: Django model class
49
+ event: Event name (e.g., 'after_update', 'before_create')
50
+ handler_cls: Hook handler class
51
+ method_name: Name of the method to call on handler
52
+ condition: Optional condition to filter records
53
+ priority: Execution priority (lower values execute first)
54
+ """
55
+ with self._lock:
56
+ key = (model, event)
57
+ hooks = self._hooks.setdefault(key, [])
58
+
59
+ # Check for duplicates before adding
60
+ hook_info = (handler_cls, method_name, condition, priority)
61
+ if hook_info not in hooks:
62
+ hooks.append(hook_info)
63
+ # Sort by priority (lower values first)
64
+ hooks.sort(key=lambda x: x[3])
65
+ else:
66
+ pass # Hook already registered
67
+
68
+ def get_hooks(self, model: type, event: str) -> list[HookInfo]:
69
+ """
70
+ Get all hooks for a model and event.
71
+
72
+ Args:
73
+ model: Django model class
74
+ event: Event name
75
+
76
+ Returns:
77
+ List of hook info tuples (handler_cls, method_name, condition, priority)
78
+ sorted by priority (lower values first)
79
+ """
80
+ with self._lock:
81
+ key = (model, event)
82
+ hooks = self._hooks.get(key, [])
83
+ return hooks
84
+
85
+ def unregister(
86
+ self,
87
+ model: type,
88
+ event: str,
89
+ handler_cls: type,
90
+ method_name: str,
91
+ ) -> None:
92
+ """
93
+ Unregister a specific hook handler.
94
+
95
+ Used when child classes override parent hook methods.
96
+
97
+ Args:
98
+ model: Django model class
99
+ event: Event name
100
+ handler_cls: Hook handler class to remove
101
+ method_name: Method name to remove
102
+ """
103
+ with self._lock:
104
+ key = (model, event)
105
+ if key not in self._hooks:
106
+ return
107
+
108
+ hooks = self._hooks[key]
109
+ # Filter out the specific hook
110
+ self._hooks[key] = [
111
+ (h_cls, m_name, cond, pri) for h_cls, m_name, cond, pri in hooks if not (h_cls == handler_cls and m_name == method_name)
112
+ ]
113
+
114
+ # Clean up empty hook lists
115
+ if not self._hooks[key]:
116
+ del self._hooks[key]
117
+
118
+
119
+ def clear(self) -> None:
120
+ """
121
+ Clear all registered hooks.
122
+
123
+ Useful for testing to ensure clean state between tests.
124
+ """
125
+ with self._lock:
126
+ self._hooks.clear()
127
+
128
+ # Also clear HookMeta state to ensure complete reset
129
+ from django_bulk_hooks.handler import HookMeta
130
+
131
+ HookMeta._registered.clear()
132
+ HookMeta._class_hook_map.clear()
133
+
134
+
135
+ def list_all(self) -> dict[tuple[type, str], list[HookInfo]]:
136
+ """
137
+ Get all registered hooks for debugging.
138
+
139
+ Returns:
140
+ Dictionary mapping (model, event) tuples to lists of hook info
141
+ """
142
+ with self._lock:
143
+ return dict(self._hooks)
144
+
145
+ @property
146
+ def hooks(self) -> dict[tuple[type, str], list[HookInfo]]:
147
+ """
148
+ Expose internal hooks dictionary for testing purposes.
149
+
150
+ This property provides direct access to the internal hooks storage
151
+ to allow tests to clear the registry state between test runs.
152
+ """
153
+ return self._hooks
154
+
155
+ def count_hooks(
156
+ self,
157
+ model: type | None = None,
158
+ event: str | None = None,
159
+ ) -> int:
160
+ """
161
+ Count registered hooks, optionally filtered by model and/or event.
162
+
163
+ Args:
164
+ model: Optional model class to filter by
165
+ event: Optional event name to filter by
166
+
167
+ Returns:
168
+ Number of matching hooks
169
+ """
170
+ with self._lock:
171
+ if model is None and event is None:
172
+ # Count all hooks
173
+ return sum(len(hooks) for hooks in self._hooks.values())
174
+ if model is not None and event is not None:
175
+ # Count hooks for specific model and event
176
+ return len(self._hooks.get((model, event), []))
177
+ if model is not None:
178
+ # Count all hooks for a model
179
+ return sum(len(hooks) for (m, _), hooks in self._hooks.items() if m == model)
180
+ # event is not None
181
+ # Count all hooks for an event
182
+ return sum(len(hooks) for (_, e), hooks in self._hooks.items() if e == event)
183
+
184
+
185
+ # Global singleton registry
186
+ _registry: HookRegistry | None = None
187
+ _registry_lock = threading.Lock()
188
+
189
+
190
+ def get_registry() -> HookRegistry:
191
+ """
192
+ Get the global hook registry instance.
193
+
194
+ Creates the registry on first access (singleton pattern).
195
+ Thread-safe initialization.
196
+
197
+ Returns:
198
+ HookRegistry singleton instance
199
+ """
200
+ global _registry
201
+
202
+ if _registry is None:
203
+ with _registry_lock:
204
+ # Double-checked locking
205
+ if _registry is None:
206
+ _registry = HookRegistry()
207
+
208
+ return _registry
209
+
210
+
211
+ # Backward-compatible module-level functions
212
+ def register_hook(
213
+ model: type,
214
+ event: str,
215
+ handler_cls: type,
216
+ method_name: str,
217
+ condition: Callable | None,
218
+ priority: int | Priority,
219
+ ) -> None:
220
+ """
221
+ Register a hook handler (backward-compatible function).
222
+
223
+ Delegates to the global registry instance.
224
+ """
225
+ registry = get_registry()
226
+ registry.register(model, event, handler_cls, method_name, condition, priority)
227
+
228
+
229
+ def get_hooks(model: type, event: str) -> list[HookInfo]:
230
+ """
231
+ Get hooks for a model and event (backward-compatible function).
232
+
233
+ Delegates to the global registry instance.
234
+ """
235
+ registry = get_registry()
236
+ return registry.get_hooks(model, event)
237
+
238
+
239
+ def unregister_hook(
240
+ model: type,
241
+ event: str,
242
+ handler_cls: type,
243
+ method_name: str,
244
+ ) -> None:
245
+ """
246
+ Unregister a hook handler (backward-compatible function).
247
+
248
+ Delegates to the global registry instance.
249
+ """
250
+ registry = get_registry()
251
+ registry.unregister(model, event, handler_cls, method_name)
252
+
253
+
254
+ def clear_hooks() -> None:
255
+ """
256
+ Clear all registered hooks (backward-compatible function).
257
+
258
+ Delegates to the global registry instance.
259
+ Useful for testing.
260
+ """
261
+ registry = get_registry()
262
+ registry.clear()
263
+
264
+
265
+ def list_all_hooks() -> dict[tuple[type, str], list[HookInfo]]:
266
+ """
267
+ List all registered hooks (backward-compatible function).
268
+
269
+ Delegates to the global registry instance.
270
+ """
271
+ registry = get_registry()
272
+ return registry.list_all()
273
+
274
+
275
+ # Expose hooks dictionary for testing purposes
276
+ # This provides backward compatibility with tests that expect to access _hooks directly
277
+ _hooks = get_registry().hooks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.41
3
+ Version: 0.2.42
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
@@ -4,23 +4,23 @@ django_bulk_hooks/conditions.py,sha256=v2DMFmWI7bppBQw5qdbO5CmQRN_QtUwnBjcyKBJLL
4
4
  django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
5
5
  django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
6
6
  django_bulk_hooks/decorators.py,sha256=P7cvzFgORJRW-YQHNAxNXqQOP9OywBmA7Rz9kiJoxUk,12237
7
- django_bulk_hooks/dispatcher.py,sha256=hO_GlL5_m1Os9xQRz5IP7Jw0tQGSDVEyp_LOg2afgzA,9097
7
+ django_bulk_hooks/dispatcher.py,sha256=CiKYe5ecUPu5TYUZq8ToaRT40TkLc5l5mczgf5XDzGA,8217
8
8
  django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
9
- django_bulk_hooks/factory.py,sha256=OgrOmLbIzhrSKTDx06oGMAVsEb0NoVOmW5IdLsMz_Qs,19938
10
- django_bulk_hooks/handler.py,sha256=i0M4mdx3vgXIb8mA1S5ZBW_8ezECd8yTZaj9QNUm8P8,4738
9
+ django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
10
+ django_bulk_hooks/handler.py,sha256=38ejMdQ9reYA07_XQ9tC8xv0lW3amO-m8gPzuRNOyj0,4200
11
11
  django_bulk_hooks/helpers.py,sha256=Nw8eXryLUUquW7AgiuKp0PQT3Pq6HAHsdP-xAtqhmjA,3216
12
12
  django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,4068
13
13
  django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
14
14
  django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
15
  django_bulk_hooks/operations/analyzer.py,sha256=dXcgk99Q9Zv7r2PMNIQE9f-hkPW3rGKXnDw28r3C7IE,10782
16
- django_bulk_hooks/operations/bulk_executor.py,sha256=gvIIssYJMNk4advEZySI3Q204vwHq2P8v8nGV8Y05_0,25851
16
+ django_bulk_hooks/operations/bulk_executor.py,sha256=ARe1Hz4N0BaCJTDaqOn9xQsdbYbs1yOJBtD4Y3VHT5s,21605
17
17
  django_bulk_hooks/operations/coordinator.py,sha256=Fx332N_mDpvdicmnOuc_NNsiOpJQ6aI09UbF_Rt6zeU,24196
18
- django_bulk_hooks/operations/mti_handler.py,sha256=G-pxkzIqHqXGshRGksqmsN1J3rlzePUZrSv4wm7D3cQ,19162
18
+ django_bulk_hooks/operations/mti_handler.py,sha256=qWQIwSzd-A6BYbuaRE6RwNbnKAwjGBGxb_aj40xrqkQ,19291
19
19
  django_bulk_hooks/operations/mti_plans.py,sha256=YP7LcV9Z8UqNS_x74OswF9_5swqruRTdAu6z-J_R6C0,3377
20
20
  django_bulk_hooks/operations/record_classifier.py,sha256=KzUoAhfoqzFVrOabNZAby9Akb54h-fAQZmb8O-fIx_0,6221
21
- django_bulk_hooks/queryset.py,sha256=rvJgQLwtSJztwc68nkJ6xwCsnbXQvkvS6_dbGGj8TFo,5886
22
- django_bulk_hooks/registry.py,sha256=QyeA2OqNdMAMaLjFU9UF0YGhKiPKbZkmFQpLgof7uNs,9038
23
- django_bulk_hooks-0.2.41.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
24
- django_bulk_hooks-0.2.41.dist-info/METADATA,sha256=4doHCie9L9l_mrtx5hOLXvESxdnJuhyg5WGcAN_0qKU,9265
25
- django_bulk_hooks-0.2.41.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
26
- django_bulk_hooks-0.2.41.dist-info/RECORD,,
21
+ django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
22
+ django_bulk_hooks/registry.py,sha256=0xm21in-DXlpE12AzCNjbY4q2xePvrZ1orJDSH6sOr4,7862
23
+ django_bulk_hooks-0.2.42.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
24
+ django_bulk_hooks-0.2.42.dist-info/METADATA,sha256=qYFJki0b5mZnpSHTIKv003xNACxaw5MXUqYdAvLRsiY,9265
25
+ django_bulk_hooks-0.2.42.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
26
+ django_bulk_hooks-0.2.42.dist-info/RECORD,,