django-bulk-hooks 0.1.227__py3-none-any.whl → 0.1.229__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/conditions.py +33 -30
- django_bulk_hooks/context.py +43 -15
- django_bulk_hooks/decorators.py +100 -8
- django_bulk_hooks/engine.py +87 -43
- django_bulk_hooks/enums.py +10 -13
- django_bulk_hooks/handler.py +36 -32
- django_bulk_hooks/manager.py +123 -101
- django_bulk_hooks/models.py +42 -11
- django_bulk_hooks/priority.py +6 -6
- django_bulk_hooks/queryset.py +177 -72
- django_bulk_hooks/registry.py +92 -43
- {django_bulk_hooks-0.1.227.dist-info → django_bulk_hooks-0.1.229.dist-info}/METADATA +1 -1
- django_bulk_hooks-0.1.229.dist-info/RECORD +17 -0
- django_bulk_hooks-0.1.227.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.227.dist-info → django_bulk_hooks-0.1.229.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.227.dist-info → django_bulk_hooks-0.1.229.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from django.db import models, transaction
|
|
3
|
+
from django.db import models, transaction, connections
|
|
4
4
|
from django.db.models import AutoField, Case, Value, When
|
|
5
5
|
from django_bulk_hooks import engine
|
|
6
6
|
from django_bulk_hooks.constants import (
|
|
@@ -26,7 +26,7 @@ class HookQuerySetMixin:
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
@transaction.atomic
|
|
29
|
-
def delete(self):
|
|
29
|
+
def delete(self) -> int:
|
|
30
30
|
"""
|
|
31
31
|
Delete objects from the database with complete hook support.
|
|
32
32
|
|
|
@@ -61,7 +61,7 @@ class HookQuerySetMixin:
|
|
|
61
61
|
return result
|
|
62
62
|
|
|
63
63
|
@transaction.atomic
|
|
64
|
-
def update(self, **kwargs):
|
|
64
|
+
def update(self, **kwargs) -> int:
|
|
65
65
|
"""
|
|
66
66
|
Update objects with field values and run complete hook cycle.
|
|
67
67
|
|
|
@@ -72,6 +72,7 @@ class HookQuerySetMixin:
|
|
|
72
72
|
"""
|
|
73
73
|
# Extract custom parameters
|
|
74
74
|
bypass_hooks = kwargs.pop('bypass_hooks', False)
|
|
75
|
+
bypass_validation = kwargs.pop('bypass_validation', False)
|
|
75
76
|
|
|
76
77
|
instances = list(self)
|
|
77
78
|
if not instances:
|
|
@@ -86,41 +87,97 @@ class HookQuerySetMixin:
|
|
|
86
87
|
}
|
|
87
88
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
88
89
|
|
|
89
|
-
#
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
# Identify complex database expressions (Subquery, Case, F, CombinedExpression, etc.)
|
|
91
|
+
complex_fields = {}
|
|
92
|
+
simple_fields = {}
|
|
93
|
+
for field_name, value in kwargs.items():
|
|
94
|
+
is_complex = (
|
|
95
|
+
(hasattr(value, "query") and hasattr(value.query, "model"))
|
|
96
|
+
or (
|
|
97
|
+
hasattr(value, "get_source_expressions")
|
|
98
|
+
and value.get_source_expressions()
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
if is_complex:
|
|
102
|
+
complex_fields[field_name] = value
|
|
103
|
+
else:
|
|
104
|
+
simple_fields[field_name] = value
|
|
105
|
+
has_subquery = bool(complex_fields)
|
|
95
106
|
|
|
96
107
|
# Run hooks only if not bypassed
|
|
97
108
|
if not bypass_hooks:
|
|
109
|
+
ctx = HookContext(model_cls)
|
|
98
110
|
# Run VALIDATE_UPDATE hooks
|
|
99
|
-
|
|
111
|
+
if not bypass_validation:
|
|
112
|
+
engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
|
|
113
|
+
|
|
114
|
+
# Resolve complex expressions in one shot per field and apply values
|
|
115
|
+
if has_subquery:
|
|
116
|
+
# Build annotations for complex fields
|
|
117
|
+
annotations = {f"__computed_{name}": expr for name, expr in complex_fields.items()}
|
|
118
|
+
annotation_aliases = list(annotations.keys())
|
|
119
|
+
if annotations:
|
|
120
|
+
computed_rows = (
|
|
121
|
+
model_cls._base_manager.filter(pk__in=pks)
|
|
122
|
+
.annotate(**annotations)
|
|
123
|
+
.values("pk", *annotation_aliases)
|
|
124
|
+
)
|
|
125
|
+
computed_map = {}
|
|
126
|
+
for row in computed_rows:
|
|
127
|
+
pk = row["pk"]
|
|
128
|
+
field_values = {}
|
|
129
|
+
for fname in complex_fields.keys():
|
|
130
|
+
alias = f"__computed_{fname}"
|
|
131
|
+
field_values[fname] = row.get(alias)
|
|
132
|
+
computed_map[pk] = field_values
|
|
133
|
+
|
|
134
|
+
for instance in instances:
|
|
135
|
+
values_for_instance = computed_map.get(instance.pk, {})
|
|
136
|
+
for fname, fval in values_for_instance.items():
|
|
137
|
+
setattr(instance, fname, fval)
|
|
138
|
+
|
|
139
|
+
# Apply simple values directly
|
|
140
|
+
if simple_fields:
|
|
141
|
+
for obj in instances:
|
|
142
|
+
for field, value in simple_fields.items():
|
|
143
|
+
setattr(obj, field, value)
|
|
100
144
|
|
|
101
|
-
# Run BEFORE_UPDATE hooks
|
|
102
|
-
engine.run(model_cls, BEFORE_UPDATE, instances, originals,
|
|
145
|
+
# Run BEFORE_UPDATE hooks with updated instances
|
|
146
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
103
147
|
|
|
104
|
-
if
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for obj in instances:
|
|
111
|
-
for field, value in kwargs.items():
|
|
112
|
-
setattr(obj, field, value)
|
|
148
|
+
# Determine if model uses MTI
|
|
149
|
+
def _is_mti(m):
|
|
150
|
+
for parent in m._meta.all_parents:
|
|
151
|
+
if parent._meta.concrete_model is not m._meta.concrete_model:
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
113
154
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
155
|
+
is_mti = _is_mti(model_cls)
|
|
156
|
+
|
|
157
|
+
if is_mti:
|
|
158
|
+
# Use MTI-aware bulk update across tables
|
|
117
159
|
fields_to_update = list(kwargs.keys())
|
|
118
|
-
|
|
119
|
-
|
|
160
|
+
result = self._mti_bulk_update(instances, fields_to_update)
|
|
161
|
+
else:
|
|
162
|
+
if has_subquery:
|
|
163
|
+
# For complex expressions on single-table models, use Django's native update
|
|
164
|
+
result = super().update(**kwargs)
|
|
165
|
+
if not bypass_hooks:
|
|
166
|
+
# Reload instances to ensure we have DB-final values
|
|
167
|
+
updated_instances = list(model_cls._base_manager.filter(pk__in=pks))
|
|
168
|
+
updated_map = {obj.pk: obj for obj in updated_instances}
|
|
169
|
+
instances = [updated_map.get(obj.pk, obj) for obj in instances]
|
|
170
|
+
else:
|
|
171
|
+
# Simple updates on single-table models
|
|
172
|
+
base_manager = model_cls._base_manager
|
|
173
|
+
fields_to_update = list(kwargs.keys())
|
|
174
|
+
base_manager.bulk_update(instances, fields_to_update)
|
|
175
|
+
result = len(instances)
|
|
120
176
|
|
|
121
177
|
# Run AFTER_UPDATE hooks only if not bypassed
|
|
122
178
|
if not bypass_hooks:
|
|
123
|
-
|
|
179
|
+
ctx = HookContext(model_cls)
|
|
180
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
124
181
|
|
|
125
182
|
return result
|
|
126
183
|
|
|
@@ -135,7 +192,7 @@ class HookQuerySetMixin:
|
|
|
135
192
|
unique_fields=None,
|
|
136
193
|
bypass_hooks=False,
|
|
137
194
|
bypass_validation=False,
|
|
138
|
-
):
|
|
195
|
+
) -> list:
|
|
139
196
|
"""
|
|
140
197
|
Insert each of the instances into the database with complete hook support.
|
|
141
198
|
|
|
@@ -223,7 +280,7 @@ class HookQuerySetMixin:
|
|
|
223
280
|
@transaction.atomic
|
|
224
281
|
def bulk_update(
|
|
225
282
|
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
226
|
-
):
|
|
283
|
+
) -> int:
|
|
227
284
|
"""
|
|
228
285
|
Bulk update objects in the database with complete hook support.
|
|
229
286
|
|
|
@@ -273,29 +330,43 @@ class HookQuerySetMixin:
|
|
|
273
330
|
|
|
274
331
|
# Run VALIDATE_UPDATE hooks
|
|
275
332
|
if not bypass_validation:
|
|
333
|
+
ctx = HookContext(model_cls)
|
|
276
334
|
engine.run(
|
|
277
|
-
model_cls, VALIDATE_UPDATE, objs, originals,
|
|
335
|
+
model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx
|
|
278
336
|
)
|
|
279
337
|
|
|
280
338
|
# Run BEFORE_UPDATE hooks
|
|
281
339
|
if not bypass_hooks:
|
|
340
|
+
ctx = HookContext(model_cls)
|
|
282
341
|
engine.run(
|
|
283
|
-
model_cls, BEFORE_UPDATE, objs, originals,
|
|
342
|
+
model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx
|
|
284
343
|
)
|
|
285
344
|
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
345
|
+
# Determine if model uses MTI
|
|
346
|
+
def _is_mti(m):
|
|
347
|
+
for parent in m._meta.all_parents:
|
|
348
|
+
if parent._meta.concrete_model is not m._meta.concrete_model:
|
|
349
|
+
return True
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
if _is_mti(model_cls):
|
|
353
|
+
# Use MTI-aware bulk update across tables
|
|
354
|
+
result = self._mti_bulk_update(objs, fields, **kwargs)
|
|
355
|
+
else:
|
|
356
|
+
# Perform database update using Django's native bulk_update
|
|
357
|
+
# We use the base manager to avoid recursion
|
|
358
|
+
base_manager = model_cls._base_manager
|
|
359
|
+
result = base_manager.bulk_update(objs, fields, **kwargs)
|
|
290
360
|
|
|
291
361
|
# Run AFTER_UPDATE hooks
|
|
292
362
|
if not bypass_hooks:
|
|
293
|
-
|
|
363
|
+
ctx = HookContext(model_cls)
|
|
364
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
294
365
|
|
|
295
366
|
return result
|
|
296
367
|
|
|
297
368
|
@transaction.atomic
|
|
298
|
-
def bulk_delete(self, objs, **kwargs):
|
|
369
|
+
def bulk_delete(self, objs, **kwargs) -> int:
|
|
299
370
|
"""
|
|
300
371
|
Delete the given objects from the database with complete hook support.
|
|
301
372
|
|
|
@@ -308,7 +379,7 @@ class HookQuerySetMixin:
|
|
|
308
379
|
model_cls = self.model
|
|
309
380
|
|
|
310
381
|
# Extract custom kwargs
|
|
311
|
-
|
|
382
|
+
kwargs.pop("bypass_hooks", False)
|
|
312
383
|
|
|
313
384
|
# Validate inputs
|
|
314
385
|
if not isinstance(objs, (list, tuple)):
|
|
@@ -433,50 +504,83 @@ class HookQuerySetMixin:
|
|
|
433
504
|
# Then we can use Django's bulk_create for the child objects
|
|
434
505
|
parent_objects_map = {}
|
|
435
506
|
|
|
436
|
-
# Step 1:
|
|
437
|
-
# Get bypass_hooks from kwargs
|
|
507
|
+
# Step 1: Insert into parent tables to get primary keys back
|
|
438
508
|
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
439
509
|
bypass_validation = kwargs.get("bypass_validation", False)
|
|
440
510
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
511
|
+
# If DB supports returning rows from bulk insert, batch per parent model
|
|
512
|
+
supports_returning = connections[self.db].features.can_return_rows_from_bulk_insert
|
|
513
|
+
|
|
514
|
+
if supports_returning:
|
|
515
|
+
# For each parent level in the chain, create instances in batch preserving order
|
|
516
|
+
current_parents_per_obj = {id(obj): None for obj in batch}
|
|
444
517
|
for model_class in inheritance_chain[:-1]:
|
|
445
|
-
|
|
446
|
-
obj, model_class,
|
|
447
|
-
|
|
518
|
+
parent_objs = [
|
|
519
|
+
self._create_parent_instance(obj, model_class, current_parents_per_obj[id(obj)])
|
|
520
|
+
for obj in batch
|
|
521
|
+
]
|
|
448
522
|
|
|
449
|
-
# Fire parent hooks if not bypassed
|
|
450
523
|
if not bypass_hooks:
|
|
451
524
|
ctx = HookContext(model_class)
|
|
452
525
|
if not bypass_validation:
|
|
453
|
-
engine.run(model_class, VALIDATE_CREATE,
|
|
454
|
-
engine.run(model_class, BEFORE_CREATE,
|
|
455
|
-
|
|
456
|
-
# Use Django's base manager to create the object and get PKs back
|
|
457
|
-
# This bypasses hooks and the MTI exception
|
|
458
|
-
field_values = {
|
|
459
|
-
field.name: getattr(parent_obj, field.name)
|
|
460
|
-
for field in model_class._meta.local_fields
|
|
461
|
-
if hasattr(parent_obj, field.name)
|
|
462
|
-
and getattr(parent_obj, field.name) is not None
|
|
463
|
-
}
|
|
464
|
-
created_obj = model_class._base_manager.using(self.db).create(
|
|
465
|
-
**field_values
|
|
466
|
-
)
|
|
526
|
+
engine.run(model_class, VALIDATE_CREATE, parent_objs, ctx=ctx)
|
|
527
|
+
engine.run(model_class, BEFORE_CREATE, parent_objs, ctx=ctx)
|
|
467
528
|
|
|
468
|
-
#
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
529
|
+
# Bulk insert parents using base manager to avoid hook recursion
|
|
530
|
+
created_parents = model_class._base_manager.using(self.db).bulk_create(
|
|
531
|
+
parent_objs, batch_size=len(parent_objs)
|
|
532
|
+
)
|
|
472
533
|
|
|
473
|
-
#
|
|
534
|
+
# After create hooks
|
|
474
535
|
if not bypass_hooks:
|
|
475
|
-
engine.run(model_class, AFTER_CREATE,
|
|
536
|
+
engine.run(model_class, AFTER_CREATE, created_parents, ctx=ctx)
|
|
537
|
+
|
|
538
|
+
# Update maps and state for next parent level
|
|
539
|
+
for obj, parent_obj in zip(batch, created_parents):
|
|
540
|
+
# Ensure state reflects saved
|
|
541
|
+
parent_obj._state.adding = False
|
|
542
|
+
parent_obj._state.db = self.db
|
|
543
|
+
# Record for this object and level
|
|
544
|
+
if id(obj) not in parent_objects_map:
|
|
545
|
+
parent_objects_map[id(obj)] = {}
|
|
546
|
+
parent_objects_map[id(obj)][model_class] = parent_obj
|
|
547
|
+
current_parents_per_obj[id(obj)] = parent_obj
|
|
548
|
+
else:
|
|
549
|
+
# Fallback: per-row parent inserts (original behavior)
|
|
550
|
+
for obj in batch:
|
|
551
|
+
parent_instances = {}
|
|
552
|
+
current_parent = None
|
|
553
|
+
for model_class in inheritance_chain[:-1]:
|
|
554
|
+
parent_obj = self._create_parent_instance(
|
|
555
|
+
obj, model_class, current_parent
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if not bypass_hooks:
|
|
559
|
+
ctx = HookContext(model_class)
|
|
560
|
+
if not bypass_validation:
|
|
561
|
+
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
562
|
+
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
563
|
+
|
|
564
|
+
field_values = {
|
|
565
|
+
field.name: getattr(parent_obj, field.name)
|
|
566
|
+
for field in model_class._meta.local_fields
|
|
567
|
+
if hasattr(parent_obj, field.name)
|
|
568
|
+
and getattr(parent_obj, field.name) is not None
|
|
569
|
+
}
|
|
570
|
+
created_obj = model_class._base_manager.using(self.db).create(
|
|
571
|
+
**field_values
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
parent_obj.pk = created_obj.pk
|
|
575
|
+
parent_obj._state.adding = False
|
|
576
|
+
parent_obj._state.db = self.db
|
|
577
|
+
|
|
578
|
+
if not bypass_hooks:
|
|
579
|
+
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
476
580
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
581
|
+
parent_instances[model_class] = parent_obj
|
|
582
|
+
current_parent = parent_obj
|
|
583
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
480
584
|
|
|
481
585
|
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
482
586
|
child_model = inheritance_chain[-1]
|
|
@@ -702,7 +806,8 @@ class HookQuerySetMixin:
|
|
|
702
806
|
# For MTI, we need to handle parent links correctly
|
|
703
807
|
# The root model (first in chain) has its own PK
|
|
704
808
|
# Child models use the parent link to reference the root PK
|
|
705
|
-
|
|
809
|
+
# Root model (first in chain) has its own PK; kept for clarity
|
|
810
|
+
# root_model = inheritance_chain[0]
|
|
706
811
|
|
|
707
812
|
# Get the primary keys from the objects
|
|
708
813
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
@@ -786,7 +891,7 @@ class HookQuerySetMixin:
|
|
|
786
891
|
**{f"{filter_field}__in": pks}
|
|
787
892
|
).update(**case_statements)
|
|
788
893
|
total_updated += updated_count
|
|
789
|
-
except Exception
|
|
894
|
+
except Exception:
|
|
790
895
|
import traceback
|
|
791
896
|
|
|
792
897
|
traceback.print_exc()
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import threading
|
|
2
3
|
from collections.abc import Callable
|
|
3
|
-
from typing import Union
|
|
4
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
4
5
|
|
|
5
6
|
from django_bulk_hooks.priority import Priority
|
|
6
7
|
|
|
7
8
|
logger = logging.getLogger(__name__)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
# Key: (ModelClass, event)
|
|
11
|
+
# Value: list of tuples (handler_cls, method_name, condition_callable, priority)
|
|
12
|
+
_hooks: Dict[Tuple[type, str], List[Tuple[type, str, Callable, int]]] = {}
|
|
13
|
+
|
|
14
|
+
# Registry lock for thread-safety during registration and clearing
|
|
15
|
+
_lock = threading.RLock()
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
def register_hook(
|
|
13
|
-
model
|
|
14
|
-
|
|
19
|
+
model: type,
|
|
20
|
+
event: str,
|
|
21
|
+
handler_cls: type,
|
|
22
|
+
method_name: str,
|
|
23
|
+
condition: Optional[Callable],
|
|
24
|
+
priority: Union[int, Priority],
|
|
25
|
+
) -> None:
|
|
15
26
|
"""
|
|
16
27
|
Register a hook for a specific model and event.
|
|
17
28
|
|
|
@@ -26,32 +37,47 @@ def register_hook(
|
|
|
26
37
|
if not model or not event or not handler_cls or not method_name:
|
|
27
38
|
logger.warning("Invalid hook registration parameters")
|
|
28
39
|
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
# Normalize event to str just in case enums are used upstream
|
|
42
|
+
event = str(event)
|
|
43
|
+
|
|
44
|
+
with _lock:
|
|
45
|
+
key = (model, event)
|
|
46
|
+
hooks = _hooks.setdefault(key, [])
|
|
47
|
+
|
|
48
|
+
# Check for duplicate registrations
|
|
49
|
+
duplicate = any(h[0] == handler_cls and h[1] == method_name for h in hooks)
|
|
50
|
+
if duplicate:
|
|
51
|
+
logger.warning(
|
|
52
|
+
"Hook %s.%s already registered for %s.%s",
|
|
53
|
+
handler_cls.__name__,
|
|
54
|
+
method_name,
|
|
55
|
+
model.__name__,
|
|
56
|
+
event,
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Add the hook
|
|
61
|
+
hooks.append((handler_cls, method_name, condition, priority))
|
|
62
|
+
|
|
63
|
+
# Sort by priority (highest numbers execute first)
|
|
64
|
+
def sort_key(hook_info: Tuple[type, str, Callable, int]) -> int:
|
|
65
|
+
p = hook_info[3]
|
|
66
|
+
return p.value if hasattr(p, "value") else int(p)
|
|
67
|
+
|
|
68
|
+
hooks.sort(key=sort_key, reverse=True)
|
|
69
|
+
|
|
70
|
+
logger.debug(
|
|
71
|
+
"Registered %s.%s for %s.%s with priority %s",
|
|
72
|
+
handler_cls.__name__,
|
|
73
|
+
method_name,
|
|
74
|
+
model.__name__,
|
|
75
|
+
event,
|
|
76
|
+
priority,
|
|
39
77
|
)
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
# Add the hook
|
|
43
|
-
hooks.append((handler_cls, method_name, condition, priority))
|
|
44
|
-
|
|
45
|
-
# Sort by priority (lowest numbers execute first, matching engine expectation)
|
|
46
|
-
hooks.sort(key=lambda x: x[3])
|
|
47
|
-
|
|
48
|
-
logger.debug(
|
|
49
|
-
f"Registered {handler_cls.__name__}.{method_name} "
|
|
50
|
-
f"for {model.__name__}.{event} with priority {priority}"
|
|
51
|
-
)
|
|
52
78
|
|
|
53
79
|
|
|
54
|
-
def get_hooks(model, event):
|
|
80
|
+
def get_hooks(model: type, event: str):
|
|
55
81
|
"""
|
|
56
82
|
Get all registered hooks for a specific model and event.
|
|
57
83
|
|
|
@@ -64,24 +90,47 @@ def get_hooks(model, event):
|
|
|
64
90
|
"""
|
|
65
91
|
if not model or not event:
|
|
66
92
|
return []
|
|
67
|
-
|
|
68
|
-
key = (model, event)
|
|
69
|
-
hooks = _hooks.get(key, [])
|
|
70
|
-
|
|
71
|
-
# Log hook discovery for debugging
|
|
72
|
-
if hooks:
|
|
73
|
-
logger.debug(f"Found {len(hooks)} hooks for {model.__name__}.{event}")
|
|
74
|
-
|
|
75
|
-
return hooks
|
|
76
93
|
|
|
94
|
+
event = str(event)
|
|
95
|
+
|
|
96
|
+
with _lock:
|
|
97
|
+
key = (model, event)
|
|
98
|
+
hooks = _hooks.get(key, [])
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
# Log hook discovery for debugging
|
|
101
|
+
if hooks:
|
|
102
|
+
logger.debug("Found %d hooks for %s.%s", len(hooks), model.__name__, event)
|
|
103
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
104
|
+
logger.debug(" - %s.%s (priority: %s)", handler_cls.__name__, method_name, priority)
|
|
105
|
+
else:
|
|
106
|
+
logger.debug("No hooks found for %s.%s", model.__name__, event)
|
|
81
107
|
|
|
108
|
+
# Return a shallow copy to prevent external mutation of registry state
|
|
109
|
+
return list(hooks)
|
|
82
110
|
|
|
83
|
-
|
|
111
|
+
|
|
112
|
+
def list_all_hooks() -> Dict[Tuple[type, str], List[Tuple[type, str, Callable, int]]]:
|
|
113
|
+
"""Debug function to list all registered hooks (shallow copy)."""
|
|
114
|
+
with _lock:
|
|
115
|
+
return {k: list(v) for k, v in _hooks.items()}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def clear_hooks() -> None:
|
|
84
119
|
"""Clear all registered hooks (mainly for testing)."""
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
with _lock:
|
|
121
|
+
_hooks.clear()
|
|
122
|
+
logger.debug("All hooks cleared")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def unregister_hook(model: type, event: str, handler_cls: type, method_name: str) -> None:
|
|
126
|
+
"""Unregister a previously registered hook (safe no-op if not present)."""
|
|
127
|
+
event = str(event)
|
|
128
|
+
with _lock:
|
|
129
|
+
key = (model, event)
|
|
130
|
+
if key not in _hooks:
|
|
131
|
+
return
|
|
132
|
+
_hooks[key] = [
|
|
133
|
+
h for h in _hooks[key] if not (h[0] == handler_cls and h[1] == method_name)
|
|
134
|
+
]
|
|
135
|
+
if not _hooks[key]:
|
|
136
|
+
del _hooks[key]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
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=3x16zCsEFIvjg2ld1tUnweH0SWqT0SwrRKRdFs0nC4Y,8523
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=HGAXa9XsG3ilWdoA9rExp6sQZpm02Pkt9CSlq_gRs0I,4974
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=ZSYPwHcjlAMrISOHb9sqNjEfxyv4XupoDoe1hn87VJg,499
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=BKthdeCrgzuLzvy_UUfGLjFFZipAHzuxoelaH6zJVWo,5468
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=uqmvGlskkzKMERf3iexxXCAaQl0v3Wc45BeN-YmfTnE,4060
|
|
10
|
+
django_bulk_hooks/models.py,sha256=1m1POWtfAhWUBrr2uTUEPzBFJpBfAf8WlKUu8muB3z8,6298
|
|
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=eIjT6xolQYSGbakTOfRDrrc8J6gv0DnYI9rpHAo6XVk,4380
|
|
14
|
+
django_bulk_hooks-0.1.229.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.229.dist-info/METADATA,sha256=1gWbfBTL8ROM7ng-JOB8fawz9xj5v3X1chqLxuApgLM,9743
|
|
16
|
+
django_bulk_hooks-0.1.229.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
django_bulk_hooks-0.1.229.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
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=wiO6HvZkBSHzt1Q3IpXmVppJl30zlsoTheuPPCrGqdU,3118
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=IRgJ6uoyD1NifqzZpL2YxOqInJ4MFlS3vZkAa5ZTIVo,5210
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
|
-
django_bulk_hooks/models.py,sha256=mj4f93L64CN1XBS29RlS02WnZjCNoUkai97vKqjgZQ8,4575
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=xgnuwl8Ha9IhVzKEvjvCX5jUuRZgc0tWYwaKreuDvpw,32478
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=h59veo8Qh4Afj8ZP_4Jqr-2S6ebXjUJ7pJTdfqzfXkE,2572
|
|
14
|
-
django_bulk_hooks-0.1.227.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.227.dist-info/METADATA,sha256=rY5wk0lHKwxaqEdbVz3BLw3DnRBjMhXSN4syZLuWGX4,9743
|
|
16
|
-
django_bulk_hooks-0.1.227.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
-
django_bulk_hooks-0.1.227.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|