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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.84
3
+ Version: 0.1.86
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -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
- handler_instance = handler_cls()
37
- func = getattr(handler_instance, method_name)
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
- new_instances,
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.84"
3
+ version = "0.1.86"
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"
@@ -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