django-bulk-hooks 0.2.70__tar.gz → 0.2.71__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.70 → django_bulk_hooks-0.2.71}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/dispatcher.py +58 -2
  3. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/coordinator.py +3 -0
  4. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/pyproject.toml +1 -1
  5. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/LICENSE +0 -0
  6. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/README.md +0 -0
  7. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/__init__.py +0 -0
  8. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/changeset.py +0 -0
  9. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/conditions.py +0 -0
  10. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/constants.py +0 -0
  11. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/context.py +0 -0
  12. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/decorators.py +0 -0
  13. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/enums.py +0 -0
  14. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/factory.py +0 -0
  15. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/helpers.py +0 -0
  17. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/manager.py +0 -0
  18. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/models.py +0 -0
  19. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/__init__.py +0 -0
  20. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/analyzer.py +0 -0
  21. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  22. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/field_utils.py +0 -0
  23. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/mti_handler.py +0 -0
  24. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/operations/record_classifier.py +0 -0
  26. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/django_bulk_hooks/queryset.py +0 -0
  27. {django_bulk_hooks-0.2.70 → django_bulk_hooks-0.2.71}/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.70
3
+ Version: 0.2.71
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
@@ -106,12 +106,68 @@ class HookDispatcher:
106
106
  if not hooks:
107
107
  return
108
108
 
109
- # Execute hooks in priority order
110
- logger.info(f"🔥 HOOKS: Executing {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}")
109
+ # Create an operation key that includes the changeset model to avoid
110
+ # deduplicating hooks across different operations on the same records
111
+ # This prevents the same hook from executing multiple times for MTI inheritance chains
112
+ # but allows different operations on the same records to execute their hooks
113
+ record_ids = set()
114
+ for change in changeset.changes:
115
+ if change.new_record and change.new_record.pk:
116
+ record_ids.add(change.new_record.pk)
117
+ if change.old_record and change.old_record.pk:
118
+ record_ids.add(change.old_record.pk)
119
+
120
+ # Sort record IDs safely (handle Mock objects and other non-comparable types)
121
+ try:
122
+ sorted_record_ids = tuple(sorted(record_ids, key=lambda x: str(x)))
123
+ except (TypeError, AttributeError):
124
+ # Fallback for non-comparable objects (like Mock objects in tests)
125
+ sorted_record_ids = tuple(record_ids)
126
+
127
+ # Include changeset model and operation details to make the key more specific
128
+ operation_meta = getattr(changeset, 'operation_meta', {}) or {}
129
+ operation_type = getattr(changeset, 'operation_type', 'unknown')
130
+
131
+ # Include update_kwargs if present to distinguish different queryset operations
132
+ update_kwargs = operation_meta.get('update_kwargs', {})
133
+ if update_kwargs:
134
+ try:
135
+ # Convert to a hashable representation
136
+ update_kwargs_key = tuple(sorted((k, str(v)) for k, v in update_kwargs.items()))
137
+ except (TypeError, AttributeError):
138
+ # Fallback if values are not convertible to string
139
+ update_kwargs_key = tuple(sorted(update_kwargs.keys()))
140
+ else:
141
+ update_kwargs_key = ()
142
+
143
+ operation_key = (event, changeset.model_cls.__name__, operation_type, sorted_record_ids, update_kwargs_key)
144
+
145
+ # Track executed hooks to prevent duplicates in MTI inheritance chains
146
+ if not hasattr(self, '_executed_hooks'):
147
+ self._executed_hooks = set()
148
+
149
+ # Filter out hooks that have already been executed for this operation
150
+ unique_hooks = []
111
151
  for handler_cls, method_name, condition, priority in hooks:
152
+ hook_key = (handler_cls, method_name, operation_key)
153
+ if hook_key not in self._executed_hooks:
154
+ unique_hooks.append((handler_cls, method_name, condition, priority))
155
+ self._executed_hooks.add(hook_key)
156
+
157
+
158
+ if not unique_hooks:
159
+ return
160
+
161
+ # Execute hooks in priority order
162
+ logger.info(f"🔥 HOOKS: Executing {len(unique_hooks)} hooks for {changeset.model_cls.__name__}.{event}")
163
+ for handler_cls, method_name, condition, priority in unique_hooks:
112
164
  logger.info(f" → {handler_cls.__name__}.{method_name} (priority={priority})")
113
165
  self._execute_hook(handler_cls, method_name, condition, changeset, event)
114
166
 
167
+ def _reset_executed_hooks(self):
168
+ """Reset the executed hooks tracking for a new operation."""
169
+ self._executed_hooks = set()
170
+
115
171
  def _execute_hook(self, handler_cls, method_name, condition, changeset, event):
116
172
  """
117
173
  Execute a single hook with condition checking.
@@ -700,6 +700,9 @@ class BulkOperationCoordinator:
700
700
  if bypass_hooks:
701
701
  return operation()
702
702
 
703
+ # Reset hook execution tracking for this new operation
704
+ self.dispatcher._reset_executed_hooks()
705
+
703
706
  # Get all models in inheritance chain
704
707
  models_in_chain = self._get_models_in_chain(changeset.model_cls)
705
708
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.70"
3
+ version = "0.2.71"
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"