django-bulk-hooks 0.2.50__tar.gz → 0.2.52__tar.gz
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-0.2.50 → django_bulk_hooks-0.2.52}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/bulk_executor.py +4 -2
- django_bulk_hooks-0.2.52/django_bulk_hooks/operations/field_utils.py +94 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/mti_handler.py +19 -25
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/record_classifier.py +9 -2
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/LICENSE +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/README.md +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -10,6 +10,8 @@ from django.db import transaction
|
|
|
10
10
|
from django.db.models import AutoField, ForeignKey, Case, When, Value
|
|
11
11
|
from django.db.models.functions import Cast
|
|
12
12
|
|
|
13
|
+
from django_bulk_hooks.operations.field_utils import get_field_value_for_db
|
|
14
|
+
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -475,8 +477,8 @@ class BulkExecutor:
|
|
|
475
477
|
if obj_pk is None:
|
|
476
478
|
continue
|
|
477
479
|
|
|
478
|
-
# Get the field value
|
|
479
|
-
value =
|
|
480
|
+
# Get the field value using centralized field extraction
|
|
481
|
+
value = get_field_value_for_db(obj, field_name, obj.__class__)
|
|
480
482
|
|
|
481
483
|
# Handle NULL values specially for ForeignKey fields
|
|
482
484
|
if is_fk and value is None:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for field value extraction and normalization.
|
|
3
|
+
|
|
4
|
+
Single source of truth for converting model instance field values
|
|
5
|
+
to their database representation. This eliminates duplicate FK handling
|
|
6
|
+
logic scattered across multiple components.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from django.db.models import ForeignKey
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_field_value_for_db(obj, field_name, model_cls=None):
|
|
17
|
+
"""
|
|
18
|
+
Get a field value from an object in its database-ready form.
|
|
19
|
+
|
|
20
|
+
For regular fields: returns the field value as-is
|
|
21
|
+
For FK fields: returns the PK (integer) instead of the related object
|
|
22
|
+
|
|
23
|
+
This ensures consistent handling across all operations (upsert classification,
|
|
24
|
+
bulk create, bulk update, etc.)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
obj: Model instance
|
|
28
|
+
field_name: Name of the field to extract
|
|
29
|
+
model_cls: Model class (optional, will infer from obj if not provided)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Database-ready value (FK as integer, regular fields as-is)
|
|
33
|
+
"""
|
|
34
|
+
if model_cls is None:
|
|
35
|
+
model_cls = obj.__class__
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
field = model_cls._meta.get_field(field_name)
|
|
39
|
+
except Exception: # noqa: BLE001
|
|
40
|
+
# Field doesn't exist - just get attribute
|
|
41
|
+
return getattr(obj, field_name, None)
|
|
42
|
+
|
|
43
|
+
# Check if it's a ForeignKey
|
|
44
|
+
if isinstance(field, ForeignKey):
|
|
45
|
+
# For FK fields, always use attname (e.g., 'business_id' not 'business')
|
|
46
|
+
# This gets the raw FK ID value from the database field
|
|
47
|
+
return getattr(obj, field.attname, None)
|
|
48
|
+
|
|
49
|
+
# Regular field - get normally
|
|
50
|
+
return getattr(obj, field_name, None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_field_values_for_db(obj, field_names, model_cls=None):
|
|
54
|
+
"""
|
|
55
|
+
Get multiple field values from an object in database-ready form.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
obj: Model instance
|
|
59
|
+
field_names: List of field names
|
|
60
|
+
model_cls: Model class (optional)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dict of {field_name: db_value}
|
|
64
|
+
"""
|
|
65
|
+
if model_cls is None:
|
|
66
|
+
model_cls = obj.__class__
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
field_name: get_field_value_for_db(obj, field_name, model_cls)
|
|
70
|
+
for field_name in field_names
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_field_name_to_db(field_name, model_cls):
|
|
75
|
+
"""
|
|
76
|
+
Normalize a field name to its database column name.
|
|
77
|
+
|
|
78
|
+
For FK fields referenced by relationship name, returns the attname (e.g., 'business' -> 'business_id')
|
|
79
|
+
For regular fields, returns as-is.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
field_name: Field name (can be 'business' or 'business_id')
|
|
83
|
+
model_cls: Model class
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Database column name
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
field = model_cls._meta.get_field(field_name)
|
|
90
|
+
if isinstance(field, ForeignKey):
|
|
91
|
+
return field.attname # Returns 'business_id' for 'business' field
|
|
92
|
+
return field_name
|
|
93
|
+
except Exception: # noqa: BLE001
|
|
94
|
+
return field_name
|
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -11,6 +11,8 @@ import logging
|
|
|
11
11
|
|
|
12
12
|
from django.db.models import AutoField
|
|
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
|
|
|
@@ -119,26 +121,29 @@ class MTIHandler:
|
|
|
119
121
|
- If parent exists but child doesn't: creating child for existing parent → AFTER_UPDATE
|
|
120
122
|
- If neither exists: creating both parent and child → AFTER_CREATE
|
|
121
123
|
|
|
122
|
-
Therefore, we
|
|
123
|
-
|
|
124
|
+
Therefore, we find the model that contains all the unique fields, regardless
|
|
125
|
+
of whether it's the parent or child model.
|
|
124
126
|
|
|
125
127
|
Args:
|
|
126
128
|
unique_fields: List of field names forming the unique constraint
|
|
127
129
|
|
|
128
130
|
Returns:
|
|
129
|
-
Model class to query for existing records (
|
|
131
|
+
Model class to query for existing records (model containing unique fields)
|
|
130
132
|
"""
|
|
131
133
|
if not unique_fields:
|
|
132
134
|
return self.model_cls
|
|
133
135
|
|
|
134
136
|
inheritance_chain = self.get_inheritance_chain()
|
|
135
137
|
|
|
136
|
-
# For MTI models
|
|
137
|
-
# This ensures we check if the parent exists, which determines create vs update hooks
|
|
138
|
+
# For MTI models, find the model in the chain that contains ALL unique fields
|
|
138
139
|
if len(inheritance_chain) > 1:
|
|
139
|
-
|
|
140
|
+
# Walk through inheritance chain from child to parent
|
|
141
|
+
for model in reversed(inheritance_chain): # Start with child, end with root parent
|
|
142
|
+
model_field_names = {f.name for f in model._meta.local_fields}
|
|
143
|
+
if all(field in model_field_names for field in unique_fields):
|
|
144
|
+
return model
|
|
140
145
|
|
|
141
|
-
# For non-MTI models
|
|
146
|
+
# For non-MTI models or as fallback
|
|
142
147
|
return self.model_cls
|
|
143
148
|
|
|
144
149
|
# ==================== MTI BULK CREATE PLANNING ====================
|
|
@@ -354,19 +359,13 @@ class MTIHandler:
|
|
|
354
359
|
"""
|
|
355
360
|
parent_obj = parent_model()
|
|
356
361
|
|
|
357
|
-
# Copy field values from source
|
|
362
|
+
# Copy field values from source using centralized field extraction
|
|
358
363
|
for field in parent_model._meta.local_fields:
|
|
359
364
|
if hasattr(source_obj, field.name):
|
|
360
|
-
|
|
365
|
+
# Use centralized field value extraction for consistent FK handling
|
|
366
|
+
value = get_field_value_for_db(source_obj, field.name, source_obj.__class__)
|
|
361
367
|
if value is not None:
|
|
362
|
-
|
|
363
|
-
# Handle FK fields
|
|
364
|
-
if hasattr(value, "pk") and value.pk is not None:
|
|
365
|
-
setattr(parent_obj, field.attname, value.pk)
|
|
366
|
-
else:
|
|
367
|
-
setattr(parent_obj, field.attname, value)
|
|
368
|
-
else:
|
|
369
|
-
setattr(parent_obj, field.name, value)
|
|
368
|
+
setattr(parent_obj, field.attname, value)
|
|
370
369
|
|
|
371
370
|
# Link to parent if exists
|
|
372
371
|
if current_parent is not None:
|
|
@@ -419,15 +418,10 @@ class MTIHandler:
|
|
|
419
418
|
continue
|
|
420
419
|
|
|
421
420
|
if hasattr(source_obj, field.name):
|
|
422
|
-
|
|
421
|
+
# Use centralized field value extraction for consistent FK handling
|
|
422
|
+
value = get_field_value_for_db(source_obj, field.name, source_obj.__class__)
|
|
423
423
|
if value is not None:
|
|
424
|
-
|
|
425
|
-
if hasattr(value, "pk") and value.pk is not None:
|
|
426
|
-
setattr(child_obj, field.attname, value.pk)
|
|
427
|
-
else:
|
|
428
|
-
setattr(child_obj, field.attname, value)
|
|
429
|
-
else:
|
|
430
|
-
setattr(child_obj, field.name, value)
|
|
424
|
+
setattr(child_obj, field.attname, value)
|
|
431
425
|
|
|
432
426
|
# Copy object state
|
|
433
427
|
if hasattr(source_obj, "_state") and hasattr(child_obj, "_state"):
|
|
@@ -11,6 +11,8 @@ 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
|
|
|
@@ -61,17 +63,22 @@ class RecordClassifier:
|
|
|
61
63
|
for obj in objs:
|
|
62
64
|
# Build lookup dict for this object's unique fields
|
|
63
65
|
lookup = {}
|
|
66
|
+
normalized_values = []
|
|
67
|
+
|
|
64
68
|
for field_name in unique_fields:
|
|
65
|
-
value
|
|
69
|
+
# Use centralized field value extraction for consistent FK handling
|
|
70
|
+
value = get_field_value_for_db(obj, field_name, query_model)
|
|
66
71
|
if value is None:
|
|
67
72
|
# Can't match on None values
|
|
68
73
|
break
|
|
69
74
|
lookup[field_name] = value
|
|
75
|
+
normalized_values.append(value)
|
|
70
76
|
else:
|
|
71
77
|
# All unique fields have values, add to query
|
|
72
78
|
if lookup:
|
|
73
79
|
queries.append(Q(**lookup))
|
|
74
|
-
|
|
80
|
+
# Store normalized values for comparison with database results
|
|
81
|
+
obj_to_unique_values[id(obj)] = tuple(normalized_values)
|
|
75
82
|
|
|
76
83
|
if not queries:
|
|
77
84
|
return set(), {}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.50 → django_bulk_hooks-0.2.52}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|