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,130 +1,134 @@
1
- from django.db import models
2
-
3
- from django_bulk_hooks.queryset import HookQuerySet
4
-
5
-
6
- class BulkHookManager(models.Manager):
7
- """
8
- Manager that provides hook-aware bulk operations.
9
-
10
- This is a simple facade that returns HookQuerySet,
11
- delegating all bulk operations to it.
12
- """
13
-
14
- def get_queryset(self):
15
- """
16
- Return a HookQuerySet for this manager.
17
-
18
- This ensures all bulk operations go through the coordinator.
19
- """
20
- base_queryset = super().get_queryset()
21
-
22
- # If the base queryset is already a HookQuerySet, return it as-is
23
- if isinstance(base_queryset, HookQuerySet):
24
- return base_queryset
25
-
26
- # Otherwise, create a new HookQuerySet with the same parameters
27
- return HookQuerySet(
28
- model=base_queryset.model,
29
- query=base_queryset.query,
30
- using=base_queryset._db,
31
- hints=base_queryset._hints,
32
- )
33
-
34
- def bulk_create(
35
- self,
36
- objs,
37
- batch_size=None,
38
- ignore_conflicts=False,
39
- update_conflicts=False,
40
- update_fields=None,
41
- unique_fields=None,
42
- bypass_hooks=False,
43
- bypass_validation=False,
44
- **kwargs,
45
- ):
46
- """
47
- Delegate to QuerySet's bulk_create implementation.
48
- This follows Django's pattern where Manager methods call QuerySet methods.
49
- """
50
- return self.get_queryset().bulk_create(
51
- objs,
52
- bypass_hooks=bypass_hooks,
53
- bypass_validation=bypass_validation,
54
- batch_size=batch_size,
55
- ignore_conflicts=ignore_conflicts,
56
- update_conflicts=update_conflicts,
57
- update_fields=update_fields,
58
- unique_fields=unique_fields,
59
- **kwargs,
60
- )
61
-
62
- def bulk_update(
63
- self,
64
- objs,
65
- fields=None,
66
- bypass_hooks=False,
67
- bypass_validation=False,
68
- **kwargs,
69
- ):
70
- """
71
- Delegate to QuerySet's bulk_update implementation.
72
- This follows Django's pattern where Manager methods call QuerySet methods.
73
-
74
- Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
75
- are not supported by bulk_update and will be ignored with a warning.
76
- These parameters are only available in bulk_create for UPSERT operations.
77
- """
78
- if fields is not None:
79
- kwargs["fields"] = fields
80
- return self.get_queryset().bulk_update(
81
- objs,
82
- bypass_hooks=bypass_hooks,
83
- bypass_validation=bypass_validation,
84
- **kwargs,
85
- )
86
-
87
- def bulk_delete(
88
- self,
89
- objs,
90
- batch_size=None,
91
- bypass_hooks=False,
92
- bypass_validation=False,
93
- **kwargs,
94
- ):
95
- """
96
- Delegate to QuerySet's bulk_delete implementation.
97
- This follows Django's pattern where Manager methods call QuerySet methods.
98
- """
99
- return self.get_queryset().bulk_delete(
100
- objs,
101
- bypass_hooks=bypass_hooks,
102
- bypass_validation=bypass_validation,
103
- batch_size=batch_size,
104
- **kwargs,
105
- )
106
-
107
- def delete(self):
108
- """
109
- Delegate to QuerySet's delete implementation.
110
- This follows Django's pattern where Manager methods call QuerySet methods.
111
- """
112
- return self.get_queryset().delete()
113
-
114
- def update(self, **kwargs):
115
- """
116
- Delegate to QuerySet's update implementation.
117
- This follows Django's pattern where Manager methods call QuerySet methods.
118
- """
119
- return self.get_queryset().update(**kwargs)
120
-
121
- def save(self, obj):
122
- """
123
- Save a single object using the appropriate bulk operation.
124
- """
125
- if obj.pk:
126
- # bulk_update now auto-detects changed fields
127
- self.bulk_update([obj])
128
- else:
129
- self.bulk_create([obj])
130
- return obj
1
+ from django.db import models
2
+
3
+ from django_bulk_hooks.queryset import HookQuerySet
4
+
5
+
6
+ def _delegate_to_queryset(self, method_name, *args, **kwargs):
7
+ """
8
+ Generic delegation to queryset method.
9
+
10
+ Args:
11
+ method_name: Name of the method to call on the queryset
12
+ *args, **kwargs: Arguments to pass to the method
13
+
14
+ Returns:
15
+ Result of the queryset method call
16
+ """
17
+ return getattr(self.get_queryset(), method_name)(*args, **kwargs)
18
+
19
+
20
+ class BulkHookManager(models.Manager):
21
+ """
22
+ Manager that provides hook-aware bulk operations.
23
+
24
+ This manager automatically applies hook functionality to its querysets.
25
+ It can be used as a base class or composed with other managers using
26
+ the queryset-based approach.
27
+ """
28
+
29
+ def get_queryset(self):
30
+ """
31
+ Return a HookQuerySet for this manager.
32
+
33
+ Uses the new with_hooks() method for better composition with other managers.
34
+ """
35
+ base_queryset = super().get_queryset()
36
+ return HookQuerySet.with_hooks(base_queryset)
37
+
38
+ def bulk_create(
39
+ self,
40
+ objs,
41
+ batch_size=None,
42
+ ignore_conflicts=False,
43
+ update_conflicts=False,
44
+ update_fields=None,
45
+ unique_fields=None,
46
+ bypass_hooks=False,
47
+ **kwargs,
48
+ ):
49
+ """
50
+ Delegate to QuerySet's bulk_create implementation.
51
+ This follows Django's pattern where Manager methods call QuerySet methods.
52
+ """
53
+ return _delegate_to_queryset(
54
+ self,
55
+ "bulk_create",
56
+ objs,
57
+ batch_size=batch_size,
58
+ ignore_conflicts=ignore_conflicts,
59
+ update_conflicts=update_conflicts,
60
+ update_fields=update_fields,
61
+ unique_fields=unique_fields,
62
+ bypass_hooks=bypass_hooks,
63
+ **kwargs,
64
+ )
65
+
66
+ def bulk_update(
67
+ self,
68
+ objs,
69
+ fields=None,
70
+ bypass_hooks=False,
71
+ **kwargs,
72
+ ):
73
+ """
74
+ Delegate to QuerySet's bulk_update implementation.
75
+ This follows Django's pattern where Manager methods call QuerySet methods.
76
+
77
+ Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
78
+ are not supported by bulk_update and will be ignored with a warning.
79
+ These parameters are only available in bulk_create for UPSERT operations.
80
+ """
81
+ if fields is not None:
82
+ kwargs["fields"] = fields
83
+ return _delegate_to_queryset(
84
+ self,
85
+ "bulk_update",
86
+ objs,
87
+ bypass_hooks=bypass_hooks,
88
+ **kwargs,
89
+ )
90
+
91
+ def bulk_delete(
92
+ self,
93
+ objs,
94
+ batch_size=None,
95
+ bypass_hooks=False,
96
+ **kwargs,
97
+ ):
98
+ """
99
+ Delegate to QuerySet's bulk_delete implementation.
100
+ This follows Django's pattern where Manager methods call QuerySet methods.
101
+ """
102
+ return _delegate_to_queryset(
103
+ self,
104
+ "bulk_delete",
105
+ objs,
106
+ batch_size=batch_size,
107
+ bypass_hooks=bypass_hooks,
108
+ **kwargs,
109
+ )
110
+
111
+ def delete(self, bypass_hooks=False):
112
+ """
113
+ Delegate to QuerySet's delete implementation.
114
+ This follows Django's pattern where Manager methods call QuerySet methods.
115
+ """
116
+ return _delegate_to_queryset(self, "delete", bypass_hooks=bypass_hooks)
117
+
118
+ def update(self, bypass_hooks=False, **kwargs):
119
+ """
120
+ Delegate to QuerySet's update implementation.
121
+ This follows Django's pattern where Manager methods call QuerySet methods.
122
+ """
123
+ return _delegate_to_queryset(self, "update", bypass_hooks=bypass_hooks, **kwargs)
124
+
125
+ def save(self, obj, bypass_hooks=False):
126
+ """
127
+ Save a single object using the appropriate bulk operation.
128
+ """
129
+ if obj.pk:
130
+ # bulk_update now auto-detects changed fields
131
+ self.bulk_update([obj], bypass_hooks=bypass_hooks)
132
+ else:
133
+ self.bulk_create([obj], bypass_hooks=bypass_hooks)
134
+ return obj
@@ -1,76 +1,89 @@
1
- import logging
2
-
3
- from django.db import models
4
-
5
- from django_bulk_hooks.manager import BulkHookManager
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
-
10
- class HookModelMixin(models.Model):
11
- objects = BulkHookManager()
12
-
13
- class Meta:
14
- abstract = True
15
-
16
- def clean(self, bypass_hooks=False):
17
- """
18
- Override clean() to hook validation hooks.
19
- This ensures that when Django calls clean() (like in admin forms),
20
- it hooks the VALIDATE_* hooks for validation only.
21
- """
22
- super().clean()
23
-
24
- # If bypass_hooks is True, skip validation hooks
25
- if bypass_hooks:
26
- return
27
-
28
- # Delegate to coordinator (consistent with save/delete)
29
- is_create = self.pk is None
30
- self.__class__.objects.get_queryset().coordinator.clean(
31
- [self], is_create=is_create
32
- )
33
-
34
- def save(self, *args, bypass_hooks=False, **kwargs):
35
- """
36
- Save the model instance.
37
-
38
- Delegates to bulk_create/bulk_update which handle all hook logic
39
- including MTI parent hooks.
40
- """
41
- if bypass_hooks:
42
- # Use super().save() to call Django's default save without our hook logic
43
- return super().save(*args, **kwargs)
44
-
45
- is_create = self.pk is None
46
-
47
- if is_create:
48
- # Delegate to bulk_create which handles all hook logic
49
- result = self.__class__.objects.bulk_create([self])
50
- return result[0] if result else self
51
- else:
52
- # Delegate to bulk_update which handles all hook logic
53
- update_fields = kwargs.get("update_fields")
54
- if update_fields is None:
55
- # Update all non-auto fields
56
- update_fields = [
57
- f.name
58
- for f in self.__class__._meta.fields
59
- if not f.auto_created and f.name != "id"
60
- ]
61
- self.__class__.objects.bulk_update([self], update_fields)
62
- return self
63
-
64
- def delete(self, *args, bypass_hooks=False, **kwargs):
65
- """
66
- Delete the model instance.
67
-
68
- Delegates to bulk_delete which handles all hook logic
69
- including MTI parent hooks.
70
- """
71
- if bypass_hooks:
72
- # Use super().delete() to call Django's default delete without our hook logic
73
- return super().delete(*args, **kwargs)
74
-
75
- # Delegate to bulk_delete (handles both MTI and non-MTI)
76
- return self.__class__.objects.filter(pk=self.pk).delete()
1
+ import logging
2
+
3
+ from django.db import models
4
+
5
+ from django_bulk_hooks.manager import BulkHookManager
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class HookModelMixin(models.Model):
11
+ """Mixin providing hook functionality."""
12
+
13
+ objects = BulkHookManager()
14
+
15
+ class Meta:
16
+ abstract = True
17
+
18
+ def clean(self, bypass_hooks=False):
19
+ """
20
+ Override clean() to hook validation hooks.
21
+ This ensures that when Django calls clean() (like in admin forms),
22
+ it hooks the VALIDATE_* hooks for validation only.
23
+ """
24
+ super().clean()
25
+
26
+ # If bypass_hooks is True, skip validation hooks
27
+ if bypass_hooks:
28
+ return
29
+
30
+ # Delegate to coordinator (consistent with save/delete)
31
+ is_create = self.pk is None
32
+ self.__class__.objects.get_queryset().coordinator.clean(
33
+ [self],
34
+ is_create=is_create,
35
+ )
36
+
37
+ def save(self, *args, bypass_hooks=False, **kwargs):
38
+ """
39
+ Save the model instance.
40
+
41
+ Delegates to bulk_create/bulk_update which handle all hook logic
42
+ including MTI parent hooks.
43
+ """
44
+ if bypass_hooks:
45
+ # Use super().save() to call Django's default save without our hook logic
46
+ return super().save(*args, **kwargs)
47
+
48
+ is_create = self.pk is None
49
+
50
+ logger.debug("💾 SAVE_START: model=%s, pk=%s, is_create=%s, __dict__=%s",
51
+ self.__class__.__name__, self.pk, is_create, list(self.__dict__.keys()))
52
+
53
+ if is_create:
54
+ # Delegate to bulk_create which handles all hook logic
55
+ result = self.__class__.objects.bulk_create([self])
56
+ return result[0] if result else self
57
+ # Delegate to bulk_update which handles all hook logic
58
+ update_fields = kwargs.get("update_fields")
59
+ if update_fields is None:
60
+ # Update all non-auto fields
61
+ update_fields = [f.name for f in self.__class__._meta.fields if not f.auto_created and f.name != "id"]
62
+
63
+ logger.debug("💾 SAVE_UPDATE_FIELDS: fields=%s (count=%d)", update_fields, len(update_fields))
64
+
65
+ # Log FK field values before bulk_update
66
+ for field in self.__class__._meta.fields:
67
+ if field.get_internal_type() == 'ForeignKey' and field.name in update_fields:
68
+ fk_id_value = getattr(self, field.attname, 'NO_ATTR')
69
+ fk_obj_value = getattr(self, field.name, 'NO_ATTR')
70
+ logger.debug("💾 SAVE_FK_CHECK: field=%s, %s=%s, %s=%s (has_pk=%s)",
71
+ field.name, field.attname, fk_id_value, field.name, fk_obj_value,
72
+ hasattr(fk_obj_value, 'pk') if fk_obj_value != 'NO_ATTR' else 'N/A')
73
+
74
+ self.__class__.objects.bulk_update([self], update_fields)
75
+ return self
76
+
77
+ def delete(self, *args, bypass_hooks=False, **kwargs):
78
+ """
79
+ Delete the model instance.
80
+
81
+ Delegates to bulk_delete which handles all hook logic
82
+ including MTI parent hooks.
83
+ """
84
+ if bypass_hooks:
85
+ # Use super().delete() to call Django's default delete without our hook logic
86
+ return super().delete(*args, **kwargs)
87
+
88
+ # Delegate to bulk_delete (handles both MTI and non-MTI)
89
+ return self.__class__.objects.filter(pk=self.pk).delete()
@@ -5,14 +5,14 @@ This module contains all services for bulk operations following
5
5
  a clean, service-based architecture.
6
6
  """
7
7
 
8
- from django_bulk_hooks.operations.coordinator import BulkOperationCoordinator
9
8
  from django_bulk_hooks.operations.analyzer import ModelAnalyzer
10
9
  from django_bulk_hooks.operations.bulk_executor import BulkExecutor
10
+ from django_bulk_hooks.operations.coordinator import BulkOperationCoordinator
11
11
  from django_bulk_hooks.operations.mti_handler import MTIHandler
12
12
 
13
13
  __all__ = [
14
- 'BulkOperationCoordinator',
15
- 'ModelAnalyzer',
16
- 'BulkExecutor',
17
- 'MTIHandler',
14
+ "BulkExecutor",
15
+ "BulkOperationCoordinator",
16
+ "MTIHandler",
17
+ "ModelAnalyzer",
18
18
  ]