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.
@@ -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
- from django.db import models, transaction
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class HookQuerySet(models.QuerySet):
16
- """
17
- QuerySet with hook support.
18
-
19
- This is a thin facade over BulkOperationCoordinator. It provides
20
- backward-compatible API for Django's QuerySet while integrating
21
- the full hook lifecycle.
22
-
23
- Key design principles:
24
- - Minimal logic (< 10 lines per method)
25
- - No business logic (delegate to coordinator)
26
- - No conditionals (let services handle it)
27
- - Transaction boundaries only
28
- """
29
-
30
- def __init__(self, *args, **kwargs):
31
- super().__init__(*args, **kwargs)
32
- self._coordinator = None
33
-
34
- @property
35
- def coordinator(self):
36
- """Lazy initialization of coordinator"""
37
- if self._coordinator is None:
38
- from django_bulk_hooks.operations import BulkOperationCoordinator
39
-
40
- self._coordinator = BulkOperationCoordinator(self)
41
- return self._coordinator
42
-
43
- @transaction.atomic
44
- def bulk_create(
45
- self,
46
- objs,
47
- batch_size=None,
48
- ignore_conflicts=False,
49
- update_conflicts=False,
50
- update_fields=None,
51
- unique_fields=None,
52
- bypass_hooks=False,
53
- bypass_validation=False,
54
- ):
55
- """
56
- Create multiple objects with hook support.
57
-
58
- This is the public API - delegates to coordinator.
59
- """
60
- return self.coordinator.create(
61
- objs=objs,
62
- batch_size=batch_size,
63
- ignore_conflicts=ignore_conflicts,
64
- update_conflicts=update_conflicts,
65
- update_fields=update_fields,
66
- unique_fields=unique_fields,
67
- bypass_hooks=bypass_hooks,
68
- bypass_validation=bypass_validation,
69
- )
70
-
71
- @transaction.atomic
72
- def bulk_update(
73
- self,
74
- objs,
75
- fields=None,
76
- batch_size=None,
77
- bypass_hooks=False,
78
- bypass_validation=False,
79
- **kwargs,
80
- ):
81
- """
82
- Update multiple objects with hook support.
83
-
84
- This is the public API - delegates to coordinator.
85
-
86
- Args:
87
- objs: List of model instances to update
88
- fields: List of field names to update (optional, will auto-detect if None)
89
- batch_size: Number of objects per batch
90
- bypass_hooks: Skip all hooks if True
91
- bypass_validation: Skip validation hooks if True
92
-
93
- Returns:
94
- Number of objects updated
95
- """
96
- # If fields is None, auto-detect changed fields using analyzer
97
- if fields is None:
98
- fields = self.coordinator.analyzer.detect_changed_fields(objs)
99
- if not fields:
100
- logger.debug(
101
- f"bulk_update: No fields changed for {len(objs)} {self.model.__name__} objects"
102
- )
103
- return 0
104
-
105
- return self.coordinator.update(
106
- objs=objs,
107
- fields=fields,
108
- batch_size=batch_size,
109
- bypass_hooks=bypass_hooks,
110
- bypass_validation=bypass_validation,
111
- )
112
-
113
- @transaction.atomic
114
- def update(self, bypass_hooks=False, bypass_validation=False, **kwargs):
115
- """
116
- Update QuerySet with hook support.
117
-
118
- This is the public API - delegates to coordinator.
119
-
120
- Args:
121
- bypass_hooks: Skip all hooks if True
122
- bypass_validation: Skip validation hooks if True
123
- **kwargs: Fields to update
124
-
125
- Returns:
126
- Number of objects updated
127
- """
128
- return self.coordinator.update_queryset(
129
- update_kwargs=kwargs,
130
- bypass_hooks=bypass_hooks,
131
- bypass_validation=bypass_validation,
132
- )
133
-
134
- @transaction.atomic
135
- def bulk_delete(
136
- self, objs, bypass_hooks=False, bypass_validation=False, **kwargs
137
- ):
138
- """
139
- Delete multiple objects with hook support.
140
-
141
- This is the public API - delegates to coordinator.
142
-
143
- Args:
144
- objs: List of objects to delete
145
- bypass_hooks: Skip all hooks if True
146
- bypass_validation: Skip validation hooks if True
147
-
148
- Returns:
149
- Tuple of (count, details dict)
150
- """
151
- # Filter queryset to only these objects
152
- pks = [obj.pk for obj in objs if obj.pk is not None]
153
- if not pks:
154
- return 0
155
-
156
- # Create a filtered queryset
157
- filtered_qs = self.filter(pk__in=pks)
158
-
159
- # Use coordinator with the filtered queryset
160
- from django_bulk_hooks.operations import BulkOperationCoordinator
161
-
162
- coordinator = BulkOperationCoordinator(filtered_qs)
163
-
164
- count, details = coordinator.delete(
165
- bypass_hooks=bypass_hooks,
166
- bypass_validation=bypass_validation,
167
- )
168
-
169
- # For bulk_delete, return just the count to match Django's behavior
170
- return count
171
-
172
- @transaction.atomic
173
- def delete(self, bypass_hooks=False, bypass_validation=False):
174
- """
175
- Delete QuerySet with hook support.
176
-
177
- This is the public API - delegates to coordinator.
178
-
179
- Args:
180
- bypass_hooks: Skip all hooks if True
181
- bypass_validation: Skip validation hooks if True
182
-
183
- Returns:
184
- Tuple of (count, details dict)
185
- """
186
- return self.coordinator.delete(
187
- bypass_hooks=bypass_hooks,
188
- bypass_validation=bypass_validation,
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
+ )