django-bulk-hooks 0.1.65__tar.gz → 0.1.67__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.

Files changed (19) hide show
  1. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/PKG-INFO +20 -21
  2. django_bulk_hooks-0.1.67/README.md +81 -0
  3. django_bulk_hooks-0.1.67/django_bulk_hooks/__init__.py +4 -0
  4. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/context.py +16 -16
  5. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/engine.py +8 -3
  6. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/handler.py +53 -29
  7. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/manager.py +15 -11
  8. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/models.py +4 -3
  9. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/queryset.py +3 -3
  10. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/registry.py +12 -6
  11. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/pyproject.toml +2 -2
  12. django_bulk_hooks-0.1.65/README.md +0 -82
  13. django_bulk_hooks-0.1.65/django_bulk_hooks/__init__.py +0 -3
  14. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/LICENSE +0 -0
  15. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/conditions.py +0 -0
  16. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/constants.py +0 -0
  17. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/decorators.py +0 -0
  18. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/enums.py +0 -0
  19. {django_bulk_hooks-0.1.65 → django_bulk_hooks-0.1.67}/django_bulk_hooks/priority.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.65
4
- Summary: Lifecycle-style hooks for Django bulk operations like bulk_create and bulk_update.
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
- Salesforce-style hooks hooks for Django bulk operations.
23
+ Bulk hooks for Django bulk operations.
24
24
 
25
- `django-bulk-hooks` brings a declarative, trigger-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety.
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
- - Salesforce-style semantics with full batch support
32
- - Lifecycle-aware manager that wraps Django’s `bulk_` operations
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 BulkLifecycleManager
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 = BulkLifecycleManager()
49
+ objects = BulkHookManager()
51
50
  ```
52
51
 
53
- ### Create a Trigger Handler
52
+ ### Create a Hook Handler
54
53
 
55
54
  ```python
56
- from django_bulk_hooks import hook, AFTER_UPDATE, TriggerHandler
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 AccountTriggerHandler(TriggerHandler):
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 Lifecycle Events
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
- Djangos `bulk_` methods bypass signals and `save()`. This package fills that gap with:
73
+ Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
75
74
 
76
- - Triggers that behave consistently across creates/updates/deletes
75
+ - Hooks that behave consistently across creates/updates/deletes
77
76
  - Scalable performance via chunking (default 200)
78
- - Support for `@hook` decorators and centralized trigger classes
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 `BulkLifecycleManager` to support formula fields or property querying.
91
+ You can extend from `BulkHookManager` to support formula fields or property querying.
93
92
 
94
93
  ```python
95
- class MyManager(BulkLifecycleManager, QueryablePropertiesManager):
94
+ class MyManager(BulkHookManager, QueryablePropertiesManager):
96
95
  pass
97
96
  ```
98
97
 
@@ -0,0 +1,81 @@
1
+
2
+ # django-bulk-hooks
3
+
4
+ ⚡ Bulk hooks for Django bulk operations.
5
+
6
+ `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.
7
+
8
+ ## ✨ Features
9
+
10
+ - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
+ - BEFORE/AFTER hooks for create, update, delete
12
+ - Hook-aware manager that wraps Django's `bulk_` operations
13
+ - Hook chaining, hook deduplication, and atomicity
14
+ - Class-based hook handlers with DI support
15
+
16
+ ## 🚀 Quickstart
17
+
18
+ ```bash
19
+ pip install django-bulk-hooks
20
+ ```
21
+
22
+ ### Define Your Model
23
+
24
+ ```python
25
+ from django.db import models
26
+ from django_bulk_hooks.manager import BulkHookManager
27
+
28
+ class Account(models.Model):
29
+ balance = models.DecimalField(max_digits=10, decimal_places=2)
30
+ objects = BulkHookManager()
31
+ ```
32
+
33
+ ### Create a Hook Handler
34
+
35
+ ```python
36
+ from django_bulk_hooks import hook, AFTER_UPDATE, HookHandler
37
+ from django_bulk_hooks.conditions import WhenFieldHasChanged
38
+ from .models import Account
39
+
40
+ class AccountHookHandler(HookHandler):
41
+ @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
42
+ def log_balance_change(self, new_objs):
43
+ print("Accounts updated:", [a.pk for a in new_objs])
44
+ ```
45
+
46
+ ## 🛠 Supported Hook Events
47
+
48
+ - `BEFORE_CREATE`, `AFTER_CREATE`
49
+ - `BEFORE_UPDATE`, `AFTER_UPDATE`
50
+ - `BEFORE_DELETE`, `AFTER_DELETE`
51
+
52
+ ## 🧠 Why?
53
+
54
+ Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
55
+
56
+ - Hooks that behave consistently across creates/updates/deletes
57
+ - Scalable performance via chunking (default 200)
58
+ - Support for `@hook` decorators and centralized hook classes
59
+
60
+ ## 📦 Usage in Views / Commands
61
+
62
+ ```python
63
+ # Calls AFTER_UPDATE hooks automatically
64
+ Account.objects.bulk_update(accounts, ['balance'])
65
+
66
+ # Triggers BEFORE_CREATE and AFTER_CREATE hooks
67
+ Account.objects.bulk_create(accounts)
68
+ ```
69
+
70
+ ## 🧩 Integration with Queryable Properties
71
+
72
+ You can extend from `BulkHookManager` to support formula fields or property querying.
73
+
74
+ ```python
75
+ class MyManager(BulkHookManager, QueryablePropertiesManager):
76
+ pass
77
+ ```
78
+
79
+ ## 📝 License
80
+
81
+ MIT © 2024 Augend / Konrad Beck
@@ -0,0 +1,4 @@
1
+ from django_bulk_hooks.handler import HookHandler
2
+ from django_bulk_hooks.manager import BulkHookManager
3
+
4
+ __all__ = ["BulkHookManager", "HookHandler"]
@@ -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 TriggerContext:
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 {}
@@ -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.debug(
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.debug(
28
+ logger.info(
24
29
  "Executing hook %s for %s.%s with priority=%s",
25
30
  func.__name__,
26
31
  model_cls.__name__,
@@ -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
- # Thread-local hook context and trigger state
11
- class TriggerVars(threading.local):
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
- trigger = TriggerVars()
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
- class TriggerContextState:
33
+
34
+ class HookContextState:
30
35
  @property
31
36
  def is_before(self):
32
- return trigger.event.startswith("before_") if trigger.event else False
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 trigger.event.startswith("after_") if trigger.event else False
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 trigger.event if trigger.event else False
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 trigger.event if trigger.event else False
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 trigger.new
53
+ return hook_vars.new
49
54
 
50
55
  @property
51
56
  def old(self):
52
- return trigger.old
57
+ return hook_vars.old
53
58
 
54
59
  @property
55
60
  def model(self):
56
- return trigger.model
61
+ return hook_vars.model
57
62
 
58
- Trigger = TriggerContextState()
59
63
 
60
- class TriggerHandlerMeta(type):
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 TriggerHandlerMeta._registered:
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
- TriggerHandlerMeta._registered.add(key)
92
+ HookHandlerMeta._registered.add(key)
79
93
  return cls
80
94
 
81
- class TriggerHandler(metaclass=TriggerHandlerMeta):
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
- trigger.depth += 1
113
- trigger.new = new_records
114
- trigger.old = old_records
115
- trigger.event = event
116
- trigger.model = model
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 = [condition.check(n, o) for n, o in zip(new_local, old_local)]
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("Running hook %s.%s on %d items", handler_cls.__name__, method_name, len(new_local))
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("Error in hook %s.%s", handler_cls.__name__, method_name)
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
- trigger.new = None
154
- trigger.old = None
155
- trigger.event = None
156
- trigger.model = None
157
- trigger.depth -= 1
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
@@ -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 TriggerContext
12
- from django_bulk_hooks.queryset import LifecycleQuerySet
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 BulkLifecycleManager(models.Manager):
20
+ class BulkHookManager(models.Manager):
19
21
  CHUNK_SIZE = 200
20
22
 
21
23
  def get_queryset(self):
22
- return LifecycleQuerySet(self.model, using=self._db)
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 = TriggerContext(model_cls)
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 = TriggerContext(model_cls)
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 = TriggerContext(model_cls)
144
+ ctx = HookContext(model_cls)
143
145
 
144
146
  if not bypass_hooks:
145
- logger.debug("Executing BEFORE_DELETE hooks for %s", model_cls.__name__)
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.debug("Executing AFTER_DELETE hooks for %s", model_cls.__name__)
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
@@ -170,7 +174,7 @@ class BulkLifecycleManager(models.Manager):
170
174
  objs = list(self.all())
171
175
  if not objs:
172
176
  return 0
173
- self.model.objects.bulk_delete(objs)
177
+ self.bulk_delete(objs)
174
178
  return len(objs)
175
179
 
176
180
  @transaction.atomic
@@ -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
- class LifecycleModelMixin(models.Model):
6
- objects = BulkLifecycleManager()
5
+
6
+ class HookModelMixin(models.Model):
7
+ objects = BulkHookManager()
7
8
 
8
9
  class Meta:
9
10
  abstract = True
@@ -1,7 +1,7 @@
1
1
  from django.db import models, transaction
2
2
 
3
3
 
4
- class LifecycleQuerySet(models.QuerySet):
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 TriggerContext
32
+ from django_bulk_hooks.context import HookContext
33
33
 
34
- ctx = TriggerContext(model_cls)
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
@@ -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.debug(
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
- logger.debug(
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(_hooks.get((model, event), [])),
37
+ len(hooks),
38
38
  )
39
- return _hooks.get((model, event), [])
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,7 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.65"
4
- description = "Lifecycle-style hooks for Django bulk operations like bulk_create and bulk_update."
3
+ version = "0.1.67"
4
+ description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -1,82 +0,0 @@
1
-
2
- # django-bulk-hooks
3
-
4
- ⚡ Salesforce-style hooks hooks for Django bulk operations.
5
-
6
- `django-bulk-hooks` brings a declarative, trigger-like experience to Django's `bulk_create`, `bulk_update`, and `bulk_delete` — including support for `BEFORE_` and `AFTER_` hooks, conditions, batching, and transactional safety.
7
-
8
- ## ✨ Features
9
-
10
- - Declarative hook system: `@hook(AFTER_UPDATE, condition=...)`
11
- - BEFORE/AFTER hooks for create, update, delete
12
- - Salesforce-style semantics with full batch support
13
- - Lifecycle-aware manager that wraps Django’s `bulk_` operations
14
- - Hook chaining, trigger deduplication, and atomicity
15
- - Class-based hook handlers with DI support
16
-
17
- ## 🚀 Quickstart
18
-
19
- ```bash
20
- pip install django-bulk-hooks
21
- ```
22
-
23
- ### Define Your Model
24
-
25
- ```python
26
- from django.db import models
27
- from django_bulk_hooks.manager import BulkLifecycleManager
28
-
29
- class Account(models.Model):
30
- balance = models.DecimalField(max_digits=10, decimal_places=2)
31
- objects = BulkLifecycleManager()
32
- ```
33
-
34
- ### Create a Trigger Handler
35
-
36
- ```python
37
- from django_bulk_hooks import hook, AFTER_UPDATE, TriggerHandler
38
- from django_bulk_hooks.conditions import WhenFieldHasChanged
39
- from .models import Account
40
-
41
- class AccountTriggerHandler(TriggerHandler):
42
- @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
43
- def log_balance_change(self, new_objs):
44
- print("Accounts updated:", [a.pk for a in new_objs])
45
- ```
46
-
47
- ## 🛠 Supported Lifecycle Events
48
-
49
- - `BEFORE_CREATE`, `AFTER_CREATE`
50
- - `BEFORE_UPDATE`, `AFTER_UPDATE`
51
- - `BEFORE_DELETE`, `AFTER_DELETE`
52
-
53
- ## 🧠 Why?
54
-
55
- Django’s `bulk_` methods bypass signals and `save()`. This package fills that gap with:
56
-
57
- - Triggers that behave consistently across creates/updates/deletes
58
- - Scalable performance via chunking (default 200)
59
- - Support for `@hook` decorators and centralized trigger classes
60
-
61
- ## 📦 Usage in Views / Commands
62
-
63
- ```python
64
- # Calls AFTER_UPDATE hooks automatically
65
- Account.objects.bulk_update(accounts, ['balance'])
66
-
67
- # Triggers BEFORE_CREATE and AFTER_CREATE
68
- Account.objects.bulk_create(accounts)
69
- ```
70
-
71
- ## 🧩 Integration with Queryable Properties
72
-
73
- You can extend from `BulkLifecycleManager` to support formula fields or property querying.
74
-
75
- ```python
76
- class MyManager(BulkLifecycleManager, QueryablePropertiesManager):
77
- pass
78
- ```
79
-
80
- ## 📝 License
81
-
82
- MIT © 2024 Augend / Konrad Beck
@@ -1,3 +0,0 @@
1
- from django_bulk_hooks.manager import BulkLifecycleManager
2
-
3
- __all__ = ["BulkLifecycleManager"]