django-bulk-hooks 0.2.44__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,96 +1,103 @@
1
- """
2
- MTI operation plans - Data structures for multi-table inheritance operations.
3
-
4
- These are pure data structures returned by MTIHandler to be executed by BulkExecutor.
5
- This separates planning (logic) from execution (database operations).
6
- """
7
-
8
- from dataclasses import dataclass
9
- from dataclasses import field
10
- from typing import Any
11
-
12
-
13
- @dataclass
14
- class ParentLevel:
15
- """
16
- Represents one level in the parent hierarchy for MTI bulk create.
17
-
18
- Attributes:
19
- model_class: The parent model class for this level
20
- objects: List of parent instances to create
21
- original_object_map: Maps parent instance id() -> original object id()
22
- update_conflicts: Whether to enable UPSERT for this level
23
- unique_fields: Fields for conflict detection (if update_conflicts=True)
24
- update_fields: Fields to update on conflict (if update_conflicts=True)
25
- """
26
- model_class: Any
27
- objects: list[Any]
28
- original_object_map: dict[int, int] = field(default_factory=dict)
29
- update_conflicts: bool = False
30
- unique_fields: list[str] = field(default_factory=list)
31
- update_fields: list[str] = field(default_factory=list)
32
-
33
-
34
- @dataclass
35
- class MTICreatePlan:
36
- """
37
- Plan for executing bulk_create on an MTI model.
38
-
39
- This plan describes WHAT to create, not HOW to create it.
40
- The executor is responsible for executing this plan.
41
-
42
- Attributes:
43
- inheritance_chain: List of model classes from root to child
44
- parent_levels: List of ParentLevel objects, one per parent model
45
- child_objects: List of child instances to create (not yet with parent links)
46
- child_model: The child model class
47
- original_objects: Original objects provided by user
48
- batch_size: Batch size for operations
49
- existing_record_ids: Set of id() of original objects that represent existing DB records
50
- update_conflicts: Whether this is an upsert operation
51
- unique_fields: Fields used for conflict detection
52
- update_fields: Fields to update on conflict
53
- """
54
- inheritance_chain: list[Any]
55
- parent_levels: list[ParentLevel]
56
- child_objects: list[Any]
57
- child_model: Any
58
- original_objects: list[Any]
59
- batch_size: int = None
60
- existing_record_ids: set = field(default_factory=set)
61
- update_conflicts: bool = False
62
- unique_fields: list[str] = field(default_factory=list)
63
- update_fields: list[str] = field(default_factory=list)
64
-
65
-
66
- @dataclass
67
- class ModelFieldGroup:
68
- """
69
- Represents fields to update for one model in the inheritance chain.
70
-
71
- Attributes:
72
- model_class: The model class
73
- fields: List of field names to update on this model
74
- filter_field: Field to use for filtering (e.g., 'pk' or parent link attname)
75
- """
76
- model_class: Any
77
- fields: list[str]
78
- filter_field: str = "pk"
79
-
80
-
81
- @dataclass
82
- class MTIUpdatePlan:
83
- """
84
- Plan for executing bulk_update on an MTI model.
85
-
86
- Attributes:
87
- inheritance_chain: List of model classes from root to child
88
- field_groups: List of ModelFieldGroup objects
89
- objects: Objects to update
90
- batch_size: Batch size for operations
91
- """
92
- inheritance_chain: list[Any]
93
- field_groups: list[ModelFieldGroup]
94
- objects: list[Any]
95
- batch_size: int = None
96
-
1
+ """
2
+ MTI operation plans - Data structures for multi-table inheritance operations.
3
+
4
+ These are pure data structures returned by MTIHandler to be executed by BulkExecutor.
5
+ This separates planning (logic) from execution (database operations).
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from dataclasses import field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class ParentLevel:
15
+ """
16
+ Represents one level in the parent hierarchy for MTI bulk create.
17
+
18
+ Attributes:
19
+ model_class: The parent model class for this level
20
+ objects: List of parent instances to create
21
+ original_object_map: Maps parent instance id() -> original object id()
22
+ update_conflicts: Whether to enable UPSERT for this level
23
+ unique_fields: Fields for conflict detection (if update_conflicts=True)
24
+ update_fields: Fields to update on conflict (if update_conflicts=True)
25
+ """
26
+
27
+ model_class: Any
28
+ objects: list[Any]
29
+ original_object_map: dict[int, int] = field(default_factory=dict)
30
+ update_conflicts: bool = False
31
+ unique_fields: list[str] = field(default_factory=list)
32
+ update_fields: list[str] = field(default_factory=list)
33
+
34
+
35
+ @dataclass
36
+ class MTICreatePlan:
37
+ """
38
+ Plan for executing bulk_create on an MTI model.
39
+
40
+ This plan describes WHAT to create, not HOW to create it.
41
+ The executor is responsible for executing this plan.
42
+
43
+ Attributes:
44
+ inheritance_chain: List of model classes from root to child
45
+ parent_levels: List of ParentLevel objects, one per parent model
46
+ child_objects: List of child instances to create (not yet with parent links)
47
+ child_model: The child model class
48
+ original_objects: Original objects provided by user
49
+ batch_size: Batch size for operations
50
+ existing_record_ids: Set of id() of original objects that represent existing DB records
51
+ update_conflicts: Whether this is an upsert operation
52
+ unique_fields: Fields used for conflict detection (original, unfiltered)
53
+ update_fields: Fields to update on conflict (original, unfiltered)
54
+ child_unique_fields: Pre-filtered field objects for child table conflict detection
55
+ child_update_fields: Pre-filtered field objects for child table updates
56
+ """
57
+
58
+ inheritance_chain: list[Any]
59
+ parent_levels: list[ParentLevel]
60
+ child_objects: list[Any]
61
+ child_model: Any
62
+ original_objects: list[Any]
63
+ batch_size: int = None
64
+ existing_record_ids: set = field(default_factory=set)
65
+ update_conflicts: bool = False
66
+ unique_fields: list[str] = field(default_factory=list)
67
+ update_fields: list[str] = field(default_factory=list)
68
+ child_unique_fields: list = field(default_factory=list) # Field objects for child table
69
+ child_update_fields: list = field(default_factory=list) # Field objects for child table
70
+
71
+
72
+ @dataclass
73
+ class ModelFieldGroup:
74
+ """
75
+ Represents fields to update for one model in the inheritance chain.
76
+
77
+ Attributes:
78
+ model_class: The model class
79
+ fields: List of field names to update on this model
80
+ filter_field: Field to use for filtering (e.g., 'pk' or parent link attname)
81
+ """
82
+
83
+ model_class: Any
84
+ fields: list[str]
85
+ filter_field: str = "pk"
86
+
87
+
88
+ @dataclass
89
+ class MTIUpdatePlan:
90
+ """
91
+ Plan for executing bulk_update on an MTI model.
92
+
93
+ Attributes:
94
+ inheritance_chain: List of model classes from root to child
95
+ field_groups: List of ModelFieldGroup objects
96
+ objects: Objects to update
97
+ batch_size: Batch size for operations
98
+ """
99
+
100
+ inheritance_chain: list[Any]
101
+ field_groups: list[ModelFieldGroup]
102
+ objects: list[Any]
103
+ batch_size: int = None
@@ -11,13 +11,15 @@ import logging
11
11
 
12
12
  from django.db.models import Q
13
13
 
14
+ from django_bulk_hooks.operations.field_utils import get_field_value_for_db
15
+
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
 
17
19
  class RecordClassifier:
18
20
  """
19
21
  Service for classifying and fetching records via database queries.
20
-
22
+
21
23
  This is the SINGLE point of truth for record classification queries.
22
24
  Keeps database access logic separate from business/planning logic.
23
25
  """
@@ -31,17 +33,18 @@ class RecordClassifier:
31
33
  """
32
34
  self.model_cls = model_cls
33
35
 
34
- def classify_for_upsert(self, objs, unique_fields):
36
+ def classify_for_upsert(self, objs, unique_fields, query_model=None):
35
37
  """
36
38
  Classify records as new or existing based on unique_fields.
37
-
39
+
38
40
  Queries the database to check which records already exist based on the
39
41
  unique_fields constraint.
40
-
42
+
41
43
  Args:
42
44
  objs: List of model instances
43
45
  unique_fields: List of field names that form the unique constraint
44
-
46
+ query_model: Optional model class to query (for MTI, may be different from self.model_cls)
47
+
45
48
  Returns:
46
49
  Tuple of (existing_record_ids, existing_pks_map)
47
50
  - existing_record_ids: Set of id() for objects that exist in DB
@@ -50,6 +53,9 @@ class RecordClassifier:
50
53
  if not unique_fields or not objs:
51
54
  return set(), {}
52
55
 
56
+ # Use query_model if provided (for MTI scenarios), otherwise use self.model_cls
57
+ query_model = query_model or self.model_cls
58
+
53
59
  # Build a query to find existing records
54
60
  queries = []
55
61
  obj_to_unique_values = {}
@@ -57,17 +63,22 @@ class RecordClassifier:
57
63
  for obj in objs:
58
64
  # Build lookup dict for this object's unique fields
59
65
  lookup = {}
66
+ normalized_values = []
67
+
60
68
  for field_name in unique_fields:
61
- value = getattr(obj, field_name, None)
69
+ # Use centralized field value extraction for consistent FK handling
70
+ value = get_field_value_for_db(obj, field_name, query_model)
62
71
  if value is None:
63
72
  # Can't match on None values
64
73
  break
65
74
  lookup[field_name] = value
75
+ normalized_values.append(value)
66
76
  else:
67
77
  # All unique fields have values, add to query
68
78
  if lookup:
69
79
  queries.append(Q(**lookup))
70
- obj_to_unique_values[id(obj)] = tuple(lookup.values())
80
+ # Store normalized values for comparison with database results
81
+ obj_to_unique_values[id(obj)] = tuple(normalized_values)
71
82
 
72
83
  if not queries:
73
84
  return set(), {}
@@ -77,9 +88,12 @@ class RecordClassifier:
77
88
  for q in queries[1:]:
78
89
  combined_query |= q
79
90
 
80
- existing_records = list(
81
- self.model_cls.objects.filter(combined_query).values("pk", *unique_fields),
82
- )
91
+ logger.info(f"Classifying for upsert: model={query_model.__name__}, query={combined_query}, unique_fields={unique_fields}")
92
+ queryset = query_model.objects.filter(combined_query)
93
+ logger.info(f"Queryset SQL: {queryset.query}")
94
+ logger.info(f"All records in table: {query_model.objects.all().count()}")
95
+ existing_records = list(queryset.values("pk", *unique_fields))
96
+ logger.info(f"Found {len(existing_records)} existing records: {existing_records}")
83
97
 
84
98
  # Map existing records back to original objects
85
99
  existing_record_ids = set()
@@ -94,8 +108,7 @@ class RecordClassifier:
94
108
  existing_pks_map[obj_id] = record["pk"]
95
109
 
96
110
  logger.info(
97
- f"Classified {len(existing_record_ids)} existing and "
98
- f"{len(objs) - len(existing_record_ids)} new records for upsert",
111
+ f"Classified {len(existing_record_ids)} existing and {len(objs) - len(existing_record_ids)} new records for upsert",
99
112
  )
100
113
 
101
114
  return existing_record_ids, existing_pks_map
@@ -103,12 +116,12 @@ class RecordClassifier:
103
116
  def fetch_by_pks(self, pks, select_related=None, prefetch_related=None):
104
117
  """
105
118
  Fetch records by primary keys with optional relationship loading.
106
-
119
+
107
120
  Args:
108
121
  pks: List of primary key values
109
122
  select_related: Optional list of fields to select_related
110
123
  prefetch_related: Optional list of fields to prefetch_related
111
-
124
+
112
125
  Returns:
113
126
  Dict[pk, instance] for O(1) lookups
114
127
  """
@@ -128,10 +141,10 @@ class RecordClassifier:
128
141
  def fetch_by_unique_constraint(self, field_values_map):
129
142
  """
130
143
  Fetch records matching a unique constraint.
131
-
144
+
132
145
  Args:
133
146
  field_values_map: Dict of {field_name: value} for unique constraint
134
-
147
+
135
148
  Returns:
136
149
  Model instance if found, None otherwise
137
150
  """
@@ -141,18 +154,17 @@ class RecordClassifier:
141
154
  return None
142
155
  except self.model_cls.MultipleObjectsReturned:
143
156
  logger.warning(
144
- f"Multiple {self.model_cls.__name__} records found for "
145
- f"unique constraint {field_values_map}",
157
+ f"Multiple {self.model_cls.__name__} records found for unique constraint {field_values_map}",
146
158
  )
147
159
  return self.model_cls.objects.filter(**field_values_map).first()
148
160
 
149
161
  def exists_by_pks(self, pks):
150
162
  """
151
163
  Check if records exist by primary keys without fetching them.
152
-
164
+
153
165
  Args:
154
166
  pks: List of primary key values
155
-
167
+
156
168
  Returns:
157
169
  Set of PKs that exist in the database
158
170
  """
@@ -168,13 +180,13 @@ class RecordClassifier:
168
180
  def count_by_unique_fields(self, objs, unique_fields):
169
181
  """
170
182
  Count how many objects already exist based on unique fields.
171
-
183
+
172
184
  Useful for validation or reporting before upsert operations.
173
-
185
+
174
186
  Args:
175
187
  objs: List of model instances
176
188
  unique_fields: List of field names that form the unique constraint
177
-
189
+
178
190
  Returns:
179
191
  Tuple of (existing_count, new_count)
180
192
  """
@@ -11,6 +11,8 @@ import logging
11
11
  from django.db import models
12
12
  from django.db import transaction
13
13
 
14
+ from django_bulk_hooks.helpers import extract_pks
15
+
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
 
@@ -33,6 +35,57 @@ class HookQuerySet(models.QuerySet):
33
35
  super().__init__(*args, **kwargs)
34
36
  self._coordinator = None
35
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
+
36
89
  @property
37
90
  def coordinator(self):
38
91
  """Lazy initialization of coordinator"""
@@ -52,7 +105,6 @@ class HookQuerySet(models.QuerySet):
52
105
  update_fields=None,
53
106
  unique_fields=None,
54
107
  bypass_hooks=False,
55
- bypass_validation=False,
56
108
  ):
57
109
  """
58
110
  Create multiple objects with hook support.
@@ -67,7 +119,6 @@ class HookQuerySet(models.QuerySet):
67
119
  update_fields=update_fields,
68
120
  unique_fields=unique_fields,
69
121
  bypass_hooks=bypass_hooks,
70
- bypass_validation=bypass_validation,
71
122
  )
72
123
 
73
124
  @transaction.atomic
@@ -77,7 +128,6 @@ class HookQuerySet(models.QuerySet):
77
128
  fields=None,
78
129
  batch_size=None,
79
130
  bypass_hooks=False,
80
- bypass_validation=False,
81
131
  **kwargs,
82
132
  ):
83
133
  """
@@ -90,7 +140,6 @@ class HookQuerySet(models.QuerySet):
90
140
  fields: List of field names to update (optional, will auto-detect if None)
91
141
  batch_size: Number of objects per batch
92
142
  bypass_hooks: Skip all hooks if True
93
- bypass_validation: Skip validation hooks if True
94
143
 
95
144
  Returns:
96
145
  Number of objects updated
@@ -106,11 +155,10 @@ class HookQuerySet(models.QuerySet):
106
155
  fields=fields,
107
156
  batch_size=batch_size,
108
157
  bypass_hooks=bypass_hooks,
109
- bypass_validation=bypass_validation,
110
158
  )
111
159
 
112
160
  @transaction.atomic
113
- def update(self, bypass_hooks=False, bypass_validation=False, **kwargs):
161
+ def update(self, bypass_hooks=False, **kwargs):
114
162
  """
115
163
  Update QuerySet with hook support.
116
164
 
@@ -118,7 +166,6 @@ class HookQuerySet(models.QuerySet):
118
166
 
119
167
  Args:
120
168
  bypass_hooks: Skip all hooks if True
121
- bypass_validation: Skip validation hooks if True
122
169
  **kwargs: Fields to update
123
170
 
124
171
  Returns:
@@ -127,12 +174,14 @@ class HookQuerySet(models.QuerySet):
127
174
  return self.coordinator.update_queryset(
128
175
  update_kwargs=kwargs,
129
176
  bypass_hooks=bypass_hooks,
130
- bypass_validation=bypass_validation,
131
177
  )
132
178
 
133
179
  @transaction.atomic
134
180
  def bulk_delete(
135
- self, objs, bypass_hooks=False, bypass_validation=False, **kwargs,
181
+ self,
182
+ objs,
183
+ bypass_hooks=False,
184
+ **kwargs,
136
185
  ):
137
186
  """
138
187
  Delete multiple objects with hook support.
@@ -142,13 +191,12 @@ class HookQuerySet(models.QuerySet):
142
191
  Args:
143
192
  objs: List of objects to delete
144
193
  bypass_hooks: Skip all hooks if True
145
- bypass_validation: Skip validation hooks if True
146
194
 
147
195
  Returns:
148
196
  Tuple of (count, details dict)
149
197
  """
150
198
  # Filter queryset to only these objects
151
- pks = [obj.pk for obj in objs if obj.pk is not None]
199
+ pks = extract_pks(objs)
152
200
  if not pks:
153
201
  return 0
154
202
 
@@ -162,14 +210,13 @@ class HookQuerySet(models.QuerySet):
162
210
 
163
211
  count, details = coordinator.delete(
164
212
  bypass_hooks=bypass_hooks,
165
- bypass_validation=bypass_validation,
166
213
  )
167
214
 
168
215
  # For bulk_delete, return just the count to match Django's behavior
169
216
  return count
170
217
 
171
218
  @transaction.atomic
172
- def delete(self, bypass_hooks=False, bypass_validation=False):
219
+ def delete(self, bypass_hooks=False):
173
220
  """
174
221
  Delete QuerySet with hook support.
175
222
 
@@ -177,12 +224,10 @@ class HookQuerySet(models.QuerySet):
177
224
 
178
225
  Args:
179
226
  bypass_hooks: Skip all hooks if True
180
- bypass_validation: Skip validation hooks if True
181
227
 
182
228
  Returns:
183
229
  Tuple of (count, details dict)
184
230
  """
185
231
  return self.coordinator.delete(
186
232
  bypass_hooks=bypass_hooks,
187
- bypass_validation=bypass_validation,
188
233
  )
@@ -116,7 +116,6 @@ class HookRegistry:
116
116
  if not self._hooks[key]:
117
117
  del self._hooks[key]
118
118
 
119
-
120
119
  def clear(self) -> None:
121
120
  """
122
121
  Clear all registered hooks.
@@ -132,7 +131,6 @@ class HookRegistry:
132
131
  HookMeta._registered.clear()
133
132
  HookMeta._class_hook_map.clear()
134
133
 
135
-
136
134
  def list_all(self) -> dict[tuple[type, str], list[HookInfo]]:
137
135
  """
138
136
  Get all registered hooks for debugging.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.44
3
+ Version: 0.2.93
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -244,17 +244,68 @@ LoanAccount.objects.bulk_update(reordered) # fields are auto-detected
244
244
 
245
245
  ## 🧩 Integration with Other Managers
246
246
 
247
- You can extend from `BulkHookManager` to work with other manager classes. The manager uses a cooperative approach that dynamically injects bulk hook functionality into any queryset, ensuring compatibility with other managers.
247
+ ### Recommended: QuerySet-based Composition (New Approach)
248
+
249
+ For the best compatibility and to avoid inheritance conflicts, use the queryset-based composition approach:
250
+
251
+ ```python
252
+ from django_bulk_hooks.queryset import HookQuerySet
253
+ from queryable_properties.managers import QueryablePropertiesManager
254
+
255
+ class MyManager(QueryablePropertiesManager):
256
+ """Manager that combines queryable properties with hooks"""
257
+
258
+ def get_queryset(self):
259
+ # Get the QueryableProperties QuerySet
260
+ qs = super().get_queryset()
261
+ # Apply hooks on top of it
262
+ return HookQuerySet.with_hooks(qs)
263
+
264
+ class Article(models.Model):
265
+ title = models.CharField(max_length=100)
266
+ published = models.BooleanField(default=False)
267
+
268
+ objects = MyManager()
269
+
270
+ # This gives you both queryable properties AND hooks
271
+ # No inheritance conflicts, no MRO issues!
272
+ ```
273
+
274
+ ### Alternative: Explicit Hook Application
275
+
276
+ For more control, you can apply hooks explicitly:
277
+
278
+ ```python
279
+ class MyManager(QueryablePropertiesManager):
280
+ def get_queryset(self):
281
+ return super().get_queryset()
282
+
283
+ def with_hooks(self):
284
+ """Apply hooks to this queryset"""
285
+ return HookQuerySet.with_hooks(self.get_queryset())
286
+
287
+ # Usage:
288
+ Article.objects.with_hooks().filter(published=True).update(title="Updated")
289
+ ```
290
+
291
+ ### Legacy: Manager Inheritance (Not Recommended)
292
+
293
+ The old inheritance approach still works but is not recommended due to potential MRO conflicts:
248
294
 
249
295
  ```python
250
296
  from django_bulk_hooks.manager import BulkHookManager
251
297
  from queryable_properties.managers import QueryablePropertiesManager
252
298
 
253
299
  class MyManager(BulkHookManager, QueryablePropertiesManager):
254
- pass
300
+ pass # ⚠️ Can cause inheritance conflicts
255
301
  ```
256
302
 
257
- This approach uses the industry-standard injection pattern, similar to how `QueryablePropertiesManager` works, ensuring both functionalities work seamlessly together without any framework-specific knowledge.
303
+ **Why the new approach is better:**
304
+ - ✅ No inheritance conflicts
305
+ - ✅ No MRO (Method Resolution Order) issues
306
+ - ✅ Works with any manager combination
307
+ - ✅ Cleaner and more maintainable
308
+ - ✅ Follows Django's queryset enhancement patterns
258
309
 
259
310
  Framework needs to:
260
311
  Register these methods
@@ -0,0 +1,27 @@
1
+ django_bulk_hooks/__init__.py,sha256=ZKjEi9Sj3lRr3hcEfknXAr1UXXwERzUCNgMkNXhW0mk,2119
2
+ django_bulk_hooks/changeset.py,sha256=qnMD3bR2cNh8ZM8J6ASR5ly5Rjx-tPzXBYkqIjKGW98,6568
3
+ django_bulk_hooks/conditions.py,sha256=ar4pGjtxLKmgSIlO4S6aZFKmaBNchLtxMmWpkn4g9RU,8114
4
+ django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
5
+ django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
6
+ django_bulk_hooks/decorators.py,sha256=TdkO4FJyFrVU2zqK6Y_6JjEJ4v3nbKkk7aa22jN10sk,11994
7
+ django_bulk_hooks/dispatcher.py,sha256=IjRAEQmWIiTqyan3hWlnV3rnjM0CYSVRmXc1xNUWWU8,24085
8
+ django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
9
+ django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
10
+ django_bulk_hooks/handler.py,sha256=SRCrMzgolrruTkvMnYBFmXLR-ABiw0JiH3605PEdCZM,4207
11
+ django_bulk_hooks/helpers.py,sha256=3rH9TJkdCPF7Vu--0tDaZzJg9Yxcv7yoSF1K1_-0psQ,8048
12
+ django_bulk_hooks/manager.py,sha256=sn4ALCuxRydjIJ91kB81Dhj4PitwytGa4wzxPos4I2Q,4096
13
+ django_bulk_hooks/models.py,sha256=H16AuIiRjkwTD-YDA9S_sMYfAzAFoBgKqiq4TvJuJ9M,3325
14
+ django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
+ django_bulk_hooks/operations/analyzer.py,sha256=Fw4rjkhpfT8b2A4c7CSMfFRtLUFVimCCz_eGIBtcNiI,15126
16
+ django_bulk_hooks/operations/bulk_executor.py,sha256=FEhC8IsjvPcTXMMNvxc3w3CB1c49mKqQ-Jq02QY5yLM,28263
17
+ django_bulk_hooks/operations/coordinator.py,sha256=3n9bKpcn3_X-zos0tYX6JWS77JleeYMVawZu2DZ1LC4,34973
18
+ django_bulk_hooks/operations/field_utils.py,sha256=EM7y3Vs_4zn-ejgHee6MaenYEhL5txN13kB5cqFHIN0,14109
19
+ django_bulk_hooks/operations/mti_handler.py,sha256=00djtjfZ0rrOfiEii8TS1aBarC0qDpCBsFfWGrljvsc,26946
20
+ django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZPV2SzwQHLRzYc,3667
21
+ django_bulk_hooks/operations/record_classifier.py,sha256=It85hJC2K-UsEOLbTR-QBdY5UPV-acQIJ91TSGa7pYo,7053
22
+ django_bulk_hooks/queryset.py,sha256=tPIkNESb47fTIpTrR6xUtc-k3gCFR15W0Xt2-HmvlJo,6811
23
+ django_bulk_hooks/registry.py,sha256=4HxP1mVK2z4VzvlohbEw2359wM21UJZJYagJJ1komM0,7947
24
+ django_bulk_hooks-0.2.93.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.93.dist-info/METADATA,sha256=ewuQ9Igpa9y-NNo3UFbmPtu2lDRFO1cUndjdQQD3FR4,10555
26
+ django_bulk_hooks-0.2.93.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.93.dist-info/RECORD,,