django-bulk-hooks 0.1.67__py3-none-any.whl → 0.1.69__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 +2 -2
- django_bulk_hooks/constants.py +3 -0
- django_bulk_hooks/engine.py +11 -1
- django_bulk_hooks/handler.py +5 -5
- django_bulk_hooks/manager.py +25 -4
- django_bulk_hooks/models.py +79 -10
- django_bulk_hooks-0.1.69.dist-info/METADATA +195 -0
- django_bulk_hooks-0.1.69.dist-info/RECORD +17 -0
- django_bulk_hooks-0.1.67.dist-info/METADATA +0 -101
- django_bulk_hooks-0.1.67.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.67.dist-info → django_bulk_hooks-0.1.69.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.67.dist-info → django_bulk_hooks-0.1.69.dist-info}/WHEEL +0 -0
django_bulk_hooks/__init__.py
CHANGED
django_bulk_hooks/constants.py
CHANGED
django_bulk_hooks/engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
3
|
from django_bulk_hooks.registry import get_hooks
|
|
4
4
|
|
|
5
5
|
logger = logging.getLogger(__name__)
|
|
@@ -21,6 +21,16 @@ def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
|
21
21
|
logger.info("No hooks found for model=%s, event=%s", model_cls.__name__, event)
|
|
22
22
|
return
|
|
23
23
|
|
|
24
|
+
# For BEFORE_* events, run model.clean() first for validation
|
|
25
|
+
if event.startswith("before_"):
|
|
26
|
+
logger.debug("Running model.clean() for %d instances", len(new_instances))
|
|
27
|
+
for instance in new_instances:
|
|
28
|
+
try:
|
|
29
|
+
instance.clean()
|
|
30
|
+
except ValidationError as e:
|
|
31
|
+
logger.error("Validation failed for %s: %s", instance, e)
|
|
32
|
+
raise
|
|
33
|
+
|
|
24
34
|
for handler_cls, method_name, condition, priority in hooks:
|
|
25
35
|
handler_instance = handler_cls()
|
|
26
36
|
func = getattr(handler_instance, method_name)
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -64,7 +64,7 @@ class HookContextState:
|
|
|
64
64
|
Hook = HookContextState()
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class
|
|
67
|
+
class HookMeta(type):
|
|
68
68
|
_registered = set()
|
|
69
69
|
|
|
70
70
|
def __new__(mcs, name, bases, namespace):
|
|
@@ -73,9 +73,9 @@ class HookHandlerMeta(type):
|
|
|
73
73
|
if hasattr(method, "hooks_hooks"):
|
|
74
74
|
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
75
75
|
key = (model_cls, event, cls, method_name)
|
|
76
|
-
if key not in
|
|
76
|
+
if key not in HookMeta._registered:
|
|
77
77
|
logger.info(
|
|
78
|
-
"Registering hook via
|
|
78
|
+
"Registering hook via HookMeta: model=%s, event=%s, handler_cls=%s, method_name=%s",
|
|
79
79
|
model_cls.__name__,
|
|
80
80
|
event,
|
|
81
81
|
cls.__name__,
|
|
@@ -89,11 +89,11 @@ class HookHandlerMeta(type):
|
|
|
89
89
|
condition=condition,
|
|
90
90
|
priority=priority,
|
|
91
91
|
)
|
|
92
|
-
|
|
92
|
+
HookMeta._registered.add(key)
|
|
93
93
|
return cls
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
class
|
|
96
|
+
class Hook(metaclass=HookMeta):
|
|
97
97
|
@classmethod
|
|
98
98
|
def handle(
|
|
99
99
|
cls,
|
django_bulk_hooks/manager.py
CHANGED
|
@@ -10,6 +10,9 @@ from django_bulk_hooks.constants import (
|
|
|
10
10
|
BEFORE_CREATE,
|
|
11
11
|
BEFORE_DELETE,
|
|
12
12
|
BEFORE_UPDATE,
|
|
13
|
+
VALIDATE_CREATE,
|
|
14
|
+
VALIDATE_UPDATE,
|
|
15
|
+
VALIDATE_DELETE,
|
|
13
16
|
)
|
|
14
17
|
from django_bulk_hooks.context import HookContext
|
|
15
18
|
from django_bulk_hooks.queryset import HookQuerySet
|
|
@@ -24,7 +27,7 @@ class BulkHookManager(models.Manager):
|
|
|
24
27
|
return HookQuerySet(self.model, using=self._db)
|
|
25
28
|
|
|
26
29
|
@transaction.atomic
|
|
27
|
-
def bulk_update(self, objs, fields, bypass_hooks=False, **kwargs):
|
|
30
|
+
def bulk_update(self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
28
31
|
if not objs:
|
|
29
32
|
return []
|
|
30
33
|
|
|
@@ -38,6 +41,12 @@ class BulkHookManager(models.Manager):
|
|
|
38
41
|
if not bypass_hooks:
|
|
39
42
|
originals = list(model_cls.objects.filter(pk__in=[obj.pk for obj in objs]))
|
|
40
43
|
ctx = HookContext(model_cls)
|
|
44
|
+
|
|
45
|
+
# Run validation hooks first
|
|
46
|
+
if not bypass_validation:
|
|
47
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
48
|
+
|
|
49
|
+
# Then run business logic hooks
|
|
41
50
|
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
42
51
|
|
|
43
52
|
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
@@ -106,7 +115,7 @@ class BulkHookManager(models.Manager):
|
|
|
106
115
|
return modified_fields
|
|
107
116
|
|
|
108
117
|
@transaction.atomic
|
|
109
|
-
def bulk_create(self, objs, bypass_hooks=False, **kwargs):
|
|
118
|
+
def bulk_create(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
110
119
|
model_cls = self.model
|
|
111
120
|
|
|
112
121
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
@@ -118,6 +127,12 @@ class BulkHookManager(models.Manager):
|
|
|
118
127
|
|
|
119
128
|
if not bypass_hooks:
|
|
120
129
|
ctx = HookContext(model_cls)
|
|
130
|
+
|
|
131
|
+
# Run validation hooks first
|
|
132
|
+
if not bypass_validation:
|
|
133
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
134
|
+
|
|
135
|
+
# Then run business logic hooks
|
|
121
136
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
122
137
|
|
|
123
138
|
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
@@ -130,7 +145,7 @@ class BulkHookManager(models.Manager):
|
|
|
130
145
|
return result
|
|
131
146
|
|
|
132
147
|
@transaction.atomic
|
|
133
|
-
def bulk_delete(self, objs, batch_size=None, bypass_hooks=False):
|
|
148
|
+
def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
|
|
134
149
|
if not objs:
|
|
135
150
|
return []
|
|
136
151
|
|
|
@@ -144,8 +159,14 @@ class BulkHookManager(models.Manager):
|
|
|
144
159
|
ctx = HookContext(model_cls)
|
|
145
160
|
|
|
146
161
|
if not bypass_hooks:
|
|
147
|
-
logger.info("Executing
|
|
162
|
+
logger.info("Executing hooks for %s", model_cls.__name__)
|
|
148
163
|
logger.info("Number of objects to delete: %d", len(objs))
|
|
164
|
+
|
|
165
|
+
# Run validation hooks first
|
|
166
|
+
if not bypass_validation:
|
|
167
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
168
|
+
|
|
169
|
+
# Then run business logic hooks
|
|
149
170
|
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
150
171
|
|
|
151
172
|
pks = [obj.pk for obj in objs if obj.pk is not None]
|
django_bulk_hooks/models.py
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
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
|
|
3
16
|
from django_bulk_hooks.manager import BulkHookManager
|
|
4
17
|
|
|
5
18
|
|
|
@@ -9,18 +22,74 @@ class HookModelMixin(models.Model):
|
|
|
9
22
|
class Meta:
|
|
10
23
|
abstract = True
|
|
11
24
|
|
|
12
|
-
def
|
|
13
|
-
|
|
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
|
+
super().clean()
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
|
|
33
|
+
# Determine if this is a create or update operation
|
|
34
|
+
is_create = self.pk is None
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
if is_create:
|
|
37
|
+
# For create operations, run VALIDATE_CREATE hooks for validation
|
|
38
|
+
ctx = HookContext(self.__class__)
|
|
39
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
40
|
+
else:
|
|
41
|
+
# For update operations, run VALIDATE_UPDATE hooks for validation
|
|
42
|
+
try:
|
|
43
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
44
|
+
ctx = HookContext(self.__class__)
|
|
45
|
+
run(self.__class__, VALIDATE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
46
|
+
except self.__class__.DoesNotExist:
|
|
47
|
+
# If the old instance doesn't exist, treat as create
|
|
48
|
+
ctx = HookContext(self.__class__)
|
|
49
|
+
run(self.__class__, VALIDATE_CREATE, [self], ctx=ctx)
|
|
19
50
|
|
|
20
|
-
|
|
51
|
+
def save(self, *args, **kwargs):
|
|
52
|
+
is_create = self.pk is None
|
|
53
|
+
|
|
54
|
+
if is_create:
|
|
55
|
+
# For create operations, we don't have old records
|
|
56
|
+
ctx = HookContext(self.__class__)
|
|
57
|
+
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
58
|
+
|
|
59
|
+
super().save(*args, **kwargs)
|
|
60
|
+
|
|
61
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
62
|
+
else:
|
|
63
|
+
# For update operations, we need to get the old record
|
|
64
|
+
try:
|
|
65
|
+
old_instance = self.__class__.objects.get(pk=self.pk)
|
|
66
|
+
ctx = HookContext(self.__class__)
|
|
67
|
+
run(self.__class__, BEFORE_UPDATE, [self], [old_instance], ctx=ctx)
|
|
68
|
+
|
|
69
|
+
super().save(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
run(self.__class__, AFTER_UPDATE, [self], [old_instance], ctx=ctx)
|
|
72
|
+
except self.__class__.DoesNotExist:
|
|
73
|
+
# If the old instance doesn't exist, treat as create
|
|
74
|
+
ctx = HookContext(self.__class__)
|
|
75
|
+
run(self.__class__, BEFORE_CREATE, [self], ctx=ctx)
|
|
76
|
+
|
|
77
|
+
super().save(*args, **kwargs)
|
|
21
78
|
|
|
22
|
-
|
|
23
|
-
pass
|
|
79
|
+
run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
|
|
24
80
|
|
|
25
|
-
|
|
26
|
-
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def delete(self, *args, **kwargs):
|
|
84
|
+
ctx = HookContext(self.__class__)
|
|
85
|
+
|
|
86
|
+
# Run validation hooks first
|
|
87
|
+
run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
|
|
88
|
+
|
|
89
|
+
# Then run business logic hooks
|
|
90
|
+
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
91
|
+
|
|
92
|
+
result = super().delete(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
95
|
+
return result
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django-bulk-hooks
|
|
3
|
+
Version: 0.1.69
|
|
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
|
+
|
|
145
|
+
## 📦 Usage Examples
|
|
146
|
+
|
|
147
|
+
### Individual Model Operations
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
# These automatically trigger hooks
|
|
151
|
+
account = Account.objects.create(balance=100.00)
|
|
152
|
+
account.balance = 200.00
|
|
153
|
+
account.save()
|
|
154
|
+
account.delete()
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Bulk Operations
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# These also trigger hooks
|
|
161
|
+
Account.objects.bulk_create(accounts)
|
|
162
|
+
Account.objects.bulk_update(accounts, ['balance'])
|
|
163
|
+
Account.objects.bulk_delete(accounts)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Advanced Hook Usage
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
class AdvancedAccountHooks(Hook):
|
|
170
|
+
@hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
171
|
+
def validate_balance_change(self, new_records, old_records):
|
|
172
|
+
for new_account, old_account in zip(new_records, old_records):
|
|
173
|
+
if new_account.balance < 0 and old_account.balance >= 0:
|
|
174
|
+
raise ValueError("Cannot set negative balance")
|
|
175
|
+
|
|
176
|
+
@hook(AFTER_CREATE, model=Account)
|
|
177
|
+
def send_welcome_email(self, new_records, old_records):
|
|
178
|
+
for account in new_records:
|
|
179
|
+
# Send welcome email logic here
|
|
180
|
+
pass
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 🧩 Integration with Queryable Properties
|
|
184
|
+
|
|
185
|
+
You can extend from `BulkHookManager` to support formula fields or property querying.
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
class MyManager(BulkHookManager, QueryablePropertiesManager):
|
|
189
|
+
pass
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 📝 License
|
|
193
|
+
|
|
194
|
+
MIT © 2024 Augend / Konrad Beck
|
|
195
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
|
|
2
|
+
django_bulk_hooks/conditions.py,sha256=S8o8JLAeYoF5hNUaxmC2QwMtmak0C73aI-j4GtgmbAw,6541
|
|
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=My339hWu8L2iZ6GQeEJ3M6LGTraKbp8vG6cO8_Vpi2Y,2632
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=w0t6cCR9y4ooGEXoxTVlJHBqdAIS0MeSh5E6kvU5Cok,5511
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=90wOzdtkM2xaXXh7pGzV0E5kPkJlfkIZJILdEFjUxUc,7450
|
|
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=nmVZ6cqkveSiBz_L4cRT-3u-mkcjkGQNJLhza8SSdKc,1398
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=2S_MUrhFJpGPZbBuqFvPEpKgEduGYUAsxqMS3_wsVXA,1233
|
|
14
|
+
django_bulk_hooks-0.1.69.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.69.dist-info/METADATA,sha256=kiyQOr6EB4UyBTeomqcjOTdTvuS6Vn1gWB1qhjlo58A,5918
|
|
16
|
+
django_bulk_hooks-0.1.69.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.69.dist-info/RECORD,,
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.67
|
|
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.
|
|
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.
|
|
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
|
-
- Hook chaining, hook deduplication, and atomicity
|
|
33
|
-
- Class-based hook handlers with DI support
|
|
34
|
-
|
|
35
|
-
## 🚀 Quickstart
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
pip install django-bulk-hooks
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Define Your Model
|
|
42
|
-
|
|
43
|
-
```python
|
|
44
|
-
from django.db import models
|
|
45
|
-
from django_bulk_hooks.manager import BulkHookManager
|
|
46
|
-
|
|
47
|
-
class Account(models.Model):
|
|
48
|
-
balance = models.DecimalField(max_digits=10, decimal_places=2)
|
|
49
|
-
objects = BulkHookManager()
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Create a Hook Handler
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
from django_bulk_hooks import hook, AFTER_UPDATE, HookHandler
|
|
56
|
-
from django_bulk_hooks.conditions import WhenFieldHasChanged
|
|
57
|
-
from .models import Account
|
|
58
|
-
|
|
59
|
-
class AccountHookHandler(HookHandler):
|
|
60
|
-
@hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
61
|
-
def log_balance_change(self, new_objs):
|
|
62
|
-
print("Accounts updated:", [a.pk for a in new_objs])
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## 🛠 Supported Hook Events
|
|
66
|
-
|
|
67
|
-
- `BEFORE_CREATE`, `AFTER_CREATE`
|
|
68
|
-
- `BEFORE_UPDATE`, `AFTER_UPDATE`
|
|
69
|
-
- `BEFORE_DELETE`, `AFTER_DELETE`
|
|
70
|
-
|
|
71
|
-
## 🧠 Why?
|
|
72
|
-
|
|
73
|
-
Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
|
|
74
|
-
|
|
75
|
-
- Hooks that behave consistently across creates/updates/deletes
|
|
76
|
-
- Scalable performance via chunking (default 200)
|
|
77
|
-
- Support for `@hook` decorators and centralized hook classes
|
|
78
|
-
|
|
79
|
-
## 📦 Usage in Views / Commands
|
|
80
|
-
|
|
81
|
-
```python
|
|
82
|
-
# Calls AFTER_UPDATE hooks automatically
|
|
83
|
-
Account.objects.bulk_update(accounts, ['balance'])
|
|
84
|
-
|
|
85
|
-
# Triggers BEFORE_CREATE and AFTER_CREATE hooks
|
|
86
|
-
Account.objects.bulk_create(accounts)
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## 🧩 Integration with Queryable Properties
|
|
90
|
-
|
|
91
|
-
You can extend from `BulkHookManager` to support formula fields or property querying.
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
class MyManager(BulkHookManager, QueryablePropertiesManager):
|
|
95
|
-
pass
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## 📝 License
|
|
99
|
-
|
|
100
|
-
MIT © 2024 Augend / Konrad Beck
|
|
101
|
-
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=Dc1AoWBC2atP8cNBHmCnDCbjn-iB8zOsXRSesiGztWo,154
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=S8o8JLAeYoF5hNUaxmC2QwMtmak0C73aI-j4GtgmbAw,6541
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=Jks1BIADYbap2fpq3Ry0e7w-CiXBCsR9b5h1yan1qoc,192
|
|
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=JPYL-EWc1NH8Sa3AuhLN8u1XLTtCKrtkaUgcGbMI0yM,2160
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=9iSXy6qkh-vs5hcrYR9Ciyfs1IllcyyX68hHRc294ZE,5553
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=v0WsJK7by_WHPNDFyvFW1d5BUPNWuRYIXlRhvKytjQ0,6637
|
|
10
|
-
django_bulk_hooks/models.py,sha256=5Hpl43yraurnOZho3frngJ9Eu3XdvnrE4iL9q7vM1r8,529
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=nmVZ6cqkveSiBz_L4cRT-3u-mkcjkGQNJLhza8SSdKc,1398
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=2S_MUrhFJpGPZbBuqFvPEpKgEduGYUAsxqMS3_wsVXA,1233
|
|
14
|
-
django_bulk_hooks-0.1.67.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.67.dist-info/METADATA,sha256=yTgClp7e_xBdHLyCyhD4k6cqVNJyZC8LRx12dBS3s0Y,2973
|
|
16
|
-
django_bulk_hooks-0.1.67.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
-
django_bulk_hooks-0.1.67.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|