django-bulk-hooks 0.1.93__tar.gz → 0.1.95__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.93
3
+ Version: 0.1.95
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
@@ -15,6 +15,7 @@ from django_bulk_hooks.conditions import (
15
15
  IsEqual,
16
16
  IsNotEqual,
17
17
  WasEqual,
18
+ IsBlank,
18
19
  safe_get_related_object,
19
20
  safe_get_related_attr,
20
21
  is_field_set,
@@ -47,4 +48,5 @@ __all__ = [
47
48
  "IsEqual",
48
49
  "IsNotEqual",
49
50
  "WasEqual",
51
+ "IsBlank",
50
52
  ]
@@ -0,0 +1,236 @@
1
+ from django.db import models
2
+
3
+
4
+ def safe_get_related_object(instance, field_name):
5
+ """
6
+ Safely get a related object without raising RelatedObjectDoesNotExist.
7
+ Returns None if the foreign key field is None or the related object doesn't exist.
8
+ """
9
+ if not hasattr(instance, field_name):
10
+ return None
11
+
12
+ # Get the foreign key field
13
+ try:
14
+ field = instance._meta.get_field(field_name)
15
+ if not field.is_relation or field.many_to_many or field.one_to_many:
16
+ return getattr(instance, field_name, None)
17
+ except models.FieldDoesNotExist:
18
+ return getattr(instance, field_name, None)
19
+
20
+ # Check if the foreign key field is None
21
+ fk_field_name = f"{field_name}_id"
22
+ if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
23
+ return None
24
+
25
+ # Try to get the related object, but catch RelatedObjectDoesNotExist
26
+ try:
27
+ return getattr(instance, field_name)
28
+ except field.related_model.RelatedObjectDoesNotExist:
29
+ return None
30
+
31
+
32
+ def safe_get_related_attr(instance, field_name, attr_name=None):
33
+ """
34
+ Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
35
+
36
+ This is particularly useful in hooks where objects might not have their related
37
+ fields populated yet (e.g., during bulk_create operations or on unsaved objects).
38
+
39
+ Args:
40
+ instance: The model instance
41
+ field_name: The foreign key field name
42
+ attr_name: Optional attribute name to access on the related object
43
+
44
+ Returns:
45
+ The related object, the attribute value, or None if not available
46
+
47
+ Example:
48
+ # Instead of: loan_transaction.status.name (which might fail)
49
+ # Use: safe_get_related_attr(loan_transaction, 'status', 'name')
50
+
51
+ status_name = safe_get_related_attr(loan_transaction, 'status', 'name')
52
+ if status_name in {Status.COMPLETE.value, Status.FAILED.value}:
53
+ # Process the transaction
54
+ pass
55
+ """
56
+ # For unsaved objects, check the foreign key ID field first
57
+ if instance.pk is None:
58
+ fk_field_name = f"{field_name}_id"
59
+ if hasattr(instance, fk_field_name):
60
+ fk_value = getattr(instance, fk_field_name, None)
61
+ if fk_value is None:
62
+ return None
63
+ # If we have an ID but the object isn't loaded, try to load it
64
+ try:
65
+ field = instance._meta.get_field(field_name)
66
+ if hasattr(field, 'related_model'):
67
+ related_obj = field.related_model.objects.get(id=fk_value)
68
+ if attr_name is None:
69
+ return related_obj
70
+ return getattr(related_obj, attr_name, None)
71
+ except (field.related_model.DoesNotExist, AttributeError):
72
+ return None
73
+
74
+ # For saved objects or when the above doesn't work, use the original method
75
+ related_obj = safe_get_related_object(instance, field_name)
76
+ if related_obj is None:
77
+ return None
78
+ if attr_name is None:
79
+ return related_obj
80
+ return getattr(related_obj, attr_name, None)
81
+
82
+
83
+ def is_field_set(instance, field_name):
84
+ """
85
+ Check if a field has been set on a model instance.
86
+
87
+ This is useful for checking if a field has been explicitly set,
88
+ even if it's been set to None.
89
+ """
90
+ return hasattr(instance, field_name)
91
+
92
+
93
+ class HookCondition:
94
+ def check(self, instance, original_instance=None):
95
+ raise NotImplementedError
96
+
97
+ def __call__(self, instance, original_instance=None):
98
+ return self.check(instance, original_instance)
99
+
100
+ def __and__(self, other):
101
+ return AndCondition(self, other)
102
+
103
+ def __or__(self, other):
104
+ return OrCondition(self, other)
105
+
106
+ def __invert__(self):
107
+ return NotCondition(self)
108
+
109
+ def get_required_fields(self):
110
+ """
111
+ Returns a set of field names that this condition needs to evaluate.
112
+ Override in subclasses to specify required fields.
113
+ """
114
+ return set()
115
+
116
+
117
+ class IsBlank(HookCondition):
118
+ """
119
+ Condition that checks if a field is blank (None or empty string).
120
+ """
121
+ def __init__(self, field_name):
122
+ self.field_name = field_name
123
+
124
+ def check(self, instance, original_instance=None):
125
+ value = getattr(instance, self.field_name, None)
126
+ return value is None or value == ""
127
+
128
+ def get_required_fields(self):
129
+ return {self.field_name}
130
+
131
+
132
+ class AndCondition(HookCondition):
133
+ def __init__(self, *conditions):
134
+ self.conditions = conditions
135
+
136
+ def check(self, instance, original_instance=None):
137
+ return all(c.check(instance, original_instance) for c in self.conditions)
138
+
139
+ def get_required_fields(self):
140
+ fields = set()
141
+ for condition in self.conditions:
142
+ fields.update(condition.get_required_fields())
143
+ return fields
144
+
145
+
146
+ class OrCondition(HookCondition):
147
+ def __init__(self, *conditions):
148
+ self.conditions = conditions
149
+
150
+ def check(self, instance, original_instance=None):
151
+ return any(c.check(instance, original_instance) for c in self.conditions)
152
+
153
+ def get_required_fields(self):
154
+ fields = set()
155
+ for condition in self.conditions:
156
+ fields.update(condition.get_required_fields())
157
+ return fields
158
+
159
+
160
+ class NotCondition(HookCondition):
161
+ def __init__(self, condition):
162
+ self.condition = condition
163
+
164
+ def check(self, instance, original_instance=None):
165
+ return not self.condition.check(instance, original_instance)
166
+
167
+ def get_required_fields(self):
168
+ return self.condition.get_required_fields()
169
+
170
+
171
+ class IsEqual(HookCondition):
172
+ def __init__(self, field_name, value):
173
+ self.field_name = field_name
174
+ self.value = value
175
+
176
+ def check(self, instance, original_instance=None):
177
+ return getattr(instance, self.field_name, None) == self.value
178
+
179
+ def get_required_fields(self):
180
+ return {self.field_name}
181
+
182
+
183
+ class WasEqual(HookCondition):
184
+ def __init__(self, field_name, value):
185
+ self.field_name = field_name
186
+ self.value = value
187
+
188
+ def check(self, instance, original_instance=None):
189
+ if original_instance is None:
190
+ return False
191
+ return getattr(original_instance, self.field_name, None) == self.value
192
+
193
+ def get_required_fields(self):
194
+ return {self.field_name}
195
+
196
+
197
+ class HasChanged(HookCondition):
198
+ def __init__(self, field_name):
199
+ self.field_name = field_name
200
+
201
+ def check(self, instance, original_instance=None):
202
+ if original_instance is None:
203
+ return True
204
+ return getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
205
+
206
+ def get_required_fields(self):
207
+ return {self.field_name}
208
+
209
+
210
+ class ChangesTo(HookCondition):
211
+ def __init__(self, field_name, value):
212
+ self.field_name = field_name
213
+ self.value = value
214
+
215
+ def check(self, instance, original_instance=None):
216
+ if original_instance is None:
217
+ return getattr(instance, self.field_name, None) == self.value
218
+ return (
219
+ getattr(instance, self.field_name, None) == self.value
220
+ and getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
221
+ )
222
+
223
+ def get_required_fields(self):
224
+ return {self.field_name}
225
+
226
+
227
+ class IsNotEqual(HookCondition):
228
+ def __init__(self, field_name, value):
229
+ self.field_name = field_name
230
+ self.value = value
231
+
232
+ def check(self, instance, original_instance=None):
233
+ return getattr(instance, self.field_name, None) != self.value
234
+
235
+ def get_required_fields(self):
236
+ return {self.field_name}
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.93"
3
+ version = "0.1.95"
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"
@@ -1,381 +0,0 @@
1
- from django.db import models
2
-
3
-
4
- def safe_get_related_object(instance, field_name):
5
- """
6
- Safely get a related object without raising RelatedObjectDoesNotExist.
7
- Returns None if the foreign key field is None or the related object doesn't exist.
8
- """
9
- if not hasattr(instance, field_name):
10
- return None
11
-
12
- # Get the foreign key field
13
- try:
14
- field = instance._meta.get_field(field_name)
15
- if not field.is_relation or field.many_to_many or field.one_to_many:
16
- return getattr(instance, field_name, None)
17
- except models.FieldDoesNotExist:
18
- return getattr(instance, field_name, None)
19
-
20
- # Check if the foreign key field is None
21
- fk_field_name = f"{field_name}_id"
22
- if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
23
- return None
24
-
25
- # Try to get the related object, but catch RelatedObjectDoesNotExist
26
- try:
27
- return getattr(instance, field_name)
28
- except field.related_model.RelatedObjectDoesNotExist:
29
- return None
30
-
31
-
32
- def is_field_set(instance, field_name):
33
- """
34
- Check if a foreign key field is set without raising RelatedObjectDoesNotExist.
35
-
36
- Args:
37
- instance: The model instance
38
- field_name: The foreign key field name
39
-
40
- Returns:
41
- True if the field is set, False otherwise
42
- """
43
- # Check the foreign key ID field first
44
- fk_field_name = f"{field_name}_id"
45
- if hasattr(instance, fk_field_name):
46
- fk_value = getattr(instance, fk_field_name, None)
47
- return fk_value is not None
48
-
49
- # Fallback to checking the field directly
50
- try:
51
- return getattr(instance, field_name) is not None
52
- except Exception:
53
- return False
54
-
55
-
56
- def safe_get_related_attr(instance, field_name, attr_name=None):
57
- """
58
- Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
59
-
60
- This is particularly useful in hooks where objects might not have their related
61
- fields populated yet (e.g., during bulk_create operations or on unsaved objects).
62
-
63
- Args:
64
- instance: The model instance
65
- field_name: The foreign key field name
66
- attr_name: Optional attribute name to access on the related object
67
-
68
- Returns:
69
- The related object, the attribute value, or None if not available
70
-
71
- Example:
72
- # Instead of: loan_transaction.status.name (which might fail)
73
- # Use: safe_get_related_attr(loan_transaction, 'status', 'name')
74
-
75
- status_name = safe_get_related_attr(loan_transaction, 'status', 'name')
76
- if status_name in {Status.COMPLETE.value, Status.FAILED.value}:
77
- # Process the transaction
78
- pass
79
- """
80
- # For unsaved objects, check the foreign key ID field first
81
- if instance.pk is None:
82
- fk_field_name = f"{field_name}_id"
83
- if hasattr(instance, fk_field_name):
84
- fk_value = getattr(instance, fk_field_name, None)
85
- if fk_value is None:
86
- return None
87
- # If we have an ID but the object isn't loaded, try to load it
88
- try:
89
- field = instance._meta.get_field(field_name)
90
- if hasattr(field, 'related_model'):
91
- related_obj = field.related_model.objects.get(id=fk_value)
92
- if attr_name is None:
93
- return related_obj
94
- return getattr(related_obj, attr_name, None)
95
- except (field.related_model.DoesNotExist, AttributeError):
96
- return None
97
-
98
- # For saved objects or when the above doesn't work, use the original method
99
- related_obj = safe_get_related_object(instance, field_name)
100
- if related_obj is None:
101
- return None
102
-
103
- if attr_name is None:
104
- return related_obj
105
-
106
- return getattr(related_obj, attr_name, None)
107
-
108
-
109
- def safe_get_related_attr_with_fallback(instance, field_name, attr_name=None, fallback_value=None):
110
- """
111
- Enhanced version of safe_get_related_attr that provides fallback handling.
112
-
113
- This function is especially useful for bulk operations where related objects
114
- might not be fully loaded or might not exist yet.
115
-
116
- Args:
117
- instance: The model instance
118
- field_name: The foreign key field name
119
- attr_name: Optional attribute name to access on the related object
120
- fallback_value: Value to return if the related object or attribute doesn't exist
121
-
122
- Returns:
123
- The related object, the attribute value, or fallback_value if not available
124
- """
125
- # First try the standard safe access
126
- result = safe_get_related_attr(instance, field_name, attr_name)
127
- if result is not None:
128
- return result
129
-
130
- # If that fails, try to get the foreign key ID and fetch the object directly
131
- fk_field_name = f"{field_name}_id"
132
- if hasattr(instance, fk_field_name):
133
- fk_id = getattr(instance, fk_field_name)
134
- if fk_id is not None:
135
- try:
136
- # Get the field to determine the related model
137
- field = instance._meta.get_field(field_name)
138
- if field.is_relation and not field.many_to_many and not field.one_to_many:
139
- # Try to fetch the related object directly
140
- related_obj = field.related_model.objects.get(pk=fk_id)
141
- if attr_name is None:
142
- return related_obj
143
- return getattr(related_obj, attr_name, fallback_value)
144
- except (field.related_model.DoesNotExist, AttributeError):
145
- pass
146
-
147
- return fallback_value
148
-
149
-
150
- def resolve_dotted_attr(instance, dotted_path):
151
- """
152
- Recursively resolve a dotted attribute path, e.g., "type.category".
153
- This function is designed to work with pre-loaded foreign keys to avoid queries.
154
- """
155
- if instance is None:
156
- return None
157
-
158
- current = instance
159
- for attr in dotted_path.split("."):
160
- if current is None:
161
- return None
162
-
163
- # Check if this is a foreign key that might trigger a query
164
- if hasattr(current, '_meta') and hasattr(current._meta, 'get_field'):
165
- try:
166
- field = current._meta.get_field(attr)
167
- if field.is_relation and not field.many_to_many and not field.one_to_many:
168
- # For foreign keys, use safe access to prevent RelatedObjectDoesNotExist
169
- current = safe_get_related_object(current, attr)
170
- else:
171
- current = getattr(current, attr, None)
172
- except Exception:
173
- # If field lookup fails, fall back to regular attribute access
174
- current = getattr(current, attr, None)
175
- else:
176
- # Not a model instance, use regular attribute access
177
- current = getattr(current, attr, None)
178
-
179
- return current
180
-
181
-
182
- class HookCondition:
183
- def check(self, instance, original_instance=None):
184
- raise NotImplementedError
185
-
186
- def __call__(self, instance, original_instance=None):
187
- return self.check(instance, original_instance)
188
-
189
- def __and__(self, other):
190
- return AndCondition(self, other)
191
-
192
- def __or__(self, other):
193
- return OrCondition(self, other)
194
-
195
- def __invert__(self):
196
- return NotCondition(self)
197
-
198
- def get_required_fields(self):
199
- """
200
- Returns a set of field names that this condition needs to evaluate.
201
- Override in subclasses to specify required fields.
202
- """
203
- return set()
204
-
205
-
206
- class IsEqual(HookCondition):
207
- def __init__(self, field, value):
208
- self.field = field
209
- self.value = value
210
-
211
- def check(self, instance, original_instance=None):
212
- # Handle the case where the field might not exist yet
213
- try:
214
- current_value = getattr(instance, self.field, None)
215
- return current_value == self.value
216
- except Exception:
217
- # If there's any error accessing the field, treat it as None
218
- return self.value is None
219
-
220
- def get_required_fields(self):
221
- return {self.field.split('.')[0]}
222
-
223
-
224
- class IsNotEqual(HookCondition):
225
- def __init__(self, field, value):
226
- self.field = field
227
- self.value = value
228
-
229
- def check(self, instance, original_instance=None):
230
- current_value = resolve_dotted_attr(instance, self.field)
231
- return current_value != self.value
232
-
233
- def get_required_fields(self):
234
- return {self.field.split('.')[0]}
235
-
236
-
237
- class WasEqual(HookCondition):
238
- def __init__(self, field, value):
239
- self.field = field
240
- self.value = value
241
-
242
- def check(self, instance, original_instance=None):
243
- if original_instance is None:
244
- return False
245
- original_value = resolve_dotted_attr(original_instance, self.field)
246
- return original_value == self.value
247
-
248
- def get_required_fields(self):
249
- return {self.field.split('.')[0]}
250
-
251
-
252
- class HasChanged(HookCondition):
253
- def __init__(self, field, has_changed=True):
254
- """
255
- Check if a field's value has changed or remained the same.
256
-
257
- Args:
258
- field: The field name to check
259
- has_changed: If True (default), condition passes when field has changed.
260
- If False, condition passes when field has remained the same.
261
- This is useful for:
262
- - Detecting stable/unchanged fields
263
- - Validating field immutability
264
- - Ensuring critical fields remain constant
265
- - State machine validations
266
- """
267
- self.field = field
268
- self.has_changed = has_changed
269
-
270
- def check(self, instance, original_instance=None):
271
- if original_instance is None:
272
- # For new instances:
273
- # - If we're checking for changes (has_changed=True), return False since there's no change yet
274
- # - If we're checking for stability (has_changed=False), return True since it's technically unchanged
275
- return not self.has_changed
276
-
277
- current_value = resolve_dotted_attr(instance, self.field)
278
- original_value = resolve_dotted_attr(original_instance, self.field)
279
- return (current_value != original_value) == self.has_changed
280
-
281
- def get_required_fields(self):
282
- return {self.field.split('.')[0]}
283
-
284
-
285
- class ChangesTo(HookCondition):
286
- def __init__(self, field, value):
287
- self.field = field
288
- self.value = value
289
-
290
- def check(self, instance, original_instance=None):
291
- if original_instance is None:
292
- current_value = resolve_dotted_attr(instance, self.field)
293
- return current_value == self.value
294
-
295
- current_value = resolve_dotted_attr(instance, self.field)
296
- original_value = resolve_dotted_attr(original_instance, self.field)
297
- return current_value == self.value and current_value != original_value
298
-
299
- def get_required_fields(self):
300
- return {self.field.split('.')[0]}
301
-
302
-
303
- class IsGreaterThan(HookCondition):
304
- def __init__(self, field, value):
305
- self.field = field
306
- self.value = value
307
-
308
- def check(self, instance, original_instance=None):
309
- current = resolve_dotted_attr(instance, self.field)
310
- return current is not None and current > self.value
311
-
312
-
313
- class IsGreaterThanOrEqual(HookCondition):
314
- def __init__(self, field, value):
315
- self.field = field
316
- self.value = value
317
-
318
- def check(self, instance, original_instance=None):
319
- current = resolve_dotted_attr(instance, self.field)
320
- return current is not None and current >= self.value
321
-
322
-
323
- class IsLessThan(HookCondition):
324
- def __init__(self, field, value):
325
- self.field = field
326
- self.value = value
327
-
328
- def check(self, instance, original_instance=None):
329
- current = resolve_dotted_attr(instance, self.field)
330
- return current is not None and current < self.value
331
-
332
-
333
- class IsLessThanOrEqual(HookCondition):
334
- def __init__(self, field, value):
335
- self.field = field
336
- self.value = value
337
-
338
- def check(self, instance, original_instance=None):
339
- current = resolve_dotted_attr(instance, self.field)
340
- return current is not None and current <= self.value
341
-
342
-
343
- class AndCondition(HookCondition):
344
- def __init__(self, condition1, condition2):
345
- self.condition1 = condition1
346
- self.condition2 = condition2
347
-
348
- def check(self, instance, original_instance=None):
349
- return (
350
- self.condition1.check(instance, original_instance)
351
- and self.condition2.check(instance, original_instance)
352
- )
353
-
354
- def get_required_fields(self):
355
- return self.condition1.get_required_fields() | self.condition2.get_required_fields()
356
-
357
-
358
- class OrCondition(HookCondition):
359
- def __init__(self, condition1, condition2):
360
- self.condition1 = condition1
361
- self.condition2 = condition2
362
-
363
- def check(self, instance, original_instance=None):
364
- return (
365
- self.condition1.check(instance, original_instance)
366
- or self.condition2.check(instance, original_instance)
367
- )
368
-
369
- def get_required_fields(self):
370
- return self.condition1.get_required_fields() | self.condition2.get_required_fields()
371
-
372
-
373
- class NotCondition(HookCondition):
374
- def __init__(self, condition):
375
- self.condition = condition
376
-
377
- def check(self, instance, original_instance=None):
378
- return not self.condition.check(instance, original_instance)
379
-
380
- def get_required_fields(self):
381
- return self.condition.get_required_fields()