django-bulk-hooks 0.1.263__tar.gz → 0.1.264__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.
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/PKG-INFO +4 -4
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/README.md +3 -3
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/manager.py +3 -6
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/queryset.py +70 -10
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/LICENSE +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.263 → django_bulk_hooks-0.1.264}/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.1.
|
|
3
|
+
Version: 0.1.264
|
|
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
|
|
@@ -114,7 +114,7 @@ Account.objects.bulk_create(accounts)
|
|
|
114
114
|
# Bulk update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
|
|
115
115
|
for account in accounts:
|
|
116
116
|
account.balance *= 1.1
|
|
117
|
-
Account.objects.bulk_update(accounts
|
|
117
|
+
Account.objects.bulk_update(accounts) # fields are auto-detected
|
|
118
118
|
|
|
119
119
|
# Bulk delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
|
|
120
120
|
Account.objects.bulk_delete(accounts)
|
|
@@ -200,7 +200,7 @@ account.delete()
|
|
|
200
200
|
```python
|
|
201
201
|
# These also trigger hooks
|
|
202
202
|
Account.objects.bulk_create(accounts)
|
|
203
|
-
Account.objects.bulk_update(accounts
|
|
203
|
+
Account.objects.bulk_update(accounts) # fields are auto-detected
|
|
204
204
|
Account.objects.bulk_delete(accounts)
|
|
205
205
|
```
|
|
206
206
|
|
|
@@ -239,7 +239,7 @@ accounts = [account1, account2, account3] # IDs: 1, 2, 3
|
|
|
239
239
|
reordered = [account3, account1, account2] # IDs: 3, 1, 2
|
|
240
240
|
|
|
241
241
|
# The hook will still receive properly paired old/new records
|
|
242
|
-
LoanAccount.objects.bulk_update(reordered
|
|
242
|
+
LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
## 🧩 Integration with Other Managers
|
|
@@ -95,7 +95,7 @@ Account.objects.bulk_create(accounts)
|
|
|
95
95
|
# Bulk update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
|
|
96
96
|
for account in accounts:
|
|
97
97
|
account.balance *= 1.1
|
|
98
|
-
Account.objects.bulk_update(accounts
|
|
98
|
+
Account.objects.bulk_update(accounts) # fields are auto-detected
|
|
99
99
|
|
|
100
100
|
# Bulk delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
|
|
101
101
|
Account.objects.bulk_delete(accounts)
|
|
@@ -181,7 +181,7 @@ account.delete()
|
|
|
181
181
|
```python
|
|
182
182
|
# These also trigger hooks
|
|
183
183
|
Account.objects.bulk_create(accounts)
|
|
184
|
-
Account.objects.bulk_update(accounts
|
|
184
|
+
Account.objects.bulk_update(accounts) # fields are auto-detected
|
|
185
185
|
Account.objects.bulk_delete(accounts)
|
|
186
186
|
```
|
|
187
187
|
|
|
@@ -220,7 +220,7 @@ accounts = [account1, account2, account3] # IDs: 1, 2, 3
|
|
|
220
220
|
reordered = [account3, account1, account2] # IDs: 3, 1, 2
|
|
221
221
|
|
|
222
222
|
# The hook will still receive properly paired old/new records
|
|
223
|
-
LoanAccount.objects.bulk_update(reordered
|
|
223
|
+
LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
|
|
224
224
|
```
|
|
225
225
|
|
|
226
226
|
## 🧩 Integration with Other Managers
|
|
@@ -51,7 +51,7 @@ class BulkHookManager(models.Manager):
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
def bulk_update(
|
|
54
|
-
self, objs,
|
|
54
|
+
self, objs, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
55
55
|
):
|
|
56
56
|
"""
|
|
57
57
|
Delegate to QuerySet's bulk_update implementation.
|
|
@@ -59,7 +59,6 @@ class BulkHookManager(models.Manager):
|
|
|
59
59
|
"""
|
|
60
60
|
return self.get_queryset().bulk_update(
|
|
61
61
|
objs,
|
|
62
|
-
fields,
|
|
63
62
|
bypass_hooks=bypass_hooks,
|
|
64
63
|
bypass_validation=bypass_validation,
|
|
65
64
|
**kwargs,
|
|
@@ -104,10 +103,8 @@ class BulkHookManager(models.Manager):
|
|
|
104
103
|
Save a single object using the appropriate bulk operation.
|
|
105
104
|
"""
|
|
106
105
|
if obj.pk:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
110
|
-
)
|
|
106
|
+
# bulk_update now auto-detects changed fields
|
|
107
|
+
self.bulk_update([obj])
|
|
111
108
|
else:
|
|
112
109
|
self.bulk_create([obj])
|
|
113
110
|
return obj
|
|
@@ -846,12 +846,70 @@ class HookQuerySetMixin:
|
|
|
846
846
|
|
|
847
847
|
return result
|
|
848
848
|
|
|
849
|
+
def _detect_changed_fields(self, objs):
|
|
850
|
+
"""
|
|
851
|
+
Auto-detect which fields have changed by comparing objects with database values.
|
|
852
|
+
Returns a set of field names that have changed across all objects.
|
|
853
|
+
"""
|
|
854
|
+
if not objs:
|
|
855
|
+
return set()
|
|
856
|
+
|
|
857
|
+
model_cls = self.model
|
|
858
|
+
changed_fields = set()
|
|
859
|
+
|
|
860
|
+
# Get primary key field names
|
|
861
|
+
pk_fields = [f.name for f in model_cls._meta.pk_fields]
|
|
862
|
+
if not pk_fields:
|
|
863
|
+
pk_fields = ['pk']
|
|
864
|
+
|
|
865
|
+
# Get all object PKs
|
|
866
|
+
obj_pks = []
|
|
867
|
+
for obj in objs:
|
|
868
|
+
if hasattr(obj, 'pk') and obj.pk is not None:
|
|
869
|
+
obj_pks.append(obj.pk)
|
|
870
|
+
else:
|
|
871
|
+
# Skip objects without PKs
|
|
872
|
+
continue
|
|
873
|
+
|
|
874
|
+
if not obj_pks:
|
|
875
|
+
return set()
|
|
876
|
+
|
|
877
|
+
# Fetch current database values for all objects
|
|
878
|
+
existing_objs = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=obj_pks)}
|
|
879
|
+
|
|
880
|
+
# Compare each object's current values with database values
|
|
881
|
+
for obj in objs:
|
|
882
|
+
if obj.pk not in existing_objs:
|
|
883
|
+
continue
|
|
884
|
+
|
|
885
|
+
db_obj = existing_objs[obj.pk]
|
|
886
|
+
|
|
887
|
+
# Check all concrete fields for changes
|
|
888
|
+
for field in model_cls._meta.concrete_fields:
|
|
889
|
+
field_name = field.name
|
|
890
|
+
|
|
891
|
+
# Skip primary key fields
|
|
892
|
+
if field_name in pk_fields:
|
|
893
|
+
continue
|
|
894
|
+
|
|
895
|
+
# Get current value from object
|
|
896
|
+
current_value = getattr(obj, field_name, None)
|
|
897
|
+
# Get database value
|
|
898
|
+
db_value = getattr(db_obj, field_name, None)
|
|
899
|
+
|
|
900
|
+
# Compare values (handle None cases)
|
|
901
|
+
if current_value != db_value:
|
|
902
|
+
changed_fields.add(field_name)
|
|
903
|
+
|
|
904
|
+
return changed_fields
|
|
905
|
+
|
|
849
906
|
@transaction.atomic
|
|
850
907
|
def bulk_update(
|
|
851
|
-
self, objs,
|
|
908
|
+
self, objs, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
852
909
|
):
|
|
853
910
|
"""
|
|
854
911
|
Bulk update objects in the database with MTI support.
|
|
912
|
+
Automatically detects which fields have changed by comparing with database values.
|
|
855
913
|
"""
|
|
856
914
|
model_cls = self.model
|
|
857
915
|
|
|
@@ -863,10 +921,14 @@ class HookQuerySetMixin:
|
|
|
863
921
|
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
864
922
|
)
|
|
865
923
|
|
|
924
|
+
# Auto-detect changed fields by comparing with database values
|
|
925
|
+
changed_fields = self._detect_changed_fields(objs)
|
|
926
|
+
logger.debug(f"Auto-detected changed fields: {changed_fields}")
|
|
927
|
+
|
|
866
928
|
logger.debug(
|
|
867
|
-
f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}
|
|
929
|
+
f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}"
|
|
868
930
|
)
|
|
869
|
-
print(f"DEBUG: bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}
|
|
931
|
+
print(f"DEBUG: bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}")
|
|
870
932
|
|
|
871
933
|
# Check for MTI
|
|
872
934
|
is_mti = False
|
|
@@ -887,7 +949,7 @@ class HookQuerySetMixin:
|
|
|
887
949
|
) # Ensure originals is defined for after_update call
|
|
888
950
|
|
|
889
951
|
# Handle auto_now fields like Django's update_or_create does
|
|
890
|
-
fields_set = set(
|
|
952
|
+
fields_set = set(changed_fields)
|
|
891
953
|
pk_fields = model_cls._meta.pk_fields
|
|
892
954
|
pk_field_names = [f.name for f in pk_fields]
|
|
893
955
|
auto_now_fields = []
|
|
@@ -921,7 +983,6 @@ class HookQuerySetMixin:
|
|
|
921
983
|
|
|
922
984
|
logger.debug(f"Auto_now fields detected: {auto_now_fields}")
|
|
923
985
|
print(f"DEBUG: Auto_now fields detected: {auto_now_fields}")
|
|
924
|
-
fields = list(fields_set)
|
|
925
986
|
|
|
926
987
|
# Set auto_now field values to current timestamp
|
|
927
988
|
if auto_now_fields:
|
|
@@ -949,7 +1010,6 @@ class HookQuerySetMixin:
|
|
|
949
1010
|
# Add this field to the update fields if it's not already there and not a primary key
|
|
950
1011
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
951
1012
|
fields_set.add(field.name)
|
|
952
|
-
fields.append(field.name)
|
|
953
1013
|
logger.debug(f"Custom field {field.name} updated via pre_save() for object {obj.pk}")
|
|
954
1014
|
print(f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}")
|
|
955
1015
|
except Exception as e:
|
|
@@ -958,7 +1018,7 @@ class HookQuerySetMixin:
|
|
|
958
1018
|
|
|
959
1019
|
# Handle MTI models differently
|
|
960
1020
|
if is_mti:
|
|
961
|
-
result = self._mti_bulk_update(objs,
|
|
1021
|
+
result = self._mti_bulk_update(objs, list(fields_set), **kwargs)
|
|
962
1022
|
else:
|
|
963
1023
|
# For single-table models, use Django's built-in bulk_update
|
|
964
1024
|
django_kwargs = {
|
|
@@ -970,12 +1030,12 @@ class HookQuerySetMixin:
|
|
|
970
1030
|
print("DEBUG: Calling Django bulk_update")
|
|
971
1031
|
# Build a per-object concrete value map to avoid leaking expressions into hooks
|
|
972
1032
|
value_map = {}
|
|
973
|
-
logger.debug(f"Building value map for {len(objs)} objects with fields: {
|
|
1033
|
+
logger.debug(f"Building value map for {len(objs)} objects with fields: {list(fields_set)}")
|
|
974
1034
|
for obj in objs:
|
|
975
1035
|
if obj.pk is None:
|
|
976
1036
|
continue
|
|
977
1037
|
field_values = {}
|
|
978
|
-
for field_name in
|
|
1038
|
+
for field_name in fields_set:
|
|
979
1039
|
# Capture raw values assigned on the object (not expressions)
|
|
980
1040
|
field_values[field_name] = getattr(obj, field_name)
|
|
981
1041
|
if field_name in auto_now_fields:
|
|
@@ -988,7 +1048,7 @@ class HookQuerySetMixin:
|
|
|
988
1048
|
set_bulk_update_value_map(value_map)
|
|
989
1049
|
|
|
990
1050
|
try:
|
|
991
|
-
result = super().bulk_update(objs,
|
|
1051
|
+
result = super().bulk_update(objs, list(fields_set), **django_kwargs)
|
|
992
1052
|
finally:
|
|
993
1053
|
# Always clear after the internal update() path finishes
|
|
994
1054
|
set_bulk_update_value_map(None)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.264"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|