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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.78
3
+ Version: 0.1.80
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -58,7 +58,7 @@ from django_bulk_hooks import hook, AFTER_UPDATE, Hook
58
58
  from django_bulk_hooks.conditions import WhenFieldHasChanged
59
59
  from .models import Account
60
60
 
61
- class AccountHooks(Hook):
61
+ class AccountHooks(HookHandler):
62
62
  @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
63
63
  def log_balance_change(self, new_records, old_records):
64
64
  print("Accounts updated:", [a.pk for a in new_records])
@@ -74,99 +74,10 @@ class AccountHooks(Hook):
74
74
  print("Accounts deleted:", [a.pk for a in old_records])
75
75
  ```
76
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
77
  ### Advanced Hook Usage
167
78
 
168
79
  ```python
169
- class AdvancedAccountHooks(Hook):
80
+ class AdvancedAccountHooks(HookHandler):
170
81
  @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
171
82
  def validate_balance_change(self, new_records, old_records):
172
83
  for new_account, old_account in zip(new_records, old_records):
@@ -179,17 +90,3 @@ class AdvancedAccountHooks(Hook):
179
90
  # Send welcome email logic here
180
91
  pass
181
92
  ```
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,73 @@
1
+
2
+ # django-bulk-hooks
3
+
4
+ ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
5
+
6
+ `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
7
+
8
+ ## ✨ Features
9
+
10
+ - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
+ - BEFORE/AFTER hooks for create, update, delete
12
+ - Hook-aware manager that wraps Django's `bulk_` operations
13
+ - **NEW**: `HookModelMixin` for individual model lifecycle events
14
+ - Hook chaining, hook deduplication, and atomicity
15
+ - Class-based hook handlers with DI support
16
+ - Support for both bulk and individual model operations
17
+
18
+ ## 🚀 Quickstart
19
+
20
+ ```bash
21
+ pip install django-bulk-hooks
22
+ ```
23
+
24
+ ### Define Your Model
25
+
26
+ ```python
27
+ from django.db import models
28
+ from django_bulk_hooks.models import HookModelMixin
29
+
30
+ class Account(HookModelMixin):
31
+ balance = models.DecimalField(max_digits=10, decimal_places=2)
32
+ # The HookModelMixin automatically provides BulkHookManager
33
+ ```
34
+
35
+ ### Create a Hook Handler
36
+
37
+ ```python
38
+ from django_bulk_hooks import hook, AFTER_UPDATE, Hook
39
+ from django_bulk_hooks.conditions import WhenFieldHasChanged
40
+ from .models import Account
41
+
42
+ class AccountHooks(HookHandler):
43
+ @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
44
+ def log_balance_change(self, new_records, old_records):
45
+ print("Accounts updated:", [a.pk for a in new_records])
46
+
47
+ @hook(BEFORE_CREATE, model=Account)
48
+ def before_create(self, new_records, old_records):
49
+ for account in new_records:
50
+ if account.balance < 0:
51
+ raise ValueError("Account cannot have negative balance")
52
+
53
+ @hook(AFTER_DELETE, model=Account)
54
+ def after_delete(self, new_records, old_records):
55
+ print("Accounts deleted:", [a.pk for a in old_records])
56
+ ```
57
+
58
+ ### Advanced Hook Usage
59
+
60
+ ```python
61
+ class AdvancedAccountHooks(HookHandler):
62
+ @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
63
+ def validate_balance_change(self, new_records, old_records):
64
+ for new_account, old_account in zip(new_records, old_records):
65
+ if new_account.balance < 0 and old_account.balance >= 0:
66
+ raise ValueError("Cannot set negative balance")
67
+
68
+ @hook(AFTER_CREATE, model=Account)
69
+ def send_welcome_email(self, new_records, old_records):
70
+ for account in new_records:
71
+ # Send welcome email logic here
72
+ pass
73
+ ```
@@ -9,9 +9,18 @@ from django_bulk_hooks.constants import (
9
9
  VALIDATE_DELETE,
10
10
  VALIDATE_UPDATE,
11
11
  )
12
+ from django_bulk_hooks.conditions import (
13
+ ChangesTo,
14
+ HasChanged,
15
+ IsEqual,
16
+ IsNotEqual,
17
+ WasEqual,
18
+ )
19
+ from django_bulk_hooks.decorators import hook, select_related
12
20
  from django_bulk_hooks.engine import safe_get_related_object, safe_get_related_attr
13
21
  from django_bulk_hooks.handler import HookHandler
14
22
  from django_bulk_hooks.models import HookModelMixin
23
+ from django_bulk_hooks.enums import Priority
15
24
 
16
25
  __all__ = [
17
26
  "HookHandler",
@@ -27,4 +36,12 @@ __all__ = [
27
36
  "VALIDATE_DELETE",
28
37
  "safe_get_related_object",
29
38
  "safe_get_related_attr",
39
+ "Priority",
40
+ "hook",
41
+ "select_related",
42
+ "ChangesTo",
43
+ "HasChanged",
44
+ "IsEqual",
45
+ "IsNotEqual",
46
+ "WasEqual",
30
47
  ]
@@ -1,4 +1,32 @@
1
- from django_bulk_hooks.engine import safe_get_related_object
1
+ from django.db import models
2
+
3
+
4
+ def safe_get_related_object(instance, field_name):
5
+ """
6
+ Safely get a related object without raising RelatedObjectDoesNotExist.
7
+ Returns None if the foreign key field is None or the related object doesn't exist.
8
+ """
9
+ if not hasattr(instance, field_name):
10
+ return None
11
+
12
+ # Get the foreign key field
13
+ try:
14
+ field = instance._meta.get_field(field_name)
15
+ if not field.is_relation or field.many_to_many or field.one_to_many:
16
+ return getattr(instance, field_name, None)
17
+ except models.FieldDoesNotExist:
18
+ return getattr(instance, field_name, None)
19
+
20
+ # Check if the foreign key field is None
21
+ fk_field_name = f"{field_name}_id"
22
+ if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
23
+ return None
24
+
25
+ # Try to get the related object, but catch RelatedObjectDoesNotExist
26
+ try:
27
+ return getattr(instance, field_name)
28
+ except field.related_model.RelatedObjectDoesNotExist:
29
+ return None
2
30
 
3
31
 
4
32
  def resolve_dotted_attr(instance, dotted_path):
@@ -3,38 +3,11 @@ import logging
3
3
  from django.core.exceptions import ValidationError
4
4
  from django.db import models
5
5
  from django_bulk_hooks.registry import get_hooks
6
+ from django_bulk_hooks.conditions import safe_get_related_object
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
9
10
 
10
- def safe_get_related_object(instance, field_name):
11
- """
12
- Safely get a related object without raising RelatedObjectDoesNotExist.
13
- Returns None if the foreign key field is None or the related object doesn't exist.
14
- """
15
- if not hasattr(instance, field_name):
16
- return None
17
-
18
- # Get the foreign key field
19
- try:
20
- field = instance._meta.get_field(field_name)
21
- if not field.is_relation or field.many_to_many or field.one_to_many:
22
- return getattr(instance, field_name, None)
23
- except models.FieldDoesNotExist:
24
- return getattr(instance, field_name, None)
25
-
26
- # Check if the foreign key field is None
27
- fk_field_name = f"{field_name}_id"
28
- if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
29
- return None
30
-
31
- # Try to get the related object, but catch RelatedObjectDoesNotExist
32
- try:
33
- return getattr(instance, field_name)
34
- except field.related_model.RelatedObjectDoesNotExist:
35
- return None
36
-
37
-
38
11
  def safe_get_related_attr(instance, field_name, attr_name=None):
39
12
  """
40
13
  Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
@@ -86,7 +86,7 @@ class HookMeta(type):
86
86
  return cls
87
87
 
88
88
 
89
- class Hook(metaclass=HookMeta):
89
+ class HookHandler(metaclass=HookMeta):
90
90
  @classmethod
91
91
  def handle(
92
92
  cls,
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Union
3
3
 
4
- from django_bulk_hooks.priority import Priority
4
+ from django_bulk_hooks.enums import Priority
5
5
 
6
6
  _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
7
 
@@ -17,8 +17,7 @@ def register_hook(
17
17
 
18
18
 
19
19
  def get_hooks(model, event):
20
- hooks = _hooks.get((model, event), [])
21
- return hooks
20
+ return _hooks.get((model, event), [])
22
21
 
23
22
 
24
23
  def list_all_hooks():
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.78"
3
+ version = "0.1.80"
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,175 +0,0 @@
1
-
2
- # django-bulk-hooks
3
-
4
- ⚡ Bulk hooks for Django bulk operations and individual model lifecycle events.
5
-
6
- `django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety. It also provides comprehensive lifecycle hooks for individual model operations.
7
-
8
- ## ✨ Features
9
-
10
- - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
- - BEFORE/AFTER hooks for create, update, delete
12
- - Hook-aware manager that wraps Django's `bulk_` operations
13
- - **NEW**: `HookModelMixin` for individual model lifecycle events
14
- - Hook chaining, hook deduplication, and atomicity
15
- - Class-based hook handlers with DI support
16
- - Support for both bulk and individual model operations
17
-
18
- ## 🚀 Quickstart
19
-
20
- ```bash
21
- pip install django-bulk-hooks
22
- ```
23
-
24
- ### Define Your Model
25
-
26
- ```python
27
- from django.db import models
28
- from django_bulk_hooks.models import HookModelMixin
29
-
30
- class Account(HookModelMixin):
31
- balance = models.DecimalField(max_digits=10, decimal_places=2)
32
- # The HookModelMixin automatically provides BulkHookManager
33
- ```
34
-
35
- ### Create a Hook Handler
36
-
37
- ```python
38
- from django_bulk_hooks import hook, AFTER_UPDATE, Hook
39
- from django_bulk_hooks.conditions import WhenFieldHasChanged
40
- from .models import Account
41
-
42
- class AccountHooks(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
@@ -1,16 +0,0 @@
1
- from enum import IntEnum
2
-
3
-
4
- class Priority(IntEnum):
5
- """
6
- Named priorities for django-bulk-hooks hooks.
7
-
8
- Lower values run earlier (higher priority).
9
- Hooks are sorted in ascending order.
10
- """
11
-
12
- HIGHEST = 0 # runs first
13
- HIGH = 25 # runs early
14
- NORMAL = 50 # default ordering
15
- LOW = 75 # runs later
16
- LOWEST = 100 # runs last