django-bulk-hooks 0.1.231__py3-none-any.whl → 0.1.233__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.
- django_bulk_hooks/__init__.py +1 -12
- django_bulk_hooks/conditions.py +30 -33
- django_bulk_hooks/context.py +15 -43
- django_bulk_hooks/decorators.py +8 -111
- django_bulk_hooks/engine.py +41 -127
- django_bulk_hooks/enums.py +13 -10
- django_bulk_hooks/handler.py +73 -40
- django_bulk_hooks/manager.py +101 -123
- django_bulk_hooks/models.py +15 -51
- django_bulk_hooks/priority.py +6 -6
- django_bulk_hooks/queryset.py +181 -306
- django_bulk_hooks/registry.py +24 -191
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/METADATA +16 -32
- django_bulk_hooks-0.1.233.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.231.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.233.dist-info}/LICENSE +0 -0
django_bulk_hooks/engine.py
CHANGED
|
@@ -1,59 +1,33 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
3
|
from django.core.exceptions import ValidationError
|
|
4
|
-
from django.db import transaction
|
|
5
|
-
from django.conf import settings
|
|
6
4
|
|
|
7
5
|
from django_bulk_hooks.registry import get_hooks
|
|
8
|
-
from django_bulk_hooks.handler import hook_vars
|
|
9
6
|
|
|
10
7
|
logger = logging.getLogger(__name__)
|
|
11
8
|
|
|
12
9
|
|
|
13
|
-
class AggregatedHookError(Exception):
|
|
14
|
-
"""Raised when multiple hook handlers fail under best-effort policy."""
|
|
15
|
-
|
|
16
|
-
def __init__(self, errors):
|
|
17
|
-
self.errors = errors
|
|
18
|
-
message_lines = [
|
|
19
|
-
"Multiple hook errors occurred:",
|
|
20
|
-
*(f"- {ctx}: {exc}" for ctx, exc in errors),
|
|
21
|
-
]
|
|
22
|
-
super().__init__("\n".join(message_lines))
|
|
23
|
-
|
|
24
|
-
|
|
25
10
|
def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
26
11
|
"""
|
|
27
12
|
Run hooks for a given model, event, and records.
|
|
28
|
-
|
|
29
|
-
Production-grade executor:
|
|
30
|
-
- Honors bypass_hooks via ctx
|
|
31
|
-
- Runs model.clean() before BEFORE_* events for validation
|
|
32
|
-
- Executes hooks in registry priority order with condition filtering
|
|
33
|
-
- Exposes thread-local context via hook_vars during execution
|
|
34
|
-
- AFTER_* timing is configurable via settings.BULK_HOOKS_AFTER_ON_COMMIT (default: False)
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
model_cls: The Django model class
|
|
38
|
-
event: The hook event (e.g., 'before_create', 'after_update')
|
|
39
|
-
new_records: List of new/updated records
|
|
40
|
-
old_records: List of original records (for comparison)
|
|
41
|
-
ctx: Optional hook context
|
|
42
13
|
"""
|
|
43
14
|
if not new_records:
|
|
44
15
|
return
|
|
45
16
|
|
|
17
|
+
# Get hooks for this model and event
|
|
46
18
|
hooks = get_hooks(model_cls, event)
|
|
19
|
+
|
|
47
20
|
if not hooks:
|
|
48
21
|
return
|
|
49
22
|
|
|
50
|
-
|
|
51
|
-
f"Running {len(hooks)} hooks for {model_cls.__name__}.{event} ({len(new_records)} records)"
|
|
52
|
-
)
|
|
23
|
+
import traceback
|
|
53
24
|
|
|
25
|
+
stack = traceback.format_stack()
|
|
26
|
+
logger.debug(f"engine.run {model_cls.__name__}.{event} {len(new_records)} records")
|
|
27
|
+
|
|
54
28
|
# Check if we're in a bypass context
|
|
55
|
-
if ctx and hasattr(ctx,
|
|
56
|
-
logger.debug("
|
|
29
|
+
if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
|
|
30
|
+
logger.debug("engine.run bypassed")
|
|
57
31
|
return
|
|
58
32
|
|
|
59
33
|
# For BEFORE_* events, run model.clean() first for validation
|
|
@@ -65,96 +39,36 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
|
|
|
65
39
|
logger.error("Validation failed for %s: %s", instance, e)
|
|
66
40
|
raise
|
|
67
41
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
handler_instance = handler_cls()
|
|
103
|
-
func = getattr(handler_instance, method_name)
|
|
104
|
-
except Exception as e:
|
|
105
|
-
logger.error(
|
|
106
|
-
"Failed to instantiate %s.%s: %s",
|
|
107
|
-
handler_cls.__name__,
|
|
108
|
-
method_name,
|
|
109
|
-
e,
|
|
110
|
-
)
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
# Condition filtering per record using the deterministic pairs
|
|
114
|
-
to_process_new = []
|
|
115
|
-
to_process_old = []
|
|
116
|
-
for new_obj, old_obj in pairs:
|
|
117
|
-
if not condition:
|
|
118
|
-
to_process_new.append(new_obj)
|
|
119
|
-
to_process_old.append(old_obj)
|
|
120
|
-
else:
|
|
121
|
-
try:
|
|
122
|
-
if condition.check(new_obj, old_obj):
|
|
123
|
-
to_process_new.append(new_obj)
|
|
124
|
-
to_process_old.append(old_obj)
|
|
125
|
-
except Exception as e:
|
|
126
|
-
logger.error(
|
|
127
|
-
"Condition failed for %s.%s: %s",
|
|
128
|
-
handler_cls.__name__,
|
|
129
|
-
method_name,
|
|
130
|
-
e,
|
|
131
|
-
)
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
if not to_process_new:
|
|
135
|
-
continue
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
func(
|
|
139
|
-
new_records=to_process_new,
|
|
140
|
-
old_records=to_process_old if any(x is not None for x in to_process_old) else None,
|
|
141
|
-
)
|
|
142
|
-
except Exception as e:
|
|
143
|
-
logger.exception(
|
|
144
|
-
"Error in hook %s.%s", handler_cls.__name__, method_name
|
|
145
|
-
)
|
|
146
|
-
if failure_policy == "best_effort":
|
|
147
|
-
collected_errors.append((f"{handler_cls.__name__}.{method_name}", e))
|
|
148
|
-
continue
|
|
149
|
-
# fail_fast
|
|
150
|
-
raise
|
|
151
|
-
|
|
152
|
-
if collected_errors:
|
|
153
|
-
raise AggregatedHookError(collected_errors)
|
|
154
|
-
finally:
|
|
155
|
-
hook_vars.event = None
|
|
156
|
-
hook_vars.depth -= 1
|
|
157
|
-
|
|
158
|
-
# Execute immediately so AFTER_* runs within the transaction.
|
|
159
|
-
# If a hook raises, the transaction is rolled back (Salesforce-style).
|
|
160
|
-
_execute()
|
|
42
|
+
# Process hooks
|
|
43
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
44
|
+
logger.debug(f"Processing {handler_cls.__name__}.{method_name}")
|
|
45
|
+
handler_instance = handler_cls()
|
|
46
|
+
func = getattr(handler_instance, method_name)
|
|
47
|
+
|
|
48
|
+
to_process_new = []
|
|
49
|
+
to_process_old = []
|
|
50
|
+
|
|
51
|
+
for new, original in zip(
|
|
52
|
+
new_records,
|
|
53
|
+
old_records or [None] * len(new_records),
|
|
54
|
+
strict=True,
|
|
55
|
+
):
|
|
56
|
+
if not condition:
|
|
57
|
+
to_process_new.append(new)
|
|
58
|
+
to_process_old.append(original)
|
|
59
|
+
else:
|
|
60
|
+
condition_result = condition.check(new, original)
|
|
61
|
+
if condition_result:
|
|
62
|
+
to_process_new.append(new)
|
|
63
|
+
to_process_old.append(original)
|
|
64
|
+
|
|
65
|
+
if to_process_new:
|
|
66
|
+
logger.debug(f"Executing {handler_cls.__name__}.{method_name} for {len(to_process_new)} records")
|
|
67
|
+
try:
|
|
68
|
+
func(
|
|
69
|
+
new_records=to_process_new,
|
|
70
|
+
old_records=to_process_old if any(to_process_old) else None,
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.debug(f"Hook execution failed: {e}")
|
|
74
|
+
raise
|
django_bulk_hooks/enums.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
Compatibility layer for priority-related exports.
|
|
1
|
+
from enum import IntEnum
|
|
3
2
|
|
|
4
|
-
This module re-exports the canonical Priority enum and a DEFAULT_PRIORITY
|
|
5
|
-
constant so existing imports like `from django_bulk_hooks.enums import ...`
|
|
6
|
-
continue to work without churn. Prefer importing from `priority` in new code.
|
|
7
|
-
"""
|
|
8
3
|
|
|
9
|
-
|
|
4
|
+
class Priority(IntEnum):
|
|
5
|
+
"""
|
|
6
|
+
Named priorities for django-bulk-hooks hooks.
|
|
7
|
+
Replaces module-level constants with a clean IntEnum.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
HIGHEST = 0 # runs first
|
|
11
|
+
HIGH = 25 # runs early
|
|
12
|
+
NORMAL = 50 # default ordering
|
|
13
|
+
LOW = 75 # runs late
|
|
14
|
+
LOWEST = 100 # runs last
|
|
10
15
|
|
|
11
|
-
# Default priority used when none is specified by the hook decorator
|
|
12
|
-
DEFAULT_PRIORITY = Priority.NORMAL
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
DEFAULT_PRIORITY = Priority.NORMAL
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -2,7 +2,9 @@ import logging
|
|
|
2
2
|
import threading
|
|
3
3
|
from collections import deque
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from django.db import transaction
|
|
6
|
+
|
|
7
|
+
from django_bulk_hooks.registry import get_hooks, register_hook
|
|
6
8
|
|
|
7
9
|
logger = logging.getLogger(__name__)
|
|
8
10
|
|
|
@@ -59,41 +61,28 @@ class HookContextState:
|
|
|
59
61
|
return hook_vars.model
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
Hook = HookContextState()
|
|
65
|
+
|
|
66
|
+
|
|
62
67
|
class HookMeta(type):
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
_registered = set()
|
|
69
|
+
|
|
65
70
|
def __new__(mcs, name, bases, namespace):
|
|
66
71
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
continue
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
attr = getattr(cls, attr_name)
|
|
76
|
-
if callable(attr) and hasattr(attr, "hooks_hooks"):
|
|
77
|
-
for model_cls, event, condition, priority in attr.hooks_hooks:
|
|
78
|
-
# Register the hook
|
|
72
|
+
for method_name, method in namespace.items():
|
|
73
|
+
if hasattr(method, "hooks_hooks"):
|
|
74
|
+
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
75
|
+
key = (model_cls, event, cls, method_name)
|
|
76
|
+
if key not in HookMeta._registered:
|
|
79
77
|
register_hook(
|
|
80
78
|
model=model_cls,
|
|
81
79
|
event=event,
|
|
82
80
|
handler_cls=cls,
|
|
83
|
-
method_name=
|
|
81
|
+
method_name=method_name,
|
|
84
82
|
condition=condition,
|
|
85
83
|
priority=priority,
|
|
86
84
|
)
|
|
87
|
-
|
|
88
|
-
logger.debug(
|
|
89
|
-
f"Registered hook {cls.__name__}.{attr_name} "
|
|
90
|
-
f"for {model_cls.__name__}.{event} with priority {priority}"
|
|
91
|
-
)
|
|
92
|
-
except Exception as e:
|
|
93
|
-
# Skip attributes that can't be accessed
|
|
94
|
-
logger.debug(f"Skipping attribute {attr_name}: {e}")
|
|
95
|
-
continue
|
|
96
|
-
|
|
85
|
+
HookMeta._registered.add(key)
|
|
97
86
|
return cls
|
|
98
87
|
|
|
99
88
|
|
|
@@ -108,11 +97,16 @@ class Hook(metaclass=HookMeta):
|
|
|
108
97
|
old_records: list = None,
|
|
109
98
|
**kwargs,
|
|
110
99
|
) -> None:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
queue = get_hook_queue()
|
|
101
|
+
queue.append((cls, event, model, new_records, old_records, kwargs))
|
|
102
|
+
|
|
103
|
+
if len(queue) > 1:
|
|
104
|
+
return # nested call, will be processed by outermost
|
|
105
|
+
|
|
106
|
+
# only outermost handle will process the queue
|
|
107
|
+
while queue:
|
|
108
|
+
cls_, event_, model_, new_, old_, kw_ = queue.popleft()
|
|
109
|
+
cls_._process(event_, model_, new_, old_, **kw_)
|
|
116
110
|
|
|
117
111
|
@classmethod
|
|
118
112
|
def _process(
|
|
@@ -123,12 +117,51 @@ class Hook(metaclass=HookMeta):
|
|
|
123
117
|
old_records,
|
|
124
118
|
**kwargs,
|
|
125
119
|
):
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
120
|
+
hook_vars.depth += 1
|
|
121
|
+
hook_vars.new = new_records
|
|
122
|
+
hook_vars.old = old_records
|
|
123
|
+
hook_vars.event = event
|
|
124
|
+
hook_vars.model = model
|
|
125
|
+
|
|
126
|
+
hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
|
|
127
|
+
|
|
128
|
+
def _execute():
|
|
129
|
+
new_local = new_records or []
|
|
130
|
+
old_local = old_records or []
|
|
131
|
+
if len(old_local) < len(new_local):
|
|
132
|
+
old_local += [None] * (len(new_local) - len(old_local))
|
|
133
|
+
|
|
134
|
+
for handler_cls, method_name, condition, priority in hooks:
|
|
135
|
+
if condition is not None:
|
|
136
|
+
checks = [
|
|
137
|
+
condition.check(n, o) for n, o in zip(new_local, old_local)
|
|
138
|
+
]
|
|
139
|
+
if not any(checks):
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
handler = handler_cls()
|
|
143
|
+
method = getattr(handler, method_name)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
method(
|
|
147
|
+
new_records=new_local,
|
|
148
|
+
old_records=old_local,
|
|
149
|
+
**kwargs,
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.exception(
|
|
153
|
+
"Error in hook %s.%s", handler_cls.__name__, method_name
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
conn = transaction.get_connection()
|
|
157
|
+
try:
|
|
158
|
+
if conn.in_atomic_block and event.startswith("after_"):
|
|
159
|
+
transaction.on_commit(_execute)
|
|
160
|
+
else:
|
|
161
|
+
_execute()
|
|
162
|
+
finally:
|
|
163
|
+
hook_vars.new = None
|
|
164
|
+
hook_vars.old = None
|
|
165
|
+
hook_vars.event = None
|
|
166
|
+
hook_vars.model = None
|
|
167
|
+
hook_vars.depth -= 1
|
django_bulk_hooks/manager.py
CHANGED
|
@@ -1,135 +1,113 @@
|
|
|
1
|
-
from typing import Iterable, Sequence, Any
|
|
2
1
|
from django.db import models
|
|
3
2
|
|
|
4
3
|
from django_bulk_hooks.queryset import HookQuerySet, HookQuerySetMixin
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class BulkHookManager(models.Manager):
|
|
8
|
-
|
|
7
|
+
def get_queryset(self):
|
|
8
|
+
# Use super().get_queryset() to let Django and MRO build the queryset
|
|
9
|
+
# This ensures cooperation with other managers
|
|
10
|
+
base_queryset = super().get_queryset()
|
|
11
|
+
|
|
12
|
+
# If the base queryset already has hook functionality, return it as-is
|
|
13
|
+
if isinstance(base_queryset, HookQuerySetMixin):
|
|
14
|
+
return base_queryset
|
|
15
|
+
|
|
16
|
+
# Otherwise, create a new HookQuerySet with the same parameters
|
|
17
|
+
# This is much simpler and avoids dynamic class creation issues
|
|
18
|
+
return HookQuerySet(
|
|
19
|
+
model=base_queryset.model,
|
|
20
|
+
query=base_queryset.query,
|
|
21
|
+
using=base_queryset._db,
|
|
22
|
+
hints=base_queryset._hints
|
|
23
|
+
)
|
|
9
24
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
def bulk_create(
|
|
26
|
+
self,
|
|
27
|
+
objs,
|
|
28
|
+
batch_size=None,
|
|
29
|
+
ignore_conflicts=False,
|
|
30
|
+
update_conflicts=False,
|
|
31
|
+
update_fields=None,
|
|
32
|
+
unique_fields=None,
|
|
33
|
+
bypass_hooks=False,
|
|
34
|
+
bypass_validation=False,
|
|
35
|
+
**kwargs,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Delegate to QuerySet's bulk_create implementation.
|
|
39
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
40
|
+
"""
|
|
41
|
+
return self.get_queryset().bulk_create(
|
|
42
|
+
objs,
|
|
43
|
+
bypass_hooks=bypass_hooks,
|
|
44
|
+
bypass_validation=bypass_validation,
|
|
45
|
+
batch_size=batch_size,
|
|
46
|
+
ignore_conflicts=ignore_conflicts,
|
|
47
|
+
update_conflicts=update_conflicts,
|
|
48
|
+
update_fields=update_fields,
|
|
49
|
+
unique_fields=unique_fields,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
14
52
|
|
|
15
|
-
|
|
16
|
-
|
|
53
|
+
def bulk_update(
|
|
54
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Delegate to QuerySet's bulk_update implementation.
|
|
58
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
59
|
+
"""
|
|
60
|
+
return self.get_queryset().bulk_update(
|
|
61
|
+
objs,
|
|
62
|
+
fields,
|
|
63
|
+
bypass_hooks=bypass_hooks,
|
|
64
|
+
bypass_validation=bypass_validation,
|
|
65
|
+
**kwargs,
|
|
66
|
+
)
|
|
17
67
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
68
|
+
def bulk_delete(
|
|
69
|
+
self,
|
|
70
|
+
objs,
|
|
71
|
+
batch_size=None,
|
|
72
|
+
bypass_hooks=False,
|
|
73
|
+
bypass_validation=False,
|
|
74
|
+
**kwargs,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Delegate to QuerySet's bulk_delete implementation.
|
|
78
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
79
|
+
"""
|
|
80
|
+
return self.get_queryset().bulk_delete(
|
|
81
|
+
objs,
|
|
82
|
+
bypass_hooks=bypass_hooks,
|
|
83
|
+
bypass_validation=bypass_validation,
|
|
84
|
+
batch_size=batch_size,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
21
87
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
88
|
+
def delete(self):
|
|
89
|
+
"""
|
|
90
|
+
Delegate to QuerySet's delete implementation.
|
|
91
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
92
|
+
"""
|
|
93
|
+
return self.get_queryset().delete()
|
|
25
94
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
composed_cls = type(composed_name, (HookQuerySetMixin, base_cls), {})
|
|
33
|
-
self._qs_compose_cache[base_cls] = composed_cls
|
|
95
|
+
def update(self, **kwargs):
|
|
96
|
+
"""
|
|
97
|
+
Delegate to QuerySet's update implementation.
|
|
98
|
+
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
99
|
+
"""
|
|
100
|
+
return self.get_queryset().update(**kwargs)
|
|
34
101
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
update_conflicts: bool = False,
|
|
48
|
-
update_fields: Sequence[str] | None = None,
|
|
49
|
-
unique_fields: Sequence[str] | None = None,
|
|
50
|
-
bypass_hooks: bool = False,
|
|
51
|
-
bypass_validation: bool = False,
|
|
52
|
-
**kwargs: Any,
|
|
53
|
-
) -> list[models.Model]:
|
|
54
|
-
"""
|
|
55
|
-
Delegate to QuerySet's bulk_create implementation.
|
|
56
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
57
|
-
"""
|
|
58
|
-
return self.get_queryset().bulk_create(
|
|
59
|
-
objs,
|
|
60
|
-
bypass_hooks=bypass_hooks,
|
|
61
|
-
bypass_validation=bypass_validation,
|
|
62
|
-
batch_size=batch_size,
|
|
63
|
-
ignore_conflicts=ignore_conflicts,
|
|
64
|
-
update_conflicts=update_conflicts,
|
|
65
|
-
update_fields=update_fields,
|
|
66
|
-
unique_fields=unique_fields,
|
|
67
|
-
**kwargs,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
def bulk_update(
|
|
71
|
-
self,
|
|
72
|
-
objs: Iterable[models.Model],
|
|
73
|
-
fields: Sequence[str],
|
|
74
|
-
bypass_hooks: bool = False,
|
|
75
|
-
bypass_validation: bool = False,
|
|
76
|
-
**kwargs: Any,
|
|
77
|
-
) -> int:
|
|
78
|
-
"""
|
|
79
|
-
Delegate to QuerySet's bulk_update implementation.
|
|
80
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
81
|
-
"""
|
|
82
|
-
return self.get_queryset().bulk_update(
|
|
83
|
-
objs,
|
|
84
|
-
fields,
|
|
85
|
-
bypass_hooks=bypass_hooks,
|
|
86
|
-
bypass_validation=bypass_validation,
|
|
87
|
-
**kwargs,
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
def bulk_delete(
|
|
91
|
-
self,
|
|
92
|
-
objs: Iterable[models.Model],
|
|
93
|
-
batch_size: int | None = None,
|
|
94
|
-
bypass_hooks: bool = False,
|
|
95
|
-
bypass_validation: bool = False,
|
|
96
|
-
**kwargs: Any,
|
|
97
|
-
) -> int:
|
|
98
|
-
"""
|
|
99
|
-
Delegate to QuerySet's bulk_delete implementation.
|
|
100
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
101
|
-
"""
|
|
102
|
-
return self.get_queryset().bulk_delete(
|
|
103
|
-
objs,
|
|
104
|
-
bypass_hooks=bypass_hooks,
|
|
105
|
-
bypass_validation=bypass_validation,
|
|
106
|
-
batch_size=batch_size,
|
|
107
|
-
**kwargs,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
def delete(self) -> int:
|
|
111
|
-
"""
|
|
112
|
-
Delegate to QuerySet's delete implementation.
|
|
113
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
114
|
-
"""
|
|
115
|
-
return self.get_queryset().delete()
|
|
116
|
-
|
|
117
|
-
def update(self, **kwargs: Any) -> int:
|
|
118
|
-
"""
|
|
119
|
-
Delegate to QuerySet's update implementation.
|
|
120
|
-
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
121
|
-
"""
|
|
122
|
-
return self.get_queryset().update(**kwargs)
|
|
123
|
-
|
|
124
|
-
def save(self, obj: models.Model) -> models.Model:
|
|
125
|
-
"""
|
|
126
|
-
Save a single object using the appropriate bulk operation.
|
|
127
|
-
"""
|
|
128
|
-
if obj.pk:
|
|
129
|
-
self.bulk_update(
|
|
130
|
-
[obj],
|
|
131
|
-
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
132
|
-
)
|
|
133
|
-
else:
|
|
134
|
-
self.bulk_create([obj])
|
|
135
|
-
return obj
|
|
102
|
+
def save(self, obj):
|
|
103
|
+
"""
|
|
104
|
+
Save a single object using the appropriate bulk operation.
|
|
105
|
+
"""
|
|
106
|
+
if obj.pk:
|
|
107
|
+
self.bulk_update(
|
|
108
|
+
[obj],
|
|
109
|
+
fields=[field.name for field in obj._meta.fields if field.name != "id"],
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
self.bulk_create([obj])
|
|
113
|
+
return obj
|