django-bulk-hooks 0.2.70__py3-none-any.whl → 0.2.72__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/dispatcher.py +70 -2
- django_bulk_hooks/operations/coordinator.py +12 -1
- django_bulk_hooks/operations/mti_handler.py +3 -0
- {django_bulk_hooks-0.2.70.dist-info → django_bulk_hooks-0.2.72.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.70.dist-info → django_bulk_hooks-0.2.72.dist-info}/RECORD +7 -7
- {django_bulk_hooks-0.2.70.dist-info → django_bulk_hooks-0.2.72.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.70.dist-info → django_bulk_hooks-0.2.72.dist-info}/WHEEL +0 -0
django_bulk_hooks/dispatcher.py
CHANGED
|
@@ -103,15 +103,83 @@ class HookDispatcher:
|
|
|
103
103
|
# Get hooks sorted by priority (deterministic order)
|
|
104
104
|
hooks = self.registry.get_hooks(changeset.model_cls, event)
|
|
105
105
|
|
|
106
|
+
logger.debug(f"🧵 DISPATCH: changeset.model_cls={changeset.model_cls.__name__}, event={event}")
|
|
107
|
+
logger.debug(f"🎣 HOOKS_FOUND: {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}: {[f'{h[0].__name__}.{h[1]}' for h in hooks]}")
|
|
108
|
+
|
|
106
109
|
if not hooks:
|
|
107
110
|
return
|
|
108
111
|
|
|
109
|
-
#
|
|
110
|
-
|
|
112
|
+
# Create an operation key that includes the changeset model to avoid
|
|
113
|
+
# deduplicating hooks across different operations on the same records
|
|
114
|
+
# This prevents the same hook from executing multiple times for MTI inheritance chains
|
|
115
|
+
# but allows different operations on the same records to execute their hooks
|
|
116
|
+
record_ids = set()
|
|
117
|
+
for change in changeset.changes:
|
|
118
|
+
if change.new_record and change.new_record.pk:
|
|
119
|
+
record_ids.add(change.new_record.pk)
|
|
120
|
+
if change.old_record and change.old_record.pk:
|
|
121
|
+
record_ids.add(change.old_record.pk)
|
|
122
|
+
|
|
123
|
+
# Sort record IDs safely (handle Mock objects and other non-comparable types)
|
|
124
|
+
try:
|
|
125
|
+
sorted_record_ids = tuple(sorted(record_ids, key=lambda x: str(x)))
|
|
126
|
+
except (TypeError, AttributeError):
|
|
127
|
+
# Fallback for non-comparable objects (like Mock objects in tests)
|
|
128
|
+
sorted_record_ids = tuple(record_ids)
|
|
129
|
+
|
|
130
|
+
# Include changeset model and operation details to make the key more specific
|
|
131
|
+
operation_meta = getattr(changeset, 'operation_meta', {}) or {}
|
|
132
|
+
operation_type = getattr(changeset, 'operation_type', 'unknown')
|
|
133
|
+
|
|
134
|
+
# Include update_kwargs if present to distinguish different queryset operations
|
|
135
|
+
update_kwargs = operation_meta.get('update_kwargs', {})
|
|
136
|
+
if update_kwargs:
|
|
137
|
+
try:
|
|
138
|
+
# Convert to a hashable representation
|
|
139
|
+
update_kwargs_key = tuple(sorted((k, str(v)) for k, v in update_kwargs.items()))
|
|
140
|
+
except (TypeError, AttributeError):
|
|
141
|
+
# Fallback if values are not convertible to string
|
|
142
|
+
update_kwargs_key = tuple(sorted(update_kwargs.keys()))
|
|
143
|
+
else:
|
|
144
|
+
update_kwargs_key = ()
|
|
145
|
+
|
|
146
|
+
operation_key = (event, changeset.model_cls.__name__, operation_type, sorted_record_ids, update_kwargs_key)
|
|
147
|
+
|
|
148
|
+
# Track executed hooks to prevent duplicates in MTI inheritance chains
|
|
149
|
+
if not hasattr(self, '_executed_hooks'):
|
|
150
|
+
self._executed_hooks = set()
|
|
151
|
+
|
|
152
|
+
# Filter out hooks that have already been executed for this operation
|
|
153
|
+
unique_hooks = []
|
|
154
|
+
skipped_hooks = []
|
|
111
155
|
for handler_cls, method_name, condition, priority in hooks:
|
|
156
|
+
hook_key = (handler_cls, method_name, operation_key)
|
|
157
|
+
if hook_key not in self._executed_hooks:
|
|
158
|
+
unique_hooks.append((handler_cls, method_name, condition, priority))
|
|
159
|
+
self._executed_hooks.add(hook_key)
|
|
160
|
+
else:
|
|
161
|
+
skipped_hooks.append((handler_cls.__name__, method_name))
|
|
162
|
+
|
|
163
|
+
# Debug logging for hook deduplication
|
|
164
|
+
if skipped_hooks:
|
|
165
|
+
logger.debug(f"⏭️ SKIPPED_DUPS: {len(skipped_hooks)} duplicate hooks: {[f'{cls}.{method}' for cls, method in skipped_hooks]}")
|
|
166
|
+
|
|
167
|
+
if unique_hooks:
|
|
168
|
+
logger.debug(f"✅ EXECUTING_UNIQUE: {len(unique_hooks)} unique hooks: {[f'{h[0].__name__}.{h[1]}' for h in unique_hooks]}")
|
|
169
|
+
|
|
170
|
+
if not unique_hooks:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Execute hooks in priority order
|
|
174
|
+
logger.info(f"🔥 HOOKS: Executing {len(unique_hooks)} hooks for {changeset.model_cls.__name__}.{event}")
|
|
175
|
+
for handler_cls, method_name, condition, priority in unique_hooks:
|
|
112
176
|
logger.info(f" → {handler_cls.__name__}.{method_name} (priority={priority})")
|
|
113
177
|
self._execute_hook(handler_cls, method_name, condition, changeset, event)
|
|
114
178
|
|
|
179
|
+
def _reset_executed_hooks(self):
|
|
180
|
+
"""Reset the executed hooks tracking for a new operation."""
|
|
181
|
+
self._executed_hooks = set()
|
|
182
|
+
|
|
115
183
|
def _execute_hook(self, handler_cls, method_name, condition, changeset, event):
|
|
116
184
|
"""
|
|
117
185
|
Execute a single hook with condition checking.
|
|
@@ -142,8 +142,11 @@ class BulkOperationCoordinator:
|
|
|
142
142
|
event_suffix: Event name suffix (e.g., 'before_create', 'validate_update')
|
|
143
143
|
bypass_hooks: Whether to skip hook execution
|
|
144
144
|
"""
|
|
145
|
-
for
|
|
145
|
+
logger.debug(f"🔄 DISPATCH_MODELS: Iterating through {len(models_in_chain)} models for {event_suffix}: {[m.__name__ for m in models_in_chain]}")
|
|
146
|
+
for i, model_cls in enumerate(models_in_chain):
|
|
147
|
+
logger.debug(f"🔄 DISPATCH_ITERATION: {i+1}/{len(models_in_chain)} - Dispatching to {model_cls.__name__} for {event_suffix}")
|
|
146
148
|
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
149
|
+
logger.debug(f"🔄 CHANGESET_MODEL: Created changeset with model_cls={model_changeset.model_cls.__name__}")
|
|
147
150
|
self.dispatcher.dispatch(model_changeset, event_suffix, bypass_hooks=bypass_hooks)
|
|
148
151
|
|
|
149
152
|
# ==================== PUBLIC API ====================
|
|
@@ -700,8 +703,13 @@ class BulkOperationCoordinator:
|
|
|
700
703
|
if bypass_hooks:
|
|
701
704
|
return operation()
|
|
702
705
|
|
|
706
|
+
# Reset hook execution tracking for this new operation
|
|
707
|
+
self.dispatcher._reset_executed_hooks()
|
|
708
|
+
logger.debug(f"🚀 MTI_OPERATION_START: {event_prefix} operation for {changeset.model_cls.__name__}")
|
|
709
|
+
|
|
703
710
|
# Get all models in inheritance chain
|
|
704
711
|
models_in_chain = self._get_models_in_chain(changeset.model_cls)
|
|
712
|
+
logger.debug(f"🔗 MTI_CHAIN_START: {len(models_in_chain)} models in chain for {changeset.model_cls.__name__}")
|
|
705
713
|
|
|
706
714
|
# VALIDATE phase - for all models in chain
|
|
707
715
|
if not bypass_validation:
|
|
@@ -796,6 +804,7 @@ class BulkOperationCoordinator:
|
|
|
796
804
|
result_objects: List of objects returned from the operation
|
|
797
805
|
models_in_chain: List of model classes in the MTI inheritance chain
|
|
798
806
|
"""
|
|
807
|
+
logger.debug(f"🔀 UPSERT_AFTER_START: Processing {len(result_objects)} result objects with {len(models_in_chain)} models in chain")
|
|
799
808
|
# Split objects based on metadata set by the executor
|
|
800
809
|
created_objects = []
|
|
801
810
|
updated_objects = []
|
|
@@ -865,6 +874,7 @@ class BulkOperationCoordinator:
|
|
|
865
874
|
|
|
866
875
|
# Dispatch after_create hooks for created objects
|
|
867
876
|
if created_objects:
|
|
877
|
+
logger.debug(f"🔀 UPSERT_DISPATCH_CREATE: Dispatching after_create for {len(created_objects)} created objects")
|
|
868
878
|
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
869
879
|
|
|
870
880
|
create_changeset = build_changeset_for_create(self.model_cls, created_objects)
|
|
@@ -873,6 +883,7 @@ class BulkOperationCoordinator:
|
|
|
873
883
|
|
|
874
884
|
# Dispatch after_update hooks for updated objects
|
|
875
885
|
if updated_objects:
|
|
886
|
+
logger.debug(f"🔀 UPSERT_DISPATCH_UPDATE: Dispatching after_update for {len(updated_objects)} updated objects")
|
|
876
887
|
# Fetch old records for proper change detection
|
|
877
888
|
old_records_map = self.analyzer.fetch_old_records_map(updated_objects)
|
|
878
889
|
|
|
@@ -78,14 +78,17 @@ class MTIHandler:
|
|
|
78
78
|
while current_model:
|
|
79
79
|
if not current_model._meta.proxy and not current_model._meta.abstract:
|
|
80
80
|
chain.append(current_model)
|
|
81
|
+
logger.debug(f"🔗 MTI_CHAIN_ADD: Added {current_model.__name__} (abstract: {current_model._meta.abstract}, proxy: {current_model._meta.proxy})")
|
|
81
82
|
|
|
82
83
|
# Get concrete parent models (not abstract, not proxy)
|
|
83
84
|
parents = [parent for parent in current_model._meta.parents.keys() if not parent._meta.proxy and not parent._meta.abstract]
|
|
85
|
+
logger.debug(f"🔗 MTI_PARENTS: {current_model.__name__} has concrete parents: {[p.__name__ for p in parents]}")
|
|
84
86
|
|
|
85
87
|
current_model = parents[0] if parents else None
|
|
86
88
|
|
|
87
89
|
# Reverse to get root-to-child order
|
|
88
90
|
chain.reverse()
|
|
91
|
+
logger.debug(f"🔗 MTI_CHAIN_FINAL: {[m.__name__ for m in chain]} (length: {len(chain)})")
|
|
89
92
|
return chain
|
|
90
93
|
|
|
91
94
|
def get_parent_models(self):
|
|
@@ -4,7 +4,7 @@ django_bulk_hooks/conditions.py,sha256=ar4pGjtxLKmgSIlO4S6aZFKmaBNchLtxMmWpkn4g9
|
|
|
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=TdkO4FJyFrVU2zqK6Y_6JjEJ4v3nbKkk7aa22jN10sk,11994
|
|
7
|
-
django_bulk_hooks/dispatcher.py,sha256=
|
|
7
|
+
django_bulk_hooks/dispatcher.py,sha256=AJJFpP5zvstF1VfKyduetDlor9Dskk-7VbwHkGHRMYM,22352
|
|
8
8
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
9
9
|
django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
|
|
10
10
|
django_bulk_hooks/handler.py,sha256=SRCrMzgolrruTkvMnYBFmXLR-ABiw0JiH3605PEdCZM,4207
|
|
@@ -14,14 +14,14 @@ django_bulk_hooks/models.py,sha256=TWN_F-SsLGPx9jrkNT9pmJFR5VsZ0Z_QaVOZOmt7bpw,2
|
|
|
14
14
|
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
15
|
django_bulk_hooks/operations/analyzer.py,sha256=Pz8mc-EL8KDOfLQFYiRuN-r0OmINW3nIBhRJJCma-yo,10360
|
|
16
16
|
django_bulk_hooks/operations/bulk_executor.py,sha256=po8V_2H3ULiE0RYJ-wbaRIz52SKhss81UHwuQjlz3H8,26214
|
|
17
|
-
django_bulk_hooks/operations/coordinator.py,sha256=
|
|
17
|
+
django_bulk_hooks/operations/coordinator.py,sha256=Gq2vqjlIyyfV8OSoipoED2hC2kb2czaHuy2Cs3gtmGI,39751
|
|
18
18
|
django_bulk_hooks/operations/field_utils.py,sha256=cQ9w4xdk-z3PrMLFvRzVV07Wc0D2qbpSepwoupqwQH8,7888
|
|
19
|
-
django_bulk_hooks/operations/mti_handler.py,sha256=
|
|
19
|
+
django_bulk_hooks/operations/mti_handler.py,sha256=x1uNvP8MkidifPp_AMp4nffsdW3dz3izV6SeaLJ0DaA,26247
|
|
20
20
|
django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZPV2SzwQHLRzYc,3667
|
|
21
21
|
django_bulk_hooks/operations/record_classifier.py,sha256=It85hJC2K-UsEOLbTR-QBdY5UPV-acQIJ91TSGa7pYo,7053
|
|
22
22
|
django_bulk_hooks/queryset.py,sha256=tHt1U2O9Hvozg9sdn5MjzAk_I6wDU-LupuKFTfv1SiQ,7449
|
|
23
23
|
django_bulk_hooks/registry.py,sha256=4HxP1mVK2z4VzvlohbEw2359wM21UJZJYagJJ1komM0,7947
|
|
24
|
-
django_bulk_hooks-0.2.
|
|
25
|
-
django_bulk_hooks-0.2.
|
|
26
|
-
django_bulk_hooks-0.2.
|
|
27
|
-
django_bulk_hooks-0.2.
|
|
24
|
+
django_bulk_hooks-0.2.72.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
+
django_bulk_hooks-0.2.72.dist-info/METADATA,sha256=QkD3zT88vaJBu79C5vYBSORg0LBh4nCzVpwObeLwpko,10555
|
|
26
|
+
django_bulk_hooks-0.2.72.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
django_bulk_hooks-0.2.72.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|