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

@@ -1,50 +1,53 @@
1
- from django_bulk_hooks.constants import (
2
- AFTER_CREATE,
3
- AFTER_DELETE,
4
- AFTER_UPDATE,
5
- BEFORE_CREATE,
6
- BEFORE_DELETE,
7
- BEFORE_UPDATE,
8
- VALIDATE_CREATE,
9
- VALIDATE_DELETE,
10
- VALIDATE_UPDATE,
11
- )
12
- from django_bulk_hooks.conditions import (
13
- ChangesTo,
14
- HasChanged,
15
- IsEqual,
16
- IsNotEqual,
17
- WasEqual,
18
- safe_get_related_object,
19
- safe_get_related_attr,
20
- is_field_set,
21
- )
22
- from django_bulk_hooks.decorators import hook, select_related
23
- from django_bulk_hooks.handler import HookHandler
24
- from django_bulk_hooks.models import HookModelMixin
25
- from django_bulk_hooks.enums import Priority
26
-
27
- __all__ = [
28
- "HookHandler",
29
- "HookModelMixin",
30
- "BEFORE_CREATE",
31
- "AFTER_CREATE",
32
- "BEFORE_UPDATE",
33
- "AFTER_UPDATE",
34
- "BEFORE_DELETE",
35
- "AFTER_DELETE",
36
- "VALIDATE_CREATE",
37
- "VALIDATE_UPDATE",
38
- "VALIDATE_DELETE",
39
- "safe_get_related_object",
40
- "safe_get_related_attr",
41
- "is_field_set",
42
- "Priority",
43
- "hook",
44
- "select_related",
45
- "ChangesTo",
46
- "HasChanged",
47
- "IsEqual",
48
- "IsNotEqual",
49
- "WasEqual",
50
- ]
1
+ import logging
2
+
3
+ from django_bulk_hooks.changeset import ChangeSet
4
+ from django_bulk_hooks.changeset import RecordChange
5
+ from django_bulk_hooks.constants import DEFAULT_BULK_UPDATE_BATCH_SIZE
6
+ from django_bulk_hooks.dispatcher import HookDispatcher
7
+ from django_bulk_hooks.dispatcher import get_dispatcher
8
+ from django_bulk_hooks.factory import clear_hook_factories
9
+ from django_bulk_hooks.factory import configure_hook_container
10
+ from django_bulk_hooks.factory import configure_nested_container
11
+ from django_bulk_hooks.factory import create_hook_instance
12
+ from django_bulk_hooks.factory import is_container_configured
13
+ from django_bulk_hooks.factory import set_default_hook_factory
14
+ from django_bulk_hooks.factory import set_hook_factory
15
+ from django_bulk_hooks.handler import Hook as HookClass
16
+ from django_bulk_hooks.helpers import build_changeset_for_create
17
+ from django_bulk_hooks.helpers import build_changeset_for_delete
18
+ from django_bulk_hooks.helpers import build_changeset_for_update
19
+ from django_bulk_hooks.helpers import dispatch_hooks_for_operation
20
+ from django_bulk_hooks.manager import BulkHookManager
21
+ from django_bulk_hooks.operations import BulkExecutor
22
+
23
+ # Service layer (NEW architecture)
24
+ from django_bulk_hooks.operations import BulkOperationCoordinator
25
+ from django_bulk_hooks.operations import ModelAnalyzer
26
+ from django_bulk_hooks.operations import MTIHandler
27
+
28
+ __all__ = [
29
+ "BulkHookManager",
30
+ "HookClass",
31
+ "set_hook_factory",
32
+ "set_default_hook_factory",
33
+ "configure_hook_container",
34
+ "configure_nested_container",
35
+ "clear_hook_factories",
36
+ "create_hook_instance",
37
+ "is_container_configured",
38
+ "DEFAULT_BULK_UPDATE_BATCH_SIZE",
39
+ # Dispatcher-centric architecture
40
+ "ChangeSet",
41
+ "RecordChange",
42
+ "get_dispatcher",
43
+ "HookDispatcher",
44
+ "build_changeset_for_create",
45
+ "build_changeset_for_update",
46
+ "build_changeset_for_delete",
47
+ "dispatch_hooks_for_operation",
48
+ # Service layer (composition-based architecture)
49
+ "BulkOperationCoordinator",
50
+ "ModelAnalyzer",
51
+ "BulkExecutor",
52
+ "MTIHandler",
53
+ ]
@@ -0,0 +1,214 @@
1
+ """
2
+ ChangeSet and RecordChange classes for Salesforce-style hook context.
3
+
4
+ Provides a first-class abstraction for tracking changes in bulk operations,
5
+ similar to Salesforce's Hook.new, Hook.old, and Hook.newMap.
6
+ """
7
+
8
+
9
+ class RecordChange:
10
+ """
11
+ Represents a single record change with old/new state.
12
+
13
+ Similar to accessing Hook.newMap.get(id) in Salesforce, but with
14
+ additional conveniences like O(1) field change detection.
15
+ """
16
+
17
+ def __init__(self, new_record, old_record=None, changed_fields=None):
18
+ """
19
+ Initialize a RecordChange.
20
+
21
+ Args:
22
+ new_record: The new/current state of the record
23
+ old_record: The old/previous state of the record (None for creates)
24
+ changed_fields: Optional pre-computed set of changed field names.
25
+ If None, will be computed lazily on first access.
26
+ """
27
+ self.new_record = new_record
28
+ self.old_record = old_record
29
+ self._changed_fields = changed_fields
30
+ self._pk = getattr(new_record, "pk", None) if new_record else None
31
+
32
+ @property
33
+ def pk(self):
34
+ """Primary key of the record."""
35
+ return self._pk
36
+
37
+ @property
38
+ def changed_fields(self):
39
+ """
40
+ Set of field names that have changed.
41
+
42
+ Computed lazily on first access and cached for O(1) subsequent checks.
43
+ """
44
+ if self._changed_fields is None:
45
+ self._changed_fields = self._compute_changed_fields()
46
+ return self._changed_fields
47
+
48
+ def has_changed(self, field_name):
49
+ """
50
+ O(1) check if a specific field has changed.
51
+
52
+ Args:
53
+ field_name: Name of the field to check
54
+
55
+ Returns:
56
+ True if the field value changed, False otherwise
57
+ """
58
+ return field_name in self.changed_fields
59
+
60
+ def get_old_value(self, field_name):
61
+ """
62
+ Get the old value for a field.
63
+
64
+ Args:
65
+ field_name: Name of the field
66
+
67
+ Returns:
68
+ The old value, or None if no old record exists
69
+ """
70
+ if self.old_record is None:
71
+ return None
72
+ return getattr(self.old_record, field_name, None)
73
+
74
+ def get_new_value(self, field_name):
75
+ """
76
+ Get the new value for a field.
77
+
78
+ Args:
79
+ field_name: Name of the field
80
+
81
+ Returns:
82
+ The new value
83
+ """
84
+ return getattr(self.new_record, field_name, None)
85
+
86
+ def _compute_changed_fields(self):
87
+ """
88
+ Compute which fields have changed between old and new records.
89
+
90
+ Uses Django's field.get_prep_value() for proper comparison that
91
+ handles database-level transformations.
92
+
93
+ Returns:
94
+ Set of field names that have changed
95
+ """
96
+ if self.old_record is None:
97
+ return set()
98
+
99
+ # Import here to avoid circular dependency
100
+ from .operations.field_utils import get_changed_fields
101
+
102
+ model_cls = self.new_record.__class__
103
+ return get_changed_fields(self.old_record, self.new_record, model_cls)
104
+
105
+
106
+ class ChangeSet:
107
+ """
108
+ Collection of RecordChanges for a bulk operation.
109
+
110
+ Similar to Salesforce's Hook context (Hook.new, Hook.old, Hook.newMap),
111
+ but enhanced for Python's bulk operations paradigm with O(1) lookups and
112
+ additional metadata.
113
+ """
114
+
115
+ def __init__(self, model_cls, changes, operation_type, operation_meta=None):
116
+ """
117
+ Initialize a ChangeSet.
118
+
119
+ Args:
120
+ model_cls: The Django model class
121
+ changes: List of RecordChange instances
122
+ operation_type: Type of operation ('create', 'update', 'delete')
123
+ operation_meta: Optional dict of additional metadata (e.g., update_kwargs)
124
+ """
125
+ self.model_cls = model_cls
126
+ self.changes = changes # List[RecordChange]
127
+ self.operation_type = operation_type
128
+ self.operation_meta = operation_meta or {}
129
+
130
+ # Build PK -> RecordChange map for O(1) lookups (like Hook.newMap)
131
+ self._pk_to_change = {c.pk: c for c in changes if c.pk is not None}
132
+
133
+ @property
134
+ def new_records(self):
135
+ """
136
+ List of new/current record states.
137
+
138
+ Similar to Hook.new in Salesforce.
139
+ """
140
+ return [c.new_record for c in self.changes if c.new_record is not None]
141
+
142
+ @property
143
+ def old_records(self):
144
+ """
145
+ List of old/previous record states.
146
+
147
+ Similar to Hook.old in Salesforce.
148
+ Only includes records that have old states (excludes creates).
149
+ """
150
+ return [c.old_record for c in self.changes if c.old_record is not None]
151
+
152
+ def has_field_changed(self, pk, field_name):
153
+ """
154
+ O(1) check if a field changed for a specific record.
155
+
156
+ Args:
157
+ pk: Primary key of the record
158
+ field_name: Name of the field to check
159
+
160
+ Returns:
161
+ True if the field changed, False otherwise
162
+ """
163
+ change = self._pk_to_change.get(pk)
164
+ return change.has_changed(field_name) if change else False
165
+
166
+ def get_old_value(self, pk, field_name):
167
+ """
168
+ Get the old value for a specific record and field.
169
+
170
+ Args:
171
+ pk: Primary key of the record
172
+ field_name: Name of the field
173
+
174
+ Returns:
175
+ The old value, or None if not found
176
+ """
177
+ change = self._pk_to_change.get(pk)
178
+ return change.get_old_value(field_name) if change else None
179
+
180
+ def get_new_value(self, pk, field_name):
181
+ """
182
+ Get the new value for a specific record and field.
183
+
184
+ Args:
185
+ pk: Primary key of the record
186
+ field_name: Name of the field
187
+
188
+ Returns:
189
+ The new value, or None if not found
190
+ """
191
+ change = self._pk_to_change.get(pk)
192
+ return change.get_new_value(field_name) if change else None
193
+
194
+ def chunk(self, chunk_size):
195
+ """
196
+ Split ChangeSet into smaller chunks for memory-efficient processing.
197
+
198
+ Useful for processing very large bulk operations without loading
199
+ all data into memory at once.
200
+
201
+ Args:
202
+ chunk_size: Number of changes per chunk
203
+
204
+ Yields:
205
+ ChangeSet instances, each with up to chunk_size changes
206
+ """
207
+ for i in range(0, len(self.changes), chunk_size):
208
+ chunk_changes = self.changes[i : i + chunk_size]
209
+ yield ChangeSet(
210
+ self.model_cls,
211
+ chunk_changes,
212
+ self.operation_type,
213
+ self.operation_meta,
214
+ )