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

Files changed (21) hide show
  1. django_bulk_hooks-0.1.69/PKG-INFO +195 -0
  2. django_bulk_hooks-0.1.69/README.md +175 -0
  3. django_bulk_hooks-0.1.69/django_bulk_hooks/__init__.py +4 -0
  4. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/constants.py +3 -0
  5. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/engine.py +11 -1
  6. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/handler.py +5 -5
  7. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/manager.py +25 -4
  8. django_bulk_hooks-0.1.69/django_bulk_hooks/models.py +95 -0
  9. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/pyproject.toml +1 -1
  10. django_bulk_hooks-0.1.67/PKG-INFO +0 -101
  11. django_bulk_hooks-0.1.67/README.md +0 -81
  12. django_bulk_hooks-0.1.67/django_bulk_hooks/__init__.py +0 -4
  13. django_bulk_hooks-0.1.67/django_bulk_hooks/models.py +0 -26
  14. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/LICENSE +0 -0
  15. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/conditions.py +0 -0
  16. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/context.py +0 -0
  17. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/decorators.py +0 -0
  18. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/enums.py +0 -0
  19. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/priority.py +0 -0
  20. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/queryset.py +0 -0
  21. {django_bulk_hooks-0.1.67 → django_bulk_hooks-0.1.69}/django_bulk_hooks/registry.py +0 -0
@@ -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,175 @@
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(Hook):
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
+ ## 🛠 Supported Hook Events
59
+
60
+ - `BEFORE_CREATE`, `AFTER_CREATE`
61
+ - `BEFORE_UPDATE`, `AFTER_UPDATE`
62
+ - `BEFORE_DELETE`, `AFTER_DELETE`
63
+
64
+ ## 🔄 Lifecycle Events
65
+
66
+ ### Individual Model Operations
67
+
68
+ The `HookModelMixin` automatically triggers hooks for individual model operations:
69
+
70
+ ```python
71
+ # These will trigger BEFORE_CREATE and AFTER_CREATE hooks
72
+ account = Account.objects.create(balance=100.00)
73
+ account.save() # for new instances
74
+
75
+ # These will trigger BEFORE_UPDATE and AFTER_UPDATE hooks
76
+ account.balance = 200.00
77
+ account.save() # for existing instances
78
+
79
+ # This will trigger BEFORE_DELETE and AFTER_DELETE hooks
80
+ account.delete()
81
+ ```
82
+
83
+ ### Bulk Operations
84
+
85
+ Bulk operations also trigger the same hooks:
86
+
87
+ ```python
88
+ # Bulk create - triggers BEFORE_CREATE and AFTER_CREATE hooks
89
+ accounts = [
90
+ Account(balance=100.00),
91
+ Account(balance=200.00),
92
+ ]
93
+ Account.objects.bulk_create(accounts)
94
+
95
+ # Bulk update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
96
+ for account in accounts:
97
+ account.balance *= 1.1
98
+ Account.objects.bulk_update(accounts, ['balance'])
99
+
100
+ # Bulk delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
101
+ Account.objects.bulk_delete(accounts)
102
+ ```
103
+
104
+ ### Queryset Operations
105
+
106
+ Queryset operations are also supported:
107
+
108
+ ```python
109
+ # Queryset update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
110
+ Account.objects.update(balance=0.00)
111
+
112
+ # Queryset delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
113
+ Account.objects.delete()
114
+ ```
115
+
116
+ ## 🧠 Why?
117
+
118
+ Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
119
+
120
+ - Hooks that behave consistently across creates/updates/deletes
121
+ - **NEW**: Individual model lifecycle hooks that work with `save()` and `delete()`
122
+ - Scalable performance via chunking (default 200)
123
+ - Support for `@hook` decorators and centralized hook classes
124
+ - **NEW**: Automatic hook triggering for admin operations and other Django features
125
+
126
+ ## 📦 Usage Examples
127
+
128
+ ### Individual Model Operations
129
+
130
+ ```python
131
+ # These automatically trigger hooks
132
+ account = Account.objects.create(balance=100.00)
133
+ account.balance = 200.00
134
+ account.save()
135
+ account.delete()
136
+ ```
137
+
138
+ ### Bulk Operations
139
+
140
+ ```python
141
+ # These also trigger hooks
142
+ Account.objects.bulk_create(accounts)
143
+ Account.objects.bulk_update(accounts, ['balance'])
144
+ Account.objects.bulk_delete(accounts)
145
+ ```
146
+
147
+ ### Advanced Hook Usage
148
+
149
+ ```python
150
+ class AdvancedAccountHooks(Hook):
151
+ @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
152
+ def validate_balance_change(self, new_records, old_records):
153
+ for new_account, old_account in zip(new_records, old_records):
154
+ if new_account.balance < 0 and old_account.balance >= 0:
155
+ raise ValueError("Cannot set negative balance")
156
+
157
+ @hook(AFTER_CREATE, model=Account)
158
+ def send_welcome_email(self, new_records, old_records):
159
+ for account in new_records:
160
+ # Send welcome email logic here
161
+ pass
162
+ ```
163
+
164
+ ## 🧩 Integration with Queryable Properties
165
+
166
+ You can extend from `BulkHookManager` to support formula fields or property querying.
167
+
168
+ ```python
169
+ class MyManager(BulkHookManager, QueryablePropertiesManager):
170
+ pass
171
+ ```
172
+
173
+ ## 📝 License
174
+
175
+ MIT © 2024 Augend / Konrad Beck
@@ -0,0 +1,4 @@
1
+ from django_bulk_hooks.handler import Hook
2
+ from django_bulk_hooks.manager import BulkHookManager
3
+
4
+ __all__ = ["BulkHookManager", "Hook"]
@@ -4,3 +4,6 @@ BEFORE_UPDATE = "before_update"
4
4
  AFTER_UPDATE = "after_update"
5
5
  BEFORE_DELETE = "before_delete"
6
6
  AFTER_DELETE = "after_delete"
7
+ VALIDATE_CREATE = "validate_create"
8
+ VALIDATE_UPDATE = "validate_update"
9
+ VALIDATE_DELETE = "validate_delete"
@@ -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)
@@ -64,7 +64,7 @@ class HookContextState:
64
64
  Hook = HookContextState()
65
65
 
66
66
 
67
- class HookHandlerMeta(type):
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 HookHandlerMeta._registered:
76
+ if key not in HookMeta._registered:
77
77
  logger.info(
78
- "Registering hook via HookHandlerMeta: model=%s, event=%s, handler_cls=%s, method_name=%s",
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
- HookHandlerMeta._registered.add(key)
92
+ HookMeta._registered.add(key)
93
93
  return cls
94
94
 
95
95
 
96
- class HookHandler(metaclass=HookHandlerMeta):
96
+ class Hook(metaclass=HookMeta):
97
97
  @classmethod
98
98
  def handle(
99
99
  cls,
@@ -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 BEFORE_DELETE hooks for %s", model_cls.__name__)
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]
@@ -0,0 +1,95 @@
1
+ from django.db import models, transaction
2
+
3
+ from django_bulk_hooks.constants import (
4
+ AFTER_CREATE,
5
+ AFTER_DELETE,
6
+ AFTER_UPDATE,
7
+ BEFORE_CREATE,
8
+ BEFORE_DELETE,
9
+ BEFORE_UPDATE,
10
+ VALIDATE_CREATE,
11
+ VALIDATE_DELETE,
12
+ VALIDATE_UPDATE,
13
+ )
14
+ from django_bulk_hooks.context import HookContext
15
+ from django_bulk_hooks.engine import run
16
+ from django_bulk_hooks.manager import BulkHookManager
17
+
18
+
19
+ class HookModelMixin(models.Model):
20
+ objects = BulkHookManager()
21
+
22
+ class Meta:
23
+ abstract = True
24
+
25
+ def clean(self):
26
+ """
27
+ Override clean() to trigger validation hooks.
28
+ This ensures that when Django calls clean() (like in admin forms),
29
+ it triggers the VALIDATE_* hooks for validation only.
30
+ """
31
+ super().clean()
32
+
33
+ # Determine if this is a create or update operation
34
+ is_create = self.pk is None
35
+
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)
50
+
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)
78
+
79
+ run(self.__class__, AFTER_CREATE, [self], ctx=ctx)
80
+
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.67"
3
+ version = "0.1.69"
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,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,81 +0,0 @@
1
-
2
- # django-bulk-hooks
3
-
4
- ⚡ Bulk hooks for Django bulk operations.
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.
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
- - Hook chaining, hook deduplication, and atomicity
14
- - Class-based hook handlers with DI support
15
-
16
- ## 🚀 Quickstart
17
-
18
- ```bash
19
- pip install django-bulk-hooks
20
- ```
21
-
22
- ### Define Your Model
23
-
24
- ```python
25
- from django.db import models
26
- from django_bulk_hooks.manager import BulkHookManager
27
-
28
- class Account(models.Model):
29
- balance = models.DecimalField(max_digits=10, decimal_places=2)
30
- objects = BulkHookManager()
31
- ```
32
-
33
- ### Create a Hook Handler
34
-
35
- ```python
36
- from django_bulk_hooks import hook, AFTER_UPDATE, HookHandler
37
- from django_bulk_hooks.conditions import WhenFieldHasChanged
38
- from .models import Account
39
-
40
- class AccountHookHandler(HookHandler):
41
- @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
42
- def log_balance_change(self, new_objs):
43
- print("Accounts updated:", [a.pk for a in new_objs])
44
- ```
45
-
46
- ## 🛠 Supported Hook Events
47
-
48
- - `BEFORE_CREATE`, `AFTER_CREATE`
49
- - `BEFORE_UPDATE`, `AFTER_UPDATE`
50
- - `BEFORE_DELETE`, `AFTER_DELETE`
51
-
52
- ## 🧠 Why?
53
-
54
- Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
55
-
56
- - Hooks that behave consistently across creates/updates/deletes
57
- - Scalable performance via chunking (default 200)
58
- - Support for `@hook` decorators and centralized hook classes
59
-
60
- ## 📦 Usage in Views / Commands
61
-
62
- ```python
63
- # Calls AFTER_UPDATE hooks automatically
64
- Account.objects.bulk_update(accounts, ['balance'])
65
-
66
- # Triggers BEFORE_CREATE and AFTER_CREATE hooks
67
- Account.objects.bulk_create(accounts)
68
- ```
69
-
70
- ## 🧩 Integration with Queryable Properties
71
-
72
- You can extend from `BulkHookManager` to support formula fields or property querying.
73
-
74
- ```python
75
- class MyManager(BulkHookManager, QueryablePropertiesManager):
76
- pass
77
- ```
78
-
79
- ## 📝 License
80
-
81
- MIT © 2024 Augend / Konrad Beck
@@ -1,4 +0,0 @@
1
- from django_bulk_hooks.handler import HookHandler
2
- from django_bulk_hooks.manager import BulkHookManager
3
-
4
- __all__ = ["BulkHookManager", "HookHandler"]
@@ -1,26 +0,0 @@
1
- from django.db import models, transaction
2
-
3
- from django_bulk_hooks.manager import BulkHookManager
4
-
5
-
6
- class HookModelMixin(models.Model):
7
- objects = BulkHookManager()
8
-
9
- class Meta:
10
- abstract = True
11
-
12
- def delete(self, *args, **kwargs):
13
- self.before_delete()
14
-
15
- with transaction.atomic():
16
- result = super().delete(*args, **kwargs)
17
-
18
- self.after_delete()
19
-
20
- return result
21
-
22
- def before_delete(self):
23
- pass
24
-
25
- def after_delete(self):
26
- pass