django-bulk-hooks 0.1.228__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 -45
- django_bulk_hooks/enums.py +11 -0
- django_bulk_hooks/handler.py +1 -10
- django_bulk_hooks/manager.py +123 -101
- django_bulk_hooks/models.py +42 -11
- django_bulk_hooks/queryset.py +164 -90
- django_bulk_hooks/registry.py +92 -47
- {django_bulk_hooks-0.1.228.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.228.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.228.dist-info → django_bulk_hooks-0.1.229.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.228.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,64 +87,92 @@ 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:
|
|
98
109
|
ctx = HookContext(model_cls)
|
|
99
110
|
# Run VALIDATE_UPDATE hooks
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#
|
|
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
|
|
104
115
|
if has_subquery:
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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:
|
|
119
141
|
for obj in instances:
|
|
120
|
-
for field, value in
|
|
142
|
+
for field, value in simple_fields.items():
|
|
121
143
|
setattr(obj, field, value)
|
|
122
144
|
|
|
123
145
|
# Run BEFORE_UPDATE hooks with updated instances
|
|
124
146
|
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
125
147
|
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
updated_map = {obj.pk: obj for obj in updated_instances}
|
|
138
|
-
instances = [updated_map.get(obj.pk, obj) for obj in instances]
|
|
139
|
-
else:
|
|
140
|
-
# For simple field updates, instances have already been updated in the hook section
|
|
141
|
-
# Perform database update using Django's native bulk_update
|
|
142
|
-
# We use the base manager to avoid recursion
|
|
143
|
-
base_manager = model_cls._base_manager
|
|
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
|
|
154
|
+
|
|
155
|
+
is_mti = _is_mti(model_cls)
|
|
156
|
+
|
|
157
|
+
if is_mti:
|
|
158
|
+
# Use MTI-aware bulk update across tables
|
|
144
159
|
fields_to_update = list(kwargs.keys())
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
176
|
|
|
148
177
|
# Run AFTER_UPDATE hooks only if not bypassed
|
|
149
178
|
if not bypass_hooks:
|
|
@@ -163,7 +192,7 @@ class HookQuerySetMixin:
|
|
|
163
192
|
unique_fields=None,
|
|
164
193
|
bypass_hooks=False,
|
|
165
194
|
bypass_validation=False,
|
|
166
|
-
):
|
|
195
|
+
) -> list:
|
|
167
196
|
"""
|
|
168
197
|
Insert each of the instances into the database with complete hook support.
|
|
169
198
|
|
|
@@ -251,7 +280,7 @@ class HookQuerySetMixin:
|
|
|
251
280
|
@transaction.atomic
|
|
252
281
|
def bulk_update(
|
|
253
282
|
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
254
|
-
):
|
|
283
|
+
) -> int:
|
|
255
284
|
"""
|
|
256
285
|
Bulk update objects in the database with complete hook support.
|
|
257
286
|
|
|
@@ -313,10 +342,21 @@ class HookQuerySetMixin:
|
|
|
313
342
|
model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx
|
|
314
343
|
)
|
|
315
344
|
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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)
|
|
320
360
|
|
|
321
361
|
# Run AFTER_UPDATE hooks
|
|
322
362
|
if not bypass_hooks:
|
|
@@ -326,7 +366,7 @@ class HookQuerySetMixin:
|
|
|
326
366
|
return result
|
|
327
367
|
|
|
328
368
|
@transaction.atomic
|
|
329
|
-
def bulk_delete(self, objs, **kwargs):
|
|
369
|
+
def bulk_delete(self, objs, **kwargs) -> int:
|
|
330
370
|
"""
|
|
331
371
|
Delete the given objects from the database with complete hook support.
|
|
332
372
|
|
|
@@ -339,7 +379,7 @@ class HookQuerySetMixin:
|
|
|
339
379
|
model_cls = self.model
|
|
340
380
|
|
|
341
381
|
# Extract custom kwargs
|
|
342
|
-
|
|
382
|
+
kwargs.pop("bypass_hooks", False)
|
|
343
383
|
|
|
344
384
|
# Validate inputs
|
|
345
385
|
if not isinstance(objs, (list, tuple)):
|
|
@@ -464,50 +504,83 @@ class HookQuerySetMixin:
|
|
|
464
504
|
# Then we can use Django's bulk_create for the child objects
|
|
465
505
|
parent_objects_map = {}
|
|
466
506
|
|
|
467
|
-
# Step 1:
|
|
468
|
-
# Get bypass_hooks from kwargs
|
|
507
|
+
# Step 1: Insert into parent tables to get primary keys back
|
|
469
508
|
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
470
509
|
bypass_validation = kwargs.get("bypass_validation", False)
|
|
471
510
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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}
|
|
475
517
|
for model_class in inheritance_chain[:-1]:
|
|
476
|
-
|
|
477
|
-
obj, model_class,
|
|
478
|
-
|
|
518
|
+
parent_objs = [
|
|
519
|
+
self._create_parent_instance(obj, model_class, current_parents_per_obj[id(obj)])
|
|
520
|
+
for obj in batch
|
|
521
|
+
]
|
|
479
522
|
|
|
480
|
-
# Fire parent hooks if not bypassed
|
|
481
523
|
if not bypass_hooks:
|
|
482
524
|
ctx = HookContext(model_class)
|
|
483
525
|
if not bypass_validation:
|
|
484
|
-
engine.run(model_class, VALIDATE_CREATE,
|
|
485
|
-
engine.run(model_class, BEFORE_CREATE,
|
|
486
|
-
|
|
487
|
-
# Use Django's base manager to create the object and get PKs back
|
|
488
|
-
# This bypasses hooks and the MTI exception
|
|
489
|
-
field_values = {
|
|
490
|
-
field.name: getattr(parent_obj, field.name)
|
|
491
|
-
for field in model_class._meta.local_fields
|
|
492
|
-
if hasattr(parent_obj, field.name)
|
|
493
|
-
and getattr(parent_obj, field.name) is not None
|
|
494
|
-
}
|
|
495
|
-
created_obj = model_class._base_manager.using(self.db).create(
|
|
496
|
-
**field_values
|
|
497
|
-
)
|
|
526
|
+
engine.run(model_class, VALIDATE_CREATE, parent_objs, ctx=ctx)
|
|
527
|
+
engine.run(model_class, BEFORE_CREATE, parent_objs, ctx=ctx)
|
|
498
528
|
|
|
499
|
-
#
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
+
)
|
|
503
533
|
|
|
504
|
-
#
|
|
534
|
+
# After create hooks
|
|
505
535
|
if not bypass_hooks:
|
|
506
|
-
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)
|
|
507
580
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
581
|
+
parent_instances[model_class] = parent_obj
|
|
582
|
+
current_parent = parent_obj
|
|
583
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
511
584
|
|
|
512
585
|
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
513
586
|
child_model = inheritance_chain[-1]
|
|
@@ -733,7 +806,8 @@ class HookQuerySetMixin:
|
|
|
733
806
|
# For MTI, we need to handle parent links correctly
|
|
734
807
|
# The root model (first in chain) has its own PK
|
|
735
808
|
# Child models use the parent link to reference the root PK
|
|
736
|
-
|
|
809
|
+
# Root model (first in chain) has its own PK; kept for clarity
|
|
810
|
+
# root_model = inheritance_chain[0]
|
|
737
811
|
|
|
738
812
|
# Get the primary keys from the objects
|
|
739
813
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
@@ -817,7 +891,7 @@ class HookQuerySetMixin:
|
|
|
817
891
|
**{f"{filter_field}__in": pks}
|
|
818
892
|
).update(**case_statements)
|
|
819
893
|
total_updated += updated_count
|
|
820
|
-
except Exception
|
|
894
|
+
except Exception:
|
|
821
895
|
import traceback
|
|
822
896
|
|
|
823
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 (highest numbers execute first)
|
|
46
|
-
hooks.sort(key=lambda x: x[3], reverse=True)
|
|
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,28 +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
|
-
for handler_cls, method_name, condition, priority in hooks:
|
|
75
|
-
logger.debug(f" - {handler_cls.__name__}.{method_name} (priority: {priority})")
|
|
76
|
-
else:
|
|
77
|
-
logger.debug(f"No hooks found for {model.__name__}.{event}")
|
|
78
|
-
|
|
79
|
-
return hooks
|
|
80
93
|
|
|
94
|
+
event = str(event)
|
|
95
|
+
|
|
96
|
+
with _lock:
|
|
97
|
+
key = (model, event)
|
|
98
|
+
hooks = _hooks.get(key, [])
|
|
81
99
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
85
107
|
|
|
108
|
+
# Return a shallow copy to prevent external mutation of registry state
|
|
109
|
+
return list(hooks)
|
|
86
110
|
|
|
87
|
-
|
|
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:
|
|
88
119
|
"""Clear all registered hooks (mainly for testing)."""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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=81NUQAppn2eCsY1Ao6SgvyXsowmxUnabYgY0GHWoo-o,3294
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=1pKFHXq1iCceAUitCj9v6WM3PVJpYJx5ZNZD1RzCJUU,87
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=sd5kPOZKwKQnxmQGir5u10ZkTHRdN4Hynl_oIWEIYzQ,5857
|
|
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=EGFBbRmX_LhwRYFCKzM8I5m8NGCsUEVJp2pfNTcoHe4,378
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=L_C0ICEnD2TxOljn0RckX0W8xB-jLa_2995ZuKDRVJY,34448
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=EJBNVDo6VOz2s5zsJbpppiM6JwRmmXLyfwSKXYaT4Fs,2802
|
|
14
|
-
django_bulk_hooks-0.1.228.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.228.dist-info/METADATA,sha256=FFM1S50BoNZtBfQyDGhJ4dtvQTsFY1OvNUmrbmHFk0Q,9743
|
|
16
|
-
django_bulk_hooks-0.1.228.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
-
django_bulk_hooks-0.1.228.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|