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

@@ -91,4 +91,144 @@ def normalize_field_name_to_db(field_name, model_cls):
91
91
  return field.attname # Returns 'business_id' for 'business' field
92
92
  return field_name
93
93
  except Exception: # noqa: BLE001
94
- return field_name
94
+ return field_name
95
+
96
+
97
+ def get_changed_fields(old_obj, new_obj, model_cls, skip_auto_fields=False):
98
+ """
99
+ Get field names that have changed between two model instances.
100
+
101
+ Uses Django's field.get_prep_value() for proper database-level comparison.
102
+ This is the canonical implementation used by both RecordChange and ModelAnalyzer.
103
+
104
+ Args:
105
+ old_obj: The old model instance
106
+ new_obj: The new model instance
107
+ model_cls: The Django model class
108
+ skip_auto_fields: Whether to skip auto_created fields (default False)
109
+
110
+ Returns:
111
+ Set of field names that have changed
112
+ """
113
+ changed = set()
114
+
115
+ for field in model_cls._meta.fields:
116
+ # Skip primary key fields - they shouldn't change
117
+ if field.primary_key:
118
+ continue
119
+
120
+ # Optionally skip auto-created fields (for bulk operations)
121
+ if skip_auto_fields and field.auto_created:
122
+ continue
123
+
124
+ old_val = getattr(old_obj, field.name, None)
125
+ new_val = getattr(new_obj, field.name, None)
126
+
127
+ # Use field's get_prep_value for database-ready comparison
128
+ # This handles timezone conversions, type coercions, etc.
129
+ try:
130
+ old_prep = field.get_prep_value(old_val)
131
+ new_prep = field.get_prep_value(new_val)
132
+ if old_prep != new_prep:
133
+ changed.add(field.name)
134
+ except (TypeError, ValueError):
135
+ # Fallback to direct comparison if get_prep_value fails
136
+ if old_val != new_val:
137
+ changed.add(field.name)
138
+
139
+ return changed
140
+
141
+
142
+ def get_auto_fields(model_cls, include_auto_now_add=True):
143
+ """
144
+ Get auto fields from a model.
145
+
146
+ Args:
147
+ model_cls: Django model class
148
+ include_auto_now_add: Whether to include auto_now_add fields
149
+
150
+ Returns:
151
+ List of field names
152
+ """
153
+ fields = []
154
+ for field in model_cls._meta.fields:
155
+ if getattr(field, "auto_now", False) or (
156
+ include_auto_now_add and getattr(field, "auto_now_add", False)
157
+ ):
158
+ fields.append(field.name)
159
+ return fields
160
+
161
+
162
+ def get_auto_now_only_fields(model_cls):
163
+ """Get only auto_now fields (excluding auto_now_add)."""
164
+ return get_auto_fields(model_cls, include_auto_now_add=False)
165
+
166
+
167
+ def get_fk_fields(model_cls):
168
+ """Get foreign key field names for a model."""
169
+ return [field.name for field in model_cls._meta.concrete_fields
170
+ if field.is_relation and not field.many_to_many]
171
+
172
+
173
+ def collect_auto_now_fields_for_inheritance_chain(inheritance_chain):
174
+ """Collect auto_now fields across an MTI inheritance chain."""
175
+ all_auto_now = set()
176
+ for model_cls in inheritance_chain:
177
+ all_auto_now.update(get_auto_now_only_fields(model_cls))
178
+ return all_auto_now
179
+
180
+
181
+ def handle_auto_now_fields_for_inheritance_chain(models, instances, for_update=True):
182
+ """
183
+ Unified auto-now field handling for any inheritance chain.
184
+
185
+ This replaces the separate collect/pre_save logic with a single comprehensive
186
+ method that handles collection, pre-saving, and field inclusion for updates.
187
+
188
+ Args:
189
+ models: List of model classes in inheritance chain
190
+ instances: List of model instances to process
191
+ for_update: Whether this is for an update operation (vs create)
192
+
193
+ Returns:
194
+ Set of auto_now field names that should be included in updates
195
+ """
196
+ all_auto_now_fields = set()
197
+
198
+ for model_cls in models:
199
+ for field in model_cls._meta.local_fields:
200
+ # For updates, only include auto_now (not auto_now_add)
201
+ # For creates, include both
202
+ if getattr(field, "auto_now", False) or (
203
+ not for_update and getattr(field, "auto_now_add", False)
204
+ ):
205
+ all_auto_now_fields.add(field.name)
206
+
207
+ # Pre-save the field on instances
208
+ for instance in instances:
209
+ if for_update:
210
+ # For updates, only pre-save auto_now fields
211
+ field.pre_save(instance, add=False)
212
+ else:
213
+ # For creates, pre-save both auto_now and auto_now_add
214
+ field.pre_save(instance, add=True)
215
+
216
+ return all_auto_now_fields
217
+
218
+
219
+ def pre_save_auto_now_fields(objects, inheritance_chain):
220
+ """Pre-save auto_now fields across inheritance chain."""
221
+ # DEPRECATED: Use handle_auto_now_fields_for_inheritance_chain instead
222
+ auto_now_fields = collect_auto_now_fields_for_inheritance_chain(inheritance_chain)
223
+
224
+ for field_name in auto_now_fields:
225
+ # Find which model has this field
226
+ for model_cls in inheritance_chain:
227
+ try:
228
+ field = model_cls._meta.get_field(field_name)
229
+ if getattr(field, "auto_now", False):
230
+ for obj in objects:
231
+ field.pre_save(obj, add=False)
232
+ break
233
+ except Exception:
234
+ continue
@@ -217,6 +217,11 @@ class MTIHandler:
217
217
  child_obj = self._create_child_instance_template(obj, inheritance_chain[-1])
218
218
  child_objects.append(child_obj)
219
219
 
220
+ # Pre-compute child-specific fields for execution efficiency
221
+ from django_bulk_hooks.helpers import get_fields_for_model, filter_field_names_for_model
222
+ child_unique_fields = get_fields_for_model(inheritance_chain[-1], unique_fields or [])
223
+ child_update_fields = get_fields_for_model(inheritance_chain[-1], update_fields or [])
224
+
220
225
  return MTICreatePlan(
221
226
  inheritance_chain=inheritance_chain,
222
227
  parent_levels=parent_levels,
@@ -228,6 +233,8 @@ class MTIHandler:
228
233
  update_conflicts=update_conflicts,
229
234
  unique_fields=unique_fields or [],
230
235
  update_fields=update_fields or [],
236
+ child_unique_fields=child_unique_fields,
237
+ child_update_fields=child_update_fields,
231
238
  )
232
239
 
233
240
  def _build_parent_levels(
@@ -458,14 +465,13 @@ class MTIHandler:
458
465
  if hasattr(source_obj._state, "db"):
459
466
  parent_obj._state.db = source_obj._state.db
460
467
 
461
- # Handle auto_now_add and auto_now fields
462
- for field in parent_model._meta.local_fields:
463
- if getattr(field, "auto_now_add", False):
464
- if getattr(parent_obj, field.name) is None:
465
- field.pre_save(parent_obj, add=True)
466
- setattr(parent_obj, field.attname, field.value_from_object(parent_obj))
467
- elif getattr(field, "auto_now", False):
468
- field.pre_save(parent_obj, add=True)
468
+ # Use unified auto-now field handling
469
+ from django_bulk_hooks.operations.field_utils import handle_auto_now_fields_for_inheritance_chain
470
+
471
+ # Handle auto fields for this single parent model
472
+ handle_auto_now_fields_for_inheritance_chain(
473
+ [parent_model], [parent_obj], for_update=False, # MTI create is like insert
474
+ )
469
475
 
470
476
  return parent_obj
471
477
 
@@ -516,14 +522,13 @@ class MTIHandler:
516
522
  if hasattr(source_obj._state, "db"):
517
523
  child_obj._state.db = source_obj._state.db
518
524
 
519
- # Handle auto_now_add and auto_now fields
520
- for field in child_model._meta.local_fields:
521
- if getattr(field, "auto_now_add", False):
522
- if getattr(child_obj, field.name) is None:
523
- field.pre_save(child_obj, add=True)
524
- setattr(child_obj, field.attname, field.value_from_object(child_obj))
525
- elif getattr(field, "auto_now", False):
526
- field.pre_save(child_obj, add=True)
525
+ # Use unified auto-now field handling
526
+ from django_bulk_hooks.operations.field_utils import handle_auto_now_fields_for_inheritance_chain
527
+
528
+ # Handle auto fields for this single child model
529
+ handle_auto_now_fields_for_inheritance_chain(
530
+ [child_model], [child_obj], for_update=False, # MTI create is like insert
531
+ )
527
532
 
528
533
  return child_obj
529
534
 
@@ -537,7 +542,7 @@ class MTIHandler:
537
542
 
538
543
  Args:
539
544
  objs: List of model instances to update
540
- fields: List of field names to update
545
+ fields: List of field names to update (auto_now fields already included by executor)
541
546
  batch_size: Number of objects per batch
542
547
 
543
548
  Returns:
@@ -555,28 +560,15 @@ class MTIHandler:
555
560
 
556
561
  batch_size = batch_size or len(objs)
557
562
 
558
- # Handle auto_now fields
559
- for obj in objs:
560
- for model in inheritance_chain:
561
- for field in model._meta.local_fields:
562
- if getattr(field, "auto_now", False):
563
- field.pre_save(obj, add=False)
564
-
565
- # Add auto_now fields to update list
566
- auto_now_fields = set()
567
- for model in inheritance_chain:
568
- for field in model._meta.local_fields:
569
- if getattr(field, "auto_now", False):
570
- auto_now_fields.add(field.name)
571
-
572
- all_fields = list(fields) + list(auto_now_fields)
563
+ # Note: auto_now fields are already handled by executor.bulk_update()
564
+ # which calls pre_save() and includes them in the fields list
573
565
 
574
566
  # Group fields by model
575
567
  field_groups = []
576
568
  for model_idx, model in enumerate(inheritance_chain):
577
569
  model_fields = []
578
570
 
579
- for field_name in all_fields:
571
+ for field_name in fields:
580
572
  try:
581
573
  field = self.model_cls._meta.get_field(field_name)
582
574
  if field in model._meta.local_fields:
@@ -49,8 +49,10 @@ class MTICreatePlan:
49
49
  batch_size: Batch size for operations
50
50
  existing_record_ids: Set of id() of original objects that represent existing DB records
51
51
  update_conflicts: Whether this is an upsert operation
52
- unique_fields: Fields used for conflict detection
53
- update_fields: Fields to update on conflict
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
54
56
  """
55
57
 
56
58
  inheritance_chain: list[Any]
@@ -63,6 +65,8 @@ class MTICreatePlan:
63
65
  update_conflicts: bool = False
64
66
  unique_fields: list[str] = field(default_factory=list)
65
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
66
70
 
67
71
 
68
72
  @dataclass
@@ -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
 
@@ -148,7 +150,7 @@ class HookQuerySet(models.QuerySet):
148
150
  Tuple of (count, details dict)
149
151
  """
150
152
  # Filter queryset to only these objects
151
- pks = [obj.pk for obj in objs if obj.pk is not None]
153
+ pks = extract_pks(objs)
152
154
  if not pks:
153
155
  return 0
154
156
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.59
3
+ Version: 0.2.61
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
@@ -0,0 +1,27 @@
1
+ django_bulk_hooks/__init__.py,sha256=ujBvX-GY4Pg4ACBh7RXm_MOmi2eAowf5s7pG2SXWdpo,2276
2
+ django_bulk_hooks/changeset.py,sha256=opWqb_x-_9KaMsUJc7lI3wScWLHn2Yhe7hB-ngFCBp0,6731
3
+ django_bulk_hooks/conditions.py,sha256=v2DMFmWI7bppBQw5qdbO5CmQRN_QtUwnBjcyKBJLLbw,8030
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=P7cvzFgORJRW-YQHNAxNXqQOP9OywBmA7Rz9kiJoxUk,12237
7
+ django_bulk_hooks/dispatcher.py,sha256=CiKYe5ecUPu5TYUZq8ToaRT40TkLc5l5mczgf5XDzGA,8217
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=38ejMdQ9reYA07_XQ9tC8xv0lW3amO-m8gPzuRNOyj0,4200
11
+ django_bulk_hooks/helpers.py,sha256=e14aYE1lKaj8-krFclY_WfJF2uWQpblfsl5hqsW-dxY,7800
12
+ django_bulk_hooks/manager.py,sha256=g11g1MZ4DJGIM4prYLpYLejTsz0YkYPWeoxWA4dcgYk,4596
13
+ django_bulk_hooks/models.py,sha256=9uh7leV3EEnTWkNKNqT1xevamPTczRhW7KbnIHraBlk,2969
14
+ django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
+ django_bulk_hooks/operations/analyzer.py,sha256=wO-LUgGExE8y3fb25kdfpbj_KdSIW8fd4QFp0Os8Muk,10679
16
+ django_bulk_hooks/operations/bulk_executor.py,sha256=byo09_65qQI_Z-7HvIoeWh-fRAFnI6qrWFjDr2Ar8LA,26275
17
+ django_bulk_hooks/operations/coordinator.py,sha256=d7DD_CMUA4yeUJeJueBOQ9LrJFFj3emVx56hG1Ju5Xg,33787
18
+ django_bulk_hooks/operations/field_utils.py,sha256=M1HfBj5EK5c6SbOmkVT9s6u_SEOh_N4bheytm5jBD4o,7980
19
+ django_bulk_hooks/operations/mti_handler.py,sha256=LbmbAzowfaQePWjjryHJxogvyDZ4GTL176gc6ezKVYA,25829
20
+ django_bulk_hooks/operations/mti_plans.py,sha256=Vl0lV7AuhmovI0_qcD73KairyPy73l36fJYk8wRBh2g,3770
21
+ django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
22
+ django_bulk_hooks/queryset.py,sha256=8xdA3jV6SeEGzW-av346I85Kq1N1uqt178aEh8vm8v8,5568
23
+ django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
24
+ django_bulk_hooks-0.2.61.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.61.dist-info/METADATA,sha256=UxXjcApyuPWulCJWCm9LJGeI6Krh7bwb8mdOsVMfmc0,9265
26
+ django_bulk_hooks-0.2.61.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.61.dist-info/RECORD,,
@@ -1,27 +0,0 @@
1
- django_bulk_hooks/__init__.py,sha256=ujBvX-GY4Pg4ACBh7RXm_MOmi2eAowf5s7pG2SXWdpo,2276
2
- django_bulk_hooks/changeset.py,sha256=wU6wckJEQ_hG5UZq_9g_kWODoKwEj99JrtacVUQ9BxA,7445
3
- django_bulk_hooks/conditions.py,sha256=v2DMFmWI7bppBQw5qdbO5CmQRN_QtUwnBjcyKBJLLbw,8030
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=P7cvzFgORJRW-YQHNAxNXqQOP9OywBmA7Rz9kiJoxUk,12237
7
- django_bulk_hooks/dispatcher.py,sha256=CiKYe5ecUPu5TYUZq8ToaRT40TkLc5l5mczgf5XDzGA,8217
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=38ejMdQ9reYA07_XQ9tC8xv0lW3amO-m8gPzuRNOyj0,4200
11
- django_bulk_hooks/helpers.py,sha256=Nw8eXryLUUquW7AgiuKp0PQT3Pq6HAHsdP-xAtqhmjA,3216
12
- django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,4068
13
- django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
14
- django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
- django_bulk_hooks/operations/analyzer.py,sha256=wAG8sAG9NwfwNqG9z81VfGR7AANDzRmMGE_o82MWji4,10689
16
- django_bulk_hooks/operations/bulk_executor.py,sha256=Y-wkvuV_X-SZmI965JVrrtwbzPZVggUfy8mR1pzP9d0,27048
17
- django_bulk_hooks/operations/coordinator.py,sha256=1Ka5eZJXTFjx3tr-BD6Tr350Y2T57SUOX3vjagBYBvM,32193
18
- django_bulk_hooks/operations/field_utils.py,sha256=Tvr5bcZLG8imH-r2S85oui1Cbw6hGv3VtuIMn4OvsU4,2895
19
- django_bulk_hooks/operations/mti_handler.py,sha256=173jghcxCE5UEZxM1QJRS-lWg0-KJxCQCbWHVKppIEM,26000
20
- django_bulk_hooks/operations/mti_plans.py,sha256=7STQ2oA2ZT8cEG3-t-6xciRAdf7OeSf0gRLXR_BRG-Q,3363
21
- django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
22
- django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
23
- django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
24
- django_bulk_hooks-0.2.59.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
- django_bulk_hooks-0.2.59.dist-info/METADATA,sha256=Zqy1xIlJ4pSLl-rxnjA96tNhdgomhT21cnHI7Z9ZbE8,9265
26
- django_bulk_hooks-0.2.59.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- django_bulk_hooks-0.2.59.dist-info/RECORD,,