django-bulk-hooks 0.2.41__py3-none-any.whl → 0.2.42__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.

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