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

Files changed (17) hide show
  1. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/queryset.py +86 -27
  3. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/README.md +0 -0
  6. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.276 → django_bulk_hooks-0.1.278}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.276
3
+ Version: 0.1.278
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  Home-page: https://github.com/AugendLimited/django-bulk-hooks
6
6
  License: MIT
@@ -698,9 +698,16 @@ class HookQuerySetMixin:
698
698
  if key in existing_db_map:
699
699
  db_record = existing_db_map[key]
700
700
  # Copy all fields from the database record to ensure completeness
701
+ # but exclude auto_now_add fields which should never be updated
701
702
  populated_fields = []
702
703
  for field in model_cls._meta.local_fields:
703
704
  if field.name != "id": # Don't overwrite the ID
705
+ # Skip auto_now_add fields for existing records
706
+ if (
707
+ hasattr(field, "auto_now_add")
708
+ and field.auto_now_add
709
+ ):
710
+ continue
704
711
  db_value = getattr(db_record, field.name)
705
712
  if (
706
713
  db_value is not None
@@ -805,6 +812,16 @@ class HookQuerySetMixin:
805
812
  "update_fields": update_fields,
806
813
  "unique_fields": unique_fields,
807
814
  }
815
+
816
+ # If we have classified records from upsert logic, pass them to MTI method
817
+ if (
818
+ update_conflicts
819
+ and unique_fields
820
+ and hasattr(ctx, "upsert_existing_records")
821
+ ):
822
+ mti_kwargs["existing_records"] = ctx.upsert_existing_records
823
+ mti_kwargs["new_records"] = ctx.upsert_new_records
824
+
808
825
  # Remove custom hook kwargs if present in self.bulk_create signature
809
826
  result = self._mti_bulk_create(
810
827
  objs,
@@ -1619,6 +1636,10 @@ class HookQuerySetMixin:
1619
1636
  then single bulk insert into childmost table.
1620
1637
  Sets auto_now_add/auto_now fields for each model in the chain.
1621
1638
  """
1639
+ # Extract classified records if available (for upsert operations)
1640
+ existing_records = kwargs.pop("existing_records", [])
1641
+ new_records = kwargs.pop("new_records", [])
1642
+
1622
1643
  # Remove custom hook kwargs before passing to Django internals
1623
1644
  django_kwargs = {
1624
1645
  k: v
@@ -1640,12 +1661,23 @@ class HookQuerySetMixin:
1640
1661
  for i in range(0, len(objs), batch_size):
1641
1662
  batch = objs[i : i + batch_size]
1642
1663
  batch_result = self._process_mti_bulk_create_batch(
1643
- batch, inheritance_chain, **django_kwargs
1664
+ batch,
1665
+ inheritance_chain,
1666
+ existing_records,
1667
+ new_records,
1668
+ **django_kwargs,
1644
1669
  )
1645
1670
  created_objects.extend(batch_result)
1646
1671
  return created_objects
1647
1672
 
1648
- def _process_mti_bulk_create_batch(self, batch, inheritance_chain, **kwargs):
1673
+ def _process_mti_bulk_create_batch(
1674
+ self,
1675
+ batch,
1676
+ inheritance_chain,
1677
+ existing_records=None,
1678
+ new_records=None,
1679
+ **kwargs,
1680
+ ):
1649
1681
  """
1650
1682
  Process a single batch of objects through the inheritance chain.
1651
1683
  Implements Django's suggested workaround #2: O(n) normal inserts into parent
@@ -1660,41 +1692,68 @@ class HookQuerySetMixin:
1660
1692
  bypass_hooks = kwargs.get("bypass_hooks", False)
1661
1693
  bypass_validation = kwargs.get("bypass_validation", False)
1662
1694
 
1695
+ # Create a list for lookup (since model instances without PKs are not hashable)
1696
+ existing_records_list = existing_records if existing_records else []
1697
+
1663
1698
  for obj in batch:
1664
1699
  parent_instances = {}
1665
1700
  current_parent = None
1701
+ is_existing_record = obj in existing_records_list
1702
+
1666
1703
  for model_class in inheritance_chain[:-1]:
1667
1704
  parent_obj = self._create_parent_instance(
1668
1705
  obj, model_class, current_parent
1669
1706
  )
1670
1707
 
1671
- # Fire parent hooks if not bypassed
1672
- if not bypass_hooks:
1673
- ctx = HookContext(model_class)
1674
- if not bypass_validation:
1675
- engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
1676
- engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
1677
-
1678
- # Use Django's base manager to create the object and get PKs back
1679
- # This bypasses hooks and the MTI exception
1680
- field_values = {
1681
- field.name: getattr(parent_obj, field.name)
1682
- for field in model_class._meta.local_fields
1683
- if hasattr(parent_obj, field.name)
1684
- and getattr(parent_obj, field.name) is not None
1685
- }
1686
- created_obj = model_class._base_manager.using(self.db).create(
1687
- **field_values
1688
- )
1708
+ if is_existing_record:
1709
+ # For existing records, we need to update the parent object instead of creating
1710
+ # The parent_obj should already have the correct PK from the database lookup
1711
+ # Fire parent hooks for updates
1712
+ if not bypass_hooks:
1713
+ ctx = HookContext(model_class)
1714
+ if not bypass_validation:
1715
+ engine.run(
1716
+ model_class, VALIDATE_UPDATE, [parent_obj], ctx=ctx
1717
+ )
1718
+ engine.run(model_class, BEFORE_UPDATE, [parent_obj], ctx=ctx)
1719
+
1720
+ # Update the existing parent object
1721
+ parent_obj.save(update_fields=kwargs.get("update_fields"))
1722
+
1723
+ # Fire AFTER_UPDATE hooks for parent
1724
+ if not bypass_hooks:
1725
+ engine.run(model_class, AFTER_UPDATE, [parent_obj], ctx=ctx)
1726
+ else:
1727
+ # For new records, create the parent object as before
1728
+ # Fire parent hooks if not bypassed
1729
+ if not bypass_hooks:
1730
+ ctx = HookContext(model_class)
1731
+ if not bypass_validation:
1732
+ engine.run(
1733
+ model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx
1734
+ )
1735
+ engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
1736
+
1737
+ # Use Django's base manager to create the object and get PKs back
1738
+ # This bypasses hooks and the MTI exception
1739
+ field_values = {
1740
+ field.name: getattr(parent_obj, field.name)
1741
+ for field in model_class._meta.local_fields
1742
+ if hasattr(parent_obj, field.name)
1743
+ and getattr(parent_obj, field.name) is not None
1744
+ }
1745
+ created_obj = model_class._base_manager.using(self.db).create(
1746
+ **field_values
1747
+ )
1689
1748
 
1690
- # Update the parent_obj with the created object's PK
1691
- parent_obj.pk = created_obj.pk
1692
- parent_obj._state.adding = False
1693
- parent_obj._state.db = self.db
1749
+ # Update the parent_obj with the created object's PK
1750
+ parent_obj.pk = created_obj.pk
1751
+ parent_obj._state.adding = False
1752
+ parent_obj._state.db = self.db
1694
1753
 
1695
- # Fire AFTER_CREATE hooks for parent
1696
- if not bypass_hooks:
1697
- engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
1754
+ # Fire AFTER_CREATE hooks for parent
1755
+ if not bypass_hooks:
1756
+ engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
1698
1757
 
1699
1758
  parent_instances[model_class] = parent_obj
1700
1759
  current_parent = parent_obj
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.276"
3
+ version = "0.1.278"
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"