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

@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.3
2
+ Name: django-bulk-hooks
3
+ Version: 0.1.81
4
+ Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
+ License: MIT
6
+ Keywords: django,bulk,hooks
7
+ Author: Konrad Beck
8
+ Author-email: konrad.beck@merchantcapital.co.za
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: Django (>=4.0)
16
+ Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
17
+ Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
18
+ Description-Content-Type: text/markdown
19
+
20
+
21
+ # django-bulk-hooks
22
+
23
+ ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
24
+
25
+ `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
26
+
27
+ ## ✨ Features
28
+
29
+ - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
30
+ - BEFORE/AFTER hooks for create, update, delete
31
+ - Hook-aware manager that wraps Django's `bulk_` operations
32
+ - **NEW**: `HookModelMixin` for individual model lifecycle events
33
+ - Hook chaining, hook deduplication, and atomicity
34
+ - Class-based hook handlers with DI support
35
+ - Support for both bulk and individual model operations
36
+ - **NEW**: Safe handling of related objects to prevent `RelatedObjectDoesNotExist` errors
37
+
38
+ ## 🚀 Quickstart
39
+
40
+ ```bash
41
+ pip install django-bulk-hooks
42
+ ```
43
+
44
+ ### Define Your Model
45
+
46
+ ```python
47
+ from django.db import models
48
+ from django_bulk_hooks.models import HookModelMixin
49
+
50
+ class Account(HookModelMixin):
51
+ balance = models.DecimalField(max_digits=10, decimal_places=2)
52
+ # The HookModelMixin automatically provides BulkHookManager
53
+ ```
54
+
55
+ ### Create a Hook Handler
56
+
57
+ ```python
58
+ from django_bulk_hooks import hook, AFTER_UPDATE, Hook
59
+ from django_bulk_hooks.conditions import WhenFieldHasChanged
60
+ from .models import Account
61
+
62
+ class AccountHooks(HookHandler):
63
+ @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
64
+ def log_balance_change(self, new_records, old_records):
65
+ print("Accounts updated:", [a.pk for a in new_records])
66
+
67
+ @hook(BEFORE_CREATE, model=Account)
68
+ def before_create(self, new_records, old_records):
69
+ for account in new_records:
70
+ if account.balance < 0:
71
+ raise ValueError("Account cannot have negative balance")
72
+
73
+ @hook(AFTER_DELETE, model=Account)
74
+ def after_delete(self, new_records, old_records):
75
+ print("Accounts deleted:", [a.pk for a in old_records])
76
+ ```
77
+
78
+ ### Advanced Hook Usage
79
+
80
+ ```python
81
+ class AdvancedAccountHooks(HookHandler):
82
+ @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
83
+ def validate_balance_change(self, new_records, old_records):
84
+ for new_account, old_account in zip(new_records, old_records):
85
+ if new_account.balance < 0 and old_account.balance >= 0:
86
+ raise ValueError("Cannot set negative balance")
87
+
88
+ @hook(AFTER_CREATE, model=Account)
89
+ def send_welcome_email(self, new_records, old_records):
90
+ for account in new_records:
91
+ # Send welcome email logic here
92
+ pass
93
+ ```
94
+
95
+ ## 🔒 Safely Handling Related Objects
96
+
97
+ One of the most common issues when working with hooks is the `RelatedObjectDoesNotExist` exception. This occurs when you try to access a related object that doesn't exist or hasn't been saved yet.
98
+
99
+ ### The Problem
100
+
101
+ ```python
102
+ # ❌ DANGEROUS: This can raise RelatedObjectDoesNotExist
103
+ @hook(AFTER_CREATE, model=Transaction)
104
+ def process_transaction(self, new_records, old_records):
105
+ for transaction in new_records:
106
+ # This will fail if transaction.status is None or doesn't exist
107
+ if transaction.status.name == "COMPLETE":
108
+ # Process the transaction
109
+ pass
110
+ ```
111
+
112
+ ### The Solution
113
+
114
+ Use the `safe_get_related_attr` utility function to safely access related object attributes:
115
+
116
+ ```python
117
+ from django_bulk_hooks.conditions import safe_get_related_attr
118
+
119
+ # ✅ SAFE: Use safe_get_related_attr to handle None values
120
+ @hook(AFTER_CREATE, model=Transaction)
121
+ def process_transaction(self, new_records, old_records):
122
+ for transaction in new_records:
123
+ # Safely get the status name, returns None if status doesn't exist
124
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
125
+
126
+ if status_name == "COMPLETE":
127
+ # Process the transaction
128
+ pass
129
+ elif status_name is None:
130
+ # Handle case where status is not set
131
+ print(f"Transaction {transaction.id} has no status")
132
+ ```
133
+
134
+ ### Complete Example
135
+
136
+ ```python
137
+ from django.db import models
138
+ from django_bulk_hooks import hook
139
+ from django_bulk_hooks.conditions import safe_get_related_attr
140
+
141
+ class Status(models.Model):
142
+ name = models.CharField(max_length=50)
143
+
144
+ class Transaction(HookModelMixin, models.Model):
145
+ amount = models.DecimalField(max_digits=10, decimal_places=2)
146
+ status = models.ForeignKey(Status, on_delete=models.CASCADE, null=True, blank=True)
147
+ category = models.ForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)
148
+
149
+ class TransactionHandler:
150
+ @hook(Transaction, "before_create")
151
+ def set_default_status(self, new_records, old_records=None):
152
+ """Set default status for new transactions."""
153
+ default_status = Status.objects.filter(name="PENDING").first()
154
+ for transaction in new_records:
155
+ if transaction.status is None:
156
+ transaction.status = default_status
157
+
158
+ @hook(Transaction, "after_create")
159
+ def process_transactions(self, new_records, old_records=None):
160
+ """Process transactions based on their status."""
161
+ for transaction in new_records:
162
+ # ✅ SAFE: Get status name safely
163
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
164
+
165
+ if status_name == "COMPLETE":
166
+ self._process_complete_transaction(transaction)
167
+ elif status_name == "FAILED":
168
+ self._process_failed_transaction(transaction)
169
+ elif status_name is None:
170
+ print(f"Transaction {transaction.id} has no status")
171
+
172
+ # ✅ SAFE: Check for related object existence
173
+ category = safe_get_related_attr(transaction, 'category')
174
+ if category:
175
+ print(f"Transaction {transaction.id} belongs to category: {category.name}")
176
+
177
+ def _process_complete_transaction(self, transaction):
178
+ # Process complete transaction logic
179
+ pass
180
+
181
+ def _process_failed_transaction(self, transaction):
182
+ # Process failed transaction logic
183
+ pass
184
+ ```
185
+
186
+ ### Best Practices for Related Objects
187
+
188
+ 1. **Always use `safe_get_related_attr`** when accessing related object attributes in hooks
189
+ 2. **Set default values in `BEFORE_CREATE` hooks** to ensure related objects exist
190
+ 3. **Handle None cases explicitly** to avoid unexpected behavior
191
+ 4. **Use bulk operations efficiently** by fetching related objects once and reusing them
192
+
193
+ ```python
194
+ class EfficientTransactionHandler:
195
+ @hook(Transaction, "before_create")
196
+ def prepare_transactions(self, new_records, old_records=None):
197
+ """Efficiently prepare transactions for bulk creation."""
198
+ # Get default objects once to avoid multiple queries
199
+ default_status = Status.objects.filter(name="PENDING").first()
200
+ default_category = Category.objects.filter(name="GENERAL").first()
201
+
202
+ for transaction in new_records:
203
+ if transaction.status is None:
204
+ transaction.status = default_status
205
+ if transaction.category is None:
206
+ transaction.category = default_category
207
+
208
+ @hook(Transaction, "after_create")
209
+ def post_creation_processing(self, new_records, old_records=None):
210
+ """Process transactions after creation."""
211
+ # Group by status for efficient processing
212
+ transactions_by_status = {}
213
+
214
+ for transaction in new_records:
215
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
216
+ if status_name not in transactions_by_status:
217
+ transactions_by_status[status_name] = []
218
+ transactions_by_status[status_name].append(transaction)
219
+
220
+ # Process each group
221
+ for status_name, transactions in transactions_by_status.items():
222
+ if status_name == "COMPLETE":
223
+ self._batch_process_complete(transactions)
224
+ elif status_name == "FAILED":
225
+ self._batch_process_failed(transactions)
226
+ ```
227
+
228
+ This approach ensures your hooks are robust and won't fail due to missing related objects, while also being efficient with database queries.
@@ -0,0 +1,209 @@
1
+
2
+ # django-bulk-hooks
3
+
4
+ ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
5
+
6
+ `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
7
+
8
+ ## ✨ Features
9
+
10
+ - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
+ - BEFORE/AFTER hooks for create, update, delete
12
+ - Hook-aware manager that wraps Django's `bulk_` operations
13
+ - **NEW**: `HookModelMixin` for individual model lifecycle events
14
+ - Hook chaining, hook deduplication, and atomicity
15
+ - Class-based hook handlers with DI support
16
+ - Support for both bulk and individual model operations
17
+ - **NEW**: Safe handling of related objects to prevent `RelatedObjectDoesNotExist` errors
18
+
19
+ ## 🚀 Quickstart
20
+
21
+ ```bash
22
+ pip install django-bulk-hooks
23
+ ```
24
+
25
+ ### Define Your Model
26
+
27
+ ```python
28
+ from django.db import models
29
+ from django_bulk_hooks.models import HookModelMixin
30
+
31
+ class Account(HookModelMixin):
32
+ balance = models.DecimalField(max_digits=10, decimal_places=2)
33
+ # The HookModelMixin automatically provides BulkHookManager
34
+ ```
35
+
36
+ ### Create a Hook Handler
37
+
38
+ ```python
39
+ from django_bulk_hooks import hook, AFTER_UPDATE, Hook
40
+ from django_bulk_hooks.conditions import WhenFieldHasChanged
41
+ from .models import Account
42
+
43
+ class AccountHooks(HookHandler):
44
+ @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
45
+ def log_balance_change(self, new_records, old_records):
46
+ print("Accounts updated:", [a.pk for a in new_records])
47
+
48
+ @hook(BEFORE_CREATE, model=Account)
49
+ def before_create(self, new_records, old_records):
50
+ for account in new_records:
51
+ if account.balance < 0:
52
+ raise ValueError("Account cannot have negative balance")
53
+
54
+ @hook(AFTER_DELETE, model=Account)
55
+ def after_delete(self, new_records, old_records):
56
+ print("Accounts deleted:", [a.pk for a in old_records])
57
+ ```
58
+
59
+ ### Advanced Hook Usage
60
+
61
+ ```python
62
+ class AdvancedAccountHooks(HookHandler):
63
+ @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
64
+ def validate_balance_change(self, new_records, old_records):
65
+ for new_account, old_account in zip(new_records, old_records):
66
+ if new_account.balance < 0 and old_account.balance >= 0:
67
+ raise ValueError("Cannot set negative balance")
68
+
69
+ @hook(AFTER_CREATE, model=Account)
70
+ def send_welcome_email(self, new_records, old_records):
71
+ for account in new_records:
72
+ # Send welcome email logic here
73
+ pass
74
+ ```
75
+
76
+ ## 🔒 Safely Handling Related Objects
77
+
78
+ One of the most common issues when working with hooks is the `RelatedObjectDoesNotExist` exception. This occurs when you try to access a related object that doesn't exist or hasn't been saved yet.
79
+
80
+ ### The Problem
81
+
82
+ ```python
83
+ # ❌ DANGEROUS: This can raise RelatedObjectDoesNotExist
84
+ @hook(AFTER_CREATE, model=Transaction)
85
+ def process_transaction(self, new_records, old_records):
86
+ for transaction in new_records:
87
+ # This will fail if transaction.status is None or doesn't exist
88
+ if transaction.status.name == "COMPLETE":
89
+ # Process the transaction
90
+ pass
91
+ ```
92
+
93
+ ### The Solution
94
+
95
+ Use the `safe_get_related_attr` utility function to safely access related object attributes:
96
+
97
+ ```python
98
+ from django_bulk_hooks.conditions import safe_get_related_attr
99
+
100
+ # ✅ SAFE: Use safe_get_related_attr to handle None values
101
+ @hook(AFTER_CREATE, model=Transaction)
102
+ def process_transaction(self, new_records, old_records):
103
+ for transaction in new_records:
104
+ # Safely get the status name, returns None if status doesn't exist
105
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
106
+
107
+ if status_name == "COMPLETE":
108
+ # Process the transaction
109
+ pass
110
+ elif status_name is None:
111
+ # Handle case where status is not set
112
+ print(f"Transaction {transaction.id} has no status")
113
+ ```
114
+
115
+ ### Complete Example
116
+
117
+ ```python
118
+ from django.db import models
119
+ from django_bulk_hooks import hook
120
+ from django_bulk_hooks.conditions import safe_get_related_attr
121
+
122
+ class Status(models.Model):
123
+ name = models.CharField(max_length=50)
124
+
125
+ class Transaction(HookModelMixin, models.Model):
126
+ amount = models.DecimalField(max_digits=10, decimal_places=2)
127
+ status = models.ForeignKey(Status, on_delete=models.CASCADE, null=True, blank=True)
128
+ category = models.ForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)
129
+
130
+ class TransactionHandler:
131
+ @hook(Transaction, "before_create")
132
+ def set_default_status(self, new_records, old_records=None):
133
+ """Set default status for new transactions."""
134
+ default_status = Status.objects.filter(name="PENDING").first()
135
+ for transaction in new_records:
136
+ if transaction.status is None:
137
+ transaction.status = default_status
138
+
139
+ @hook(Transaction, "after_create")
140
+ def process_transactions(self, new_records, old_records=None):
141
+ """Process transactions based on their status."""
142
+ for transaction in new_records:
143
+ # ✅ SAFE: Get status name safely
144
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
145
+
146
+ if status_name == "COMPLETE":
147
+ self._process_complete_transaction(transaction)
148
+ elif status_name == "FAILED":
149
+ self._process_failed_transaction(transaction)
150
+ elif status_name is None:
151
+ print(f"Transaction {transaction.id} has no status")
152
+
153
+ # ✅ SAFE: Check for related object existence
154
+ category = safe_get_related_attr(transaction, 'category')
155
+ if category:
156
+ print(f"Transaction {transaction.id} belongs to category: {category.name}")
157
+
158
+ def _process_complete_transaction(self, transaction):
159
+ # Process complete transaction logic
160
+ pass
161
+
162
+ def _process_failed_transaction(self, transaction):
163
+ # Process failed transaction logic
164
+ pass
165
+ ```
166
+
167
+ ### Best Practices for Related Objects
168
+
169
+ 1. **Always use `safe_get_related_attr`** when accessing related object attributes in hooks
170
+ 2. **Set default values in `BEFORE_CREATE` hooks** to ensure related objects exist
171
+ 3. **Handle None cases explicitly** to avoid unexpected behavior
172
+ 4. **Use bulk operations efficiently** by fetching related objects once and reusing them
173
+
174
+ ```python
175
+ class EfficientTransactionHandler:
176
+ @hook(Transaction, "before_create")
177
+ def prepare_transactions(self, new_records, old_records=None):
178
+ """Efficiently prepare transactions for bulk creation."""
179
+ # Get default objects once to avoid multiple queries
180
+ default_status = Status.objects.filter(name="PENDING").first()
181
+ default_category = Category.objects.filter(name="GENERAL").first()
182
+
183
+ for transaction in new_records:
184
+ if transaction.status is None:
185
+ transaction.status = default_status
186
+ if transaction.category is None:
187
+ transaction.category = default_category
188
+
189
+ @hook(Transaction, "after_create")
190
+ def post_creation_processing(self, new_records, old_records=None):
191
+ """Process transactions after creation."""
192
+ # Group by status for efficient processing
193
+ transactions_by_status = {}
194
+
195
+ for transaction in new_records:
196
+ status_name = safe_get_related_attr(transaction, 'status', 'name')
197
+ if status_name not in transactions_by_status:
198
+ transactions_by_status[status_name] = []
199
+ transactions_by_status[status_name].append(transaction)
200
+
201
+ # Process each group
202
+ for status_name, transactions in transactions_by_status.items():
203
+ if status_name == "COMPLETE":
204
+ self._batch_process_complete(transactions)
205
+ elif status_name == "FAILED":
206
+ self._batch_process_failed(transactions)
207
+ ```
208
+
209
+ This approach ensures your hooks are robust and won't fail due to missing related objects, while also being efficient with database queries.
@@ -29,6 +29,81 @@ def safe_get_related_object(instance, field_name):
29
29
  return None
30
30
 
31
31
 
32
+ def safe_get_related_attr(instance, field_name, attr_name=None):
33
+ """
34
+ Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
35
+
36
+ This is particularly useful in hooks where objects might not have their related
37
+ fields populated yet (e.g., during bulk_create operations).
38
+
39
+ Args:
40
+ instance: The model instance
41
+ field_name: The foreign key field name
42
+ attr_name: Optional attribute name to access on the related object
43
+
44
+ Returns:
45
+ The related object, the attribute value, or None if not available
46
+
47
+ Example:
48
+ # Instead of: loan_transaction.status.name (which might fail)
49
+ # Use: safe_get_related_attr(loan_transaction, 'status', 'name')
50
+
51
+ status_name = safe_get_related_attr(loan_transaction, 'status', 'name')
52
+ if status_name in {Status.COMPLETE.value, Status.FAILED.value}:
53
+ # Process the transaction
54
+ pass
55
+ """
56
+ related_obj = safe_get_related_object(instance, field_name)
57
+ if related_obj is None:
58
+ return None
59
+
60
+ if attr_name is None:
61
+ return related_obj
62
+
63
+ return getattr(related_obj, attr_name, None)
64
+
65
+
66
+ def safe_get_related_attr_with_fallback(instance, field_name, attr_name=None, fallback_value=None):
67
+ """
68
+ Enhanced version of safe_get_related_attr that provides fallback handling.
69
+
70
+ This function is especially useful for bulk operations where related objects
71
+ might not be fully loaded or might not exist yet.
72
+
73
+ Args:
74
+ instance: The model instance
75
+ field_name: The foreign key field name
76
+ attr_name: Optional attribute name to access on the related object
77
+ fallback_value: Value to return if the related object or attribute doesn't exist
78
+
79
+ Returns:
80
+ The related object, the attribute value, or fallback_value if not available
81
+ """
82
+ # First try the standard safe access
83
+ result = safe_get_related_attr(instance, field_name, attr_name)
84
+ if result is not None:
85
+ return result
86
+
87
+ # If that fails, try to get the foreign key ID and fetch the object directly
88
+ fk_field_name = f"{field_name}_id"
89
+ if hasattr(instance, fk_field_name):
90
+ fk_id = getattr(instance, fk_field_name)
91
+ if fk_id is not None:
92
+ try:
93
+ # Get the field to determine the related model
94
+ field = instance._meta.get_field(field_name)
95
+ if field.is_relation and not field.many_to_many and not field.one_to_many:
96
+ # Try to fetch the related object directly
97
+ related_obj = field.related_model.objects.get(pk=fk_id)
98
+ if attr_name is None:
99
+ return related_obj
100
+ return getattr(related_obj, attr_name, fallback_value)
101
+ except (field.related_model.DoesNotExist, AttributeError):
102
+ pass
103
+
104
+ return fallback_value
105
+
106
+
32
107
  def resolve_dotted_attr(instance, dotted_path):
33
108
  """
34
109
  Recursively resolve a dotted attribute path, e.g., "type.category".
@@ -3,33 +3,11 @@ import logging
3
3
  from django.core.exceptions import ValidationError
4
4
  from django.db import models
5
5
  from django_bulk_hooks.registry import get_hooks
6
- from django_bulk_hooks.conditions import safe_get_related_object
6
+ from django_bulk_hooks.conditions import safe_get_related_object, safe_get_related_attr
7
7
 
8
8
  logger = logging.getLogger(__name__)
9
9
 
10
10
 
11
- def safe_get_related_attr(instance, field_name, attr_name=None):
12
- """
13
- Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
14
-
15
- Args:
16
- instance: The model instance
17
- field_name: The foreign key field name
18
- attr_name: Optional attribute name to access on the related object
19
-
20
- Returns:
21
- The related object, the attribute value, or None if not available
22
- """
23
- related_obj = safe_get_related_object(instance, field_name)
24
- if related_obj is None:
25
- return None
26
-
27
- if attr_name is None:
28
- return related_obj
29
-
30
- return getattr(related_obj, attr_name, None)
31
-
32
-
33
11
  def run(model_cls, event, new_instances, original_instances=None, ctx=None):
34
12
  hooks = get_hooks(model_cls, event)
35
13
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.80"
3
+ version = "0.1.81"
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,92 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: django-bulk-hooks
3
- Version: 0.1.80
4
- Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
- License: MIT
6
- Keywords: django,bulk,hooks
7
- Author: Konrad Beck
8
- Author-email: konrad.beck@merchantcapital.co.za
9
- Requires-Python: >=3.11,<4.0
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: Django (>=4.0)
16
- Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
17
- Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
18
- Description-Content-Type: text/markdown
19
-
20
-
21
- # django-bulk-hooks
22
-
23
- ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
24
-
25
- `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
26
-
27
- ## ✨ Features
28
-
29
- - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
30
- - BEFORE/AFTER hooks for create, update, delete
31
- - Hook-aware manager that wraps Django's `bulk_` operations
32
- - **NEW**: `HookModelMixin` for individual model lifecycle events
33
- - Hook chaining, hook deduplication, and atomicity
34
- - Class-based hook handlers with DI support
35
- - Support for both bulk and individual model operations
36
-
37
- ## 🚀 Quickstart
38
-
39
- ```bash
40
- pip install django-bulk-hooks
41
- ```
42
-
43
- ### Define Your Model
44
-
45
- ```python
46
- from django.db import models
47
- from django_bulk_hooks.models import HookModelMixin
48
-
49
- class Account(HookModelMixin):
50
- balance = models.DecimalField(max_digits=10, decimal_places=2)
51
- # The HookModelMixin automatically provides BulkHookManager
52
- ```
53
-
54
- ### Create a Hook Handler
55
-
56
- ```python
57
- from django_bulk_hooks import hook, AFTER_UPDATE, Hook
58
- from django_bulk_hooks.conditions import WhenFieldHasChanged
59
- from .models import Account
60
-
61
- class AccountHooks(HookHandler):
62
- @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
63
- def log_balance_change(self, new_records, old_records):
64
- print("Accounts updated:", [a.pk for a in new_records])
65
-
66
- @hook(BEFORE_CREATE, model=Account)
67
- def before_create(self, new_records, old_records):
68
- for account in new_records:
69
- if account.balance < 0:
70
- raise ValueError("Account cannot have negative balance")
71
-
72
- @hook(AFTER_DELETE, model=Account)
73
- def after_delete(self, new_records, old_records):
74
- print("Accounts deleted:", [a.pk for a in old_records])
75
- ```
76
-
77
- ### Advanced Hook Usage
78
-
79
- ```python
80
- class AdvancedAccountHooks(HookHandler):
81
- @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
82
- def validate_balance_change(self, new_records, old_records):
83
- for new_account, old_account in zip(new_records, old_records):
84
- if new_account.balance < 0 and old_account.balance >= 0:
85
- raise ValueError("Cannot set negative balance")
86
-
87
- @hook(AFTER_CREATE, model=Account)
88
- def send_welcome_email(self, new_records, old_records):
89
- for account in new_records:
90
- # Send welcome email logic here
91
- pass
92
- ```
@@ -1,73 +0,0 @@
1
-
2
- # django-bulk-hooks
3
-
4
- ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
5
-
6
- `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
7
-
8
- ## ✨ Features
9
-
10
- - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
- - BEFORE/AFTER hooks for create, update, delete
12
- - Hook-aware manager that wraps Django's `bulk_` operations
13
- - **NEW**: `HookModelMixin` for individual model lifecycle events
14
- - Hook chaining, hook deduplication, and atomicity
15
- - Class-based hook handlers with DI support
16
- - Support for both bulk and individual model operations
17
-
18
- ## 🚀 Quickstart
19
-
20
- ```bash
21
- pip install django-bulk-hooks
22
- ```
23
-
24
- ### Define Your Model
25
-
26
- ```python
27
- from django.db import models
28
- from django_bulk_hooks.models import HookModelMixin
29
-
30
- class Account(HookModelMixin):
31
- balance = models.DecimalField(max_digits=10, decimal_places=2)
32
- # The HookModelMixin automatically provides BulkHookManager
33
- ```
34
-
35
- ### Create a Hook Handler
36
-
37
- ```python
38
- from django_bulk_hooks import hook, AFTER_UPDATE, Hook
39
- from django_bulk_hooks.conditions import WhenFieldHasChanged
40
- from .models import Account
41
-
42
- class AccountHooks(HookHandler):
43
- @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
44
- def log_balance_change(self, new_records, old_records):
45
- print("Accounts updated:", [a.pk for a in new_records])
46
-
47
- @hook(BEFORE_CREATE, model=Account)
48
- def before_create(self, new_records, old_records):
49
- for account in new_records:
50
- if account.balance < 0:
51
- raise ValueError("Account cannot have negative balance")
52
-
53
- @hook(AFTER_DELETE, model=Account)
54
- def after_delete(self, new_records, old_records):
55
- print("Accounts deleted:", [a.pk for a in old_records])
56
- ```
57
-
58
- ### Advanced Hook Usage
59
-
60
- ```python
61
- class AdvancedAccountHooks(HookHandler):
62
- @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
63
- def validate_balance_change(self, new_records, old_records):
64
- for new_account, old_account in zip(new_records, old_records):
65
- if new_account.balance < 0 and old_account.balance >= 0:
66
- raise ValueError("Cannot set negative balance")
67
-
68
- @hook(AFTER_CREATE, model=Account)
69
- def send_welcome_email(self, new_records, old_records):
70
- for account in new_records:
71
- # Send welcome email logic here
72
- pass
73
- ```