django-bulk-hooks 0.1.84__tar.gz → 0.1.86__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.84 → django_bulk_hooks-0.1.86}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/engine.py +26 -8
- django_bulk_hooks-0.1.86/django_bulk_hooks/models.py +126 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/pyproject.toml +1 -1
- django_bulk_hooks-0.1.84/django_bulk_hooks/models.py +0 -106
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/LICENSE +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/README.md +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.1.84 → django_bulk_hooks-0.1.86}/django_bulk_hooks/registry.py +0 -0
|
@@ -8,7 +8,12 @@ from django_bulk_hooks.conditions import safe_get_related_object, safe_get_relat
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
# Cache for hook handlers to avoid creating them repeatedly
|
|
12
|
+
_handler_cache = {}
|
|
13
|
+
|
|
11
14
|
def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
15
|
+
# Get hooks from cache or fetch them
|
|
16
|
+
cache_key = (model_cls, event)
|
|
12
17
|
hooks = get_hooks(model_cls, event)
|
|
13
18
|
|
|
14
19
|
if not hooks:
|
|
@@ -32,19 +37,32 @@ def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
|
32
37
|
logger.error("Unexpected error during validation for %s: %s", instance, e)
|
|
33
38
|
raise
|
|
34
39
|
|
|
40
|
+
# Pre-create None list for originals if needed
|
|
41
|
+
if original_instances is None:
|
|
42
|
+
original_instances = [None] * len(new_instances)
|
|
43
|
+
|
|
44
|
+
# Process all hooks
|
|
35
45
|
for handler_cls, method_name, condition, priority in hooks:
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
# Get or create handler instance from cache
|
|
47
|
+
handler_key = (handler_cls, method_name)
|
|
48
|
+
if handler_key not in _handler_cache:
|
|
49
|
+
handler_instance = handler_cls()
|
|
50
|
+
func = getattr(handler_instance, method_name)
|
|
51
|
+
_handler_cache[handler_key] = (handler_instance, func)
|
|
52
|
+
else:
|
|
53
|
+
handler_instance, func = _handler_cache[handler_key]
|
|
54
|
+
|
|
55
|
+
# If no condition, process all instances at once
|
|
56
|
+
if not condition:
|
|
57
|
+
func(new_records=new_instances, old_records=original_instances if any(original_instances) else None)
|
|
58
|
+
continue
|
|
38
59
|
|
|
60
|
+
# For conditional hooks, filter instances first
|
|
39
61
|
to_process_new = []
|
|
40
62
|
to_process_old = []
|
|
41
63
|
|
|
42
|
-
for new, original in zip(
|
|
43
|
-
|
|
44
|
-
original_instances or [None] * len(new_instances),
|
|
45
|
-
strict=True,
|
|
46
|
-
):
|
|
47
|
-
if not condition or condition.check(new, original):
|
|
64
|
+
for new, original in zip(new_instances, original_instances, strict=True):
|
|
65
|
+
if condition.check(new, original):
|
|
48
66
|
to_process_new.append(new)
|
|
49
67
|
to_process_old.append(original)
|
|
50
68
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from django.db import models, transaction
|
|
2
|
+
|
|
3
|
+
from django_bulk_hooks.constants import (
|
|
4
|
+
AFTER_CREATE,
|
|
5
|
+
AFTER_DELETE,
|
|
6
|
+
AFTER_UPDATE,
|
|
7
|
+
BEFORE_CREATE,
|
|
8
|
+
BEFORE_DELETE,
|
|
9
|
+
BEFORE_UPDATE,
|
|
10
|
+
VALIDATE_CREATE,
|
|
11
|
+
VALIDATE_DELETE,
|
|
12
|
+
VALIDATE_UPDATE,
|
|
13
|
+
)
|
|
14
|
+
from django_bulk_hooks.context import HookContext
|
|
15
|
+
from django_bulk_hooks.engine import run
|
|
16
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
17
|
+
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
|
|
18
|
+
from functools import wraps
|
|
19
|
+
import contextlib
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@contextlib.contextmanager
|
|
23
|
+
def patch_foreign_key_behavior():
|
|
24
|
+
"""
|
|
25
|
+
Temporarily patches Django's foreign key descriptor to return None instead of raising
|
|
26
|
+
RelatedObjectDoesNotExist when accessing an unset foreign key field.
|
|
27
|
+
"""
|
|
28
|
+
original_get = ForwardManyToOneDescriptor.__get__
|
|
29
|
+
|
|
30
|
+
@wraps(original_get)
|
|
31
|
+
def safe_get(self, instance, cls=None):
|
|
32
|
+
if instance is None:
|
|
33
|
+
return self
|
|
34
|
+
try:
|
|
35
|
+
return original_get(self, instance, cls)
|
|
36
|
+
except self.RelatedObjectDoesNotExist:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
# Patch the descriptor
|
|
40
|
+
ForwardManyToOneDescriptor.__get__ = safe_get
|
|
41
|
+
try:
|
|
42
|
+
yield
|
|
43
|
+
finally:
|
|
44
|
+
# Restore original behavior
|
|
45
|
+
ForwardManyToOneDescriptor.__get__ = original_get
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HookModelMixin(models.Model):
|
|
49
|
+
objects = BulkHookManager()
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
abstract = True
|
|
53
|
+
|
|
54
|
+
def clean(self):
|
|
55
|
+
"""
|
|
56
|
+
Override clean() to trigger validation hooks.
|
|
57
|
+
This ensures that when Django calls clean() (like in admin forms),
|
|
58
|
+
it triggers the VALIDATE_* hooks for validation only.
|
|
59
|
+
"""
|
|
60
|
+
# Call Django's clean first
|
|
61
|
+
super().clean()
|
|
62
|
+
|
|
63
|
+
# Skip hook validation during admin form validation
|
|
64
|
+
# This prevents RelatedObjectDoesNotExist errors when Django hasn't
|
|
65
|
+
# fully set up the object's relationships yet
|
|
66
|
+
if hasattr(self, '_state') and getattr(self._state, 'validating', False):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# Determine if this is a create or update operation
|
|
70
|
+
is_create = self.pk is None
|
|
71
|
+
|
|
72
|
+
if is_create:
|
|
73
|
+
# For create operations, run VALIDATE_CREATE hooks for validation
|
|
74
|
+
ctx = HookContext(self.__class__)
|
|
75
|
+
with patch_foreign_key_behavior():
|
|
76
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
77
|
+
else:
|
|
78
|
+
# For update operations, run VALIDATE_UPDATE hooks for validation
|
|
79
|
+
try:
|
|
80
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
81
|
+
ctx = HookContext(self.__class__)
|
|
82
|
+
with patch_foreign_key_behavior():
|
|
83
|
+
run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
84
|
+
except self.__class__.DoesNotExist:
|
|
85
|
+
# If the old instance doesn't exist, treat as create
|
|
86
|
+
ctx = HookContext(self.__class__)
|
|
87
|
+
with patch_foreign_key_behavior():
|
|
88
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
89
|
+
|
|
90
|
+
def save(self, *args, **kwargs):
|
|
91
|
+
is_create = self.pk is None
|
|
92
|
+
ctx = HookContext(self.__class__)
|
|
93
|
+
|
|
94
|
+
# Use a single context manager for all hooks
|
|
95
|
+
with patch_foreign_key_behavior():
|
|
96
|
+
if is_create:
|
|
97
|
+
# For create operations
|
|
98
|
+
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
99
|
+
super().save(*args, **kwargs)
|
|
100
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
101
|
+
else:
|
|
102
|
+
# For update operations
|
|
103
|
+
try:
|
|
104
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
105
|
+
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
106
|
+
super().save(*args, **kwargs)
|
|
107
|
+
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
108
|
+
except self.__class__.DoesNotExist:
|
|
109
|
+
# If the old instance doesn't exist, treat as create
|
|
110
|
+
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
111
|
+
super().save(*args, **kwargs)
|
|
112
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
113
|
+
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def delete(self, *args, **kwargs):
|
|
117
|
+
ctx = HookContext(self.__class__)
|
|
118
|
+
|
|
119
|
+
# Use a single context manager for all hooks
|
|
120
|
+
with patch_foreign_key_behavior():
|
|
121
|
+
run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
|
|
122
|
+
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
123
|
+
result = super().delete(*args, **kwargs)
|
|
124
|
+
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
125
|
+
|
|
126
|
+
return result
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
|
|
3
|
-
from django_bulk_hooks.constants import (
|
|
4
|
-
AFTER_CREATE,
|
|
5
|
-
AFTER_DELETE,
|
|
6
|
-
AFTER_UPDATE,
|
|
7
|
-
BEFORE_CREATE,
|
|
8
|
-
BEFORE_DELETE,
|
|
9
|
-
BEFORE_UPDATE,
|
|
10
|
-
VALIDATE_CREATE,
|
|
11
|
-
VALIDATE_DELETE,
|
|
12
|
-
VALIDATE_UPDATE,
|
|
13
|
-
)
|
|
14
|
-
from django_bulk_hooks.context import HookContext
|
|
15
|
-
from django_bulk_hooks.engine import run
|
|
16
|
-
from django_bulk_hooks.manager import BulkHookManager
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class HookModelMixin(models.Model):
|
|
20
|
-
objects = BulkHookManager()
|
|
21
|
-
|
|
22
|
-
class Meta:
|
|
23
|
-
abstract = True
|
|
24
|
-
|
|
25
|
-
def clean(self):
|
|
26
|
-
"""
|
|
27
|
-
Override clean() to trigger validation hooks.
|
|
28
|
-
This ensures that when Django calls clean() (like in admin forms),
|
|
29
|
-
it triggers the VALIDATE_* hooks for validation only.
|
|
30
|
-
"""
|
|
31
|
-
# Call Django's clean first
|
|
32
|
-
super().clean()
|
|
33
|
-
|
|
34
|
-
# Skip hook validation during admin form validation
|
|
35
|
-
# This prevents RelatedObjectDoesNotExist errors when Django hasn't
|
|
36
|
-
# fully set up the object's relationships yet
|
|
37
|
-
if hasattr(self, '_state') and getattr(self._state, 'validating', False):
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
# Determine if this is a create or update operation
|
|
41
|
-
is_create = self.pk is None
|
|
42
|
-
|
|
43
|
-
if is_create:
|
|
44
|
-
# For create operations, run VALIDATE_CREATE hooks for validation
|
|
45
|
-
ctx = HookContext(self.__class__)
|
|
46
|
-
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
47
|
-
else:
|
|
48
|
-
# For update operations, run VALIDATE_UPDATE hooks for validation
|
|
49
|
-
try:
|
|
50
|
-
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
51
|
-
ctx = HookContext(self.__class__)
|
|
52
|
-
run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
53
|
-
except self.__class__.DoesNotExist:
|
|
54
|
-
# If the old instance doesn't exist, treat as create
|
|
55
|
-
ctx = HookContext(self.__class__)
|
|
56
|
-
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
57
|
-
|
|
58
|
-
def save(self, *args, **kwargs):
|
|
59
|
-
is_create = self.pk is None
|
|
60
|
-
ctx = HookContext(self.__class__)
|
|
61
|
-
|
|
62
|
-
if is_create:
|
|
63
|
-
# For create operations, run BEFORE hooks first
|
|
64
|
-
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
65
|
-
|
|
66
|
-
# Then let Django save
|
|
67
|
-
super().save(*args, **kwargs)
|
|
68
|
-
|
|
69
|
-
# Then run AFTER hooks
|
|
70
|
-
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
71
|
-
else:
|
|
72
|
-
# For update operations, we need to get the old record
|
|
73
|
-
try:
|
|
74
|
-
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
75
|
-
|
|
76
|
-
# Run BEFORE hooks first
|
|
77
|
-
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
78
|
-
|
|
79
|
-
# Then let Django save
|
|
80
|
-
super().save(*args, **kwargs)
|
|
81
|
-
|
|
82
|
-
# Then run AFTER hooks
|
|
83
|
-
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
84
|
-
except self.__class__.DoesNotExist:
|
|
85
|
-
# If the old instance doesn't exist, treat as create
|
|
86
|
-
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
87
|
-
|
|
88
|
-
super().save(*args, **kwargs)
|
|
89
|
-
|
|
90
|
-
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
91
|
-
|
|
92
|
-
return self
|
|
93
|
-
|
|
94
|
-
def delete(self, *args, **kwargs):
|
|
95
|
-
ctx = HookContext(self.__class__)
|
|
96
|
-
|
|
97
|
-
# Run validation hooks first
|
|
98
|
-
run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
|
|
99
|
-
|
|
100
|
-
# Then run business logic hooks
|
|
101
|
-
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
102
|
-
|
|
103
|
-
result = super().delete(*args, **kwargs)
|
|
104
|
-
|
|
105
|
-
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
106
|
-
return result
|
|
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
|