django-bulk-hooks 0.1.93__py3-none-any.whl → 0.1.94__py3-none-any.whl

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.

@@ -29,30 +29,6 @@ def safe_get_related_object(instance, field_name):
29
29
  return None
30
30
 
31
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
32
  def safe_get_related_attr(instance, field_name, attr_name=None):
57
33
  """
58
34
  Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
@@ -99,84 +75,19 @@ def safe_get_related_attr(instance, field_name, attr_name=None):
99
75
  related_obj = safe_get_related_object(instance, field_name)
100
76
  if related_obj is None:
101
77
  return None
102
-
103
78
  if attr_name is None:
104
79
  return related_obj
105
-
106
80
  return getattr(related_obj, attr_name, None)
107
81
 
108
82
 
109
- def safe_get_related_attr_with_fallback(instance, field_name, attr_name=None, fallback_value=None):
83
+ def is_field_set(instance, field_name):
110
84
  """
111
- Enhanced version of safe_get_related_attr that provides fallback handling.
85
+ Check if a field has been set on a model instance.
112
86
 
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
87
+ This is useful for checking if a field has been explicitly set,
88
+ even if it's been set to None.
124
89
  """
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
90
+ return hasattr(instance, field_name)
180
91
 
181
92
 
182
93
  class HookCondition:
@@ -203,179 +114,123 @@ class HookCondition:
203
114
  return set()
204
115
 
205
116
 
206
- class IsEqual(HookCondition):
207
- def __init__(self, field, value):
208
- self.field = field
209
- self.value = value
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
210
123
 
211
124
  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
125
+ value = getattr(instance, self.field_name, None)
126
+ return value is None or value == ""
219
127
 
220
128
  def get_required_fields(self):
221
- return {self.field.split('.')[0]}
129
+ return {self.field_name}
222
130
 
223
131
 
224
- class IsNotEqual(HookCondition):
225
- def __init__(self, field, value):
226
- self.field = field
227
- self.value = value
132
+ class AndCondition(HookCondition):
133
+ def __init__(self, *conditions):
134
+ self.conditions = conditions
228
135
 
229
136
  def check(self, instance, original_instance=None):
230
- current_value = resolve_dotted_attr(instance, self.field)
231
- return current_value != self.value
137
+ return all(c.check(instance, original_instance) for c in self.conditions)
232
138
 
233
139
  def get_required_fields(self):
234
- return {self.field.split('.')[0]}
140
+ fields = set()
141
+ for condition in self.conditions:
142
+ fields.update(condition.get_required_fields())
143
+ return fields
235
144
 
236
145
 
237
- class WasEqual(HookCondition):
238
- def __init__(self, field, value):
239
- self.field = field
240
- self.value = value
146
+ class OrCondition(HookCondition):
147
+ def __init__(self, *conditions):
148
+ self.conditions = conditions
241
149
 
242
150
  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
151
+ return any(c.check(instance, original_instance) for c in self.conditions)
247
152
 
248
153
  def get_required_fields(self):
249
- return {self.field.split('.')[0]}
154
+ fields = set()
155
+ for condition in self.conditions:
156
+ fields.update(condition.get_required_fields())
157
+ return fields
250
158
 
251
159
 
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
160
+ class NotCondition(HookCondition):
161
+ def __init__(self, condition):
162
+ self.condition = condition
269
163
 
270
164
  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
165
+ return not self.condition.check(instance, original_instance)
280
166
 
281
167
  def get_required_fields(self):
282
- return {self.field.split('.')[0]}
168
+ return self.condition.get_required_fields()
283
169
 
284
170
 
285
- class ChangesTo(HookCondition):
286
- def __init__(self, field, value):
287
- self.field = field
171
+ class IsEqual(HookCondition):
172
+ def __init__(self, field_name, value):
173
+ self.field_name = field_name
288
174
  self.value = value
289
175
 
290
176
  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
177
+ return getattr(instance, self.field_name, None) == self.value
298
178
 
299
179
  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
180
+ return {self.field_name}
321
181
 
322
182
 
323
- class IsLessThan(HookCondition):
324
- def __init__(self, field, value):
325
- self.field = field
183
+ class WasEqual(HookCondition):
184
+ def __init__(self, field_name, value):
185
+ self.field_name = field_name
326
186
  self.value = value
327
187
 
328
188
  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
189
+ if original_instance is None:
190
+ return False
191
+ return getattr(original_instance, self.field_name, None) == self.value
337
192
 
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
193
+ def get_required_fields(self):
194
+ return {self.field_name}
341
195
 
342
196
 
343
- class AndCondition(HookCondition):
344
- def __init__(self, condition1, condition2):
345
- self.condition1 = condition1
346
- self.condition2 = condition2
197
+ class HasChanged(HookCondition):
198
+ def __init__(self, field_name):
199
+ self.field_name = field_name
347
200
 
348
201
  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
- )
202
+ if original_instance is None:
203
+ return True
204
+ return getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
353
205
 
354
206
  def get_required_fields(self):
355
- return self.condition1.get_required_fields() | self.condition2.get_required_fields()
207
+ return {self.field_name}
356
208
 
357
209
 
358
- class OrCondition(HookCondition):
359
- def __init__(self, condition1, condition2):
360
- self.condition1 = condition1
361
- self.condition2 = condition2
210
+ class ChangesTo(HookCondition):
211
+ def __init__(self, field_name, value):
212
+ self.field_name = field_name
213
+ self.value = value
362
214
 
363
215
  def check(self, instance, original_instance=None):
216
+ if original_instance is None:
217
+ return getattr(instance, self.field_name, None) == self.value
364
218
  return (
365
- self.condition1.check(instance, original_instance)
366
- or self.condition2.check(instance, original_instance)
219
+ getattr(instance, self.field_name, None) == self.value
220
+ and getattr(instance, self.field_name, None) != getattr(original_instance, self.field_name, None)
367
221
  )
368
222
 
369
223
  def get_required_fields(self):
370
- return self.condition1.get_required_fields() | self.condition2.get_required_fields()
224
+ return {self.field_name}
371
225
 
372
226
 
373
- class NotCondition(HookCondition):
374
- def __init__(self, condition):
375
- self.condition = condition
227
+ class IsNotEqual(HookCondition):
228
+ def __init__(self, field_name, value):
229
+ self.field_name = field_name
230
+ self.value = value
376
231
 
377
232
  def check(self, instance, original_instance=None):
378
- return not self.condition.check(instance, original_instance)
233
+ return getattr(instance, self.field_name, None) != self.value
379
234
 
380
235
  def get_required_fields(self):
381
- return self.condition.get_required_fields()
236
+ return {self.field_name}
@@ -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.94
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
@@ -1,5 +1,5 @@
1
1
  django_bulk_hooks/__init__.py,sha256=2PcJ6xz7t7Du0nmLO_5732G6u_oZTygogG0fKESRHHk,1082
2
- django_bulk_hooks/conditions.py,sha256=w3uxXlsJcxSsoH7_7dfmYkO6oIr8OqgoaUGp6oDIGZo,13573
2
+ django_bulk_hooks/conditions.py,sha256=HTavJ6K6xJtk2pZS9YBcgAW495myDCNxpLo60YiXeDY,7747
3
3
  django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
4
  django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
5
5
  django_bulk_hooks/decorators.py,sha256=zstmb27dKcOHu3Atg7cauewCTzPvUmq03mzVKJRi56o,7230
@@ -10,7 +10,7 @@ django_bulk_hooks/manager.py,sha256=DcVosEA4RS79KSYgw3Z14_a9Sd8CfxNNc5F3eSb8xc0,
10
10
  django_bulk_hooks/models.py,sha256=a9XoGgIG4Sfi_kvGnPBbG2DlvgZDz6Qck4VG-DGqFT0,4981
11
11
  django_bulk_hooks/queryset.py,sha256=7lLqhZ-XOYsZ1I3Loxi4Nhz79M8HlTYE413AW8nyeDI,1330
12
12
  django_bulk_hooks/registry.py,sha256=Vh78exKYcdZhM27120kQm-iXGOjd_kf9ZUYBZ8eQ2V0,683
13
- django_bulk_hooks-0.1.93.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
14
- django_bulk_hooks-0.1.93.dist-info/METADATA,sha256=zygRGrXMlCDgNdYIRTuD12kLmWLiO5Mn0ae1kvJFVWM,9051
15
- django_bulk_hooks-0.1.93.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
16
- django_bulk_hooks-0.1.93.dist-info/RECORD,,
13
+ django_bulk_hooks-0.1.94.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
14
+ django_bulk_hooks-0.1.94.dist-info/METADATA,sha256=33bEnBka7zaJnRj3ov8NcZRU_CCTe-7GNlsDPU0Yjng,9051
15
+ django_bulk_hooks-0.1.94.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
16
+ django_bulk_hooks-0.1.94.dist-info/RECORD,,