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.

Files changed (27) hide show
  1. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/dispatcher.py +22 -6
  3. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/coordinator.py +58 -1
  4. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_handler.py +3 -0
  5. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/pyproject.toml +1 -1
  6. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/LICENSE +0 -0
  7. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/README.md +0 -0
  8. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/__init__.py +0 -0
  9. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/changeset.py +0 -0
  10. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/conditions.py +0 -0
  11. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/constants.py +0 -0
  12. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/context.py +0 -0
  13. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/decorators.py +0 -0
  14. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/enums.py +0 -0
  15. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/factory.py +0 -0
  16. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/handler.py +0 -0
  17. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/helpers.py +0 -0
  18. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/manager.py +0 -0
  19. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/models.py +0 -0
  20. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/__init__.py +0 -0
  21. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/analyzer.py +0 -0
  22. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  23. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/field_utils.py +0 -0
  24. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/operations/record_classifier.py +0 -0
  26. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/queryset.py +0 -0
  27. {django_bulk_hooks-0.2.71 → django_bulk_hooks-0.2.73}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.71
3
+ Version: 0.2.73
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
@@ -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
- # NEW: Preload relationships needed for condition evaluation
194
+ # Preload relationships needed for condition evaluation (skip if already done upfront)
183
195
  if condition:
184
- condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
185
- logger.info(f"🔍 CONDITION: {handler_cls.__name__}.{method_name} has condition, extracted relationships: {condition_relationships}")
186
- if condition_relationships:
187
- logger.info(f"🔗 PRELOADING: Preloading condition relationships for {len(changeset.changes)} records")
188
- self._preload_condition_relationships(changeset, condition_relationships)
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:
@@ -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 model_cls in models_in_chain:
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.
@@ -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):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.71"
3
+ version = "0.2.73"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"