django-bulk-hooks 0.1.58__py3-none-any.whl → 0.1.61__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.

@@ -1,16 +1,16 @@
1
- import threading
2
- from collections import deque
3
-
4
- _hook_context = threading.local()
5
-
6
-
7
- def get_hook_queue():
8
- if not hasattr(_hook_context, "queue"):
9
- _hook_context.queue = deque()
10
- return _hook_context.queue
11
-
12
-
13
- class TriggerContext:
14
- def __init__(self, model_cls, metadata=None):
15
- self.model_cls = model_cls
16
- self.metadata = metadata or {}
1
+ import threading
2
+ from collections import deque
3
+
4
+ _hook_context = threading.local()
5
+
6
+
7
+ def get_hook_queue():
8
+ if not hasattr(_hook_context, "queue"):
9
+ _hook_context.queue = deque()
10
+ return _hook_context.queue
11
+
12
+
13
+ class TriggerContext:
14
+ def __init__(self, model_cls, metadata=None):
15
+ self.model_cls = model_cls
16
+ self.metadata = metadata or {}
@@ -3,6 +3,8 @@ from functools import wraps
3
3
 
4
4
  from django.core.exceptions import FieldDoesNotExist
5
5
  from django_bulk_hooks.enums import DEFAULT_PRIORITY
6
+ from django_bulk_hooks.constants import BEFORE_UPDATE
7
+ from django_bulk_hooks.registry import register_hook
6
8
 
7
9
 
8
10
  def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
@@ -52,19 +54,29 @@ def select_related(*related_fields):
52
54
  if not new_records:
53
55
  return func(*args, **kwargs)
54
56
 
55
- # In-place preload
57
+ # Determine which instances actually need preloading
56
58
  model_cls = new_records[0].__class__
57
- ids = [obj.pk for obj in new_records if obj.pk is not None]
58
- if not ids:
59
- return func(*args, **kwargs)
59
+ ids_to_fetch = []
60
+ for obj in new_records:
61
+ if obj.pk is None:
62
+ continue
63
+ # if any related field is not already cached on the instance,
64
+ # mark it for fetching
65
+ if any(field not in obj._state.fields_cache for field in related_fields):
66
+ ids_to_fetch.append(obj.pk)
60
67
 
61
- fetched = model_cls.objects.select_related(*related_fields).in_bulk(ids)
68
+ fetched = {}
69
+ if ids_to_fetch:
70
+ fetched = model_cls.objects.select_related(*related_fields).in_bulk(ids_to_fetch)
62
71
 
63
72
  for obj in new_records:
64
73
  preloaded = fetched.get(obj.pk)
65
74
  if not preloaded:
66
75
  continue
67
76
  for field in related_fields:
77
+ if field in obj._state.fields_cache:
78
+ # don't override values that were explicitly set or already loaded
79
+ continue
68
80
  if "." in field:
69
81
  raise ValueError(
70
82
  f"@preload_related does not support nested fields like '{field}'"
@@ -91,3 +103,35 @@ def select_related(*related_fields):
91
103
  return wrapper
92
104
 
93
105
  return decorator
106
+
107
+
108
+ def bulk_hook(model_cls, event, when=None, priority=None):
109
+ """
110
+ Decorator to register a bulk hook for a model.
111
+
112
+ Args:
113
+ model_cls: The model class to hook into
114
+ event: The event to hook into (e.g., BEFORE_UPDATE, AFTER_UPDATE)
115
+ when: Optional condition for when the hook should run
116
+ priority: Optional priority for hook execution order
117
+ """
118
+ def decorator(func):
119
+ # Create a simple handler class for the function
120
+ class FunctionHandler:
121
+ def __init__(self):
122
+ self.func = func
123
+
124
+ def handle(self, new_instances, original_instances):
125
+ return self.func(new_instances, original_instances)
126
+
127
+ # Register the hook using the registry
128
+ register_hook(
129
+ model=model_cls,
130
+ event=event,
131
+ handler_cls=FunctionHandler,
132
+ method_name='handle',
133
+ condition=when,
134
+ priority=priority or DEFAULT_PRIORITY,
135
+ )
136
+ return func
137
+ return decorator
@@ -10,6 +10,9 @@ from django_bulk_hooks.constants import (
10
10
  )
11
11
  from django_bulk_hooks.context import TriggerContext
12
12
  from django_bulk_hooks.queryset import LifecycleQuerySet
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
13
16
 
14
17
 
15
18
  class BulkLifecycleManager(models.Manager):
@@ -34,6 +37,18 @@ class BulkLifecycleManager(models.Manager):
34
37
  originals = list(model_cls.objects.filter(pk__in=[obj.pk for obj in objs]))
35
38
  ctx = TriggerContext(model_cls)
36
39
  engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
40
+
41
+ # Automatically detect fields that were modified during BEFORE_UPDATE hooks
42
+ modified_fields = self._detect_modified_fields(objs, originals)
43
+ if modified_fields:
44
+ # Convert to set for efficient union operation
45
+ fields_set = set(fields)
46
+ fields_set.update(modified_fields)
47
+ fields = list(fields_set)
48
+ logger.info(
49
+ "Automatically including modified fields in bulk_update: %s",
50
+ modified_fields
51
+ )
37
52
 
38
53
  for i in range(0, len(objs), self.CHUNK_SIZE):
39
54
  chunk = objs[i : i + self.CHUNK_SIZE]
@@ -47,6 +62,49 @@ class BulkLifecycleManager(models.Manager):
47
62
 
48
63
  return objs
49
64
 
65
+ def _detect_modified_fields(self, new_instances, original_instances):
66
+ """
67
+ Detect fields that were modified during BEFORE_UPDATE hooks by comparing
68
+ new instances with their original values.
69
+ """
70
+ if not original_instances:
71
+ return set()
72
+
73
+ # Create a mapping of pk to original instance for efficient lookup
74
+ original_map = {obj.pk: obj for obj in original_instances if obj.pk is not None}
75
+
76
+ modified_fields = set()
77
+
78
+ for new_instance in new_instances:
79
+ if new_instance.pk is None:
80
+ continue
81
+
82
+ original = original_map.get(new_instance.pk)
83
+ if not original:
84
+ continue
85
+
86
+ # Compare all fields to detect changes
87
+ for field in new_instance._meta.fields:
88
+ if field.name == 'id':
89
+ continue
90
+
91
+ new_value = getattr(new_instance, field.name)
92
+ original_value = getattr(original, field.name)
93
+
94
+ # Handle different field types appropriately
95
+ if field.is_relation:
96
+ # For foreign keys, compare the pk values
97
+ new_pk = new_value.pk if new_value else None
98
+ original_pk = original_value.pk if original_value else None
99
+ if new_pk != original_pk:
100
+ modified_fields.add(field.name)
101
+ else:
102
+ # For regular fields, use direct comparison
103
+ if new_value != original_value:
104
+ modified_fields.add(field.name)
105
+
106
+ return modified_fields
107
+
50
108
  @transaction.atomic
51
109
  def bulk_create(
52
110
  self, objs, batch_size=None, ignore_conflicts=False, bypass_hooks=False
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.58
3
+ Version: 0.1.61
4
4
  Summary: Lifecycle-style hooks for Django bulk operations like bulk_create and bulk_update.
5
- Home-page: https://github.com/AugendLimited/django-bulk-hooks
6
5
  License: MIT
7
6
  Keywords: django,bulk,hooks
8
7
  Author: Konrad Beck
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Programming Language :: Python :: 3.13
16
15
  Requires-Dist: Django (>=4.0)
16
+ Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
17
17
  Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -1,17 +1,17 @@
1
1
  django_bulk_hooks/__init__.py,sha256=6VFU52Kz5Yjhjr_RhID41nMaXUMZ4na_MVDXEhIMXc4,98
2
2
  django_bulk_hooks/conditions.py,sha256=UQP5Y7yyJXqDdUcUukRPxCbvrsV6HqNmIxERFZqBh58,5317
3
3
  django_bulk_hooks/constants.py,sha256=Jks1BIADYbap2fpq3Ry0e7w-CiXBCsR9b5h1yan1qoc,192
4
- django_bulk_hooks/context.py,sha256=VyjgaubjJTTXQHgyoGY4OLBbdCpBwo-RX4bvxRfZ1c4,383
5
- django_bulk_hooks/decorators.py,sha256=wbT9lbpKZqDbG5vPvtTC6MHYWvjLaVccCwFM9U2nHkA,3159
4
+ django_bulk_hooks/context.py,sha256=hElvgYZoH1nEM2dR7JIi7pbRt67za90ZVy0OzyFWMNg,367
5
+ django_bulk_hooks/decorators.py,sha256=YiczUdfUBI6bk0-7ShfpuvU_n8YCDB6QD4i57NnmvNE,4886
6
6
  django_bulk_hooks/engine.py,sha256=l7BzU3lfYhZHZazks8FgHhZB4iLVKuzmcnVcyEQkGpQ,1978
7
7
  django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
8
8
  django_bulk_hooks/handler.py,sha256=qUulFSPhi__gWHISC4GogeVQ9aDo45bz0Dj421Y6skE,4968
9
- django_bulk_hooks/manager.py,sha256=jsTgbjmRe5SeakVJxV8wYxqH6f3fRX5zm3q0Wy-M9Nc,4271
9
+ django_bulk_hooks/manager.py,sha256=ajsb0-HaIcb48WCHsD-Cz78umjNWQdbZarMIvBiyA_I,6691
10
10
  django_bulk_hooks/models.py,sha256=_cqIVHKhXb1EOnXxhsuJUlLnPjTRM_sCP5OKbq4q-I8,542
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
12
  django_bulk_hooks/queryset.py,sha256=YG9wlvOdujapmxzNhOXTO74Uph0sozUsfc8e5n9KniU,1409
13
13
  django_bulk_hooks/registry.py,sha256=yeTi0IhodL61J86ohb5OyITufE28T3ecMbt6RWvkzTs,585
14
- django_bulk_hooks-0.1.58.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.58.dist-info/METADATA,sha256=kaiVFuUtCftxD836kAaBTkFgH_sBqMtiwAf25A_9iuE,3101
16
- django_bulk_hooks-0.1.58.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
- django_bulk_hooks-0.1.58.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.61.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.61.dist-info/METADATA,sha256=HLbhpnq0Z5CuOyHm0t7YF_s83UCUMEYd_cbKCpw0la8,3113
16
+ django_bulk_hooks-0.1.61.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.61.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any