django-bulk-hooks 0.2.9__py3-none-any.whl → 0.2.93__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.
- django_bulk_hooks/__init__.py +20 -27
- django_bulk_hooks/changeset.py +214 -230
- django_bulk_hooks/conditions.py +12 -12
- django_bulk_hooks/decorators.py +68 -26
- django_bulk_hooks/dispatcher.py +369 -58
- django_bulk_hooks/factory.py +541 -565
- django_bulk_hooks/handler.py +106 -115
- django_bulk_hooks/helpers.py +258 -99
- django_bulk_hooks/manager.py +134 -130
- django_bulk_hooks/models.py +89 -76
- django_bulk_hooks/operations/__init__.py +5 -5
- django_bulk_hooks/operations/analyzer.py +299 -172
- django_bulk_hooks/operations/bulk_executor.py +742 -437
- django_bulk_hooks/operations/coordinator.py +928 -472
- django_bulk_hooks/operations/field_utils.py +335 -0
- django_bulk_hooks/operations/mti_handler.py +696 -473
- django_bulk_hooks/operations/mti_plans.py +103 -87
- django_bulk_hooks/operations/record_classifier.py +196 -0
- django_bulk_hooks/queryset.py +233 -189
- django_bulk_hooks/registry.py +276 -288
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/METADATA +55 -4
- django_bulk_hooks-0.2.93.dist-info/RECORD +27 -0
- django_bulk_hooks/debug_utils.py +0 -145
- django_bulk_hooks-0.2.9.dist-info/RECORD +0 -26
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.93.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,189 +1,233 @@
|
|
|
1
|
-
"""
|
|
2
|
-
HookQuerySet - Django QuerySet with hook support.
|
|
3
|
-
|
|
4
|
-
This is a thin coordinator that delegates all complex logic to services.
|
|
5
|
-
It follows the Facade pattern, providing a simple interface over the
|
|
6
|
-
complex coordination required for bulk operations with hooks.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
bypass_hooks
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
1
|
+
"""
|
|
2
|
+
HookQuerySet - Django QuerySet with hook support.
|
|
3
|
+
|
|
4
|
+
This is a thin coordinator that delegates all complex logic to services.
|
|
5
|
+
It follows the Facade pattern, providing a simple interface over the
|
|
6
|
+
complex coordination required for bulk operations with hooks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from django.db import models
|
|
12
|
+
from django.db import transaction
|
|
13
|
+
|
|
14
|
+
from django_bulk_hooks.helpers import extract_pks
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HookQuerySet(models.QuerySet):
|
|
20
|
+
"""
|
|
21
|
+
QuerySet with hook support.
|
|
22
|
+
|
|
23
|
+
This is a thin facade over BulkOperationCoordinator. It provides
|
|
24
|
+
backward-compatible API for Django's QuerySet while integrating
|
|
25
|
+
the full hook lifecycle.
|
|
26
|
+
|
|
27
|
+
Key design principles:
|
|
28
|
+
- Minimal logic (< 10 lines per method)
|
|
29
|
+
- No business logic (delegate to coordinator)
|
|
30
|
+
- No conditionals (let services handle it)
|
|
31
|
+
- Transaction boundaries only
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
self._coordinator = None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def with_hooks(cls, queryset):
|
|
40
|
+
"""
|
|
41
|
+
Apply hook functionality to any queryset.
|
|
42
|
+
|
|
43
|
+
This enables hooks to work with any manager by applying hook
|
|
44
|
+
capabilities at the queryset level rather than through inheritance.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
queryset: Any Django QuerySet instance
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
HookQuerySet instance with the same query parameters
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(queryset, cls):
|
|
53
|
+
return queryset # Already has hooks
|
|
54
|
+
|
|
55
|
+
# Create a new HookQuerySet with the same parameters as the original queryset
|
|
56
|
+
hook_qs = cls(
|
|
57
|
+
model=queryset.model,
|
|
58
|
+
query=queryset.query,
|
|
59
|
+
using=queryset._db,
|
|
60
|
+
hints=getattr(queryset, '_hints', {}),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Preserve any additional attributes from the original queryset
|
|
64
|
+
# This allows composition with other queryset enhancements
|
|
65
|
+
cls._preserve_queryset_attributes(hook_qs, queryset)
|
|
66
|
+
|
|
67
|
+
return hook_qs
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def _preserve_queryset_attributes(cls, hook_qs, original_qs):
|
|
71
|
+
"""
|
|
72
|
+
Preserve attributes from the original queryset.
|
|
73
|
+
|
|
74
|
+
This enables composition with other queryset enhancements like
|
|
75
|
+
queryable properties, annotations, etc.
|
|
76
|
+
"""
|
|
77
|
+
# Copy non-method attributes that might be set by other managers
|
|
78
|
+
for attr_name in dir(original_qs):
|
|
79
|
+
if (not attr_name.startswith('_') and
|
|
80
|
+
not hasattr(cls, attr_name) and
|
|
81
|
+
not callable(getattr(original_qs, attr_name, None))):
|
|
82
|
+
try:
|
|
83
|
+
value = getattr(original_qs, attr_name)
|
|
84
|
+
setattr(hook_qs, attr_name, value)
|
|
85
|
+
except (AttributeError, TypeError):
|
|
86
|
+
# Skip attributes that can't be copied
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def coordinator(self):
|
|
91
|
+
"""Lazy initialization of coordinator"""
|
|
92
|
+
if self._coordinator is None:
|
|
93
|
+
from django_bulk_hooks.operations import BulkOperationCoordinator
|
|
94
|
+
|
|
95
|
+
self._coordinator = BulkOperationCoordinator(self)
|
|
96
|
+
return self._coordinator
|
|
97
|
+
|
|
98
|
+
@transaction.atomic
|
|
99
|
+
def bulk_create(
|
|
100
|
+
self,
|
|
101
|
+
objs,
|
|
102
|
+
batch_size=None,
|
|
103
|
+
ignore_conflicts=False,
|
|
104
|
+
update_conflicts=False,
|
|
105
|
+
update_fields=None,
|
|
106
|
+
unique_fields=None,
|
|
107
|
+
bypass_hooks=False,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Create multiple objects with hook support.
|
|
111
|
+
|
|
112
|
+
This is the public API - delegates to coordinator.
|
|
113
|
+
"""
|
|
114
|
+
return self.coordinator.create(
|
|
115
|
+
objs=objs,
|
|
116
|
+
batch_size=batch_size,
|
|
117
|
+
ignore_conflicts=ignore_conflicts,
|
|
118
|
+
update_conflicts=update_conflicts,
|
|
119
|
+
update_fields=update_fields,
|
|
120
|
+
unique_fields=unique_fields,
|
|
121
|
+
bypass_hooks=bypass_hooks,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@transaction.atomic
|
|
125
|
+
def bulk_update(
|
|
126
|
+
self,
|
|
127
|
+
objs,
|
|
128
|
+
fields=None,
|
|
129
|
+
batch_size=None,
|
|
130
|
+
bypass_hooks=False,
|
|
131
|
+
**kwargs,
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
Update multiple objects with hook support.
|
|
135
|
+
|
|
136
|
+
This is the public API - delegates to coordinator.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
objs: List of model instances to update
|
|
140
|
+
fields: List of field names to update (optional, will auto-detect if None)
|
|
141
|
+
batch_size: Number of objects per batch
|
|
142
|
+
bypass_hooks: Skip all hooks if True
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Number of objects updated
|
|
146
|
+
"""
|
|
147
|
+
# If fields is None, auto-detect changed fields using analyzer
|
|
148
|
+
if fields is None:
|
|
149
|
+
fields = self.coordinator.analyzer.detect_changed_fields(objs)
|
|
150
|
+
if not fields:
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
return self.coordinator.update(
|
|
154
|
+
objs=objs,
|
|
155
|
+
fields=fields,
|
|
156
|
+
batch_size=batch_size,
|
|
157
|
+
bypass_hooks=bypass_hooks,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@transaction.atomic
|
|
161
|
+
def update(self, bypass_hooks=False, **kwargs):
|
|
162
|
+
"""
|
|
163
|
+
Update QuerySet with hook support.
|
|
164
|
+
|
|
165
|
+
This is the public API - delegates to coordinator.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
bypass_hooks: Skip all hooks if True
|
|
169
|
+
**kwargs: Fields to update
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Number of objects updated
|
|
173
|
+
"""
|
|
174
|
+
return self.coordinator.update_queryset(
|
|
175
|
+
update_kwargs=kwargs,
|
|
176
|
+
bypass_hooks=bypass_hooks,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@transaction.atomic
|
|
180
|
+
def bulk_delete(
|
|
181
|
+
self,
|
|
182
|
+
objs,
|
|
183
|
+
bypass_hooks=False,
|
|
184
|
+
**kwargs,
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
Delete multiple objects with hook support.
|
|
188
|
+
|
|
189
|
+
This is the public API - delegates to coordinator.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
objs: List of objects to delete
|
|
193
|
+
bypass_hooks: Skip all hooks if True
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Tuple of (count, details dict)
|
|
197
|
+
"""
|
|
198
|
+
# Filter queryset to only these objects
|
|
199
|
+
pks = extract_pks(objs)
|
|
200
|
+
if not pks:
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
# Create a filtered queryset
|
|
204
|
+
filtered_qs = self.filter(pk__in=pks)
|
|
205
|
+
|
|
206
|
+
# Use coordinator with the filtered queryset
|
|
207
|
+
from django_bulk_hooks.operations import BulkOperationCoordinator
|
|
208
|
+
|
|
209
|
+
coordinator = BulkOperationCoordinator(filtered_qs)
|
|
210
|
+
|
|
211
|
+
count, details = coordinator.delete(
|
|
212
|
+
bypass_hooks=bypass_hooks,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# For bulk_delete, return just the count to match Django's behavior
|
|
216
|
+
return count
|
|
217
|
+
|
|
218
|
+
@transaction.atomic
|
|
219
|
+
def delete(self, bypass_hooks=False):
|
|
220
|
+
"""
|
|
221
|
+
Delete QuerySet with hook support.
|
|
222
|
+
|
|
223
|
+
This is the public API - delegates to coordinator.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
bypass_hooks: Skip all hooks if True
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (count, details dict)
|
|
230
|
+
"""
|
|
231
|
+
return self.coordinator.delete(
|
|
232
|
+
bypass_hooks=bypass_hooks,
|
|
233
|
+
)
|