django-bulk-hooks 0.1.102__tar.gz → 0.1.103__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 (24) hide show
  1. django_bulk_hooks-0.1.103/PKG-INFO +217 -0
  2. django_bulk_hooks-0.1.103/README.md +197 -0
  3. django_bulk_hooks-0.1.103/django_bulk_hooks/__init__.py +4 -0
  4. django_bulk_hooks-0.1.103/django_bulk_hooks/conditions.py +181 -0
  5. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/decorators.py +3 -48
  6. django_bulk_hooks-0.1.103/django_bulk_hooks/engine.py +43 -0
  7. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/handler.py +1 -1
  8. django_bulk_hooks-0.1.103/django_bulk_hooks/manager.py +203 -0
  9. django_bulk_hooks-0.1.103/django_bulk_hooks/models.py +95 -0
  10. django_bulk_hooks-0.1.103/django_bulk_hooks/priority.py +16 -0
  11. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/queryset.py +3 -2
  12. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/registry.py +3 -2
  13. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/pyproject.toml +1 -1
  14. django_bulk_hooks-0.1.102/PKG-INFO +0 -228
  15. django_bulk_hooks-0.1.102/README.md +0 -209
  16. django_bulk_hooks-0.1.102/django_bulk_hooks/__init__.py +0 -50
  17. django_bulk_hooks-0.1.102/django_bulk_hooks/conditions.py +0 -376
  18. django_bulk_hooks-0.1.102/django_bulk_hooks/engine.py +0 -71
  19. django_bulk_hooks-0.1.102/django_bulk_hooks/manager.py +0 -334
  20. django_bulk_hooks-0.1.102/django_bulk_hooks/models.py +0 -126
  21. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/LICENSE +0 -0
  22. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/constants.py +0 -0
  23. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/context.py +0 -0
  24. {django_bulk_hooks-0.1.102 → django_bulk_hooks-0.1.103}/django_bulk_hooks/enums.py +0 -0
@@ -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,197 @@
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
+ - **NEW**: Proper ordering guarantees for old/new record pairing in hooks (Salesforce-like behavior)
126
+
127
+ ## 📦 Usage Examples
128
+
129
+ ### Individual Model Operations
130
+
131
+ ```python
132
+ # These automatically trigger hooks
133
+ account = Account.objects.create(balance=100.00)
134
+ account.balance = 200.00
135
+ account.save()
136
+ account.delete()
137
+ ```
138
+
139
+ ### Bulk Operations
140
+
141
+ ```python
142
+ # These also trigger hooks
143
+ Account.objects.bulk_create(accounts)
144
+ Account.objects.bulk_update(accounts, ['balance'])
145
+ Account.objects.bulk_delete(accounts)
146
+ ```
147
+
148
+ ### Advanced Hook Usage
149
+
150
+ ```python
151
+ class AdvancedAccountHooks(Hook):
152
+ @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
153
+ def validate_balance_change(self, new_records, old_records):
154
+ for new_account, old_account in zip(new_records, old_records):
155
+ if new_account.balance < 0 and old_account.balance >= 0:
156
+ raise ValueError("Cannot set negative balance")
157
+
158
+ @hook(AFTER_CREATE, model=Account)
159
+ def send_welcome_email(self, new_records, old_records):
160
+ for account in new_records:
161
+ # Send welcome email logic here
162
+ pass
163
+ ```
164
+
165
+ ### Salesforce-like Ordering Guarantees
166
+
167
+ 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:
168
+
169
+ ```python
170
+ class LoanAccountHooks(Hook):
171
+ @hook(BEFORE_UPDATE, model=LoanAccount)
172
+ def validate_account_number(self, new_records, old_records):
173
+ # old_records[i] always corresponds to new_records[i]
174
+ for new_account, old_account in zip(new_records, old_records):
175
+ if old_account.account_number != new_account.account_number:
176
+ raise ValidationError("Account number cannot be changed")
177
+
178
+ # This works correctly even with reordered objects:
179
+ accounts = [account1, account2, account3] # IDs: 1, 2, 3
180
+ reordered = [account3, account1, account2] # IDs: 3, 1, 2
181
+
182
+ # The hook will still receive properly paired old/new records
183
+ LoanAccount.objects.bulk_update(reordered, ['balance'])
184
+ ```
185
+
186
+ ## 🧩 Integration with Queryable Properties
187
+
188
+ You can extend from `BulkHookManager` to support formula fields or property querying.
189
+
190
+ ```python
191
+ class MyManager(BulkHookManager, QueryablePropertiesManager):
192
+ pass
193
+ ```
194
+
195
+ ## 📝 License
196
+
197
+ 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"]
@@ -0,0 +1,181 @@
1
+ def resolve_dotted_attr(instance, dotted_path):
2
+ """
3
+ Recursively resolve a dotted attribute path, e.g., "type.category".
4
+ """
5
+ for attr in dotted_path.split("."):
6
+ if instance is None:
7
+ return None
8
+ instance = getattr(instance, attr, None)
9
+ return instance
10
+
11
+
12
+ class HookCondition:
13
+ def check(self, instance, original_instance=None):
14
+ raise NotImplementedError
15
+
16
+ def __call__(self, instance, original_instance=None):
17
+ return self.check(instance, original_instance)
18
+
19
+ def __and__(self, other):
20
+ return AndCondition(self, other)
21
+
22
+ def __or__(self, other):
23
+ return OrCondition(self, other)
24
+
25
+ def __invert__(self):
26
+ return NotCondition(self)
27
+
28
+
29
+ class IsNotEqual(HookCondition):
30
+ def __init__(self, field, value, only_on_change=False):
31
+ self.field = field
32
+ self.value = value
33
+ self.only_on_change = only_on_change
34
+
35
+ def check(self, instance, original_instance=None):
36
+ current = resolve_dotted_attr(instance, self.field)
37
+ if self.only_on_change:
38
+ if original_instance is None:
39
+ return False
40
+ previous = resolve_dotted_attr(original_instance, self.field)
41
+ return previous == self.value and current != self.value
42
+ else:
43
+ return current != self.value
44
+
45
+
46
+ class IsEqual(HookCondition):
47
+ def __init__(self, field, value, only_on_change=False):
48
+ self.field = field
49
+ self.value = value
50
+ self.only_on_change = only_on_change
51
+
52
+ def check(self, instance, original_instance=None):
53
+ current = resolve_dotted_attr(instance, self.field)
54
+ if self.only_on_change:
55
+ if original_instance is None:
56
+ return False
57
+ previous = resolve_dotted_attr(original_instance, self.field)
58
+ return previous != self.value and current == self.value
59
+ else:
60
+ return current == self.value
61
+
62
+
63
+ class HasChanged(HookCondition):
64
+ def __init__(self, field, has_changed=True):
65
+ self.field = field
66
+ self.has_changed = has_changed
67
+
68
+ def check(self, instance, original_instance=None):
69
+ if not original_instance:
70
+ return False
71
+ current = resolve_dotted_attr(instance, self.field)
72
+ previous = resolve_dotted_attr(original_instance, self.field)
73
+ return (current != previous) == self.has_changed
74
+
75
+
76
+ class WasEqual(HookCondition):
77
+ def __init__(self, field, value, only_on_change=False):
78
+ """
79
+ Check if a field's original value was `value`.
80
+ If only_on_change is True, only return True when the field has changed away from that value.
81
+ """
82
+ self.field = field
83
+ self.value = value
84
+ self.only_on_change = only_on_change
85
+
86
+ def check(self, instance, original_instance=None):
87
+ if original_instance is None:
88
+ return False
89
+ previous = resolve_dotted_attr(original_instance, self.field)
90
+ if self.only_on_change:
91
+ current = resolve_dotted_attr(instance, self.field)
92
+ return previous == self.value and current != self.value
93
+ else:
94
+ return previous == self.value
95
+
96
+
97
+ class ChangesTo(HookCondition):
98
+ def __init__(self, field, value):
99
+ """
100
+ Check if a field's value has changed to `value`.
101
+ Only returns True when original value != value and current value == value.
102
+ """
103
+ self.field = field
104
+ self.value = value
105
+
106
+ def check(self, instance, original_instance=None):
107
+ if original_instance is None:
108
+ return False
109
+ previous = resolve_dotted_attr(original_instance, self.field)
110
+ current = resolve_dotted_attr(instance, self.field)
111
+ return previous != self.value and current == self.value
112
+
113
+
114
+ class IsGreaterThan(HookCondition):
115
+ def __init__(self, field, value):
116
+ self.field = field
117
+ self.value = value
118
+
119
+ def check(self, instance, original_instance=None):
120
+ current = resolve_dotted_attr(instance, self.field)
121
+ return current is not None and current > self.value
122
+
123
+
124
+ class IsGreaterThanOrEqual(HookCondition):
125
+ def __init__(self, field, value):
126
+ self.field = field
127
+ self.value = value
128
+
129
+ def check(self, instance, original_instance=None):
130
+ current = resolve_dotted_attr(instance, self.field)
131
+ return current is not None and current >= self.value
132
+
133
+
134
+ class IsLessThan(HookCondition):
135
+ def __init__(self, field, value):
136
+ self.field = field
137
+ self.value = value
138
+
139
+ def check(self, instance, original_instance=None):
140
+ current = resolve_dotted_attr(instance, self.field)
141
+ return current is not None and current < self.value
142
+
143
+
144
+ class IsLessThanOrEqual(HookCondition):
145
+ def __init__(self, field, value):
146
+ self.field = field
147
+ self.value = value
148
+
149
+ def check(self, instance, original_instance=None):
150
+ current = resolve_dotted_attr(instance, self.field)
151
+ return current is not None and current <= self.value
152
+
153
+
154
+ class AndCondition(HookCondition):
155
+ def __init__(self, cond1, cond2):
156
+ self.cond1 = cond1
157
+ self.cond2 = cond2
158
+
159
+ def check(self, instance, original_instance=None):
160
+ return self.cond1.check(instance, original_instance) and self.cond2.check(
161
+ instance, original_instance
162
+ )
163
+
164
+
165
+ class OrCondition(HookCondition):
166
+ def __init__(self, cond1, cond2):
167
+ self.cond1 = cond1
168
+ self.cond2 = cond2
169
+
170
+ def check(self, instance, original_instance=None):
171
+ return self.cond1.check(instance, original_instance) or self.cond2.check(
172
+ instance, original_instance
173
+ )
174
+
175
+
176
+ class NotCondition(HookCondition):
177
+ def __init__(self, cond):
178
+ self.cond = cond
179
+
180
+ def check(self, instance, original_instance=None):
181
+ return not self.cond.check(instance, original_instance)