django-bulk-hooks 0.2.38__tar.gz → 0.2.93__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.
Files changed (33) hide show
  1. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/PKG-INFO +55 -4
  2. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/README.md +54 -3
  3. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/__init__.py +0 -3
  4. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/changeset.py +214 -230
  5. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/conditions.py +7 -3
  6. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/decorators.py +5 -15
  7. django_bulk_hooks-0.2.93/django_bulk_hooks/dispatcher.py +546 -0
  8. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/factory.py +541 -563
  9. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/handler.py +106 -114
  10. django_bulk_hooks-0.2.93/django_bulk_hooks/helpers.py +258 -0
  11. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/manager.py +134 -130
  12. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/models.py +89 -75
  13. django_bulk_hooks-0.2.93/django_bulk_hooks/operations/analyzer.py +466 -0
  14. django_bulk_hooks-0.2.93/django_bulk_hooks/operations/bulk_executor.py +742 -0
  15. django_bulk_hooks-0.2.93/django_bulk_hooks/operations/coordinator.py +928 -0
  16. django_bulk_hooks-0.2.93/django_bulk_hooks/operations/field_utils.py +335 -0
  17. django_bulk_hooks-0.2.93/django_bulk_hooks/operations/mti_handler.py +696 -0
  18. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/operations/mti_plans.py +103 -96
  19. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/operations/record_classifier.py +35 -23
  20. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/queryset.py +233 -191
  21. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/registry.py +276 -298
  22. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/pyproject.toml +1 -1
  23. django_bulk_hooks-0.2.38/django_bulk_hooks/dispatcher.py +0 -255
  24. django_bulk_hooks-0.2.38/django_bulk_hooks/helpers.py +0 -100
  25. django_bulk_hooks-0.2.38/django_bulk_hooks/operations/analyzer.py +0 -277
  26. django_bulk_hooks-0.2.38/django_bulk_hooks/operations/bulk_executor.py +0 -561
  27. django_bulk_hooks-0.2.38/django_bulk_hooks/operations/coordinator.py +0 -670
  28. django_bulk_hooks-0.2.38/django_bulk_hooks/operations/mti_handler.py +0 -504
  29. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/LICENSE +0 -0
  30. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/constants.py +0 -0
  31. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/context.py +0 -0
  32. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/enums.py +0 -0
  33. {django_bulk_hooks-0.2.38 → django_bulk_hooks-0.2.93}/django_bulk_hooks/operations/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.38
3
+ Version: 0.2.93
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
@@ -244,17 +244,68 @@ LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
244
244
 
245
245
  ## 🧩 Integration with Other Managers
246
246
 
247
- You can extend from `BulkHookManager` to work with other manager classes. The manager uses a cooperative approach that dynamically injects bulk hook functionality into any queryset, ensuring compatibility with other managers.
247
+ ### Recommended: QuerySet-based Composition (New Approach)
248
+
249
+ For the best compatibility and to avoid inheritance conflicts, use the queryset-based composition approach:
250
+
251
+ ```python
252
+ from django_bulk_hooks.queryset import HookQuerySet
253
+ from queryable_properties.managers import QueryablePropertiesManager
254
+
255
+ class MyManager(QueryablePropertiesManager):
256
+ """Manager that combines queryable properties with hooks"""
257
+
258
+ def get_queryset(self):
259
+ # Get the QueryableProperties QuerySet
260
+ qs = super().get_queryset()
261
+ # Apply hooks on top of it
262
+ return HookQuerySet.with_hooks(qs)
263
+
264
+ class Article(models.Model):
265
+ title = models.CharField(max_length=100)
266
+ published = models.BooleanField(default=False)
267
+
268
+ objects = MyManager()
269
+
270
+ # This gives you both queryable properties AND hooks
271
+ # No inheritance conflicts, no MRO issues!
272
+ ```
273
+
274
+ ### Alternative: Explicit Hook Application
275
+
276
+ For more control, you can apply hooks explicitly:
277
+
278
+ ```python
279
+ class MyManager(QueryablePropertiesManager):
280
+ def get_queryset(self):
281
+ return super().get_queryset()
282
+
283
+ def with_hooks(self):
284
+ """Apply hooks to this queryset"""
285
+ return HookQuerySet.with_hooks(self.get_queryset())
286
+
287
+ # Usage:
288
+ Article.objects.with_hooks().filter(published=True).update(title="Updated")
289
+ ```
290
+
291
+ ### Legacy: Manager Inheritance (Not Recommended)
292
+
293
+ The old inheritance approach still works but is not recommended due to potential MRO conflicts:
248
294
 
249
295
  ```python
250
296
  from django_bulk_hooks.manager import BulkHookManager
251
297
  from queryable_properties.managers import QueryablePropertiesManager
252
298
 
253
299
  class MyManager(BulkHookManager, QueryablePropertiesManager):
254
- pass
300
+ pass # ⚠️ Can cause inheritance conflicts
255
301
  ```
256
302
 
257
- This approach uses the industry-standard injection pattern, similar to how `QueryablePropertiesManager` works, ensuring both functionalities work seamlessly together without any framework-specific knowledge.
303
+ **Why the new approach is better:**
304
+ - ✅ No inheritance conflicts
305
+ - ✅ No MRO (Method Resolution Order) issues
306
+ - ✅ Works with any manager combination
307
+ - ✅ Cleaner and more maintainable
308
+ - ✅ Follows Django's queryset enhancement patterns
258
309
 
259
310
  Framework needs to:
260
311
  Register these methods
@@ -225,17 +225,68 @@ LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
225
225
 
226
226
  ## 🧩 Integration with Other Managers
227
227
 
228
- You can extend from `BulkHookManager` to work with other manager classes. The manager uses a cooperative approach that dynamically injects bulk hook functionality into any queryset, ensuring compatibility with other managers.
228
+ ### Recommended: QuerySet-based Composition (New Approach)
229
+
230
+ For the best compatibility and to avoid inheritance conflicts, use the queryset-based composition approach:
231
+
232
+ ```python
233
+ from django_bulk_hooks.queryset import HookQuerySet
234
+ from queryable_properties.managers import QueryablePropertiesManager
235
+
236
+ class MyManager(QueryablePropertiesManager):
237
+ """Manager that combines queryable properties with hooks"""
238
+
239
+ def get_queryset(self):
240
+ # Get the QueryableProperties QuerySet
241
+ qs = super().get_queryset()
242
+ # Apply hooks on top of it
243
+ return HookQuerySet.with_hooks(qs)
244
+
245
+ class Article(models.Model):
246
+ title = models.CharField(max_length=100)
247
+ published = models.BooleanField(default=False)
248
+
249
+ objects = MyManager()
250
+
251
+ # This gives you both queryable properties AND hooks
252
+ # No inheritance conflicts, no MRO issues!
253
+ ```
254
+
255
+ ### Alternative: Explicit Hook Application
256
+
257
+ For more control, you can apply hooks explicitly:
258
+
259
+ ```python
260
+ class MyManager(QueryablePropertiesManager):
261
+ def get_queryset(self):
262
+ return super().get_queryset()
263
+
264
+ def with_hooks(self):
265
+ """Apply hooks to this queryset"""
266
+ return HookQuerySet.with_hooks(self.get_queryset())
267
+
268
+ # Usage:
269
+ Article.objects.with_hooks().filter(published=True).update(title="Updated")
270
+ ```
271
+
272
+ ### Legacy: Manager Inheritance (Not Recommended)
273
+
274
+ The old inheritance approach still works but is not recommended due to potential MRO conflicts:
229
275
 
230
276
  ```python
231
277
  from django_bulk_hooks.manager import BulkHookManager
232
278
  from queryable_properties.managers import QueryablePropertiesManager
233
279
 
234
280
  class MyManager(BulkHookManager, QueryablePropertiesManager):
235
- pass
281
+ pass # ⚠️ Can cause inheritance conflicts
236
282
  ```
237
283
 
238
- This approach uses the industry-standard injection pattern, similar to how `QueryablePropertiesManager` works, ensuring both functionalities work seamlessly together without any framework-specific knowledge.
284
+ **Why the new approach is better:**
285
+ - ✅ No inheritance conflicts
286
+ - ✅ No MRO (Method Resolution Order) issues
287
+ - ✅ Works with any manager combination
288
+ - ✅ Cleaner and more maintainable
289
+ - ✅ Follows Django's queryset enhancement patterns
239
290
 
240
291
  Framework needs to:
241
292
  Register these methods
@@ -25,9 +25,6 @@ from django_bulk_hooks.operations import BulkOperationCoordinator
25
25
  from django_bulk_hooks.operations import ModelAnalyzer
26
26
  from django_bulk_hooks.operations import MTIHandler
27
27
 
28
- # Add NullHandler to prevent logging messages if the application doesn't configure logging
29
- logging.getLogger(__name__).addHandler(logging.NullHandler())
30
-
31
28
  __all__ = [
32
29
  "BulkHookManager",
33
30
  "HookClass",
@@ -1,230 +1,214 @@
1
- """
2
- ChangeSet and RecordChange classes for Salesforce-style hook context.
3
-
4
- Provides a first-class abstraction for tracking changes in bulk operations,
5
- similar to Salesforce's Hook.new, Hook.old, and Hook.newMap.
6
- """
7
-
8
-
9
- class RecordChange:
10
- """
11
- Represents a single record change with old/new state.
12
-
13
- Similar to accessing Hook.newMap.get(id) in Salesforce, but with
14
- additional conveniences like O(1) field change detection.
15
- """
16
-
17
- def __init__(self, new_record, old_record=None, changed_fields=None):
18
- """
19
- Initialize a RecordChange.
20
-
21
- Args:
22
- new_record: The new/current state of the record
23
- old_record: The old/previous state of the record (None for creates)
24
- changed_fields: Optional pre-computed set of changed field names.
25
- If None, will be computed lazily on first access.
26
- """
27
- self.new_record = new_record
28
- self.old_record = old_record
29
- self._changed_fields = changed_fields
30
- self._pk = getattr(new_record, "pk", None) if new_record else None
31
-
32
- @property
33
- def pk(self):
34
- """Primary key of the record."""
35
- return self._pk
36
-
37
- @property
38
- def changed_fields(self):
39
- """
40
- Set of field names that have changed.
41
-
42
- Computed lazily on first access and cached for O(1) subsequent checks.
43
- """
44
- if self._changed_fields is None:
45
- self._changed_fields = self._compute_changed_fields()
46
- return self._changed_fields
47
-
48
- def has_changed(self, field_name):
49
- """
50
- O(1) check if a specific field has changed.
51
-
52
- Args:
53
- field_name: Name of the field to check
54
-
55
- Returns:
56
- True if the field value changed, False otherwise
57
- """
58
- return field_name in self.changed_fields
59
-
60
- def get_old_value(self, field_name):
61
- """
62
- Get the old value for a field.
63
-
64
- Args:
65
- field_name: Name of the field
66
-
67
- Returns:
68
- The old value, or None if no old record exists
69
- """
70
- if self.old_record is None:
71
- return None
72
- return getattr(self.old_record, field_name, None)
73
-
74
- def get_new_value(self, field_name):
75
- """
76
- Get the new value for a field.
77
-
78
- Args:
79
- field_name: Name of the field
80
-
81
- Returns:
82
- The new value
83
- """
84
- return getattr(self.new_record, field_name, None)
85
-
86
- def _compute_changed_fields(self):
87
- """
88
- Compute which fields have changed between old and new records.
89
-
90
- Uses Django's field.get_prep_value() for proper comparison that
91
- handles database-level transformations.
92
-
93
- Returns:
94
- Set of field names that have changed
95
- """
96
- if self.old_record is None:
97
- return set()
98
-
99
- changed = set()
100
- model_cls = self.new_record.__class__
101
-
102
- for field in model_cls._meta.fields:
103
- # Skip primary key - it shouldn't change
104
- if field.primary_key:
105
- continue
106
-
107
- old_val = getattr(self.old_record, field.name, None)
108
- new_val = getattr(self.new_record, field.name, None)
109
-
110
- # Use field's get_prep_value for proper comparison
111
- # This handles database-level transformations (e.g., timezone conversions)
112
- try:
113
- old_prep = field.get_prep_value(old_val)
114
- new_prep = field.get_prep_value(new_val)
115
- if old_prep != new_prep:
116
- changed.add(field.name)
117
- except Exception:
118
- # Fallback to direct comparison if get_prep_value fails
119
- if old_val != new_val:
120
- changed.add(field.name)
121
-
122
- return changed
123
-
124
-
125
- class ChangeSet:
126
- """
127
- Collection of RecordChanges for a bulk operation.
128
-
129
- Similar to Salesforce's Hook context (Hook.new, Hook.old, Hook.newMap),
130
- but enhanced for Python's bulk operations paradigm with O(1) lookups and
131
- additional metadata.
132
- """
133
-
134
- def __init__(self, model_cls, changes, operation_type, operation_meta=None):
135
- """
136
- Initialize a ChangeSet.
137
-
138
- Args:
139
- model_cls: The Django model class
140
- changes: List of RecordChange instances
141
- operation_type: Type of operation ('create', 'update', 'delete')
142
- operation_meta: Optional dict of additional metadata (e.g., update_kwargs)
143
- """
144
- self.model_cls = model_cls
145
- self.changes = changes # List[RecordChange]
146
- self.operation_type = operation_type
147
- self.operation_meta = operation_meta or {}
148
-
149
- # Build PK -> RecordChange map for O(1) lookups (like Hook.newMap)
150
- self._pk_to_change = {c.pk: c for c in changes if c.pk is not None}
151
-
152
- @property
153
- def new_records(self):
154
- """
155
- List of new/current record states.
156
-
157
- Similar to Hook.new in Salesforce.
158
- """
159
- return [c.new_record for c in self.changes if c.new_record is not None]
160
-
161
- @property
162
- def old_records(self):
163
- """
164
- List of old/previous record states.
165
-
166
- Similar to Hook.old in Salesforce.
167
- Only includes records that have old states (excludes creates).
168
- """
169
- return [c.old_record for c in self.changes if c.old_record is not None]
170
-
171
- def has_field_changed(self, pk, field_name):
172
- """
173
- O(1) check if a field changed for a specific record.
174
-
175
- Args:
176
- pk: Primary key of the record
177
- field_name: Name of the field to check
178
-
179
- Returns:
180
- True if the field changed, False otherwise
181
- """
182
- change = self._pk_to_change.get(pk)
183
- return change.has_changed(field_name) if change else False
184
-
185
- def get_old_value(self, pk, field_name):
186
- """
187
- Get the old value for a specific record and field.
188
-
189
- Args:
190
- pk: Primary key of the record
191
- field_name: Name of the field
192
-
193
- Returns:
194
- The old value, or None if not found
195
- """
196
- change = self._pk_to_change.get(pk)
197
- return change.get_old_value(field_name) if change else None
198
-
199
- def get_new_value(self, pk, field_name):
200
- """
201
- Get the new value for a specific record and field.
202
-
203
- Args:
204
- pk: Primary key of the record
205
- field_name: Name of the field
206
-
207
- Returns:
208
- The new value, or None if not found
209
- """
210
- change = self._pk_to_change.get(pk)
211
- return change.get_new_value(field_name) if change else None
212
-
213
- def chunk(self, chunk_size):
214
- """
215
- Split ChangeSet into smaller chunks for memory-efficient processing.
216
-
217
- Useful for processing very large bulk operations without loading
218
- all data into memory at once.
219
-
220
- Args:
221
- chunk_size: Number of changes per chunk
222
-
223
- Yields:
224
- ChangeSet instances, each with up to chunk_size changes
225
- """
226
- for i in range(0, len(self.changes), chunk_size):
227
- chunk_changes = self.changes[i : i + chunk_size]
228
- yield ChangeSet(
229
- self.model_cls, chunk_changes, self.operation_type, self.operation_meta,
230
- )
1
+ """
2
+ ChangeSet and RecordChange classes for Salesforce-style hook context.
3
+
4
+ Provides a first-class abstraction for tracking changes in bulk operations,
5
+ similar to Salesforce's Hook.new, Hook.old, and Hook.newMap.
6
+ """
7
+
8
+
9
+ class RecordChange:
10
+ """
11
+ Represents a single record change with old/new state.
12
+
13
+ Similar to accessing Hook.newMap.get(id) in Salesforce, but with
14
+ additional conveniences like O(1) field change detection.
15
+ """
16
+
17
+ def __init__(self, new_record, old_record=None, changed_fields=None):
18
+ """
19
+ Initialize a RecordChange.
20
+
21
+ Args:
22
+ new_record: The new/current state of the record
23
+ old_record: The old/previous state of the record (None for creates)
24
+ changed_fields: Optional pre-computed set of changed field names.
25
+ If None, will be computed lazily on first access.
26
+ """
27
+ self.new_record = new_record
28
+ self.old_record = old_record
29
+ self._changed_fields = changed_fields
30
+ self._pk = getattr(new_record, "pk", None) if new_record else None
31
+
32
+ @property
33
+ def pk(self):
34
+ """Primary key of the record."""
35
+ return self._pk
36
+
37
+ @property
38
+ def changed_fields(self):
39
+ """
40
+ Set of field names that have changed.
41
+
42
+ Computed lazily on first access and cached for O(1) subsequent checks.
43
+ """
44
+ if self._changed_fields is None:
45
+ self._changed_fields = self._compute_changed_fields()
46
+ return self._changed_fields
47
+
48
+ def has_changed(self, field_name):
49
+ """
50
+ O(1) check if a specific field has changed.
51
+
52
+ Args:
53
+ field_name: Name of the field to check
54
+
55
+ Returns:
56
+ True if the field value changed, False otherwise
57
+ """
58
+ return field_name in self.changed_fields
59
+
60
+ def get_old_value(self, field_name):
61
+ """
62
+ Get the old value for a field.
63
+
64
+ Args:
65
+ field_name: Name of the field
66
+
67
+ Returns:
68
+ The old value, or None if no old record exists
69
+ """
70
+ if self.old_record is None:
71
+ return None
72
+ return getattr(self.old_record, field_name, None)
73
+
74
+ def get_new_value(self, field_name):
75
+ """
76
+ Get the new value for a field.
77
+
78
+ Args:
79
+ field_name: Name of the field
80
+
81
+ Returns:
82
+ The new value
83
+ """
84
+ return getattr(self.new_record, field_name, None)
85
+
86
+ def _compute_changed_fields(self):
87
+ """
88
+ Compute which fields have changed between old and new records.
89
+
90
+ Uses Django's field.get_prep_value() for proper comparison that
91
+ handles database-level transformations.
92
+
93
+ Returns:
94
+ Set of field names that have changed
95
+ """
96
+ if self.old_record is None:
97
+ return set()
98
+
99
+ # Import here to avoid circular dependency
100
+ from .operations.field_utils import get_changed_fields
101
+
102
+ model_cls = self.new_record.__class__
103
+ return get_changed_fields(self.old_record, self.new_record, model_cls)
104
+
105
+
106
+ class ChangeSet:
107
+ """
108
+ Collection of RecordChanges for a bulk operation.
109
+
110
+ Similar to Salesforce's Hook context (Hook.new, Hook.old, Hook.newMap),
111
+ but enhanced for Python's bulk operations paradigm with O(1) lookups and
112
+ additional metadata.
113
+ """
114
+
115
+ def __init__(self, model_cls, changes, operation_type, operation_meta=None):
116
+ """
117
+ Initialize a ChangeSet.
118
+
119
+ Args:
120
+ model_cls: The Django model class
121
+ changes: List of RecordChange instances
122
+ operation_type: Type of operation ('create', 'update', 'delete')
123
+ operation_meta: Optional dict of additional metadata (e.g., update_kwargs)
124
+ """
125
+ self.model_cls = model_cls
126
+ self.changes = changes # List[RecordChange]
127
+ self.operation_type = operation_type
128
+ self.operation_meta = operation_meta or {}
129
+
130
+ # Build PK -> RecordChange map for O(1) lookups (like Hook.newMap)
131
+ self._pk_to_change = {c.pk: c for c in changes if c.pk is not None}
132
+
133
+ @property
134
+ def new_records(self):
135
+ """
136
+ List of new/current record states.
137
+
138
+ Similar to Hook.new in Salesforce.
139
+ """
140
+ return [c.new_record for c in self.changes if c.new_record is not None]
141
+
142
+ @property
143
+ def old_records(self):
144
+ """
145
+ List of old/previous record states.
146
+
147
+ Similar to Hook.old in Salesforce.
148
+ Only includes records that have old states (excludes creates).
149
+ """
150
+ return [c.old_record for c in self.changes if c.old_record is not None]
151
+
152
+ def has_field_changed(self, pk, field_name):
153
+ """
154
+ O(1) check if a field changed for a specific record.
155
+
156
+ Args:
157
+ pk: Primary key of the record
158
+ field_name: Name of the field to check
159
+
160
+ Returns:
161
+ True if the field changed, False otherwise
162
+ """
163
+ change = self._pk_to_change.get(pk)
164
+ return change.has_changed(field_name) if change else False
165
+
166
+ def get_old_value(self, pk, field_name):
167
+ """
168
+ Get the old value for a specific record and field.
169
+
170
+ Args:
171
+ pk: Primary key of the record
172
+ field_name: Name of the field
173
+
174
+ Returns:
175
+ The old value, or None if not found
176
+ """
177
+ change = self._pk_to_change.get(pk)
178
+ return change.get_old_value(field_name) if change else None
179
+
180
+ def get_new_value(self, pk, field_name):
181
+ """
182
+ Get the new value for a specific record and field.
183
+
184
+ Args:
185
+ pk: Primary key of the record
186
+ field_name: Name of the field
187
+
188
+ Returns:
189
+ The new value, or None if not found
190
+ """
191
+ change = self._pk_to_change.get(pk)
192
+ return change.get_new_value(field_name) if change else None
193
+
194
+ def chunk(self, chunk_size):
195
+ """
196
+ Split ChangeSet into smaller chunks for memory-efficient processing.
197
+
198
+ Useful for processing very large bulk operations without loading
199
+ all data into memory at once.
200
+
201
+ Args:
202
+ chunk_size: Number of changes per chunk
203
+
204
+ Yields:
205
+ ChangeSet instances, each with up to chunk_size changes
206
+ """
207
+ for i in range(0, len(self.changes), chunk_size):
208
+ chunk_changes = self.changes[i : i + chunk_size]
209
+ yield ChangeSet(
210
+ self.model_cls,
211
+ chunk_changes,
212
+ self.operation_type,
213
+ self.operation_meta,
214
+ )
@@ -40,7 +40,9 @@ def resolve_field_path(instance, field_path):
40
40
  if field.is_relation and not field.many_to_many:
41
41
  # Use attname for the final FK field access
42
42
  current_instance = getattr(
43
- current_instance, field.attname, None,
43
+ current_instance,
44
+ field.attname,
45
+ None,
44
46
  )
45
47
  continue
46
48
  except:
@@ -203,7 +205,8 @@ class AndCondition(HookCondition):
203
205
 
204
206
  def check(self, instance, original_instance=None):
205
207
  return self.cond1.check(instance, original_instance) and self.cond2.check(
206
- instance, original_instance,
208
+ instance,
209
+ original_instance,
207
210
  )
208
211
 
209
212
 
@@ -214,7 +217,8 @@ class OrCondition(HookCondition):
214
217
 
215
218
  def check(self, instance, original_instance=None):
216
219
  return self.cond1.check(instance, original_instance) or self.cond2.check(
217
- instance, original_instance,
220
+ instance,
221
+ original_instance,
218
222
  )
219
223
 
220
224