django-bulk-hooks 0.1.83__py3-none-any.whl → 0.2.100__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 +53 -50
- django_bulk_hooks/changeset.py +214 -0
- django_bulk_hooks/conditions.py +230 -351
- django_bulk_hooks/constants.py +4 -0
- django_bulk_hooks/context.py +49 -9
- django_bulk_hooks/decorators.py +219 -96
- django_bulk_hooks/dispatcher.py +588 -0
- django_bulk_hooks/factory.py +541 -0
- django_bulk_hooks/handler.py +106 -167
- django_bulk_hooks/helpers.py +258 -0
- django_bulk_hooks/manager.py +134 -208
- django_bulk_hooks/models.py +89 -101
- django_bulk_hooks/operations/__init__.py +18 -0
- django_bulk_hooks/operations/analyzer.py +466 -0
- django_bulk_hooks/operations/bulk_executor.py +757 -0
- django_bulk_hooks/operations/coordinator.py +928 -0
- django_bulk_hooks/operations/field_utils.py +341 -0
- django_bulk_hooks/operations/mti_handler.py +696 -0
- django_bulk_hooks/operations/mti_plans.py +103 -0
- django_bulk_hooks/operations/record_classifier.py +196 -0
- django_bulk_hooks/queryset.py +233 -43
- django_bulk_hooks/registry.py +276 -25
- django_bulk_hooks-0.2.100.dist-info/METADATA +320 -0
- django_bulk_hooks-0.2.100.dist-info/RECORD +27 -0
- django_bulk_hooks/engine.py +0 -53
- django_bulk_hooks-0.1.83.dist-info/METADATA +0 -228
- django_bulk_hooks-0.1.83.dist-info/RECORD +0 -16
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.83.dist-info → django_bulk_hooks-0.2.100.dist-info}/WHEEL +0 -0
django_bulk_hooks/manager.py
CHANGED
|
@@ -1,208 +1,134 @@
|
|
|
1
|
-
from django.db import models
|
|
2
|
-
|
|
3
|
-
from django_bulk_hooks import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
137
|
-
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
138
|
-
result.extend(super(models.Manager, self).bulk_create(chunk, **kwargs))
|
|
139
|
-
|
|
140
|
-
if not bypass_hooks:
|
|
141
|
-
engine.run(model_cls, AFTER_CREATE, result, ctx=ctx)
|
|
142
|
-
|
|
143
|
-
return result
|
|
144
|
-
|
|
145
|
-
@transaction.atomic
|
|
146
|
-
def bulk_delete(
|
|
147
|
-
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
148
|
-
):
|
|
149
|
-
if not objs:
|
|
150
|
-
return []
|
|
151
|
-
|
|
152
|
-
model_cls = self.model
|
|
153
|
-
|
|
154
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
155
|
-
raise TypeError(
|
|
156
|
-
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
ctx = HookContext(model_cls)
|
|
160
|
-
|
|
161
|
-
if not bypass_hooks:
|
|
162
|
-
# Run validation hooks first
|
|
163
|
-
if not bypass_validation:
|
|
164
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
165
|
-
|
|
166
|
-
# Then run business logic hooks
|
|
167
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
168
|
-
|
|
169
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
170
|
-
|
|
171
|
-
# Use base manager for the actual deletion to prevent recursion
|
|
172
|
-
# The hooks have already been fired above, so we don't need them again
|
|
173
|
-
model_cls._base_manager.filter(pk__in=pks).delete()
|
|
174
|
-
|
|
175
|
-
if not bypass_hooks:
|
|
176
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
177
|
-
|
|
178
|
-
return objs
|
|
179
|
-
|
|
180
|
-
@transaction.atomic
|
|
181
|
-
def update(self, **kwargs):
|
|
182
|
-
objs = list(self.all())
|
|
183
|
-
if not objs:
|
|
184
|
-
return 0
|
|
185
|
-
for key, value in kwargs.items():
|
|
186
|
-
for obj in objs:
|
|
187
|
-
setattr(obj, key, value)
|
|
188
|
-
self.bulk_update(objs, fields=list(kwargs.keys()))
|
|
189
|
-
return len(objs)
|
|
190
|
-
|
|
191
|
-
@transaction.atomic
|
|
192
|
-
def delete(self):
|
|
193
|
-
objs = list(self.all())
|
|
194
|
-
if not objs:
|
|
195
|
-
return 0
|
|
196
|
-
self.bulk_delete(objs)
|
|
197
|
-
return len(objs)
|
|
198
|
-
|
|
199
|
-
@transaction.atomic
|
|
200
|
-
def save(self, obj):
|
|
201
|
-
if obj.pk:
|
|
202
|
-
self.bulk_update(
|
|
203
|
-
[obj],
|
|
204
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
205
|
-
)
|
|
206
|
-
else:
|
|
207
|
-
self.bulk_create([obj])
|
|
208
|
-
return obj
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from django_bulk_hooks.queryset import HookQuerySet
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _delegate_to_queryset(self, method_name, *args, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Generic delegation to queryset method.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
method_name: Name of the method to call on the queryset
|
|
12
|
+
*args, **kwargs: Arguments to pass to the method
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Result of the queryset method call
|
|
16
|
+
"""
|
|
17
|
+
return getattr(self.get_queryset(), method_name)(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BulkHookManager(models.Manager):
|
|
21
|
+
"""
|
|
22
|
+
Manager that provides hook-aware bulk operations.
|
|
23
|
+
|
|
24
|
+
This manager automatically applies hook functionality to its querysets.
|
|
25
|
+
It can be used as a base class or composed with other managers using
|
|
26
|
+
the queryset-based approach.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get_queryset(self):
|
|
30
|
+
"""
|
|
31
|
+
Return a HookQuerySet for this manager.
|
|
32
|
+
|
|
33
|
+
Uses the new with_hooks() method for better composition with other managers.
|
|
34
|
+
"""
|
|
35
|
+
base_queryset = super().get_queryset()
|
|
36
|
+
return HookQuerySet.with_hooks(base_queryset)
|
|
37
|
+
|
|
38
|
+
def bulk_create(
|
|
39
|
+
self,
|
|
40
|
+
objs,
|
|
41
|
+
batch_size=None,
|
|
42
|
+
ignore_conflicts=False,
|
|
43
|
+
update_conflicts=False,
|
|
44
|
+
update_fields=None,
|
|
45
|
+
unique_fields=None,
|
|
46
|
+
bypass_hooks=False,
|
|
47
|
+
**kwargs,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Delegate to QuerySet's bulk_create implementation.
|
|
51
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
52
|
+
"""
|
|
53
|
+
return _delegate_to_queryset(
|
|
54
|
+
self,
|
|
55
|
+
"bulk_create",
|
|
56
|
+
objs,
|
|
57
|
+
batch_size=batch_size,
|
|
58
|
+
ignore_conflicts=ignore_conflicts,
|
|
59
|
+
update_conflicts=update_conflicts,
|
|
60
|
+
update_fields=update_fields,
|
|
61
|
+
unique_fields=unique_fields,
|
|
62
|
+
bypass_hooks=bypass_hooks,
|
|
63
|
+
**kwargs,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def bulk_update(
|
|
67
|
+
self,
|
|
68
|
+
objs,
|
|
69
|
+
fields=None,
|
|
70
|
+
bypass_hooks=False,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Delegate to QuerySet's bulk_update implementation.
|
|
75
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
76
|
+
|
|
77
|
+
Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
|
|
78
|
+
are not supported by bulk_update and will be ignored with a warning.
|
|
79
|
+
These parameters are only available in bulk_create for UPSERT operations.
|
|
80
|
+
"""
|
|
81
|
+
if fields is not None:
|
|
82
|
+
kwargs["fields"] = fields
|
|
83
|
+
return _delegate_to_queryset(
|
|
84
|
+
self,
|
|
85
|
+
"bulk_update",
|
|
86
|
+
objs,
|
|
87
|
+
bypass_hooks=bypass_hooks,
|
|
88
|
+
**kwargs,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def bulk_delete(
|
|
92
|
+
self,
|
|
93
|
+
objs,
|
|
94
|
+
batch_size=None,
|
|
95
|
+
bypass_hooks=False,
|
|
96
|
+
**kwargs,
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Delegate to QuerySet's bulk_delete implementation.
|
|
100
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
101
|
+
"""
|
|
102
|
+
return _delegate_to_queryset(
|
|
103
|
+
self,
|
|
104
|
+
"bulk_delete",
|
|
105
|
+
objs,
|
|
106
|
+
batch_size=batch_size,
|
|
107
|
+
bypass_hooks=bypass_hooks,
|
|
108
|
+
**kwargs,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def delete(self, bypass_hooks=False):
|
|
112
|
+
"""
|
|
113
|
+
Delegate to QuerySet's delete implementation.
|
|
114
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
115
|
+
"""
|
|
116
|
+
return _delegate_to_queryset(self, "delete", bypass_hooks=bypass_hooks)
|
|
117
|
+
|
|
118
|
+
def update(self, bypass_hooks=False, **kwargs):
|
|
119
|
+
"""
|
|
120
|
+
Delegate to QuerySet's update implementation.
|
|
121
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
122
|
+
"""
|
|
123
|
+
return _delegate_to_queryset(self, "update", bypass_hooks=bypass_hooks, **kwargs)
|
|
124
|
+
|
|
125
|
+
def save(self, obj, bypass_hooks=False):
|
|
126
|
+
"""
|
|
127
|
+
Save a single object using the appropriate bulk operation.
|
|
128
|
+
"""
|
|
129
|
+
if obj.pk:
|
|
130
|
+
# bulk_update now auto-detects changed fields
|
|
131
|
+
self.bulk_update([obj], bypass_hooks=bypass_hooks)
|
|
132
|
+
else:
|
|
133
|
+
self.bulk_create([obj], bypass_hooks=bypass_hooks)
|
|
134
|
+
return obj
|
django_bulk_hooks/models.py
CHANGED
|
@@ -1,101 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
ctx = HookContext(self.__class__)
|
|
91
|
-
|
|
92
|
-
# Run validation hooks first
|
|
93
|
-
run(self.__class__, VALIDATE_DELETE, [self], ctx=ctx)
|
|
94
|
-
|
|
95
|
-
# Then run business logic hooks
|
|
96
|
-
run(self.__class__, BEFORE_DELETE, [self], ctx=ctx)
|
|
97
|
-
|
|
98
|
-
result = super().delete(*args, **kwargs)
|
|
99
|
-
|
|
100
|
-
run(self.__class__, AFTER_DELETE, [self], ctx=ctx)
|
|
101
|
-
return result
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HookModelMixin(models.Model):
|
|
11
|
+
"""Mixin providing hook functionality."""
|
|
12
|
+
|
|
13
|
+
objects = BulkHookManager()
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
abstract = True
|
|
17
|
+
|
|
18
|
+
def clean(self, bypass_hooks=False):
|
|
19
|
+
"""
|
|
20
|
+
Override clean() to hook validation hooks.
|
|
21
|
+
This ensures that when Django calls clean() (like in admin forms),
|
|
22
|
+
it hooks the VALIDATE_* hooks for validation only.
|
|
23
|
+
"""
|
|
24
|
+
super().clean()
|
|
25
|
+
|
|
26
|
+
# If bypass_hooks is True, skip validation hooks
|
|
27
|
+
if bypass_hooks:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# Delegate to coordinator (consistent with save/delete)
|
|
31
|
+
is_create = self.pk is None
|
|
32
|
+
self.__class__.objects.get_queryset().coordinator.clean(
|
|
33
|
+
[self],
|
|
34
|
+
is_create=is_create,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def save(self, *args, bypass_hooks=False, **kwargs):
|
|
38
|
+
"""
|
|
39
|
+
Save the model instance.
|
|
40
|
+
|
|
41
|
+
Delegates to bulk_create/bulk_update which handle all hook logic
|
|
42
|
+
including MTI parent hooks.
|
|
43
|
+
"""
|
|
44
|
+
if bypass_hooks:
|
|
45
|
+
# Use super().save() to call Django's default save without our hook logic
|
|
46
|
+
return super().save(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
is_create = self.pk is None
|
|
49
|
+
|
|
50
|
+
logger.debug("💾 SAVE_START: model=%s, pk=%s, is_create=%s, __dict__=%s",
|
|
51
|
+
self.__class__.__name__, self.pk, is_create, list(self.__dict__.keys()))
|
|
52
|
+
|
|
53
|
+
if is_create:
|
|
54
|
+
# Delegate to bulk_create which handles all hook logic
|
|
55
|
+
result = self.__class__.objects.bulk_create([self])
|
|
56
|
+
return result[0] if result else self
|
|
57
|
+
# Delegate to bulk_update which handles all hook logic
|
|
58
|
+
update_fields = kwargs.get("update_fields")
|
|
59
|
+
if update_fields is None:
|
|
60
|
+
# Update all non-auto fields
|
|
61
|
+
update_fields = [f.name for f in self.__class__._meta.fields if not f.auto_created and f.name != "id"]
|
|
62
|
+
|
|
63
|
+
logger.debug("💾 SAVE_UPDATE_FIELDS: fields=%s (count=%d)", update_fields, len(update_fields))
|
|
64
|
+
|
|
65
|
+
# Log FK field values before bulk_update
|
|
66
|
+
for field in self.__class__._meta.fields:
|
|
67
|
+
if field.get_internal_type() == 'ForeignKey' and field.name in update_fields:
|
|
68
|
+
fk_id_value = getattr(self, field.attname, 'NO_ATTR')
|
|
69
|
+
fk_obj_value = getattr(self, field.name, 'NO_ATTR')
|
|
70
|
+
logger.debug("💾 SAVE_FK_CHECK: field=%s, %s=%s, %s=%s (has_pk=%s)",
|
|
71
|
+
field.name, field.attname, fk_id_value, field.name, fk_obj_value,
|
|
72
|
+
hasattr(fk_obj_value, 'pk') if fk_obj_value != 'NO_ATTR' else 'N/A')
|
|
73
|
+
|
|
74
|
+
self.__class__.objects.bulk_update([self], update_fields)
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def delete(self, *args, bypass_hooks=False, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Delete the model instance.
|
|
80
|
+
|
|
81
|
+
Delegates to bulk_delete which handles all hook logic
|
|
82
|
+
including MTI parent hooks.
|
|
83
|
+
"""
|
|
84
|
+
if bypass_hooks:
|
|
85
|
+
# Use super().delete() to call Django's default delete without our hook logic
|
|
86
|
+
return super().delete(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
# Delegate to bulk_delete (handles both MTI and non-MTI)
|
|
89
|
+
return self.__class__.objects.filter(pk=self.pk).delete()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Operations module for django-bulk-hooks.
|
|
3
|
+
|
|
4
|
+
This module contains all services for bulk operations following
|
|
5
|
+
a clean, service-based architecture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from django_bulk_hooks.operations.analyzer import ModelAnalyzer
|
|
9
|
+
from django_bulk_hooks.operations.bulk_executor import BulkExecutor
|
|
10
|
+
from django_bulk_hooks.operations.coordinator import BulkOperationCoordinator
|
|
11
|
+
from django_bulk_hooks.operations.mti_handler import MTIHandler
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BulkExecutor",
|
|
15
|
+
"BulkOperationCoordinator",
|
|
16
|
+
"MTIHandler",
|
|
17
|
+
"ModelAnalyzer",
|
|
18
|
+
]
|