django-bulk-hooks 0.1.66__py3-none-any.whl → 0.1.67__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 +3 -2
- django_bulk_hooks/context.py +16 -16
- django_bulk_hooks/engine.py +8 -3
- django_bulk_hooks/handler.py +53 -29
- django_bulk_hooks/manager.py +14 -10
- django_bulk_hooks/models.py +4 -3
- django_bulk_hooks/queryset.py +3 -3
- django_bulk_hooks/registry.py +12 -6
- {django_bulk_hooks-0.1.66.dist-info → django_bulk_hooks-0.1.67.dist-info}/METADATA +20 -21
- django_bulk_hooks-0.1.67.dist-info/RECORD +17 -0
- {django_bulk_hooks-0.1.66.dist-info → django_bulk_hooks-0.1.67.dist-info}/WHEEL +1 -1
- django_bulk_hooks-0.1.66.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.66.dist-info → django_bulk_hooks-0.1.67.dist-info}/LICENSE +0 -0
django_bulk_hooks/__init__.py
CHANGED
django_bulk_hooks/context.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
from collections import deque
|
|
3
|
-
|
|
4
|
-
_hook_context = threading.local()
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def get_hook_queue():
|
|
8
|
-
if not hasattr(_hook_context, "queue"):
|
|
9
|
-
_hook_context.queue = deque()
|
|
10
|
-
return _hook_context.queue
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class
|
|
14
|
-
def __init__(self, model_cls, metadata=None):
|
|
15
|
-
self.model_cls = model_cls
|
|
16
|
-
self.metadata = metadata or {}
|
|
1
|
+
import threading
|
|
2
|
+
from collections import deque
|
|
3
|
+
|
|
4
|
+
_hook_context = threading.local()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_hook_queue():
|
|
8
|
+
if not hasattr(_hook_context, "queue"):
|
|
9
|
+
_hook_context.queue = deque()
|
|
10
|
+
return _hook_context.queue
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HookContext:
|
|
14
|
+
def __init__(self, model_cls, metadata=None):
|
|
15
|
+
self.model_cls = model_cls
|
|
16
|
+
self.metadata = metadata or {}
|
django_bulk_hooks/engine.py
CHANGED
|
@@ -8,19 +8,24 @@ logger = logging.getLogger(__name__)
|
|
|
8
8
|
def run(model_cls, event, new_instances, original_instances=None, ctx=None):
|
|
9
9
|
hooks = get_hooks(model_cls, event)
|
|
10
10
|
|
|
11
|
-
logger.
|
|
12
|
-
"Executing engine.run: model=%s, event=%s, #new_instances=%d, #original_instances=%d",
|
|
11
|
+
logger.info(
|
|
12
|
+
"Executing engine.run: model=%s, event=%s, #new_instances=%d, #original_instances=%d, #hooks=%d",
|
|
13
13
|
model_cls.__name__,
|
|
14
14
|
event,
|
|
15
15
|
len(new_instances),
|
|
16
16
|
len(original_instances or []),
|
|
17
|
+
len(hooks),
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
if not hooks:
|
|
21
|
+
logger.info("No hooks found for model=%s, event=%s", model_cls.__name__, event)
|
|
22
|
+
return
|
|
23
|
+
|
|
19
24
|
for handler_cls, method_name, condition, priority in hooks:
|
|
20
25
|
handler_instance = handler_cls()
|
|
21
26
|
func = getattr(handler_instance, method_name)
|
|
22
27
|
|
|
23
|
-
logger.
|
|
28
|
+
logger.info(
|
|
24
29
|
"Executing hook %s for %s.%s with priority=%s",
|
|
25
30
|
func.__name__,
|
|
26
31
|
model_cls.__name__,
|
django_bulk_hooks/handler.py
CHANGED
|
@@ -3,12 +3,14 @@ import threading
|
|
|
3
3
|
from collections import deque
|
|
4
4
|
|
|
5
5
|
from django.db import transaction
|
|
6
|
+
|
|
6
7
|
from django_bulk_hooks.registry import get_hooks, register_hook
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
# Thread-local hook context and hook state
|
|
13
|
+
class HookVars(threading.local):
|
|
12
14
|
def __init__(self):
|
|
13
15
|
self.new = None
|
|
14
16
|
self.old = None
|
|
@@ -16,48 +18,53 @@ class TriggerVars(threading.local):
|
|
|
16
18
|
self.model = None
|
|
17
19
|
self.depth = 0
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
hook_vars = HookVars()
|
|
20
23
|
|
|
21
24
|
# Hook queue per thread
|
|
22
25
|
_hook_context = threading.local()
|
|
23
26
|
|
|
27
|
+
|
|
24
28
|
def get_hook_queue():
|
|
25
29
|
if not hasattr(_hook_context, "queue"):
|
|
26
30
|
_hook_context.queue = deque()
|
|
27
31
|
return _hook_context.queue
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
|
|
34
|
+
class HookContextState:
|
|
30
35
|
@property
|
|
31
36
|
def is_before(self):
|
|
32
|
-
return
|
|
37
|
+
return hook_vars.event.startswith("before_") if hook_vars.event else False
|
|
33
38
|
|
|
34
39
|
@property
|
|
35
40
|
def is_after(self):
|
|
36
|
-
return
|
|
41
|
+
return hook_vars.event.startswith("after_") if hook_vars.event else False
|
|
37
42
|
|
|
38
43
|
@property
|
|
39
44
|
def is_create(self):
|
|
40
|
-
return "create" in
|
|
45
|
+
return "create" in hook_vars.event if hook_vars.event else False
|
|
41
46
|
|
|
42
47
|
@property
|
|
43
48
|
def is_update(self):
|
|
44
|
-
return "update" in
|
|
49
|
+
return "update" in hook_vars.event if hook_vars.event else False
|
|
45
50
|
|
|
46
51
|
@property
|
|
47
52
|
def new(self):
|
|
48
|
-
return
|
|
53
|
+
return hook_vars.new
|
|
49
54
|
|
|
50
55
|
@property
|
|
51
56
|
def old(self):
|
|
52
|
-
return
|
|
57
|
+
return hook_vars.old
|
|
53
58
|
|
|
54
59
|
@property
|
|
55
60
|
def model(self):
|
|
56
|
-
return
|
|
61
|
+
return hook_vars.model
|
|
57
62
|
|
|
58
|
-
Trigger = TriggerContextState()
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
Hook = HookContextState()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HookHandlerMeta(type):
|
|
61
68
|
_registered = set()
|
|
62
69
|
|
|
63
70
|
def __new__(mcs, name, bases, namespace):
|
|
@@ -66,7 +73,14 @@ class TriggerHandlerMeta(type):
|
|
|
66
73
|
if hasattr(method, "hooks_hooks"):
|
|
67
74
|
for model_cls, event, condition, priority in method.hooks_hooks:
|
|
68
75
|
key = (model_cls, event, cls, method_name)
|
|
69
|
-
if key not in
|
|
76
|
+
if key not in HookHandlerMeta._registered:
|
|
77
|
+
logger.info(
|
|
78
|
+
"Registering hook via HookHandlerMeta: model=%s, event=%s, handler_cls=%s, method_name=%s",
|
|
79
|
+
model_cls.__name__,
|
|
80
|
+
event,
|
|
81
|
+
cls.__name__,
|
|
82
|
+
method_name,
|
|
83
|
+
)
|
|
70
84
|
register_hook(
|
|
71
85
|
model=model_cls,
|
|
72
86
|
event=event,
|
|
@@ -75,10 +89,11 @@ class TriggerHandlerMeta(type):
|
|
|
75
89
|
condition=condition,
|
|
76
90
|
priority=priority,
|
|
77
91
|
)
|
|
78
|
-
|
|
92
|
+
HookHandlerMeta._registered.add(key)
|
|
79
93
|
return cls
|
|
80
94
|
|
|
81
|
-
|
|
95
|
+
|
|
96
|
+
class HookHandler(metaclass=HookHandlerMeta):
|
|
82
97
|
@classmethod
|
|
83
98
|
def handle(
|
|
84
99
|
cls,
|
|
@@ -109,11 +124,11 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
109
124
|
old_records,
|
|
110
125
|
**kwargs,
|
|
111
126
|
):
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
hook_vars.depth += 1
|
|
128
|
+
hook_vars.new = new_records
|
|
129
|
+
hook_vars.old = old_records
|
|
130
|
+
hook_vars.event = event
|
|
131
|
+
hook_vars.model = model
|
|
117
132
|
|
|
118
133
|
hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
|
|
119
134
|
logger.debug("Processing %d hooks for %s.%s", len(hooks), model.__name__, event)
|
|
@@ -126,14 +141,21 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
126
141
|
|
|
127
142
|
for handler_cls, method_name, condition, priority in hooks:
|
|
128
143
|
if condition is not None:
|
|
129
|
-
checks = [
|
|
144
|
+
checks = [
|
|
145
|
+
condition.check(n, o) for n, o in zip(new_local, old_local)
|
|
146
|
+
]
|
|
130
147
|
if not any(checks):
|
|
131
148
|
continue
|
|
132
149
|
|
|
133
150
|
handler = handler_cls()
|
|
134
151
|
method = getattr(handler, method_name)
|
|
135
152
|
|
|
136
|
-
logger.info(
|
|
153
|
+
logger.info(
|
|
154
|
+
"Running hook %s.%s on %d items",
|
|
155
|
+
handler_cls.__name__,
|
|
156
|
+
method_name,
|
|
157
|
+
len(new_local),
|
|
158
|
+
)
|
|
137
159
|
try:
|
|
138
160
|
method(
|
|
139
161
|
new_records=new_local,
|
|
@@ -141,7 +163,9 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
141
163
|
**kwargs,
|
|
142
164
|
)
|
|
143
165
|
except Exception:
|
|
144
|
-
logger.exception(
|
|
166
|
+
logger.exception(
|
|
167
|
+
"Error in hook %s.%s", handler_cls.__name__, method_name
|
|
168
|
+
)
|
|
145
169
|
|
|
146
170
|
conn = transaction.get_connection()
|
|
147
171
|
try:
|
|
@@ -150,8 +174,8 @@ class TriggerHandler(metaclass=TriggerHandlerMeta):
|
|
|
150
174
|
else:
|
|
151
175
|
_execute()
|
|
152
176
|
finally:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
hook_vars.new = None
|
|
178
|
+
hook_vars.old = None
|
|
179
|
+
hook_vars.event = None
|
|
180
|
+
hook_vars.model = None
|
|
181
|
+
hook_vars.depth -= 1
|
django_bulk_hooks/manager.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
1
3
|
from django.db import models, transaction
|
|
4
|
+
|
|
2
5
|
from django_bulk_hooks import engine
|
|
3
6
|
from django_bulk_hooks.constants import (
|
|
4
7
|
AFTER_CREATE,
|
|
@@ -8,18 +11,17 @@ from django_bulk_hooks.constants import (
|
|
|
8
11
|
BEFORE_DELETE,
|
|
9
12
|
BEFORE_UPDATE,
|
|
10
13
|
)
|
|
11
|
-
from django_bulk_hooks.context import
|
|
12
|
-
from django_bulk_hooks.queryset import
|
|
13
|
-
import logging
|
|
14
|
+
from django_bulk_hooks.context import HookContext
|
|
15
|
+
from django_bulk_hooks.queryset import HookQuerySet
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
class
|
|
20
|
+
class BulkHookManager(models.Manager):
|
|
19
21
|
CHUNK_SIZE = 200
|
|
20
22
|
|
|
21
23
|
def get_queryset(self):
|
|
22
|
-
return
|
|
24
|
+
return HookQuerySet(self.model, using=self._db)
|
|
23
25
|
|
|
24
26
|
@transaction.atomic
|
|
25
27
|
def bulk_update(self, objs, fields, bypass_hooks=False, **kwargs):
|
|
@@ -35,7 +37,7 @@ class BulkLifecycleManager(models.Manager):
|
|
|
35
37
|
|
|
36
38
|
if not bypass_hooks:
|
|
37
39
|
originals = list(model_cls.objects.filter(pk__in=[obj.pk for obj in objs]))
|
|
38
|
-
ctx =
|
|
40
|
+
ctx = HookContext(model_cls)
|
|
39
41
|
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
40
42
|
|
|
41
43
|
# Automatically detect fields that were modified during BEFORE_UPDATE hooks
|
|
@@ -115,7 +117,7 @@ class BulkLifecycleManager(models.Manager):
|
|
|
115
117
|
result = []
|
|
116
118
|
|
|
117
119
|
if not bypass_hooks:
|
|
118
|
-
ctx =
|
|
120
|
+
ctx = HookContext(model_cls)
|
|
119
121
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
120
122
|
|
|
121
123
|
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
@@ -139,17 +141,19 @@ class BulkLifecycleManager(models.Manager):
|
|
|
139
141
|
f"bulk_delete expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
140
142
|
)
|
|
141
143
|
|
|
142
|
-
ctx =
|
|
144
|
+
ctx = HookContext(model_cls)
|
|
143
145
|
|
|
144
146
|
if not bypass_hooks:
|
|
145
|
-
logger.
|
|
147
|
+
logger.info("Executing BEFORE_DELETE hooks for %s", model_cls.__name__)
|
|
148
|
+
logger.info("Number of objects to delete: %d", len(objs))
|
|
146
149
|
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
147
150
|
|
|
148
151
|
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
149
152
|
model_cls.objects.filter(pk__in=pks).delete()
|
|
150
153
|
|
|
151
154
|
if not bypass_hooks:
|
|
152
|
-
logger.
|
|
155
|
+
logger.info("Executing AFTER_DELETE hooks for %s", model_cls.__name__)
|
|
156
|
+
logger.info("Number of objects deleted: %d", len(objs))
|
|
153
157
|
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
154
158
|
|
|
155
159
|
return objs
|
django_bulk_hooks/models.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
|
-
from django_bulk_hooks.manager import BulkLifecycleManager
|
|
3
2
|
|
|
3
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
class HookModelMixin(models.Model):
|
|
7
|
+
objects = BulkHookManager()
|
|
7
8
|
|
|
8
9
|
class Meta:
|
|
9
10
|
abstract = True
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
4
|
+
class HookQuerySet(models.QuerySet):
|
|
5
5
|
@transaction.atomic
|
|
6
6
|
def delete(self):
|
|
7
7
|
objs = list(self)
|
|
@@ -29,9 +29,9 @@ class LifecycleQuerySet(models.QuerySet):
|
|
|
29
29
|
|
|
30
30
|
# Run BEFORE_UPDATE hooks
|
|
31
31
|
from django_bulk_hooks import engine
|
|
32
|
-
from django_bulk_hooks.context import
|
|
32
|
+
from django_bulk_hooks.context import HookContext
|
|
33
33
|
|
|
34
|
-
ctx =
|
|
34
|
+
ctx = HookContext(model_cls)
|
|
35
35
|
engine.run(model_cls, "before_update", instances, originals, ctx=ctx)
|
|
36
36
|
|
|
37
37
|
# Use Django's built-in update logic directly
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from collections.abc import Callable
|
|
2
3
|
from typing import Union
|
|
3
4
|
|
|
4
5
|
from django_bulk_hooks.priority import Priority
|
|
5
6
|
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
7
|
logger = logging.getLogger(__name__)
|
|
9
8
|
|
|
10
9
|
_hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
|
|
@@ -18,7 +17,7 @@ def register_hook(
|
|
|
18
17
|
hooks.append((handler_cls, method_name, condition, priority))
|
|
19
18
|
# keep sorted by priority
|
|
20
19
|
hooks.sort(key=lambda x: x[3])
|
|
21
|
-
logger.
|
|
20
|
+
logger.info(
|
|
22
21
|
"Registering hook: model=%s, event=%s, handler_cls=%s, method_name=%s, condition=%s, priority=%s",
|
|
23
22
|
model.__name__,
|
|
24
23
|
event,
|
|
@@ -30,10 +29,17 @@ def register_hook(
|
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def get_hooks(model, event):
|
|
33
|
-
|
|
32
|
+
hooks = _hooks.get((model, event), [])
|
|
33
|
+
logger.info(
|
|
34
34
|
"Retrieving hooks: model=%s, event=%s, hooks_found=%d",
|
|
35
35
|
model.__name__,
|
|
36
36
|
event,
|
|
37
|
-
len(
|
|
37
|
+
len(hooks),
|
|
38
38
|
)
|
|
39
|
-
return
|
|
39
|
+
return hooks
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_all_hooks():
|
|
43
|
+
"""Debug function to list all registered hooks"""
|
|
44
|
+
logger.debug("All registered hooks: %s", _hooks)
|
|
45
|
+
return _hooks
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.67
|
|
4
|
+
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
+
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
5
6
|
License: MIT
|
|
6
7
|
Keywords: django,bulk,hooks
|
|
7
8
|
Author: Konrad Beck
|
|
@@ -13,24 +14,22 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Requires-Dist: Django (>=4.0)
|
|
16
|
-
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
# django-bulk-hooks
|
|
22
22
|
|
|
23
|
-
⚡
|
|
23
|
+
⚡ Bulk hooks for Django bulk operations.
|
|
24
24
|
|
|
25
|
-
`django-bulk-hooks` brings a declarative,
|
|
25
|
+
`django-bulk-hooks` brings a declarative, hook-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety.
|
|
26
26
|
|
|
27
27
|
## ✨ Features
|
|
28
28
|
|
|
29
29
|
- Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
|
|
30
30
|
- BEFORE/AFTER hooks for create, update, delete
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- Hook chaining, trigger deduplication, and atomicity
|
|
31
|
+
- Hook-aware manager that wraps Django's `bulk_` operations
|
|
32
|
+
- Hook chaining, hook deduplication, and atomicity
|
|
34
33
|
- Class-based hook handlers with DI support
|
|
35
34
|
|
|
36
35
|
## 🚀 Quickstart
|
|
@@ -43,27 +42,27 @@ pip install django-bulk-hooks
|
|
|
43
42
|
|
|
44
43
|
```python
|
|
45
44
|
from django.db import models
|
|
46
|
-
from django_bulk_hooks.manager import
|
|
45
|
+
from django_bulk_hooks.manager import BulkHookManager
|
|
47
46
|
|
|
48
47
|
class Account(models.Model):
|
|
49
48
|
balance = models.DecimalField(max_digits=10, decimal_places=2)
|
|
50
|
-
objects =
|
|
49
|
+
objects = BulkHookManager()
|
|
51
50
|
```
|
|
52
51
|
|
|
53
|
-
### Create a
|
|
52
|
+
### Create a Hook Handler
|
|
54
53
|
|
|
55
54
|
```python
|
|
56
|
-
from django_bulk_hooks import hook, AFTER_UPDATE,
|
|
55
|
+
from django_bulk_hooks import hook, AFTER_UPDATE, HookHandler
|
|
57
56
|
from django_bulk_hooks.conditions import WhenFieldHasChanged
|
|
58
57
|
from .models import Account
|
|
59
58
|
|
|
60
|
-
class
|
|
59
|
+
class AccountHookHandler(HookHandler):
|
|
61
60
|
@hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
|
|
62
61
|
def log_balance_change(self, new_objs):
|
|
63
62
|
print("Accounts updated:", [a.pk for a in new_objs])
|
|
64
63
|
```
|
|
65
64
|
|
|
66
|
-
## 🛠 Supported
|
|
65
|
+
## 🛠 Supported Hook Events
|
|
67
66
|
|
|
68
67
|
- `BEFORE_CREATE`, `AFTER_CREATE`
|
|
69
68
|
- `BEFORE_UPDATE`, `AFTER_UPDATE`
|
|
@@ -71,11 +70,11 @@ class AccountTriggerHandler(TriggerHandler):
|
|
|
71
70
|
|
|
72
71
|
## 🧠 Why?
|
|
73
72
|
|
|
74
|
-
Django
|
|
73
|
+
Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
|
|
75
74
|
|
|
76
|
-
-
|
|
75
|
+
- Hooks that behave consistently across creates/updates/deletes
|
|
77
76
|
- Scalable performance via chunking (default 200)
|
|
78
|
-
- Support for `@hook` decorators and centralized
|
|
77
|
+
- Support for `@hook` decorators and centralized hook classes
|
|
79
78
|
|
|
80
79
|
## 📦 Usage in Views / Commands
|
|
81
80
|
|
|
@@ -83,16 +82,16 @@ Django’s `bulk_` methods bypass signals and `save()`. This package fills that
|
|
|
83
82
|
# Calls AFTER_UPDATE hooks automatically
|
|
84
83
|
Account.objects.bulk_update(accounts, ['balance'])
|
|
85
84
|
|
|
86
|
-
# Triggers BEFORE_CREATE and AFTER_CREATE
|
|
85
|
+
# Triggers BEFORE_CREATE and AFTER_CREATE hooks
|
|
87
86
|
Account.objects.bulk_create(accounts)
|
|
88
87
|
```
|
|
89
88
|
|
|
90
89
|
## 🧩 Integration with Queryable Properties
|
|
91
90
|
|
|
92
|
-
You can extend from `
|
|
91
|
+
You can extend from `BulkHookManager` to support formula fields or property querying.
|
|
93
92
|
|
|
94
93
|
```python
|
|
95
|
-
class MyManager(
|
|
94
|
+
class MyManager(BulkHookManager, QueryablePropertiesManager):
|
|
96
95
|
pass
|
|
97
96
|
```
|
|
98
97
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=Dc1AoWBC2atP8cNBHmCnDCbjn-iB8zOsXRSesiGztWo,154
|
|
2
|
+
django_bulk_hooks/conditions.py,sha256=S8o8JLAeYoF5hNUaxmC2QwMtmak0C73aI-j4GtgmbAw,6541
|
|
3
|
+
django_bulk_hooks/constants.py,sha256=Jks1BIADYbap2fpq3Ry0e7w-CiXBCsR9b5h1yan1qoc,192
|
|
4
|
+
django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
|
|
5
|
+
django_bulk_hooks/decorators.py,sha256=tckDcxtOzKCbgvS9QydgeIAWTFDEl-ch3_Q--ruEGdQ,4831
|
|
6
|
+
django_bulk_hooks/engine.py,sha256=JPYL-EWc1NH8Sa3AuhLN8u1XLTtCKrtkaUgcGbMI0yM,2160
|
|
7
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
+
django_bulk_hooks/handler.py,sha256=9iSXy6qkh-vs5hcrYR9Ciyfs1IllcyyX68hHRc294ZE,5553
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=v0WsJK7by_WHPNDFyvFW1d5BUPNWuRYIXlRhvKytjQ0,6637
|
|
10
|
+
django_bulk_hooks/models.py,sha256=5Hpl43yraurnOZho3frngJ9Eu3XdvnrE4iL9q7vM1r8,529
|
|
11
|
+
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=nmVZ6cqkveSiBz_L4cRT-3u-mkcjkGQNJLhza8SSdKc,1398
|
|
13
|
+
django_bulk_hooks/registry.py,sha256=2S_MUrhFJpGPZbBuqFvPEpKgEduGYUAsxqMS3_wsVXA,1233
|
|
14
|
+
django_bulk_hooks-0.1.67.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.67.dist-info/METADATA,sha256=yTgClp7e_xBdHLyCyhD4k6cqVNJyZC8LRx12dBS3s0Y,2973
|
|
16
|
+
django_bulk_hooks-0.1.67.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.67.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=6VFU52Kz5Yjhjr_RhID41nMaXUMZ4na_MVDXEhIMXc4,98
|
|
2
|
-
django_bulk_hooks/conditions.py,sha256=S8o8JLAeYoF5hNUaxmC2QwMtmak0C73aI-j4GtgmbAw,6541
|
|
3
|
-
django_bulk_hooks/constants.py,sha256=Jks1BIADYbap2fpq3Ry0e7w-CiXBCsR9b5h1yan1qoc,192
|
|
4
|
-
django_bulk_hooks/context.py,sha256=hElvgYZoH1nEM2dR7JIi7pbRt67za90ZVy0OzyFWMNg,367
|
|
5
|
-
django_bulk_hooks/decorators.py,sha256=tckDcxtOzKCbgvS9QydgeIAWTFDEl-ch3_Q--ruEGdQ,4831
|
|
6
|
-
django_bulk_hooks/engine.py,sha256=VZJiESt-QSeppnDDsqiJbnDVAc0EBa6fH7Ywr9sT2Qs,2004
|
|
7
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
|
-
django_bulk_hooks/handler.py,sha256=qUulFSPhi__gWHISC4GogeVQ9aDo45bz0Dj421Y6skE,4968
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=0vnrfz0oYrtARvFzWr8hBUx2688ECNuj0CpixlsCqBM,6522
|
|
10
|
-
django_bulk_hooks/models.py,sha256=_cqIVHKhXb1EOnXxhsuJUlLnPjTRM_sCP5OKbq4q-I8,542
|
|
11
|
-
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=YG9wlvOdujapmxzNhOXTO74Uph0sozUsfc8e5n9KniU,1409
|
|
13
|
-
django_bulk_hooks/registry.py,sha256=eFe6U6kUm3ecBVqKtvF-Zh16k3KZVsUEOoTj0axoPl8,1088
|
|
14
|
-
django_bulk_hooks-0.1.66.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
-
django_bulk_hooks-0.1.66.dist-info/METADATA,sha256=NyHujXArUIsO_ZC_pXzxkd3CKAM_A60p9Rguhy6eJQc,3113
|
|
16
|
-
django_bulk_hooks-0.1.66.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
-
django_bulk_hooks-0.1.66.dist-info/RECORD,,
|
|
File without changes
|