django-bulk-hooks 0.2.61__py3-none-any.whl → 0.2.63__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/changeset.py +214 -211
- django_bulk_hooks/conditions.py +7 -3
- django_bulk_hooks/decorators.py +5 -15
- django_bulk_hooks/dispatcher.py +97 -7
- django_bulk_hooks/handler.py +2 -2
- django_bulk_hooks/helpers.py +251 -245
- django_bulk_hooks/manager.py +150 -150
- django_bulk_hooks/models.py +74 -87
- django_bulk_hooks/operations/analyzer.py +319 -319
- django_bulk_hooks/operations/bulk_executor.py +22 -31
- django_bulk_hooks/operations/coordinator.py +10 -7
- django_bulk_hooks/operations/field_utils.py +5 -13
- django_bulk_hooks/operations/mti_handler.py +10 -5
- django_bulk_hooks/operations/mti_plans.py +103 -103
- django_bulk_hooks/operations/record_classifier.py +1 -1
- django_bulk_hooks/queryset.py +5 -1
- django_bulk_hooks/registry.py +0 -2
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.63.dist-info}/METADATA +1 -1
- django_bulk_hooks-0.2.63.dist-info/RECORD +27 -0
- django_bulk_hooks-0.2.61.dist-info/RECORD +0 -27
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.63.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.63.dist-info}/WHEEL +0 -0
django_bulk_hooks/manager.py
CHANGED
|
@@ -1,150 +1,150 @@
|
|
|
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 is a simple facade that returns HookQuerySet,
|
|
25
|
-
delegating all bulk operations to it.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def get_queryset(self):
|
|
29
|
-
"""
|
|
30
|
-
Return a HookQuerySet for this manager.
|
|
31
|
-
|
|
32
|
-
This ensures all bulk operations go through the coordinator.
|
|
33
|
-
"""
|
|
34
|
-
base_queryset = super().get_queryset()
|
|
35
|
-
|
|
36
|
-
# If the base queryset is already a HookQuerySet, return it as-is
|
|
37
|
-
if isinstance(base_queryset, HookQuerySet):
|
|
38
|
-
return base_queryset
|
|
39
|
-
|
|
40
|
-
# Otherwise, create a new HookQuerySet with the same parameters
|
|
41
|
-
return HookQuerySet(
|
|
42
|
-
model=base_queryset.model,
|
|
43
|
-
query=base_queryset.query,
|
|
44
|
-
using=base_queryset._db,
|
|
45
|
-
hints=base_queryset._hints,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
def bulk_create(
|
|
49
|
-
self,
|
|
50
|
-
objs,
|
|
51
|
-
batch_size=None,
|
|
52
|
-
ignore_conflicts=False,
|
|
53
|
-
update_conflicts=False,
|
|
54
|
-
update_fields=None,
|
|
55
|
-
unique_fields=None,
|
|
56
|
-
bypass_hooks=False,
|
|
57
|
-
bypass_validation=False,
|
|
58
|
-
**kwargs,
|
|
59
|
-
):
|
|
60
|
-
"""
|
|
61
|
-
Delegate to QuerySet's bulk_create implementation.
|
|
62
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
63
|
-
"""
|
|
64
|
-
return _delegate_to_queryset(
|
|
65
|
-
self,
|
|
66
|
-
"bulk_create",
|
|
67
|
-
objs,
|
|
68
|
-
batch_size=batch_size,
|
|
69
|
-
ignore_conflicts=ignore_conflicts,
|
|
70
|
-
update_conflicts=update_conflicts,
|
|
71
|
-
update_fields=update_fields,
|
|
72
|
-
unique_fields=unique_fields,
|
|
73
|
-
bypass_hooks=bypass_hooks,
|
|
74
|
-
bypass_validation=bypass_validation,
|
|
75
|
-
**kwargs,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
def bulk_update(
|
|
79
|
-
self,
|
|
80
|
-
objs,
|
|
81
|
-
fields=None,
|
|
82
|
-
bypass_hooks=False,
|
|
83
|
-
bypass_validation=False,
|
|
84
|
-
**kwargs,
|
|
85
|
-
):
|
|
86
|
-
"""
|
|
87
|
-
Delegate to QuerySet's bulk_update implementation.
|
|
88
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
89
|
-
|
|
90
|
-
Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
|
|
91
|
-
are not supported by bulk_update and will be ignored with a warning.
|
|
92
|
-
These parameters are only available in bulk_create for UPSERT operations.
|
|
93
|
-
"""
|
|
94
|
-
if fields is not None:
|
|
95
|
-
kwargs["fields"] = fields
|
|
96
|
-
return _delegate_to_queryset(
|
|
97
|
-
self,
|
|
98
|
-
"bulk_update",
|
|
99
|
-
objs,
|
|
100
|
-
bypass_hooks=bypass_hooks,
|
|
101
|
-
bypass_validation=bypass_validation,
|
|
102
|
-
**kwargs,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
def bulk_delete(
|
|
106
|
-
self,
|
|
107
|
-
objs,
|
|
108
|
-
batch_size=None,
|
|
109
|
-
bypass_hooks=False,
|
|
110
|
-
bypass_validation=False,
|
|
111
|
-
**kwargs,
|
|
112
|
-
):
|
|
113
|
-
"""
|
|
114
|
-
Delegate to QuerySet's bulk_delete implementation.
|
|
115
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
116
|
-
"""
|
|
117
|
-
return _delegate_to_queryset(
|
|
118
|
-
self,
|
|
119
|
-
"bulk_delete",
|
|
120
|
-
objs,
|
|
121
|
-
batch_size=batch_size,
|
|
122
|
-
bypass_hooks=bypass_hooks,
|
|
123
|
-
bypass_validation=bypass_validation,
|
|
124
|
-
**kwargs,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
def delete(self):
|
|
128
|
-
"""
|
|
129
|
-
Delegate to QuerySet's delete implementation.
|
|
130
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
131
|
-
"""
|
|
132
|
-
return _delegate_to_queryset(self, "delete")
|
|
133
|
-
|
|
134
|
-
def update(self, **kwargs):
|
|
135
|
-
"""
|
|
136
|
-
Delegate to QuerySet's update implementation.
|
|
137
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
138
|
-
"""
|
|
139
|
-
return _delegate_to_queryset(self, "update", **kwargs)
|
|
140
|
-
|
|
141
|
-
def save(self, obj):
|
|
142
|
-
"""
|
|
143
|
-
Save a single object using the appropriate bulk operation.
|
|
144
|
-
"""
|
|
145
|
-
if obj.pk:
|
|
146
|
-
# bulk_update now auto-detects changed fields
|
|
147
|
-
self.bulk_update([obj])
|
|
148
|
-
else:
|
|
149
|
-
self.bulk_create([obj])
|
|
150
|
-
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 is a simple facade that returns HookQuerySet,
|
|
25
|
+
delegating all bulk operations to it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def get_queryset(self):
|
|
29
|
+
"""
|
|
30
|
+
Return a HookQuerySet for this manager.
|
|
31
|
+
|
|
32
|
+
This ensures all bulk operations go through the coordinator.
|
|
33
|
+
"""
|
|
34
|
+
base_queryset = super().get_queryset()
|
|
35
|
+
|
|
36
|
+
# If the base queryset is already a HookQuerySet, return it as-is
|
|
37
|
+
if isinstance(base_queryset, HookQuerySet):
|
|
38
|
+
return base_queryset
|
|
39
|
+
|
|
40
|
+
# Otherwise, create a new HookQuerySet with the same parameters
|
|
41
|
+
return HookQuerySet(
|
|
42
|
+
model=base_queryset.model,
|
|
43
|
+
query=base_queryset.query,
|
|
44
|
+
using=base_queryset._db,
|
|
45
|
+
hints=base_queryset._hints,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def bulk_create(
|
|
49
|
+
self,
|
|
50
|
+
objs,
|
|
51
|
+
batch_size=None,
|
|
52
|
+
ignore_conflicts=False,
|
|
53
|
+
update_conflicts=False,
|
|
54
|
+
update_fields=None,
|
|
55
|
+
unique_fields=None,
|
|
56
|
+
bypass_hooks=False,
|
|
57
|
+
bypass_validation=False,
|
|
58
|
+
**kwargs,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Delegate to QuerySet's bulk_create implementation.
|
|
62
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
63
|
+
"""
|
|
64
|
+
return _delegate_to_queryset(
|
|
65
|
+
self,
|
|
66
|
+
"bulk_create",
|
|
67
|
+
objs,
|
|
68
|
+
batch_size=batch_size,
|
|
69
|
+
ignore_conflicts=ignore_conflicts,
|
|
70
|
+
update_conflicts=update_conflicts,
|
|
71
|
+
update_fields=update_fields,
|
|
72
|
+
unique_fields=unique_fields,
|
|
73
|
+
bypass_hooks=bypass_hooks,
|
|
74
|
+
bypass_validation=bypass_validation,
|
|
75
|
+
**kwargs,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def bulk_update(
|
|
79
|
+
self,
|
|
80
|
+
objs,
|
|
81
|
+
fields=None,
|
|
82
|
+
bypass_hooks=False,
|
|
83
|
+
bypass_validation=False,
|
|
84
|
+
**kwargs,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Delegate to QuerySet's bulk_update implementation.
|
|
88
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
89
|
+
|
|
90
|
+
Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
|
|
91
|
+
are not supported by bulk_update and will be ignored with a warning.
|
|
92
|
+
These parameters are only available in bulk_create for UPSERT operations.
|
|
93
|
+
"""
|
|
94
|
+
if fields is not None:
|
|
95
|
+
kwargs["fields"] = fields
|
|
96
|
+
return _delegate_to_queryset(
|
|
97
|
+
self,
|
|
98
|
+
"bulk_update",
|
|
99
|
+
objs,
|
|
100
|
+
bypass_hooks=bypass_hooks,
|
|
101
|
+
bypass_validation=bypass_validation,
|
|
102
|
+
**kwargs,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def bulk_delete(
|
|
106
|
+
self,
|
|
107
|
+
objs,
|
|
108
|
+
batch_size=None,
|
|
109
|
+
bypass_hooks=False,
|
|
110
|
+
bypass_validation=False,
|
|
111
|
+
**kwargs,
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Delegate to QuerySet's bulk_delete implementation.
|
|
115
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
116
|
+
"""
|
|
117
|
+
return _delegate_to_queryset(
|
|
118
|
+
self,
|
|
119
|
+
"bulk_delete",
|
|
120
|
+
objs,
|
|
121
|
+
batch_size=batch_size,
|
|
122
|
+
bypass_hooks=bypass_hooks,
|
|
123
|
+
bypass_validation=bypass_validation,
|
|
124
|
+
**kwargs,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def delete(self):
|
|
128
|
+
"""
|
|
129
|
+
Delegate to QuerySet's delete implementation.
|
|
130
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
131
|
+
"""
|
|
132
|
+
return _delegate_to_queryset(self, "delete")
|
|
133
|
+
|
|
134
|
+
def update(self, **kwargs):
|
|
135
|
+
"""
|
|
136
|
+
Delegate to QuerySet's update implementation.
|
|
137
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
138
|
+
"""
|
|
139
|
+
return _delegate_to_queryset(self, "update", **kwargs)
|
|
140
|
+
|
|
141
|
+
def save(self, obj):
|
|
142
|
+
"""
|
|
143
|
+
Save a single object using the appropriate bulk operation.
|
|
144
|
+
"""
|
|
145
|
+
if obj.pk:
|
|
146
|
+
# bulk_update now auto-detects changed fields
|
|
147
|
+
self.bulk_update([obj])
|
|
148
|
+
else:
|
|
149
|
+
self.bulk_create([obj])
|
|
150
|
+
return obj
|
django_bulk_hooks/models.py
CHANGED
|
@@ -1,87 +1,74 @@
|
|
|
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
|
|
11
|
-
"""Mixin providing
|
|
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
|
-
def delete(self, *args, bypass_hooks=False, **kwargs):
|
|
76
|
-
"""
|
|
77
|
-
Delete the model instance.
|
|
78
|
-
|
|
79
|
-
Delegates to bulk_delete which handles all hook logic
|
|
80
|
-
including MTI parent hooks.
|
|
81
|
-
"""
|
|
82
|
-
if bypass_hooks:
|
|
83
|
-
# Use super().delete() to call Django's default delete without our hook logic
|
|
84
|
-
return super().delete(*args, **kwargs)
|
|
85
|
-
|
|
86
|
-
# Delegate to bulk_delete (handles both MTI and non-MTI)
|
|
87
|
-
return self.__class__.objects.filter(pk=self.pk).delete()
|
|
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
|
+
if is_create:
|
|
51
|
+
# Delegate to bulk_create which handles all hook logic
|
|
52
|
+
result = self.__class__.objects.bulk_create([self])
|
|
53
|
+
return result[0] if result else self
|
|
54
|
+
# Delegate to bulk_update which handles all hook logic
|
|
55
|
+
update_fields = kwargs.get("update_fields")
|
|
56
|
+
if update_fields is None:
|
|
57
|
+
# Update all non-auto fields
|
|
58
|
+
update_fields = [f.name for f in self.__class__._meta.fields if not f.auto_created and f.name != "id"]
|
|
59
|
+
self.__class__.objects.bulk_update([self], update_fields)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def delete(self, *args, bypass_hooks=False, **kwargs):
|
|
63
|
+
"""
|
|
64
|
+
Delete the model instance.
|
|
65
|
+
|
|
66
|
+
Delegates to bulk_delete which handles all hook logic
|
|
67
|
+
including MTI parent hooks.
|
|
68
|
+
"""
|
|
69
|
+
if bypass_hooks:
|
|
70
|
+
# Use super().delete() to call Django's default delete without our hook logic
|
|
71
|
+
return super().delete(*args, **kwargs)
|
|
72
|
+
|
|
73
|
+
# Delegate to bulk_delete (handles both MTI and non-MTI)
|
|
74
|
+
return self.__class__.objects.filter(pk=self.pk).delete()
|