django-bulk-hooks 0.1.217__tar.gz → 0.1.219__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 (18) hide show
  1. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/conditions.py +1 -6
  3. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/engine.py +5 -17
  4. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/queryset.py +21 -43
  5. django_bulk_hooks-0.1.219/django_bulk_hooks/registry.py +29 -0
  6. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/pyproject.toml +1 -1
  7. django_bulk_hooks-0.1.217/django_bulk_hooks/registry.py +0 -37
  8. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/LICENSE +0 -0
  9. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/README.md +0 -0
  10. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/__init__.py +0 -0
  11. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/constants.py +0 -0
  12. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/context.py +0 -0
  13. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/decorators.py +0 -0
  14. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/enums.py +0 -0
  15. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/manager.py +0 -0
  17. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/models.py +0 -0
  18. {django_bulk_hooks-0.1.217 → django_bulk_hooks-0.1.219}/django_bulk_hooks/priority.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.217
3
+ Version: 0.1.219
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
@@ -77,13 +77,8 @@ class HasChanged(HookCondition):
77
77
  previous = resolve_dotted_attr(original_instance, self.field)
78
78
 
79
79
  # Add more detailed debugging
80
- print(f"DEBUG: Field '{self.field}' - current value: {current} (type: {type(current)})")
81
- print(f"DEBUG: Field '{self.field}' - previous value: {previous} (type: {type(previous)})")
82
- print(f"DEBUG: Values are equal: {current == previous}")
83
-
84
80
  result = (current != previous) == self.has_changed
85
- print(f"DEBUG: HasChanged result: current={current}, previous={previous}, has_changed={self.has_changed}, result={result}")
86
-
81
+ print(f"DEBUG: HasChanged {self.field} result={result}")
87
82
  return result
88
83
 
89
84
 
@@ -23,17 +23,11 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
23
23
  import traceback
24
24
 
25
25
  stack = traceback.format_stack()
26
- print(
27
- f"DEBUG: engine.run called for {model_cls.__name__}.{event} with {len(new_records)} records"
28
- )
29
- print(f"DEBUG: Call stack (last 3 frames):")
30
- for line in stack[-4:-1]: # Show last 3 frames before this one
31
- print(f" {line.strip()}")
32
- print(f"DEBUG: Total hooks found: {len(hooks)}")
26
+ logger.debug(f"engine.run {model_cls.__name__}.{event} {len(new_records)} records")
33
27
 
34
28
  # Check if we're in a bypass context
35
29
  if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
36
- print(f"DEBUG: Context has bypass_hooks=True, skipping hook execution")
30
+ logger.debug("engine.run bypassed")
37
31
  return
38
32
 
39
33
  # For BEFORE_* events, run model.clean() first for validation
@@ -47,7 +41,7 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
47
41
 
48
42
  # Process hooks
49
43
  for handler_cls, method_name, condition, priority in hooks:
50
- print(f"DEBUG: Processing hook {handler_cls.__name__}.{method_name} with condition: {condition}")
44
+ logger.debug(f"Processing {handler_cls.__name__}.{method_name}")
51
45
  handler_instance = handler_cls()
52
46
  func = getattr(handler_instance, method_name)
53
47
 
@@ -60,27 +54,21 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
60
54
  strict=True,
61
55
  ):
62
56
  if not condition:
63
- print(f"DEBUG: No condition, adding record {new.pk if hasattr(new, 'pk') else 'No PK'}")
64
57
  to_process_new.append(new)
65
58
  to_process_old.append(original)
66
59
  else:
67
60
  condition_result = condition.check(new, original)
68
- print(f"DEBUG: Condition {condition.__class__.__name__} check result: {condition_result} for record {new.pk if hasattr(new, 'pk') else 'No PK'}")
69
61
  if condition_result:
70
62
  to_process_new.append(new)
71
63
  to_process_old.append(original)
72
64
 
73
65
  if to_process_new:
74
- print(
75
- f"DEBUG: Executing hook {handler_cls.__name__}.{method_name} for {len(to_process_new)} records"
76
- )
66
+ logger.debug(f"Executing {handler_cls.__name__}.{method_name} for {len(to_process_new)} records")
77
67
  try:
78
68
  func(
79
69
  new_records=to_process_new,
80
70
  old_records=to_process_old if any(to_process_old) else None,
81
71
  )
82
72
  except Exception as e:
83
- print(f"DEBUG: Hook execution failed: {e}")
73
+ logger.debug(f"Hook execution failed: {e}")
84
74
  raise
85
- else:
86
- print(f"DEBUG: No records to process for hook {handler_cls.__name__}.{method_name}")
@@ -1,7 +1,10 @@
1
+ import logging
1
2
  from django.db import models, transaction
2
3
  from django.db.models import AutoField, Case, Field, Value, When
3
4
 
4
5
  from django_bulk_hooks import engine
6
+
7
+ logger = logging.getLogger(__name__)
5
8
  from django_bulk_hooks.constants import (
6
9
  AFTER_CREATE,
7
10
  AFTER_DELETE,
@@ -78,12 +81,14 @@ class HookQuerySetMixin:
78
81
 
79
82
  # If we're in a bulk operation context, skip hooks to prevent double execution
80
83
  if current_bypass_hooks:
81
- print(f"DEBUG: update method skipping hooks due to bulk operation context")
84
+ logger.debug("update skipping hooks (bulk context)")
82
85
  ctx = HookContext(model_cls, bypass_hooks=True)
83
86
  else:
84
- print(f"DEBUG: update method running hooks (standalone update)")
87
+ logger.debug("update running hooks (standalone)")
85
88
  ctx = HookContext(model_cls, bypass_hooks=False)
86
- # Run BEFORE_UPDATE hooks only for standalone updates
89
+ # Run validation hooks first
90
+ engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
91
+ # Then run BEFORE_UPDATE hooks
87
92
  engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
88
93
 
89
94
  # Use Django's built-in update logic directly
@@ -113,10 +118,10 @@ class HookQuerySetMixin:
113
118
 
114
119
  # Run AFTER_UPDATE hooks only for standalone updates
115
120
  if not current_bypass_hooks:
116
- print(f"DEBUG: update method running AFTER_UPDATE hooks")
121
+ logger.debug("update running AFTER_UPDATE")
117
122
  engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
118
123
  else:
119
- print(f"DEBUG: update method skipping AFTER_UPDATE hooks due to bulk operation context")
124
+ logger.debug("update skipping AFTER_UPDATE (bulk context)")
120
125
 
121
126
  return update_count
122
127
 
@@ -181,7 +186,7 @@ class HookQuerySetMixin:
181
186
  engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
182
187
  else:
183
188
  ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
184
- print(f"DEBUG: Set thread-local bypass_hooks=True for nested calls in bulk_create")
189
+ logger.debug("bulk_create bypassed hooks")
185
190
 
186
191
  # For MTI models, we need to handle them specially
187
192
  if is_mti:
@@ -236,9 +241,7 @@ class HookQuerySetMixin:
236
241
  f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
237
242
  )
238
243
 
239
- print(f"DEBUG: bulk_update called with bypass_hooks={bypass_hooks} (type: {type(bypass_hooks)}), bypass_validation={bypass_validation}")
240
- print(f"DEBUG: bulk_update for model {model_cls.__name__} with {len(objs)} objects")
241
- print(f"DEBUG: kwargs: {kwargs}")
244
+ logger.debug(f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}")
242
245
 
243
246
  # Check for MTI
244
247
  is_mti = False
@@ -248,31 +251,12 @@ class HookQuerySetMixin:
248
251
  break
249
252
 
250
253
  if not bypass_hooks:
251
- print(f"DEBUG: bulk_update running hooks for {len(objs)} objects")
252
- original_map = {
253
- obj.pk: obj
254
- for obj in model_cls._base_manager.filter(
255
- pk__in=[obj.pk for obj in objs]
256
- )
257
- }
258
- originals = [original_map.get(obj.pk) for obj in objs]
254
+ logger.debug("bulk_update setting bypass_hooks=False (hooks will run in update())")
259
255
  ctx = HookContext(model_cls, bypass_hooks=False)
260
- # Run validation hooks first
261
- engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
262
-
263
- # Then run business logic hooks
264
- engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
265
-
266
- # Detect modified fields during hooks
267
- modified_fields = self._detect_modified_fields(objs, originals)
268
- if modified_fields:
269
- fields_set = set(fields)
270
- fields_set.update(modified_fields)
271
- fields = list(fields_set)
256
+ originals = [None] * len(objs) # Placeholder for after_update call
272
257
  else:
273
- print(f"DEBUG: bulk_update skipping hooks and setting bulk context to prevent double execution")
258
+ logger.debug("bulk_update setting bypass_hooks=True (no hooks)")
274
259
  ctx = HookContext(model_cls, bypass_hooks=True)
275
- print(f"DEBUG: Set thread-local bypass_hooks=True to prevent nested update() calls from running hooks")
276
260
  originals = [None] * len(objs) # Ensure originals is defined for after_update call
277
261
 
278
262
  # Handle auto_now fields like Django's update_or_create does
@@ -298,22 +282,16 @@ class HookQuerySetMixin:
298
282
  for k, v in kwargs.items()
299
283
  if k not in ["bypass_hooks", "bypass_validation"]
300
284
  }
301
- print(f"DEBUG: Calling Django's bulk_update with kwargs: {django_kwargs}")
302
- # Call Django's bulk_update with hook suspension to prevent double execution
303
- # Django's bulk_update internally calls .update() which would trigger our hooks again
304
- print(f"DEBUG: About to call Django's bulk_update")
285
+ logger.debug("Calling Django bulk_update")
305
286
  result = super().bulk_update(objs, fields, **django_kwargs)
306
- print(f"DEBUG: Django's bulk_update completed, result: {result}")
287
+ logger.debug(f"Django bulk_update done: {result}")
307
288
 
289
+ # Note: We don't run AFTER_UPDATE hooks here to prevent double execution
290
+ # The update() method will handle all hook execution based on thread-local state
308
291
  if not bypass_hooks:
309
- print(
310
- f"DEBUG: bypass_hooks=False, running AFTER_UPDATE hooks for {len(objs)} objects"
311
- )
312
- engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
292
+ logger.debug("bulk_update skipping AFTER_UPDATE (update() will handle)")
313
293
  else:
314
- print(
315
- f"DEBUG: bypass_hooks=True, skipping AFTER_UPDATE hooks for {len(objs)} objects"
316
- )
294
+ logger.debug("bulk_update bypassed hooks")
317
295
 
318
296
  return result
319
297
 
@@ -0,0 +1,29 @@
1
+ from collections.abc import Callable
2
+ from typing import Union
3
+
4
+ from django_bulk_hooks.priority import Priority
5
+
6
+ _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
+
8
+
9
+ def register_hook(
10
+ model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
11
+ ):
12
+ key = (model, event)
13
+ hooks = _hooks.setdefault(key, [])
14
+ hooks.append((handler_cls, method_name, condition, priority))
15
+ # keep sorted by priority
16
+ hooks.sort(key=lambda x: x[3])
17
+ logger.debug(f"Registered {handler_cls.__name__}.{method_name} for {model.__name__}.{event}")
18
+
19
+
20
+ def get_hooks(model, event):
21
+ key = (model, event)
22
+ hooks = _hooks.get(key, [])
23
+ logger.debug(f"get_hooks {model.__name__}.{event} found {len(hooks)} hooks")
24
+ return hooks
25
+
26
+
27
+ def list_all_hooks():
28
+ """Debug function to list all registered hooks"""
29
+ return _hooks
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.217"
3
+ version = "0.1.219"
4
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"
@@ -1,37 +0,0 @@
1
- from collections.abc import Callable
2
- from typing import Union
3
-
4
- from django_bulk_hooks.priority import Priority
5
-
6
- _hooks: dict[tuple[type, str], list[tuple[type, str, Callable, int]]] = {}
7
-
8
-
9
- def register_hook(
10
- model, event, handler_cls, method_name, condition, priority: Union[int, Priority]
11
- ):
12
- key = (model, event)
13
- hooks = _hooks.setdefault(key, [])
14
- hooks.append((handler_cls, method_name, condition, priority))
15
- # keep sorted by priority
16
- hooks.sort(key=lambda x: x[3])
17
- print(f"DEBUG: Registered hook {handler_cls.__name__}.{method_name} for {model.__name__}.{event} with condition: {condition}")
18
- print(f"DEBUG: Model class: {model} (id: {id(model)})")
19
- print(f"DEBUG: Total hooks for {model.__name__}.{event}: {len(hooks)}")
20
-
21
-
22
- def get_hooks(model, event):
23
- key = (model, event)
24
- hooks = _hooks.get(key, [])
25
- print(f"DEBUG: get_hooks called for {model.__name__}.{event}, found {len(hooks)} hooks")
26
- print(f"DEBUG: Hook key: {key}")
27
- print(f"DEBUG: Model class: {model} (id: {id(model)})")
28
- print(f"DEBUG: All registered hook keys: {list(_hooks.keys())}")
29
- for hook in hooks:
30
- handler_cls, method_name, condition, priority = hook
31
- print(f"DEBUG: Hook: {handler_cls.__name__}.{method_name} with condition: {condition}")
32
- return hooks
33
-
34
-
35
- def list_all_hooks():
36
- """Debug function to list all registered hooks"""
37
- return _hooks