django-bulk-hooks 0.2.43__tar.gz → 0.2.44__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.2.43 → django_bulk_hooks-0.2.44}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/bulk_executor.py +42 -7
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/coordinator.py +118 -7
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/registry.py +1 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/LICENSE +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/README.md +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/mti_handler.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/queryset.py +0 -0
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -47,6 +47,8 @@ class BulkExecutor:
|
|
|
47
47
|
update_conflicts=False,
|
|
48
48
|
update_fields=None,
|
|
49
49
|
unique_fields=None,
|
|
50
|
+
existing_record_ids=None,
|
|
51
|
+
existing_pks_map=None,
|
|
50
52
|
**kwargs,
|
|
51
53
|
):
|
|
52
54
|
"""
|
|
@@ -74,11 +76,12 @@ class BulkExecutor:
|
|
|
74
76
|
if self.mti_handler.is_mti_model():
|
|
75
77
|
logger.info(f"Detected MTI model {self.model_cls.__name__}, using MTI bulk create")
|
|
76
78
|
|
|
77
|
-
#
|
|
78
|
-
existing_record_ids
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
# Use pre-classified records if provided, otherwise classify now
|
|
80
|
+
if existing_record_ids is None or existing_pks_map is None:
|
|
81
|
+
existing_record_ids = set()
|
|
82
|
+
existing_pks_map = {}
|
|
83
|
+
if update_conflicts and unique_fields:
|
|
84
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
82
85
|
|
|
83
86
|
# Build execution plan with classification results
|
|
84
87
|
plan = self.mti_handler.build_create_plan(
|
|
@@ -91,10 +94,16 @@ class BulkExecutor:
|
|
|
91
94
|
existing_pks_map=existing_pks_map,
|
|
92
95
|
)
|
|
93
96
|
# Execute the plan
|
|
94
|
-
|
|
97
|
+
result = self._execute_mti_create_plan(plan)
|
|
98
|
+
|
|
99
|
+
# Tag objects with upsert metadata for hook dispatching
|
|
100
|
+
if update_conflicts and unique_fields:
|
|
101
|
+
self._tag_upsert_metadata(result, existing_record_ids)
|
|
102
|
+
|
|
103
|
+
return result
|
|
95
104
|
|
|
96
105
|
# Non-MTI model - use Django's native bulk_create
|
|
97
|
-
|
|
106
|
+
result = self._execute_bulk_create(
|
|
98
107
|
objs,
|
|
99
108
|
batch_size,
|
|
100
109
|
ignore_conflicts,
|
|
@@ -103,6 +112,15 @@ class BulkExecutor:
|
|
|
103
112
|
unique_fields,
|
|
104
113
|
**kwargs,
|
|
105
114
|
)
|
|
115
|
+
|
|
116
|
+
# Tag objects with upsert metadata for hook dispatching
|
|
117
|
+
if update_conflicts and unique_fields:
|
|
118
|
+
# Use pre-classified results if available, otherwise classify now
|
|
119
|
+
if existing_record_ids is None:
|
|
120
|
+
existing_record_ids, _ = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
121
|
+
self._tag_upsert_metadata(result, existing_record_ids)
|
|
122
|
+
|
|
123
|
+
return result
|
|
106
124
|
|
|
107
125
|
def _execute_bulk_create(
|
|
108
126
|
self,
|
|
@@ -510,3 +528,20 @@ class BulkExecutor:
|
|
|
510
528
|
from django.db.models import QuerySet
|
|
511
529
|
|
|
512
530
|
return QuerySet.delete(self.queryset)
|
|
531
|
+
|
|
532
|
+
def _tag_upsert_metadata(self, result_objects, existing_record_ids):
|
|
533
|
+
"""
|
|
534
|
+
Tag objects with metadata indicating whether they were created or updated.
|
|
535
|
+
|
|
536
|
+
This metadata is used by the coordinator to determine which hooks to fire.
|
|
537
|
+
The metadata is temporary and will be cleaned up after hook execution.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
result_objects: List of objects returned from bulk operation
|
|
541
|
+
existing_record_ids: Set of id() for objects that existed before the operation
|
|
542
|
+
"""
|
|
543
|
+
for obj in result_objects:
|
|
544
|
+
# Tag with metadata for hook dispatching
|
|
545
|
+
was_created = id(obj) not in existing_record_ids
|
|
546
|
+
obj._bulk_hooks_was_created = was_created
|
|
547
|
+
obj._bulk_hooks_upsert_metadata = True
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -133,6 +133,18 @@ class BulkOperationCoordinator:
|
|
|
133
133
|
# Validate
|
|
134
134
|
self.analyzer.validate_for_create(objs)
|
|
135
135
|
|
|
136
|
+
# For upsert operations, classify records upfront
|
|
137
|
+
existing_record_ids = set()
|
|
138
|
+
existing_pks_map = {}
|
|
139
|
+
if update_conflicts and unique_fields:
|
|
140
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(
|
|
141
|
+
objs, unique_fields
|
|
142
|
+
)
|
|
143
|
+
logger.info(
|
|
144
|
+
f"Upsert operation: {len(existing_record_ids)} existing, "
|
|
145
|
+
f"{len(objs) - len(existing_record_ids)} new records"
|
|
146
|
+
)
|
|
147
|
+
|
|
136
148
|
# Build initial changeset
|
|
137
149
|
changeset = build_changeset_for_create(
|
|
138
150
|
self.model_cls,
|
|
@@ -153,6 +165,8 @@ class BulkOperationCoordinator:
|
|
|
153
165
|
update_conflicts=update_conflicts,
|
|
154
166
|
update_fields=update_fields,
|
|
155
167
|
unique_fields=unique_fields,
|
|
168
|
+
existing_record_ids=existing_record_ids,
|
|
169
|
+
existing_pks_map=existing_pks_map,
|
|
156
170
|
)
|
|
157
171
|
|
|
158
172
|
return self._execute_with_mti_hooks(
|
|
@@ -627,13 +641,24 @@ class BulkOperationCoordinator:
|
|
|
627
641
|
# AFTER phase - for all models in chain
|
|
628
642
|
# Use result if operation returns modified data (for create operations)
|
|
629
643
|
if result and isinstance(result, list) and event_prefix == "create":
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
644
|
+
# Check if this was an upsert operation
|
|
645
|
+
is_upsert = self._is_upsert_operation(result)
|
|
646
|
+
if is_upsert:
|
|
647
|
+
# Split hooks for upsert: after_create for created, after_update for updated
|
|
648
|
+
self._dispatch_upsert_after_hooks(result, models_in_chain)
|
|
649
|
+
else:
|
|
650
|
+
# Normal create operation
|
|
651
|
+
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
652
|
+
changeset = build_changeset_for_create(changeset.model_cls, result)
|
|
653
|
+
|
|
654
|
+
for model_cls in models_in_chain:
|
|
655
|
+
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
656
|
+
self.dispatcher.dispatch(model_changeset, f"after_{event_prefix}", bypass_hooks=False)
|
|
657
|
+
else:
|
|
658
|
+
# Non-create operations (update, delete)
|
|
659
|
+
for model_cls in models_in_chain:
|
|
660
|
+
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
661
|
+
self.dispatcher.dispatch(model_changeset, f"after_{event_prefix}", bypass_hooks=False)
|
|
637
662
|
|
|
638
663
|
return result
|
|
639
664
|
|
|
@@ -668,3 +693,89 @@ class BulkOperationCoordinator:
|
|
|
668
693
|
continue
|
|
669
694
|
|
|
670
695
|
return fk_relationships
|
|
696
|
+
|
|
697
|
+
def _is_upsert_operation(self, result_objects):
|
|
698
|
+
"""
|
|
699
|
+
Check if the operation was an upsert (mixed create/update).
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
result_objects: List of objects returned from the operation
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
True if this was an upsert operation, False otherwise
|
|
706
|
+
"""
|
|
707
|
+
if not result_objects:
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
# Check if any object has upsert metadata
|
|
711
|
+
return hasattr(result_objects[0], '_bulk_hooks_upsert_metadata')
|
|
712
|
+
|
|
713
|
+
def _dispatch_upsert_after_hooks(self, result_objects, models_in_chain):
|
|
714
|
+
"""
|
|
715
|
+
Dispatch after hooks for upsert operations, splitting by create/update.
|
|
716
|
+
|
|
717
|
+
This matches Salesforce behavior:
|
|
718
|
+
- Records that were created fire after_create hooks
|
|
719
|
+
- Records that were updated fire after_update hooks
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
result_objects: List of objects returned from the operation
|
|
723
|
+
models_in_chain: List of model classes in the MTI inheritance chain
|
|
724
|
+
"""
|
|
725
|
+
# Split objects by operation type
|
|
726
|
+
created_objects = []
|
|
727
|
+
updated_objects = []
|
|
728
|
+
|
|
729
|
+
for obj in result_objects:
|
|
730
|
+
was_created = getattr(obj, '_bulk_hooks_was_created', True)
|
|
731
|
+
if was_created:
|
|
732
|
+
created_objects.append(obj)
|
|
733
|
+
else:
|
|
734
|
+
updated_objects.append(obj)
|
|
735
|
+
|
|
736
|
+
logger.info(
|
|
737
|
+
f"Upsert after hooks: {len(created_objects)} created, "
|
|
738
|
+
f"{len(updated_objects)} updated"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# Dispatch after_create hooks for created objects
|
|
742
|
+
if created_objects:
|
|
743
|
+
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
744
|
+
create_changeset = build_changeset_for_create(self.model_cls, created_objects)
|
|
745
|
+
|
|
746
|
+
for model_cls in models_in_chain:
|
|
747
|
+
model_changeset = self._build_changeset_for_model(create_changeset, model_cls)
|
|
748
|
+
self.dispatcher.dispatch(model_changeset, "after_create", bypass_hooks=False)
|
|
749
|
+
|
|
750
|
+
# Dispatch after_update hooks for updated objects
|
|
751
|
+
if updated_objects:
|
|
752
|
+
# Fetch old records for proper change detection
|
|
753
|
+
old_records_map = self.analyzer.fetch_old_records_map(updated_objects)
|
|
754
|
+
|
|
755
|
+
from django_bulk_hooks.helpers import build_changeset_for_update
|
|
756
|
+
update_changeset = build_changeset_for_update(
|
|
757
|
+
self.model_cls,
|
|
758
|
+
updated_objects,
|
|
759
|
+
update_kwargs={}, # Empty since we don't know specific fields
|
|
760
|
+
old_records_map=old_records_map,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
for model_cls in models_in_chain:
|
|
764
|
+
model_changeset = self._build_changeset_for_model(update_changeset, model_cls)
|
|
765
|
+
self.dispatcher.dispatch(model_changeset, "after_update", bypass_hooks=False)
|
|
766
|
+
|
|
767
|
+
# Clean up temporary metadata
|
|
768
|
+
self._cleanup_upsert_metadata(result_objects)
|
|
769
|
+
|
|
770
|
+
def _cleanup_upsert_metadata(self, result_objects):
|
|
771
|
+
"""
|
|
772
|
+
Clean up temporary metadata added during upsert operations.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
result_objects: List of objects to clean up
|
|
776
|
+
"""
|
|
777
|
+
for obj in result_objects:
|
|
778
|
+
if hasattr(obj, '_bulk_hooks_was_created'):
|
|
779
|
+
delattr(obj, '_bulk_hooks_was_created')
|
|
780
|
+
if hasattr(obj, '_bulk_hooks_upsert_metadata'):
|
|
781
|
+
delattr(obj, '_bulk_hooks_upsert_metadata')
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.44}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|