django-bulk-hooks 0.2.71__tar.gz → 0.2.73__tar.gz
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-0.2.71 → django_bulk_hooks-0.2.73}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/dispatcher.py +22 -6
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/coordinator.py +58 -1
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_handler.py +3 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/LICENSE +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/README.md +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/bulk_executor.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/field_utils.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/registry.py +0 -0
|
@@ -103,6 +103,9 @@ 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
|
|
|
@@ -148,12 +151,21 @@ class HookDispatcher:
|
|
|
148
151
|
|
|
149
152
|
# Filter out hooks that have already been executed for this operation
|
|
150
153
|
unique_hooks = []
|
|
154
|
+
skipped_hooks = []
|
|
151
155
|
for handler_cls, method_name, condition, priority in hooks:
|
|
152
156
|
hook_key = (handler_cls, method_name, operation_key)
|
|
153
157
|
if hook_key not in self._executed_hooks:
|
|
154
158
|
unique_hooks.append((handler_cls, method_name, condition, priority))
|
|
155
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]}")
|
|
156
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]}")
|
|
157
169
|
|
|
158
170
|
if not unique_hooks:
|
|
159
171
|
return
|
|
@@ -179,13 +191,17 @@ class HookDispatcher:
|
|
|
179
191
|
changeset: ChangeSet with all record changes
|
|
180
192
|
event: The hook event (e.g., 'before_create')
|
|
181
193
|
"""
|
|
182
|
-
#
|
|
194
|
+
# Preload relationships needed for condition evaluation (skip if already done upfront)
|
|
183
195
|
if condition:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
logger.info(f"
|
|
188
|
-
|
|
196
|
+
# Skip per-hook preloading if relationships were already preloaded upfront
|
|
197
|
+
if not changeset.operation_meta.get('relationships_preloaded', False):
|
|
198
|
+
condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
|
|
199
|
+
logger.info(f"🔍 CONDITION: {handler_cls.__name__}.{method_name} has condition, extracted relationships: {condition_relationships}")
|
|
200
|
+
if condition_relationships:
|
|
201
|
+
logger.info(f"🔗 PRELOADING: Preloading condition relationships for {len(changeset.changes)} records")
|
|
202
|
+
self._preload_condition_relationships(changeset, condition_relationships)
|
|
203
|
+
else:
|
|
204
|
+
logger.debug(f"🔍 CONDITION: {handler_cls.__name__}.{method_name} has condition (relationships already preloaded)")
|
|
189
205
|
|
|
190
206
|
# Filter records based on condition (now safe - relationships are preloaded)
|
|
191
207
|
if condition:
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -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 ====================
|
|
@@ -702,9 +705,22 @@ class BulkOperationCoordinator:
|
|
|
702
705
|
|
|
703
706
|
# Reset hook execution tracking for this new operation
|
|
704
707
|
self.dispatcher._reset_executed_hooks()
|
|
708
|
+
logger.debug(f"🚀 MTI_OPERATION_START: {event_prefix} operation for {changeset.model_cls.__name__}")
|
|
705
709
|
|
|
706
710
|
# Get all models in inheritance chain
|
|
707
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__}")
|
|
713
|
+
|
|
714
|
+
# Extract and preload relationships needed by hook conditions upfront
|
|
715
|
+
# This prevents duplicate queries by avoiding per-hook preloading
|
|
716
|
+
condition_relationships = self._extract_condition_relationships_for_operation(changeset, models_in_chain)
|
|
717
|
+
if condition_relationships:
|
|
718
|
+
logger.info(f"🔗 BULK PRELOAD: Preloading {len(condition_relationships)} condition relationships for {changeset.model_cls.__name__} hooks")
|
|
719
|
+
self.dispatcher._preload_condition_relationships(changeset, condition_relationships)
|
|
720
|
+
# Mark that relationships have been preloaded to avoid per-hook duplication
|
|
721
|
+
changeset.operation_meta['relationships_preloaded'] = True
|
|
722
|
+
else:
|
|
723
|
+
logger.info(f"🔗 BULK PRELOAD: No condition relationships to preload for {changeset.model_cls.__name__} hooks")
|
|
708
724
|
|
|
709
725
|
# VALIDATE phase - for all models in chain
|
|
710
726
|
if not bypass_validation:
|
|
@@ -799,6 +815,7 @@ class BulkOperationCoordinator:
|
|
|
799
815
|
result_objects: List of objects returned from the operation
|
|
800
816
|
models_in_chain: List of model classes in the MTI inheritance chain
|
|
801
817
|
"""
|
|
818
|
+
logger.debug(f"🔀 UPSERT_AFTER_START: Processing {len(result_objects)} result objects with {len(models_in_chain)} models in chain")
|
|
802
819
|
# Split objects based on metadata set by the executor
|
|
803
820
|
created_objects = []
|
|
804
821
|
updated_objects = []
|
|
@@ -868,14 +885,18 @@ class BulkOperationCoordinator:
|
|
|
868
885
|
|
|
869
886
|
# Dispatch after_create hooks for created objects
|
|
870
887
|
if created_objects:
|
|
888
|
+
logger.debug(f"🔀 UPSERT_DISPATCH_CREATE: Dispatching after_create for {len(created_objects)} created objects")
|
|
871
889
|
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
872
890
|
|
|
873
891
|
create_changeset = build_changeset_for_create(self.model_cls, created_objects)
|
|
892
|
+
# Mark that relationships have been preloaded to avoid per-hook duplication
|
|
893
|
+
create_changeset.operation_meta['relationships_preloaded'] = True
|
|
874
894
|
|
|
875
895
|
self._dispatch_hooks_for_models(models_in_chain, create_changeset, "after_create", bypass_hooks=False)
|
|
876
896
|
|
|
877
897
|
# Dispatch after_update hooks for updated objects
|
|
878
898
|
if updated_objects:
|
|
899
|
+
logger.debug(f"🔀 UPSERT_DISPATCH_UPDATE: Dispatching after_update for {len(updated_objects)} updated objects")
|
|
879
900
|
# Fetch old records for proper change detection
|
|
880
901
|
old_records_map = self.analyzer.fetch_old_records_map(updated_objects)
|
|
881
902
|
|
|
@@ -887,12 +908,48 @@ class BulkOperationCoordinator:
|
|
|
887
908
|
update_kwargs={}, # Empty since we don't know specific fields
|
|
888
909
|
old_records_map=old_records_map,
|
|
889
910
|
)
|
|
911
|
+
# Mark that relationships have been preloaded to avoid per-hook duplication
|
|
912
|
+
update_changeset.operation_meta['relationships_preloaded'] = True
|
|
890
913
|
|
|
891
914
|
self._dispatch_hooks_for_models(models_in_chain, update_changeset, "after_update", bypass_hooks=False)
|
|
892
915
|
|
|
893
916
|
# Clean up temporary metadata
|
|
894
917
|
self._cleanup_upsert_metadata(result_objects)
|
|
895
918
|
|
|
919
|
+
def _extract_condition_relationships_for_operation(self, changeset, models_in_chain):
|
|
920
|
+
"""
|
|
921
|
+
Extract relationships needed by hook conditions for this specific operation.
|
|
922
|
+
|
|
923
|
+
This is different from _extract_hook_relationships which gets ALL possible relationships
|
|
924
|
+
for queryset operations. This method only gets relationships needed by hooks that will
|
|
925
|
+
actually run in this operation.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
changeset: The changeset for this operation
|
|
929
|
+
models_in_chain: List of model classes in inheritance chain
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
set: Set of relationship field names to preload
|
|
933
|
+
"""
|
|
934
|
+
relationships = set()
|
|
935
|
+
dispatcher = self.dispatcher
|
|
936
|
+
|
|
937
|
+
# Get the events that will run in this operation
|
|
938
|
+
event_prefix = changeset.operation_type
|
|
939
|
+
events_to_check = [f"validate_{event_prefix}", f"before_{event_prefix}", f"after_{event_prefix}"]
|
|
940
|
+
|
|
941
|
+
for model_cls in models_in_chain:
|
|
942
|
+
for event in events_to_check:
|
|
943
|
+
hooks = dispatcher.registry.get_hooks(model_cls, event)
|
|
944
|
+
|
|
945
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
946
|
+
# Only extract relationships from conditions (not @select_related)
|
|
947
|
+
if condition:
|
|
948
|
+
condition_relationships = dispatcher._extract_condition_relationships(condition, model_cls)
|
|
949
|
+
relationships.update(condition_relationships)
|
|
950
|
+
|
|
951
|
+
return relationships
|
|
952
|
+
|
|
896
953
|
def _extract_hook_relationships(self):
|
|
897
954
|
"""
|
|
898
955
|
Extract all relationship paths that hooks might access for this model and its MTI parents.
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|