django-bulk-hooks 0.1.57__tar.gz → 0.1.60__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.57 → django_bulk_hooks-0.1.60}/PKG-INFO +3 -3
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/context.py +16 -16
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/decorators.py +34 -5
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/manager.py +58 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/queryset.py +5 -4
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/LICENSE +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/README.md +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.57 → django_bulk_hooks-0.1.60}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.60
|
|
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,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.handler import TriggerHandler
|
|
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
|
-
#
|
|
57
|
+
# Determine which instances actually need preloading
|
|
56
58
|
model_cls = new_records[0].__class__
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 =
|
|
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,20 @@ 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
|
+
# Register the hook
|
|
120
|
+
TriggerHandler.register_hook(model_cls, event, func, when, priority)
|
|
121
|
+
return func
|
|
122
|
+
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
|
|
@@ -28,16 +28,17 @@ class LifecycleQuerySet(models.QuerySet):
|
|
|
28
28
|
setattr(obj, field, value)
|
|
29
29
|
|
|
30
30
|
# Run BEFORE_UPDATE hooks
|
|
31
|
-
from django_bulk_hooks.context import TriggerContext
|
|
32
31
|
from django_bulk_hooks import engine
|
|
32
|
+
from django_bulk_hooks.context import TriggerContext
|
|
33
|
+
|
|
33
34
|
ctx = TriggerContext(model_cls)
|
|
34
35
|
engine.run(model_cls, "before_update", instances, originals, ctx=ctx)
|
|
35
36
|
|
|
36
|
-
# Use Django's built-in update logic directly
|
|
37
|
-
|
|
37
|
+
# Use Django's built-in update logic directly
|
|
38
|
+
queryset = self.model._base_manager.filter(pk__in=pks)
|
|
39
|
+
update_count = queryset.update(**kwargs)
|
|
38
40
|
|
|
39
41
|
# Run AFTER_UPDATE hooks
|
|
40
42
|
engine.run(model_cls, "after_update", instances, originals, ctx=ctx)
|
|
41
43
|
|
|
42
44
|
return update_count
|
|
43
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.60"
|
|
4
4
|
description = "Lifecycle-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
|