django-bulk-hooks 0.1.83__py3-none-any.whl → 0.2.100__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 +53 -50
- django_bulk_hooks/changeset.py +214 -0
- django_bulk_hooks/conditions.py +230 -351
- django_bulk_hooks/constants.py +4 -0
- django_bulk_hooks/context.py +49 -9
- django_bulk_hooks/decorators.py +219 -96
- django_bulk_hooks/dispatcher.py +588 -0
- django_bulk_hooks/factory.py +541 -0
- django_bulk_hooks/handler.py +106 -167
- django_bulk_hooks/helpers.py +258 -0
- django_bulk_hooks/manager.py +134 -208
- django_bulk_hooks/models.py +89 -101
- django_bulk_hooks/operations/__init__.py +18 -0
- django_bulk_hooks/operations/analyzer.py +466 -0
- django_bulk_hooks/operations/bulk_executor.py +757 -0
- django_bulk_hooks/operations/coordinator.py +928 -0
- django_bulk_hooks/operations/field_utils.py +341 -0
- django_bulk_hooks/operations/mti_handler.py +696 -0
- django_bulk_hooks/operations/mti_plans.py +103 -0
- django_bulk_hooks/operations/record_classifier.py +196 -0
- django_bulk_hooks/queryset.py +233 -43
- django_bulk_hooks/registry.py +276 -25
- django_bulk_hooks-0.2.100.dist-info/METADATA +320 -0
- django_bulk_hooks-0.2.100.dist-info/RECORD +27 -0
- django_bulk_hooks/engine.py +0 -53
- django_bulk_hooks-0.1.83.dist-info/METADATA +0 -228
- django_bulk_hooks-0.1.83.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/WHEEL +0 -0
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.83
|
|
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.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=2PcJ6xz7t7Du0nmLO_5732G6u_oZTygogG0fKESRHHk,1082
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=cif5R4C_N2HosuYiB03STc2rHb800EJV0Nby-ZPac1s,12460
|
|
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=zstmb27dKcOHu3Atg7cauewCTzPvUmq03mzVKJRi56o,7230
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=b1AO8qyl2VMt1CHyefn1Gt1_ARaV80yBZ3mGXsJ9_eA,2021
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=Qpg_zT6SsQiTlhduvzXxPdG6uynjyR2fBjj-R6HZiXI,4861
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=-V128ACxPAz82ua4jQRFUkjAKtKW4MN5ppz0bHcv5s4,7138
|
|
10
|
-
django_bulk_hooks/models.py,sha256=m_dsCcKX8euGU-aDvlkY-vi5QTsRaXhyT-6EZotYuXM,3813
|
|
11
|
-
django_bulk_hooks/queryset.py,sha256=7lLqhZ-XOYsZ1I3Loxi4Nhz79M8HlTYE413AW8nyeDI,1330
|
|
12
|
-
django_bulk_hooks/registry.py,sha256=Vh78exKYcdZhM27120kQm-iXGOjd_kf9ZUYBZ8eQ2V0,683
|
|
13
|
-
django_bulk_hooks-0.1.83.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
14
|
-
django_bulk_hooks-0.1.83.dist-info/METADATA,sha256=G6_6NsM-MqUem0Y5fmZz0pktXtmsaIVpglFeBDIv7JM,9051
|
|
15
|
-
django_bulk_hooks-0.1.83.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
16
|
-
django_bulk_hooks-0.1.83.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|