django-bulk-hooks 0.1.77__py3-none-any.whl → 0.1.79__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,4 +1,32 @@
1
- from django_bulk_hooks.handler import Hook
2
- from django_bulk_hooks.manager import BulkHookManager
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.engine import safe_get_related_object, safe_get_related_attr
13
+ from django_bulk_hooks.handler import HookHandler
14
+ from django_bulk_hooks.models import HookModelMixin
15
+ from django_bulk_hooks.enums import Priority
3
16
 
4
- __all__ = ["BulkHookManager", "Hook"]
17
+ __all__ = [
18
+ "HookHandler",
19
+ "HookModelMixin",
20
+ "BEFORE_CREATE",
21
+ "AFTER_CREATE",
22
+ "BEFORE_UPDATE",
23
+ "AFTER_UPDATE",
24
+ "BEFORE_DELETE",
25
+ "AFTER_DELETE",
26
+ "VALIDATE_CREATE",
27
+ "VALIDATE_UPDATE",
28
+ "VALIDATE_DELETE",
29
+ "safe_get_related_object",
30
+ "safe_get_related_attr",
31
+ "Priority",
32
+ ]
@@ -1,4 +1,32 @@
1
- from django_bulk_hooks.engine import safe_get_related_object
1
+ from django.db import models
2
+
3
+
4
+ def safe_get_related_object(instance, field_name):
5
+ """
6
+ Safely get a related object without raising RelatedObjectDoesNotExist.
7
+ Returns None if the foreign key field is None or the related object doesn't exist.
8
+ """
9
+ if not hasattr(instance, field_name):
10
+ return None
11
+
12
+ # Get the foreign key field
13
+ try:
14
+ field = instance._meta.get_field(field_name)
15
+ if not field.is_relation or field.many_to_many or field.one_to_many:
16
+ return getattr(instance, field_name, None)
17
+ except models.FieldDoesNotExist:
18
+ return getattr(instance, field_name, None)
19
+
20
+ # Check if the foreign key field is None
21
+ fk_field_name = f"{field_name}_id"
22
+ if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
23
+ return None
24
+
25
+ # Try to get the related object, but catch RelatedObjectDoesNotExist
26
+ try:
27
+ return getattr(instance, field_name)
28
+ except field.related_model.RelatedObjectDoesNotExist:
29
+ return None
2
30
 
3
31
 
4
32
  def resolve_dotted_attr(instance, dotted_path):
@@ -4,6 +4,7 @@ from functools import wraps
4
4
  from django.core.exceptions import FieldDoesNotExist
5
5
  from django_bulk_hooks.enums import DEFAULT_PRIORITY
6
6
  from django_bulk_hooks.registry import register_hook
7
+ from django_bulk_hooks.engine import safe_get_related_object
7
8
 
8
9
 
9
10
  def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
@@ -133,18 +134,13 @@ def select_related(*related_fields):
133
134
  # Check if the foreign key field is set
134
135
  fk_field_name = f"{field}_id"
135
136
  if hasattr(obj, fk_field_name) and getattr(obj, fk_field_name) is not None:
136
- # The foreign key ID is set, so we can try to get the related object
137
- try:
138
- rel_obj = getattr(obj, field)
139
- if rel_obj is not None:
140
- # Ensure it's cached to prevent future queries
141
- if not hasattr(obj._state, 'fields_cache'):
142
- obj._state.fields_cache = {}
143
- obj._state.fields_cache[field] = rel_obj
144
- except Exception:
145
- # If we can't get the related object, that's okay
146
- # The foreign key ID is set, so the relationship exists
147
- pass
137
+ # The foreign key ID is set, so we can try to get the related object safely
138
+ rel_obj = safe_get_related_object(obj, field)
139
+ if rel_obj is not None:
140
+ # Ensure it's cached to prevent future queries
141
+ if not hasattr(obj._state, 'fields_cache'):
142
+ obj._state.fields_cache = {}
143
+ obj._state.fields_cache[field] = rel_obj
148
144
 
149
145
  return func(*bound.args, **bound.kwargs)
150
146
 
@@ -3,38 +3,11 @@ import logging
3
3
  from django.core.exceptions import ValidationError
4
4
  from django.db import models
5
5
  from django_bulk_hooks.registry import get_hooks
6
+ from django_bulk_hooks.conditions import safe_get_related_object
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
9
10
 
10
- def safe_get_related_object(instance, field_name):
11
- """
12
- Safely get a related object without raising RelatedObjectDoesNotExist.
13
- Returns None if the foreign key field is None or the related object doesn't exist.
14
- """
15
- if not hasattr(instance, field_name):
16
- return None
17
-
18
- # Get the foreign key field
19
- try:
20
- field = instance._meta.get_field(field_name)
21
- if not field.is_relation or field.many_to_many or field.one_to_many:
22
- return getattr(instance, field_name, None)
23
- except models.FieldDoesNotExist:
24
- return getattr(instance, field_name, None)
25
-
26
- # Check if the foreign key field is None
27
- fk_field_name = f"{field_name}_id"
28
- if hasattr(instance, fk_field_name) and getattr(instance, fk_field_name) is None:
29
- return None
30
-
31
- # Try to get the related object, but catch RelatedObjectDoesNotExist
32
- try:
33
- return getattr(instance, field_name)
34
- except field.related_model.RelatedObjectDoesNotExist:
35
- return None
36
-
37
-
38
11
  def safe_get_related_attr(instance, field_name, attr_name=None):
39
12
  """
40
13
  Safely get a related object or its attribute without raising RelatedObjectDoesNotExist.
@@ -86,7 +86,7 @@ class HookMeta(type):
86
86
  return cls
87
87
 
88
88
 
89
- class Hook(metaclass=HookMeta):
89
+ class HookHandler(metaclass=HookMeta):
90
90
  @classmethod
91
91
  def handle(
92
92
  cls,
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Union
3
3
 
4
- from django_bulk_hooks.priority import Priority
4
+ from django_bulk_hooks.enums import Priority
5
5
 
6
6
  _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
7
 
@@ -17,8 +17,7 @@ def register_hook(
17
17
 
18
18
 
19
19
  def get_hooks(model, event):
20
- hooks = _hooks.get((model, event), [])
21
- return hooks
20
+ return _hooks.get((model, event), [])
22
21
 
23
22
 
24
23
  def list_all_hooks():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.77
3
+ Version: 0.1.79
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -58,7 +58,7 @@ from django_bulk_hooks import hook, AFTER_UPDATE, Hook
58
58
  from django_bulk_hooks.conditions import WhenFieldHasChanged
59
59
  from .models import Account
60
60
 
61
- class AccountHooks(Hook):
61
+ class AccountHooks(HookHandler):
62
62
  @hook(AFTER_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
63
63
  def log_balance_change(self, new_records, old_records):
64
64
  print("Accounts updated:", [a.pk for a in new_records])
@@ -74,99 +74,10 @@ class AccountHooks(Hook):
74
74
  print("Accounts deleted:", [a.pk for a in old_records])
75
75
  ```
76
76
 
77
- ## 🛠 Supported Hook Events
78
-
79
- - `BEFORE_CREATE`, `AFTER_CREATE`
80
- - `BEFORE_UPDATE`, `AFTER_UPDATE`
81
- - `BEFORE_DELETE`, `AFTER_DELETE`
82
-
83
- ## 🔄 Lifecycle Events
84
-
85
- ### Individual Model Operations
86
-
87
- The `HookModelMixin` automatically triggers hooks for individual model operations:
88
-
89
- ```python
90
- # These will trigger BEFORE_CREATE and AFTER_CREATE hooks
91
- account = Account.objects.create(balance=100.00)
92
- account.save() # for new instances
93
-
94
- # These will trigger BEFORE_UPDATE and AFTER_UPDATE hooks
95
- account.balance = 200.00
96
- account.save() # for existing instances
97
-
98
- # This will trigger BEFORE_DELETE and AFTER_DELETE hooks
99
- account.delete()
100
- ```
101
-
102
- ### Bulk Operations
103
-
104
- Bulk operations also trigger the same hooks:
105
-
106
- ```python
107
- # Bulk create - triggers BEFORE_CREATE and AFTER_CREATE hooks
108
- accounts = [
109
- Account(balance=100.00),
110
- Account(balance=200.00),
111
- ]
112
- Account.objects.bulk_create(accounts)
113
-
114
- # Bulk update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
115
- for account in accounts:
116
- account.balance *= 1.1
117
- Account.objects.bulk_update(accounts, ['balance'])
118
-
119
- # Bulk delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
120
- Account.objects.bulk_delete(accounts)
121
- ```
122
-
123
- ### Queryset Operations
124
-
125
- Queryset operations are also supported:
126
-
127
- ```python
128
- # Queryset update - triggers BEFORE_UPDATE and AFTER_UPDATE hooks
129
- Account.objects.update(balance=0.00)
130
-
131
- # Queryset delete - triggers BEFORE_DELETE and AFTER_DELETE hooks
132
- Account.objects.delete()
133
- ```
134
-
135
- ## 🧠 Why?
136
-
137
- Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
138
-
139
- - Hooks that behave consistently across creates/updates/deletes
140
- - **NEW**: Individual model lifecycle hooks that work with `save()` and `delete()`
141
- - Scalable performance via chunking (default 200)
142
- - Support for `@hook` decorators and centralized hook classes
143
- - **NEW**: Automatic hook triggering for admin operations and other Django features
144
-
145
- ## 📦 Usage Examples
146
-
147
- ### Individual Model Operations
148
-
149
- ```python
150
- # These automatically trigger hooks
151
- account = Account.objects.create(balance=100.00)
152
- account.balance = 200.00
153
- account.save()
154
- account.delete()
155
- ```
156
-
157
- ### Bulk Operations
158
-
159
- ```python
160
- # These also trigger hooks
161
- Account.objects.bulk_create(accounts)
162
- Account.objects.bulk_update(accounts, ['balance'])
163
- Account.objects.bulk_delete(accounts)
164
- ```
165
-
166
77
  ### Advanced Hook Usage
167
78
 
168
79
  ```python
169
- class AdvancedAccountHooks(Hook):
80
+ class AdvancedAccountHooks(HookHandler):
170
81
  @hook(BEFORE_UPDATE, model=Account, condition=WhenFieldHasChanged("balance"))
171
82
  def validate_balance_change(self, new_records, old_records):
172
83
  for new_account, old_account in zip(new_records, old_records):
@@ -179,17 +90,3 @@ class AdvancedAccountHooks(Hook):
179
90
  # Send welcome email logic here
180
91
  pass
181
92
  ```
182
-
183
- ## 🧩 Integration with Queryable Properties
184
-
185
- You can extend from `BulkHookManager` to support formula fields or property querying.
186
-
187
- ```python
188
- class MyManager(BulkHookManager, QueryablePropertiesManager):
189
- pass
190
- ```
191
-
192
- ## 📝 License
193
-
194
- MIT © 2024 Augend / Konrad Beck
195
-
@@ -0,0 +1,16 @@
1
+ django_bulk_hooks/__init__.py,sha256=JU6GMrqIPDm1n8zc78iTd1I1pzubmdkBJwROMaecZPM,805
2
+ django_bulk_hooks/conditions.py,sha256=o31qTnSfEkAKMVnFPF0JdkuvMFWb92Lc-o-WQ2CoWAk,8107
3
+ django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
+ django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
5
+ django_bulk_hooks/decorators.py,sha256=zstmb27dKcOHu3Atg7cauewCTzPvUmq03mzVKJRi56o,7230
6
+ django_bulk_hooks/engine.py,sha256=ru6oxoXEj4eC8JM30hzLj-jcD-_Mcg-SaPqexxR87c0,2183
7
+ django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
8
+ django_bulk_hooks/handler.py,sha256=Qpg_zT6SsQiTlhduvzXxPdG6uynjyR2fBjj-R6HZiXI,4861
9
+ django_bulk_hooks/manager.py,sha256=-V128ACxPAz82ua4jQRFUkjAKtKW4MN5ppz0bHcv5s4,7138
10
+ django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
11
+ django_bulk_hooks/queryset.py,sha256=7lLqhZ-XOYsZ1I3Loxi4Nhz79M8HlTYE413AW8nyeDI,1330
12
+ django_bulk_hooks/registry.py,sha256=Vh78exKYcdZhM27120kQm-iXGOjd_kf9ZUYBZ8eQ2V0,683
13
+ django_bulk_hooks-0.1.79.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
14
+ django_bulk_hooks-0.1.79.dist-info/METADATA,sha256=MfSByYpJLtek9y5Vr8GjUt635pK0McIEZRCE2W0O0mA,3388
15
+ django_bulk_hooks-0.1.79.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
16
+ django_bulk_hooks-0.1.79.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- from enum import IntEnum
2
-
3
-
4
- class Priority(IntEnum):
5
- """
6
- Named priorities for django-bulk-hooks hooks.
7
-
8
- Lower values run earlier (higher priority).
9
- Hooks are sorted in ascending order.
10
- """
11
-
12
- HIGHEST = 0 # runs first
13
- HIGH = 25 # runs early
14
- NORMAL = 50 # default ordering
15
- LOW = 75 # runs later
16
- LOWEST = 100 # runs last
@@ -1,17 +0,0 @@
1
- django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
2
- django_bulk_hooks/conditions.py,sha256=yPrWj1nA1ptpQFG5zhHmRwt4rswhnkKSWM4ZyicDF58,7094
3
- django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
- django_bulk_hooks/context.py,sha256=HVDT73uSzvgrOR6mdXTvsBm3hLOgBU8ant_mB7VlFuM,380
5
- django_bulk_hooks/decorators.py,sha256=QnZEHSR4JNek3DkTs8P5FZS3yEW9dKztuEEHeiKzqpg,7440
6
- django_bulk_hooks/engine.py,sha256=5zj7R2Pot6T17RCq9BZ8ajc7vgRJJYAUODA3qzwSAe0,3162
7
- django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
8
- django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,4854
9
- django_bulk_hooks/manager.py,sha256=-V128ACxPAz82ua4jQRFUkjAKtKW4MN5ppz0bHcv5s4,7138
10
- django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
11
- django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=7lLqhZ-XOYsZ1I3Loxi4Nhz79M8HlTYE413AW8nyeDI,1330
13
- django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.77.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.77.dist-info/METADATA,sha256=-6TsAFggFzX4j4ppWGCApRmWFy0E6TV8TTCaIfonV44,5930
16
- django_bulk_hooks-0.1.77.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.77.dist-info/RECORD,,