django-bulk-hooks 0.1.101__py3-none-any.whl → 0.1.103__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.
- django_bulk_hooks/__init__.py +3 -61
- django_bulk_hooks/conditions.py +100 -277
- django_bulk_hooks/decorators.py +70 -10
- django_bulk_hooks/engine.py +20 -115
- django_bulk_hooks/handler.py +4 -29
- django_bulk_hooks/manager.py +48 -141
- django_bulk_hooks/models.py +35 -85
- django_bulk_hooks/priority.py +16 -0
- django_bulk_hooks/queryset.py +3 -2
- django_bulk_hooks/registry.py +6 -5
- django_bulk_hooks-0.1.103.dist-info/METADATA +217 -0
- django_bulk_hooks-0.1.103.dist-info/RECORD +17 -0
- django_bulk_hooks-0.1.101.dist-info/METADATA +0 -295
- django_bulk_hooks-0.1.101.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.101.dist-info → django_bulk_hooks-0.1.103.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.101.dist-info → django_bulk_hooks-0.1.103.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -18,8 +18,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
18
18
|
model_cls = self.model
|
|
19
19
|
pks = [obj.pk for obj in instances]
|
|
20
20
|
|
|
21
|
-
# Load originals for hook comparison
|
|
22
|
-
|
|
21
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
22
|
+
original_map = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=pks)}
|
|
23
|
+
originals = [original_map.get(obj.pk) for obj in instances]
|
|
23
24
|
|
|
24
25
|
# Apply field updates to instances
|
|
25
26
|
for obj in instances:
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from typing import Union
|
|
3
3
|
|
|
4
|
-
from django_bulk_hooks.
|
|
4
|
+
from django_bulk_hooks.priority import Priority
|
|
5
5
|
|
|
6
|
-
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int
|
|
6
|
+
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def register_hook(
|
|
10
|
-
model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
|
|
10
|
+
model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
|
|
11
11
|
):
|
|
12
12
|
key = (model, event)
|
|
13
13
|
hooks = _hooks.setdefault(key, [])
|
|
14
|
-
hooks.append((handler_cls, method_name, condition, priority
|
|
14
|
+
hooks.append((handler_cls, method_name, condition, priority))
|
|
15
15
|
# keep sorted by priority
|
|
16
16
|
hooks.sort(key=lambda x: x[3])
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def get_hooks(model, event):
|
|
20
|
-
|
|
20
|
+
hooks = _hooks.get((model, event), [])
|
|
21
|
+
return hooks
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def list_all_hooks():
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-bulk-hooks
|
|
3
|
+
Version: 0.1.103
|
|
4
|
+
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
+
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: django,bulk,hooks
|
|
8
|
+
Author: Konrad Beck
|
|
9
|
+
Author-email: konrad.beck@merchantcapital.co.za
|
|
10
|
+
Requires-Python: >=3.11,<4.0
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: Django (>=4.0)
|
|
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(Hook):
|
|
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
|
+
## 🛠 Supported Hook Events
|
|
78
|
+
|
|
79
|
+
- `BEFORE_CREATE`, `AFTER_CREATE`
|
|
80
|
+
- `BEFORE_UPDATE`, `AFTER_UPDATE`
|
|
81
|
+
- `BEFORE_DELETE`, `AFTER_DELETE`
|
|
82
|
+
|
|
83
|
+
## 🔄 Lifecycle Events
|
|
84
|
+
|
|
85
|
+
### Individual Model Operations
|
|
86
|
+
|
|
87
|
+
The `HookModelMixin` automatically triggers hooks for individual model operations:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# These will trigger BEFORE_CREATE and AFTER_CREATE hooks
|
|
91
|
+
account = Account.objects.create(balance=100.00)
|
|
92
|
+
account.save() # for new instances
|
|
93
|
+
|
|
94
|
+
# These will trigger BEFORE_UPDATE and AFTER_UPDATE hooks
|
|
95
|
+
account.balance = 200.00
|
|
96
|
+
account.save() # for existing instances
|
|
97
|
+
|
|
98
|
+
# This will trigger BEFORE_DELETE and AFTER_DELETE hooks
|
|
99
|
+
account.delete()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Bulk Operations
|
|
103
|
+
|
|
104
|
+
Bulk operations also trigger the same hooks:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# Bulk create - triggers BEFORE_CREATE and AFTER_CREATE hooks
|
|
108
|
+
accounts = [
|
|
109
|
+
Account(balance=100.00),
|
|
110
|
+
Account(balance=200.00),
|
|
111
|
+
]
|
|
112
|
+
Account.objects.bulk_create(accounts)
|
|
113
|
+
|
|
114
|
+
# Bulk update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
|
|
115
|
+
for account in accounts:
|
|
116
|
+
account.balance *= 1.1
|
|
117
|
+
Account.objects.bulk_update(accounts, ['balance'])
|
|
118
|
+
|
|
119
|
+
# Bulk delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
|
|
120
|
+
Account.objects.bulk_delete(accounts)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Queryset Operations
|
|
124
|
+
|
|
125
|
+
Queryset operations are also supported:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# Queryset update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
|
|
129
|
+
Account.objects.update(balance=0.00)
|
|
130
|
+
|
|
131
|
+
# Queryset delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
|
|
132
|
+
Account.objects.delete()
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 🧠 Why?
|
|
136
|
+
|
|
137
|
+
Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
|
|
138
|
+
|
|
139
|
+
- Hooks that behave consistently across creates/updates/deletes
|
|
140
|
+
- **NEW**: Individual model lifecycle hooks that work with `save()` and `delete()`
|
|
141
|
+
- Scalable performance via chunking (default 200)
|
|
142
|
+
- Support for `@hook` decorators and centralized hook classes
|
|
143
|
+
- **NEW**: Automatic hook triggering for admin operations and other Django features
|
|
144
|
+
- **NEW**: Proper ordering guarantees for old/new record pairing in hooks (Salesforce-like behavior)
|
|
145
|
+
|
|
146
|
+
## 📦 Usage Examples
|
|
147
|
+
|
|
148
|
+
### Individual Model Operations
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# These automatically trigger hooks
|
|
152
|
+
account = Account.objects.create(balance=100.00)
|
|
153
|
+
account.balance = 200.00
|
|
154
|
+
account.save()
|
|
155
|
+
account.delete()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Bulk Operations
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# These also trigger hooks
|
|
162
|
+
Account.objects.bulk_create(accounts)
|
|
163
|
+
Account.objects.bulk_update(accounts, ['balance'])
|
|
164
|
+
Account.objects.bulk_delete(accounts)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Advanced Hook Usage
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
class AdvancedAccountHooks(Hook):
|
|
171
|
+
@hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
172
|
+
def validate_balance_change(self, new_records, old_records):
|
|
173
|
+
for new_account, old_account in zip(new_records, old_records):
|
|
174
|
+
if new_account.balance < 0 and old_account.balance >= 0:
|
|
175
|
+
raise ValueError("Cannot set negative balance")
|
|
176
|
+
|
|
177
|
+
@hook(AFTER_CREATE, model=Account)
|
|
178
|
+
def send_welcome_email(self, new_records, old_records):
|
|
179
|
+
for account in new_records:
|
|
180
|
+
# Send welcome email logic here
|
|
181
|
+
pass
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Salesforce-like Ordering Guarantees
|
|
185
|
+
|
|
186
|
+
The system ensures that `old_records` and `new_records` are always properly paired, regardless of the order in which you pass objects to bulk operations:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
class LoanAccountHooks(Hook):
|
|
190
|
+
@hook(BEFORE_UPDATE, model=LoanAccount)
|
|
191
|
+
def validate_account_number(self, new_records, old_records):
|
|
192
|
+
# old_records[i] always corresponds to new_records[i]
|
|
193
|
+
for new_account, old_account in zip(new_records, old_records):
|
|
194
|
+
if old_account.account_number != new_account.account_number:
|
|
195
|
+
raise ValidationError("Account number cannot be changed")
|
|
196
|
+
|
|
197
|
+
# This works correctly even with reordered objects:
|
|
198
|
+
accounts = [account1, account2, account3] # IDs: 1, 2, 3
|
|
199
|
+
reordered = [account3, account1, account2] # IDs: 3, 1, 2
|
|
200
|
+
|
|
201
|
+
# The hook will still receive properly paired old/new records
|
|
202
|
+
LoanAccount.objects.bulk_update(reordered, ['balance'])
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 🧩 Integration with Queryable Properties
|
|
206
|
+
|
|
207
|
+
You can extend from `BulkHookManager` to support formula fields or property querying.
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
class MyManager(BulkHookManager, QueryablePropertiesManager):
|
|
211
|
+
pass
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 📝 License
|
|
215
|
+
|
|
216
|
+
MIT © 2024 Augend / Konrad Beck
|
|
217
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
|
+
django_bulk_hooks/conditions.py,sha256=mTvlLcttixbXRkTSNZU5VewkPUavbXRuD2BkJbVWMkw,6041
|
|
3
|
+
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
+
django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
|
|
5
|
+
django_bulk_hooks/decorators.py,sha256=tckDcxtOzKCbgvS9QydgeIAWTFDEl-ch3_Q--ruEGdQ,4831
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=3HbgV12JRYIy9IlygHPxZiHnFXj7EwzLyTuJNQeVIoI,1402
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=JkhOw9XbEUt8VpunhUSP0z-NlazIFUWmAesj7H4ge8w,7148
|
|
10
|
+
django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
|
|
11
|
+
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=iet4z-9SKhnresA4FBQbxx9rdYnoaOWbw9LUlGftlP0,1466
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
|
|
14
|
+
django_bulk_hooks-0.1.103.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.103.dist-info/METADATA,sha256=Y_okaTk73sz_FCHWBOczBT8tzRz-wQJmNzmofLWy3MI,6939
|
|
16
|
+
django_bulk_hooks-0.1.103.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.103.dist-info/RECORD,,
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.101
|
|
4
|
-
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
-
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
6
|
-
License: MIT
|
|
7
|
-
Keywords: django,bulk,hooks
|
|
8
|
-
Author: Konrad Beck
|
|
9
|
-
Author-email: konrad.beck@merchantcapital.co.za
|
|
10
|
-
Requires-Python: >=3.11,<4.0
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Requires-Dist: Django (>=4.0)
|
|
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
|
-
- **NEW**: `@select_related` decorator to prevent queries in loops
|
|
38
|
-
|
|
39
|
-
## 🚀 Quickstart
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
pip install django-bulk-hooks
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Define Your Model
|
|
46
|
-
|
|
47
|
-
```python
|
|
48
|
-
from django.db import models
|
|
49
|
-
from django_bulk_hooks.models import HookModelMixin
|
|
50
|
-
|
|
51
|
-
class Account(HookModelMixin):
|
|
52
|
-
balance = models.DecimalField(max_digits=10, decimal_places=2)
|
|
53
|
-
# The HookModelMixin automatically provides BulkHookManager
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Create a Hook Handler
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
from django_bulk_hooks import hook, AFTER_UPDATE, select_related
|
|
60
|
-
from django_bulk_hooks.conditions import WhenFieldHasChanged
|
|
61
|
-
from .models import Account
|
|
62
|
-
|
|
63
|
-
class AccountHandler:
|
|
64
|
-
@hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
65
|
-
@select_related("user") # Preload user to prevent queries in loops
|
|
66
|
-
def notify_balance_change(self, new_records, old_records):
|
|
67
|
-
for account in new_records:
|
|
68
|
-
# This won't cause a query since user is preloaded
|
|
69
|
-
user_email = account.user.email
|
|
70
|
-
self.send_notification(user_email, account.balance)
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## 🔧 Using `@select_related` to Prevent Queries in Loops
|
|
74
|
-
|
|
75
|
-
The `@select_related` decorator is essential when your hook logic needs to access related objects. Without it, you might end up with N+1 query problems.
|
|
76
|
-
|
|
77
|
-
### ❌ Without `@select_related` (causes queries in loops)
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
@hook(AFTER_CREATE, model=LoanAccount)
|
|
81
|
-
def process_accounts(self, new_records, old_records):
|
|
82
|
-
for account in new_records:
|
|
83
|
-
# ❌ This causes a query for each account!
|
|
84
|
-
status_name = account.status.name
|
|
85
|
-
if status_name == "ACTIVE":
|
|
86
|
-
self.activate_account(account)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
### ✅ With `@select_related` (bulk loads related objects)
|
|
90
|
-
|
|
91
|
-
```python
|
|
92
|
-
@hook(AFTER_CREATE, model=LoanAccount)
|
|
93
|
-
@select_related("status") # Bulk load status objects
|
|
94
|
-
def process_accounts(self, new_records, old_records):
|
|
95
|
-
for account in new_records:
|
|
96
|
-
# ✅ No query here - status is preloaded
|
|
97
|
-
status_name = account.status.name
|
|
98
|
-
if status_name == "ACTIVE":
|
|
99
|
-
self.activate_account(account)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Multiple Related Fields
|
|
103
|
-
|
|
104
|
-
```python
|
|
105
|
-
@hook(AFTER_UPDATE, model=Transaction)
|
|
106
|
-
@select_related("account", "category", "status")
|
|
107
|
-
def process_transactions(self, new_records, old_records):
|
|
108
|
-
for transaction in new_records:
|
|
109
|
-
# All related objects are preloaded - no queries in loops
|
|
110
|
-
account_name = transaction.account.name
|
|
111
|
-
category_type = transaction.category.type
|
|
112
|
-
status_name = transaction.status.name
|
|
113
|
-
|
|
114
|
-
if status_name == "COMPLETE":
|
|
115
|
-
self.process_complete_transaction(transaction)
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
### Your Original Example (Fixed)
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
@hook(BEFORE_CREATE, model=LoanAccount, condition=IsEqual("status.name", value=Status.ACTIVE.value))
|
|
122
|
-
@hook(
|
|
123
|
-
BEFORE_UPDATE,
|
|
124
|
-
model=LoanAccount,
|
|
125
|
-
condition=HasChanged("status", has_changed=True) & IsEqual("status.name", value=Status.ACTIVE.value),
|
|
126
|
-
priority=Priority.HIGH,
|
|
127
|
-
)
|
|
128
|
-
@select_related("status") # This ensures status is preloaded
|
|
129
|
-
def _set_activated_date(self, old_records: list[LoanAccount], new_records: list[LoanAccount], **kwargs) -> None:
|
|
130
|
-
logger.info(f"Setting activated date for {new_records}")
|
|
131
|
-
# No queries in loops - status objects are preloaded
|
|
132
|
-
self._loan_account_service.set_activated_date(new_records)
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## 🛡️ Safe Handling of Related Objects
|
|
136
|
-
|
|
137
|
-
Use the `safe_get_related_attr` utility function to safely access related object attributes:
|
|
138
|
-
|
|
139
|
-
```python
|
|
140
|
-
from django_bulk_hooks.conditions import safe_get_related_attr
|
|
141
|
-
|
|
142
|
-
# ✅ SAFE: Use safe_get_related_attr to handle None values
|
|
143
|
-
@hook(AFTER_CREATE, model=Transaction)
|
|
144
|
-
def process_transaction(self, new_records, old_records):
|
|
145
|
-
for transaction in new_records:
|
|
146
|
-
# Safely get the status name, returns None if status doesn't exist
|
|
147
|
-
status_name = safe_get_related_attr(transaction, 'status', 'name')
|
|
148
|
-
|
|
149
|
-
if status_name == "COMPLETE":
|
|
150
|
-
# Process the transaction
|
|
151
|
-
pass
|
|
152
|
-
elif status_name is None:
|
|
153
|
-
# Handle case where status is not set
|
|
154
|
-
print(f"Transaction {transaction.id} has no status")
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Complete Example
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
from django.db import models
|
|
161
|
-
from django_bulk_hooks import hook, select_related
|
|
162
|
-
from django_bulk_hooks.conditions import safe_get_related_attr
|
|
163
|
-
|
|
164
|
-
class Status(models.Model):
|
|
165
|
-
name = models.CharField(max_length=50)
|
|
166
|
-
|
|
167
|
-
class Transaction(HookModelMixin, models.Model):
|
|
168
|
-
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
|
169
|
-
status = models.ForeignKey(Status, on_delete=models.CASCADE, null=True, blank=True)
|
|
170
|
-
category = models.ForeignKey('Category', on_delete=models.CASCADE, null=True, blank=True)
|
|
171
|
-
|
|
172
|
-
class TransactionHandler:
|
|
173
|
-
@hook(Transaction, "before_create")
|
|
174
|
-
def set_default_status(self, new_records, old_records=None):
|
|
175
|
-
"""Set default status for new transactions."""
|
|
176
|
-
default_status = Status.objects.filter(name="PENDING").first()
|
|
177
|
-
for transaction in new_records:
|
|
178
|
-
if transaction.status is None:
|
|
179
|
-
transaction.status = default_status
|
|
180
|
-
|
|
181
|
-
@hook(Transaction, "after_create")
|
|
182
|
-
@select_related("status", "category") # Preload related objects
|
|
183
|
-
def process_transactions(self, new_records, old_records=None):
|
|
184
|
-
"""Process transactions based on their status."""
|
|
185
|
-
for transaction in new_records:
|
|
186
|
-
# ✅ SAFE: Get status name safely (no queries in loops)
|
|
187
|
-
status_name = safe_get_related_attr(transaction, 'status', 'name')
|
|
188
|
-
|
|
189
|
-
if status_name == "COMPLETE":
|
|
190
|
-
self._process_complete_transaction(transaction)
|
|
191
|
-
elif status_name == "FAILED":
|
|
192
|
-
self._process_failed_transaction(transaction)
|
|
193
|
-
elif status_name is None:
|
|
194
|
-
print(f"Transaction {transaction.id} has no status")
|
|
195
|
-
|
|
196
|
-
# ✅ SAFE: Check for related object existence (no queries in loops)
|
|
197
|
-
category = safe_get_related_attr(transaction, 'category')
|
|
198
|
-
if category:
|
|
199
|
-
print(f"Transaction {transaction.id} belongs to category: {category.name}")
|
|
200
|
-
|
|
201
|
-
def _process_complete_transaction(self, transaction):
|
|
202
|
-
# Process complete transaction logic
|
|
203
|
-
pass
|
|
204
|
-
|
|
205
|
-
def _process_failed_transaction(self, transaction):
|
|
206
|
-
# Process failed transaction logic
|
|
207
|
-
pass
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### Best Practices for Related Objects
|
|
211
|
-
|
|
212
|
-
1. **Always use `@select_related`** when accessing related object attributes in hooks
|
|
213
|
-
2. **Use `safe_get_related_attr`** for safe access to related object attributes
|
|
214
|
-
3. **Set default values in `BEFORE_CREATE` hooks** to ensure related objects exist
|
|
215
|
-
4. **Handle None cases explicitly** to avoid unexpected behavior
|
|
216
|
-
5. **Use bulk operations efficiently** by fetching related objects once and reusing them
|
|
217
|
-
|
|
218
|
-
## 🔍 Performance Tips
|
|
219
|
-
|
|
220
|
-
### Monitor Query Count
|
|
221
|
-
|
|
222
|
-
```python
|
|
223
|
-
from django.db import connection, reset_queries
|
|
224
|
-
|
|
225
|
-
# Before your bulk operation
|
|
226
|
-
reset_queries()
|
|
227
|
-
|
|
228
|
-
# Your bulk operation
|
|
229
|
-
accounts = Account.objects.bulk_create(account_list)
|
|
230
|
-
|
|
231
|
-
# After your bulk operation
|
|
232
|
-
print(f"Total queries: {len(connection.queries)}")
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Use `@select_related` Strategically
|
|
236
|
-
|
|
237
|
-
```python
|
|
238
|
-
# Only select_related fields you actually use
|
|
239
|
-
@select_related("status") # Good - only what you need
|
|
240
|
-
@select_related("status", "category", "user", "account") # Only if you use all of them
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
### Avoid Nested Loops with Related Objects
|
|
244
|
-
|
|
245
|
-
```python
|
|
246
|
-
# ❌ Bad - nested loops with related objects
|
|
247
|
-
@hook(AFTER_CREATE, model=Order)
|
|
248
|
-
def process_orders(self, new_records, old_records):
|
|
249
|
-
for order in new_records:
|
|
250
|
-
for item in order.items.all(): # This causes queries!
|
|
251
|
-
process_item(item)
|
|
252
|
-
|
|
253
|
-
# ✅ Good - use prefetch_related for many-to-many/one-to-many
|
|
254
|
-
@hook(AFTER_CREATE, model=Order)
|
|
255
|
-
@select_related("customer")
|
|
256
|
-
def process_orders(self, new_records, old_records):
|
|
257
|
-
# Prefetch items for all orders at once
|
|
258
|
-
from django.db.models import Prefetch
|
|
259
|
-
orders_with_items = Order.objects.prefetch_related(
|
|
260
|
-
Prefetch('items', queryset=Item.objects.select_related('product'))
|
|
261
|
-
).filter(id__in=[order.id for order in new_records])
|
|
262
|
-
|
|
263
|
-
for order in orders_with_items:
|
|
264
|
-
for item in order.items.all(): # No queries here
|
|
265
|
-
process_item(item)
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## 📚 API Reference
|
|
269
|
-
|
|
270
|
-
### Decorators
|
|
271
|
-
|
|
272
|
-
- `@hook(event, model, condition=None, priority=DEFAULT_PRIORITY)` - Register a hook
|
|
273
|
-
- `@select_related(*fields)` - Preload related fields to prevent queries in loops
|
|
274
|
-
|
|
275
|
-
### Conditions
|
|
276
|
-
|
|
277
|
-
- `IsEqual(field, value)` - Check if field equals value
|
|
278
|
-
- `HasChanged(field, has_changed=True)` - Check if field has changed
|
|
279
|
-
- `safe_get_related_attr(instance, field, attr=None)` - Safely get related object attributes
|
|
280
|
-
|
|
281
|
-
### Events
|
|
282
|
-
|
|
283
|
-
- `BEFORE_CREATE`, `AFTER_CREATE`
|
|
284
|
-
- `BEFORE_UPDATE`, `AFTER_UPDATE`
|
|
285
|
-
- `BEFORE_DELETE`, `AFTER_DELETE`
|
|
286
|
-
- `VALIDATE_CREATE`, `VALIDATE_UPDATE`, `VALIDATE_DELETE`
|
|
287
|
-
|
|
288
|
-
## 🤝 Contributing
|
|
289
|
-
|
|
290
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
291
|
-
|
|
292
|
-
## 📄 License
|
|
293
|
-
|
|
294
|
-
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
295
|
-
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=EAWve4HjrrIuPbl8uc1s1ISDM3RPDtwCvTOPRwFpX8w,1392
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=wDtY90Kv3xjWx8HEA4aAjva8fDDaYegJhn0Eu6G0F60,12150
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
|
|
4
|
-
django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
|
|
5
|
-
django_bulk_hooks/decorators.py,sha256=_bTcC4zJiRjpJIMBhjZbuTsqeR0y4GAQ70EJvp8Q0wU,2517
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=T3vIrYDRfGLr6GQfDwwlQQKpEGzsCfCca012DfPl7Z4,6137
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=1viPTjT9U-5rUPETOtyGHp_UaSPNxVoSYhbBwIHigy8,6076
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=DcVosEA4RS79KSYgw3Z14_a9Sd8CfxNNc5F3eSb8xc0,11459
|
|
10
|
-
django_bulk_hooks/models.py,sha256=U5nCxingZS2sznDjgW8fWo93SisA03WKcGpxxApqhuM,5519
|
|
11
|
-
django_bulk_hooks/queryset.py,sha256=7lLqhZ-XOYsZ1I3Loxi4Nhz79M8HlTYE413AW8nyeDI,1330
|
|
12
|
-
django_bulk_hooks/registry.py,sha256=MY-JOuDphsxay9GHqpZGY_NHGGkvqaH_8RW5kiStDuI,741
|
|
13
|
-
django_bulk_hooks-0.1.101.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
14
|
-
django_bulk_hooks-0.1.101.dist-info/METADATA,sha256=UgcHNzW2TURJUzwXHW4bcnDeaPUMh9ySdEl8FnIK8fY,10700
|
|
15
|
-
django_bulk_hooks-0.1.101.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
16
|
-
django_bulk_hooks-0.1.101.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|