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

@@ -43,7 +43,8 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
43
43
  logger.error("Validation failed for %s: %s", instance, e)
44
44
  raise
45
45
 
46
- # Process hooks in priority order
46
+ # Process hooks in priority order (highest priority first)
47
+ # Registry now sorts by priority (highest first)
47
48
  for handler_cls, method_name, condition, priority in hooks:
48
49
  logger.debug(f"Processing {handler_cls.__name__}.{method_name} (priority: {priority})")
49
50
 
@@ -80,8 +81,9 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
80
81
  try:
81
82
  func(
82
83
  new_records=to_process_new,
83
- old_records=to_process_old if any(to_process_old) else None,
84
+ old_records=to_process_old if any(x is not None for x in to_process_old) else None,
84
85
  )
85
86
  except Exception as e:
86
87
  logger.error(f"Hook execution failed in {handler_cls.__name__}.{method_name}: {e}")
88
+ # Re-raise the exception to ensure proper error handling
87
89
  raise
@@ -1,17 +1,3 @@
1
- from enum import IntEnum
2
-
3
-
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
15
-
1
+ from django_bulk_hooks.priority import Priority
16
2
 
17
3
  DEFAULT_PRIORITY = Priority.NORMAL
@@ -61,36 +61,43 @@ class HookContextState:
61
61
  return hook_vars.model
62
62
 
63
63
 
64
- Hook = HookContextState()
65
-
66
-
67
64
  class HookMeta(type):
68
65
  """Metaclass that automatically registers hooks when Hook classes are defined."""
69
66
 
70
67
  def __new__(mcs, name, bases, namespace):
71
68
  cls = super().__new__(mcs, name, bases, namespace)
72
69
 
73
- # Register hooks for this class
74
- for method_name, method in namespace.items():
75
- if hasattr(method, "hooks_hooks"):
76
- for model_cls, event, condition, priority in method.hooks_hooks:
77
- # Create a unique key for this hook registration
78
- key = (model_cls, event, cls, method_name)
79
-
80
- # Register the hook
81
- register_hook(
82
- model=model_cls,
83
- event=event,
84
- handler_cls=cls,
85
- method_name=method_name,
86
- condition=condition,
87
- priority=priority,
88
- )
89
-
90
- logger.debug(
91
- f"Registered hook {cls.__name__}.{method_name} "
92
- f"for {model_cls.__name__}.{event} with priority {priority}"
93
- )
70
+ # Register hooks for this class, including inherited methods
71
+ # We need to check all methods in the MRO to handle inheritance
72
+ for attr_name in dir(cls):
73
+ if attr_name.startswith('_'):
74
+ continue
75
+
76
+ try:
77
+ attr = getattr(cls, attr_name)
78
+ if callable(attr) and hasattr(attr, "hooks_hooks"):
79
+ for model_cls, event, condition, priority in attr.hooks_hooks:
80
+ # Create a unique key for this hook registration
81
+ key = (model_cls, event, cls, attr_name)
82
+
83
+ # Register the hook
84
+ register_hook(
85
+ model=model_cls,
86
+ event=event,
87
+ handler_cls=cls,
88
+ method_name=attr_name,
89
+ condition=condition,
90
+ priority=priority,
91
+ )
92
+
93
+ logger.debug(
94
+ f"Registered hook {cls.__name__}.{attr_name} "
95
+ f"for {model_cls.__name__}.{event} with priority {priority}"
96
+ )
97
+ except Exception as e:
98
+ # Skip attributes that can't be accessed
99
+ logger.debug(f"Skipping attribute {attr_name}: {e}")
100
+ continue
94
101
 
95
102
  return cls
96
103
 
@@ -132,7 +139,7 @@ class Hook(metaclass=HookMeta):
132
139
  hook_vars.event = event
133
140
  hook_vars.model = model
134
141
 
135
- hooks = sorted(get_hooks(model, event), key=lambda x: x[3])
142
+ hooks = get_hooks(model, event)
136
143
 
137
144
  def _execute():
138
145
  new_local = new_records or []
@@ -161,6 +168,8 @@ class Hook(metaclass=HookMeta):
161
168
  logger.exception(
162
169
  "Error in hook %s.%s", handler_cls.__name__, method_name
163
170
  )
171
+ # Re-raise the exception to ensure proper error handling
172
+ raise
164
173
 
165
174
  conn = transaction.get_connection()
166
175
  try:
@@ -174,3 +183,7 @@ class Hook(metaclass=HookMeta):
174
183
  hook_vars.event = None
175
184
  hook_vars.model = None
176
185
  hook_vars.depth -= 1
186
+
187
+
188
+ # Create a global Hook instance for context access
189
+ HookContext = HookContextState()
@@ -5,12 +5,12 @@ class Priority(IntEnum):
5
5
  """
6
6
  Named priorities for django-bulk-hooks hooks.
7
7
 
8
- Lower values run earlier (higher priority).
9
- Hooks are sorted in ascending order.
8
+ Higher values run earlier (higher priority).
9
+ Hooks are sorted in descending order.
10
10
  """
11
11
 
12
- HIGHEST = 0 # runs first
13
- HIGH = 25 # runs early
12
+ LOWEST = 0 # runs last
13
+ LOW = 25 # runs later
14
14
  NORMAL = 50 # default ordering
15
- LOW = 75 # runs later
16
- LOWEST = 100 # runs last
15
+ HIGH = 75 # runs early
16
+ HIGHEST = 100 # runs first
@@ -95,22 +95,49 @@ class HookQuerySetMixin:
95
95
 
96
96
  # Run hooks only if not bypassed
97
97
  if not bypass_hooks:
98
+ ctx = HookContext(model_cls)
98
99
  # Run VALIDATE_UPDATE hooks
99
- engine.run(model_cls, VALIDATE_UPDATE, instances, originals, HookContext(model_cls))
100
+ engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
100
101
 
101
- # Run BEFORE_UPDATE hooks
102
- engine.run(model_cls, BEFORE_UPDATE, instances, originals, HookContext(model_cls))
102
+ # For subqueries, we need to compute the values and apply them to instances
103
+ # before running BEFORE_UPDATE hooks
104
+ if has_subquery:
105
+ # Create a temporary update to compute the values
106
+ # We'll use a subquery to compute values without actually updating
107
+ for field_name, value in kwargs.items():
108
+ if (hasattr(value, "query") and hasattr(value.query, "model")) or \
109
+ (hasattr(value, "get_source_expressions") and value.get_source_expressions()):
110
+ # This is a complex expression - compute it for each instance
111
+ for instance in instances:
112
+ # Create a single-instance queryset to compute the value
113
+ single_qs = model_cls._base_manager.filter(pk=instance.pk)
114
+ computed_values = single_qs.annotate(computed_field=value).values_list('computed_field', flat=True)
115
+ if computed_values:
116
+ setattr(instance, field_name, computed_values[0])
117
+ else:
118
+ # For simple updates, apply the values directly
119
+ for obj in instances:
120
+ for field, value in kwargs.items():
121
+ setattr(obj, field, value)
122
+
123
+ # Run BEFORE_UPDATE hooks with updated instances
124
+ engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
103
125
 
104
126
  if has_subquery:
105
127
  # For complex expressions, use Django's native update
106
128
  # This handles Subquery, Case, F expressions, etc. correctly
107
129
  result = super().update(**kwargs)
130
+
131
+ # After updating with complex expressions, we need to reload the instances
132
+ # to get the computed values for the AFTER_UPDATE hooks
133
+ if not bypass_hooks:
134
+ # Reload instances to get computed values
135
+ updated_instances = list(model_cls._base_manager.filter(pk__in=pks))
136
+ # Maintain the original order
137
+ updated_map = {obj.pk: obj for obj in updated_instances}
138
+ instances = [updated_map.get(obj.pk, obj) for obj in instances]
108
139
  else:
109
- # For simple field updates, apply changes to instances first
110
- for obj in instances:
111
- for field, value in kwargs.items():
112
- setattr(obj, field, value)
113
-
140
+ # For simple field updates, instances have already been updated in the hook section
114
141
  # Perform database update using Django's native bulk_update
115
142
  # We use the base manager to avoid recursion
116
143
  base_manager = model_cls._base_manager
@@ -120,7 +147,8 @@ class HookQuerySetMixin:
120
147
 
121
148
  # Run AFTER_UPDATE hooks only if not bypassed
122
149
  if not bypass_hooks:
123
- engine.run(model_cls, AFTER_UPDATE, instances, originals, HookContext(model_cls))
150
+ ctx = HookContext(model_cls)
151
+ engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
124
152
 
125
153
  return result
126
154
 
@@ -273,14 +301,16 @@ class HookQuerySetMixin:
273
301
 
274
302
  # Run VALIDATE_UPDATE hooks
275
303
  if not bypass_validation:
304
+ ctx = HookContext(model_cls)
276
305
  engine.run(
277
- model_cls, VALIDATE_UPDATE, objs, originals, HookContext(model_cls)
306
+ model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx
278
307
  )
279
308
 
280
309
  # Run BEFORE_UPDATE hooks
281
310
  if not bypass_hooks:
311
+ ctx = HookContext(model_cls)
282
312
  engine.run(
283
- model_cls, BEFORE_UPDATE, objs, originals, HookContext(model_cls)
313
+ model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx
284
314
  )
285
315
 
286
316
  # Perform database update using Django's native bulk_update
@@ -290,7 +320,8 @@ class HookQuerySetMixin:
290
320
 
291
321
  # Run AFTER_UPDATE hooks
292
322
  if not bypass_hooks:
293
- engine.run(model_cls, AFTER_UPDATE, objs, originals, HookContext(model_cls))
323
+ ctx = HookContext(model_cls)
324
+ engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
294
325
 
295
326
  return result
296
327
 
@@ -42,8 +42,8 @@ def register_hook(
42
42
  # Add the hook
43
43
  hooks.append((handler_cls, method_name, condition, priority))
44
44
 
45
- # Sort by priority (lowest numbers execute first, matching engine expectation)
46
- hooks.sort(key=lambda x: x[3])
45
+ # Sort by priority (highest numbers execute first)
46
+ hooks.sort(key=lambda x: x[3], reverse=True)
47
47
 
48
48
  logger.debug(
49
49
  f"Registered {handler_cls.__name__}.{method_name} "
@@ -71,6 +71,10 @@ def get_hooks(model, event):
71
71
  # Log hook discovery for debugging
72
72
  if hooks:
73
73
  logger.debug(f"Found {len(hooks)} hooks for {model.__name__}.{event}")
74
+ for handler_cls, method_name, condition, priority in hooks:
75
+ logger.debug(f" - {handler_cls.__name__}.{method_name} (priority: {priority})")
76
+ else:
77
+ logger.debug(f"No hooks found for {model.__name__}.{event}")
74
78
 
75
79
  return hooks
76
80
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.227
3
+ Version: 0.1.228
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
@@ -0,0 +1,17 @@
1
+ django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
2
+ django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
3
+ django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
+ django_bulk_hooks/context.py,sha256=_NbGWTq9s66g0vbFIaqN4GlIHWQmFg3EQ44qY8YvvEg,1537
5
+ django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
6
+ django_bulk_hooks/engine.py,sha256=81NUQAppn2eCsY1Ao6SgvyXsowmxUnabYgY0GHWoo-o,3294
7
+ django_bulk_hooks/enums.py,sha256=1pKFHXq1iCceAUitCj9v6WM3PVJpYJx5ZNZD1RzCJUU,87
8
+ django_bulk_hooks/handler.py,sha256=sd5kPOZKwKQnxmQGir5u10ZkTHRdN4Hynl_oIWEIYzQ,5857
9
+ django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
10
+ django_bulk_hooks/models.py,sha256=mj4f93L64CN1XBS29RlS02WnZjCNoUkai97vKqjgZQ8,4575
11
+ django_bulk_hooks/priority.py,sha256=EGFBbRmX_LhwRYFCKzM8I5m8NGCsUEVJp2pfNTcoHe4,378
12
+ django_bulk_hooks/queryset.py,sha256=L_C0ICEnD2TxOljn0RckX0W8xB-jLa_2995ZuKDRVJY,34448
13
+ django_bulk_hooks/registry.py,sha256=EJBNVDo6VOz2s5zsJbpppiM6JwRmmXLyfwSKXYaT4Fs,2802
14
+ django_bulk_hooks-0.1.228.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.228.dist-info/METADATA,sha256=FFM1S50BoNZtBfQyDGhJ4dtvQTsFY1OvNUmrbmHFk0Q,9743
16
+ django_bulk_hooks-0.1.228.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.228.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- django_bulk_hooks/__init__.py,sha256=uUgpnb9AWjIAcWNpCMqBcOewSnpJjJYH6cjPbQkzoNU,140
2
- django_bulk_hooks/conditions.py,sha256=V_f3Di2uCVUjoyfiU4BQCHmI4uUIRSRroApDcXlvnso,6349
3
- django_bulk_hooks/constants.py,sha256=3x1H1fSUUNo0DZONN7GUVDuySZctTR-jtByBHmAIX5w,303
4
- django_bulk_hooks/context.py,sha256=_NbGWTq9s66g0vbFIaqN4GlIHWQmFg3EQ44qY8YvvEg,1537
5
- django_bulk_hooks/decorators.py,sha256=WD7Jn7QAvY8F4wOsYlIpjoM9-FdHXSKB7hH9ot-lkYQ,4896
6
- django_bulk_hooks/engine.py,sha256=wiO6HvZkBSHzt1Q3IpXmVppJl30zlsoTheuPPCrGqdU,3118
7
- django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
8
- django_bulk_hooks/handler.py,sha256=IRgJ6uoyD1NifqzZpL2YxOqInJ4MFlS3vZkAa5ZTIVo,5210
9
- django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
10
- django_bulk_hooks/models.py,sha256=mj4f93L64CN1XBS29RlS02WnZjCNoUkai97vKqjgZQ8,4575
11
- django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=xgnuwl8Ha9IhVzKEvjvCX5jUuRZgc0tWYwaKreuDvpw,32478
13
- django_bulk_hooks/registry.py,sha256=h59veo8Qh4Afj8ZP_4Jqr-2S6ebXjUJ7pJTdfqzfXkE,2572
14
- django_bulk_hooks-0.1.227.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.227.dist-info/METADATA,sha256=rY5wk0lHKwxaqEdbVz3BLw3DnRBjMhXSN4syZLuWGX4,9743
16
- django_bulk_hooks-0.1.227.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.227.dist-info/RECORD,,