django-bulk-hooks 0.1.230__py3-none-any.whl → 0.1.232__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,14 +1,11 @@
1
1
  import logging
2
- from typing import Any, Optional
3
2
 
4
3
  logger = logging.getLogger(__name__)
5
4
 
6
5
 
7
- def resolve_dotted_attr(instance: Any, dotted_path: str) -> Any:
6
+ def resolve_dotted_attr(instance, dotted_path):
8
7
  """
9
8
  Recursively resolve a dotted attribute path, e.g., "type.category".
10
-
11
- Returns None if any intermediate value is None or missing.
12
9
  """
13
10
  for attr in dotted_path.split("."):
14
11
  if instance is None:
@@ -18,29 +15,29 @@ def resolve_dotted_attr(instance: Any, dotted_path: str) -> Any:
18
15
 
19
16
 
20
17
  class HookCondition:
21
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
18
+ def check(self, instance, original_instance=None):
22
19
  raise NotImplementedError
23
20
 
24
- def __call__(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
21
+ def __call__(self, instance, original_instance=None):
25
22
  return self.check(instance, original_instance)
26
23
 
27
- def __and__(self, other: "HookCondition") -> "HookCondition":
24
+ def __and__(self, other):
28
25
  return AndCondition(self, other)
29
26
 
30
- def __or__(self, other: "HookCondition") -> "HookCondition":
27
+ def __or__(self, other):
31
28
  return OrCondition(self, other)
32
29
 
33
- def __invert__(self) -> "HookCondition":
30
+ def __invert__(self):
34
31
  return NotCondition(self)
35
32
 
36
33
 
37
34
  class IsNotEqual(HookCondition):
38
- def __init__(self, field: str, value: Any, only_on_change: bool = False):
35
+ def __init__(self, field, value, only_on_change=False):
39
36
  self.field = field
40
37
  self.value = value
41
38
  self.only_on_change = only_on_change
42
39
 
43
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
40
+ def check(self, instance, original_instance=None):
44
41
  current = resolve_dotted_attr(instance, self.field)
45
42
  if self.only_on_change:
46
43
  if original_instance is None:
@@ -52,12 +49,12 @@ class IsNotEqual(HookCondition):
52
49
 
53
50
 
54
51
  class IsEqual(HookCondition):
55
- def __init__(self, field: str, value: Any, only_on_change: bool = False):
52
+ def __init__(self, field, value, only_on_change=False):
56
53
  self.field = field
57
54
  self.value = value
58
55
  self.only_on_change = only_on_change
59
56
 
60
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
57
+ def check(self, instance, original_instance=None):
61
58
  current = resolve_dotted_attr(instance, self.field)
62
59
  if self.only_on_change:
63
60
  if original_instance is None:
@@ -69,11 +66,11 @@ class IsEqual(HookCondition):
69
66
 
70
67
 
71
68
  class HasChanged(HookCondition):
72
- def __init__(self, field: str, has_changed: bool = True):
69
+ def __init__(self, field, has_changed=True):
73
70
  self.field = field
74
71
  self.has_changed = has_changed
75
72
 
76
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
73
+ def check(self, instance, original_instance=None):
77
74
  if not original_instance:
78
75
  return False
79
76
 
@@ -88,7 +85,7 @@ class HasChanged(HookCondition):
88
85
 
89
86
 
90
87
  class WasEqual(HookCondition):
91
- def __init__(self, field: str, value: Any, only_on_change: bool = False):
88
+ def __init__(self, field, value, only_on_change=False):
92
89
  """
93
90
  Check if a field's original value was `value`.
94
91
  If only_on_change is True, only return True when the field has changed away from that value.
@@ -97,7 +94,7 @@ class WasEqual(HookCondition):
97
94
  self.value = value
98
95
  self.only_on_change = only_on_change
99
96
 
100
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
97
+ def check(self, instance, original_instance=None):
101
98
  if original_instance is None:
102
99
  return False
103
100
  previous = resolve_dotted_attr(original_instance, self.field)
@@ -109,7 +106,7 @@ class WasEqual(HookCondition):
109
106
 
110
107
 
111
108
  class ChangesTo(HookCondition):
112
- def __init__(self, field: str, value: Any):
109
+ def __init__(self, field, value):
113
110
  """
114
111
  Check if a field's value has changed to `value`.
115
112
  Only returns True when original value != value and current value == value.
@@ -117,7 +114,7 @@ class ChangesTo(HookCondition):
117
114
  self.field = field
118
115
  self.value = value
119
116
 
120
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
117
+ def check(self, instance, original_instance=None):
121
118
  if original_instance is None:
122
119
  return False
123
120
  previous = resolve_dotted_attr(original_instance, self.field)
@@ -126,70 +123,70 @@ class ChangesTo(HookCondition):
126
123
 
127
124
 
128
125
  class IsGreaterThan(HookCondition):
129
- def __init__(self, field: str, value: Any):
126
+ def __init__(self, field, value):
130
127
  self.field = field
131
128
  self.value = value
132
129
 
133
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
130
+ def check(self, instance, original_instance=None):
134
131
  current = resolve_dotted_attr(instance, self.field)
135
132
  return current is not None and current > self.value
136
133
 
137
134
 
138
135
  class IsGreaterThanOrEqual(HookCondition):
139
- def __init__(self, field: str, value: Any):
136
+ def __init__(self, field, value):
140
137
  self.field = field
141
138
  self.value = value
142
139
 
143
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
140
+ def check(self, instance, original_instance=None):
144
141
  current = resolve_dotted_attr(instance, self.field)
145
142
  return current is not None and current >= self.value
146
143
 
147
144
 
148
145
  class IsLessThan(HookCondition):
149
- def __init__(self, field: str, value: Any):
146
+ def __init__(self, field, value):
150
147
  self.field = field
151
148
  self.value = value
152
149
 
153
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
150
+ def check(self, instance, original_instance=None):
154
151
  current = resolve_dotted_attr(instance, self.field)
155
152
  return current is not None and current < self.value
156
153
 
157
154
 
158
155
  class IsLessThanOrEqual(HookCondition):
159
- def __init__(self, field: str, value: Any):
156
+ def __init__(self, field, value):
160
157
  self.field = field
161
158
  self.value = value
162
159
 
163
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
160
+ def check(self, instance, original_instance=None):
164
161
  current = resolve_dotted_attr(instance, self.field)
165
162
  return current is not None and current <= self.value
166
163
 
167
164
 
168
165
  class AndCondition(HookCondition):
169
- def __init__(self, cond1: HookCondition, cond2: HookCondition):
166
+ def __init__(self, cond1, cond2):
170
167
  self.cond1 = cond1
171
168
  self.cond2 = cond2
172
169
 
173
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
170
+ def check(self, instance, original_instance=None):
174
171
  return self.cond1.check(instance, original_instance) and self.cond2.check(
175
172
  instance, original_instance
176
173
  )
177
174
 
178
175
 
179
176
  class OrCondition(HookCondition):
180
- def __init__(self, cond1: HookCondition, cond2: HookCondition):
177
+ def __init__(self, cond1, cond2):
181
178
  self.cond1 = cond1
182
179
  self.cond2 = cond2
183
180
 
184
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
181
+ def check(self, instance, original_instance=None):
185
182
  return self.cond1.check(instance, original_instance) or self.cond2.check(
186
183
  instance, original_instance
187
184
  )
188
185
 
189
186
 
190
187
  class NotCondition(HookCondition):
191
- def __init__(self, cond: HookCondition):
188
+ def __init__(self, cond):
192
189
  self.cond = cond
193
190
 
194
- def check(self, instance: Any, original_instance: Optional[Any] = None) -> bool:
191
+ def check(self, instance, original_instance=None):
195
192
  return not self.cond.check(instance, original_instance)
@@ -1,61 +1,36 @@
1
1
  import threading
2
- from typing import Deque, Optional
3
- from django_bulk_hooks.handler import hook_vars, get_hook_queue as _handler_get_hook_queue
2
+ from collections import deque
3
+ from django_bulk_hooks.handler import hook_vars
4
4
 
5
5
 
6
6
  _hook_context = threading.local()
7
7
 
8
8
 
9
- def get_hook_queue() -> Deque:
10
- """
11
- Return the per-thread hook execution queue used by the handler.
9
+ def get_hook_queue():
10
+ if not hasattr(_hook_context, "queue"):
11
+ _hook_context.queue = deque()
12
+ return _hook_context.queue
12
13
 
13
- This proxies to the centralized queue in the handler module to avoid
14
- divergent queues.
15
- """
16
- return _handler_get_hook_queue()
17
14
 
18
-
19
- def set_bypass_hooks(bypass_hooks: bool) -> None:
15
+ def set_bypass_hooks(bypass_hooks):
20
16
  """Set the current bypass_hooks state for the current thread."""
21
17
  _hook_context.bypass_hooks = bypass_hooks
22
18
 
23
19
 
24
- def get_bypass_hooks() -> bool:
20
+ def get_bypass_hooks():
25
21
  """Get the current bypass_hooks state for the current thread."""
26
22
  return getattr(_hook_context, 'bypass_hooks', False)
27
23
 
28
24
 
29
25
  class HookContext:
30
- """
31
- Hook execution context helper.
32
-
33
- - Carries the model and bypass_hooks flag.
34
- - On construction sets thread-local bypass state;
35
- when used as a context manager, restores previous state on exit.
36
- """
37
-
38
- def __init__(self, model: type, bypass_hooks: bool = False):
39
- self.model: type = model
40
- self.bypass_hooks: bool = bypass_hooks
41
- self._prev_bypass: Optional[bool] = None
42
- # Preserve legacy behavior: set bypass state at construction
26
+ def __init__(self, model, bypass_hooks=False):
27
+ self.model = model
28
+ self.bypass_hooks = bypass_hooks
29
+ # Set the thread-local bypass state when creating a context
43
30
  set_bypass_hooks(bypass_hooks)
44
31
 
45
- def __enter__(self) -> "HookContext":
46
- self._prev_bypass = get_bypass_hooks()
47
- set_bypass_hooks(self.bypass_hooks)
48
- return self
49
-
50
- def __exit__(self, exc_type, exc, tb) -> bool:
51
- # Restore previous bypass state
52
- if self._prev_bypass is not None:
53
- set_bypass_hooks(self._prev_bypass)
54
- # Do not suppress exceptions
55
- return False
56
-
57
32
  @property
58
- def is_executing(self) -> bool:
33
+ def is_executing(self):
59
34
  """
60
35
  Check if we're currently in a hook execution context.
61
36
  Similar to Salesforce's Trigger.isExecuting.
@@ -64,18 +39,15 @@ class HookContext:
64
39
  return hasattr(hook_vars, 'event') and hook_vars.event is not None
65
40
 
66
41
  @property
67
- def current_event(self) -> Optional[str]:
42
+ def current_event(self):
68
43
  """
69
44
  Get the current hook event being executed.
70
45
  """
71
46
  return getattr(hook_vars, 'event', None)
72
47
 
73
48
  @property
74
- def execution_depth(self) -> int:
49
+ def execution_depth(self):
75
50
  """
76
51
  Get the current execution depth to detect deep recursion.
77
52
  """
78
53
  return getattr(hook_vars, 'depth', 0)
79
-
80
- def __repr__(self) -> str:
81
- return f"HookContext(model={getattr(self.model, '__name__', self.model)!r}, bypass_hooks={self.bypass_hooks})"
@@ -1,28 +1,18 @@
1
- """
2
- Decorators for defining and optimizing hook handlers.
3
-
4
- Notes:
5
- - Hook registration occurs at import time; importing modules that define Hook
6
- subclasses or use @hook will register handlers in the global registry.
7
- - The preload helpers below are safe, in-place optimizations to avoid N+1s.
8
- """
9
-
10
1
  import inspect
11
2
  from functools import wraps
12
- from typing import Any, Callable, Optional
13
3
 
14
4
  from django.core.exceptions import FieldDoesNotExist
15
5
  from django_bulk_hooks.enums import DEFAULT_PRIORITY
16
6
  from django_bulk_hooks.registry import register_hook
17
7
 
18
8
 
19
- def hook(event: str, *, model: type, condition: Optional[Callable] = None, priority: int = DEFAULT_PRIORITY):
9
+ def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
20
10
  """
21
11
  Decorator to annotate a method with multiple hooks hook registrations.
22
12
  If no priority is provided, uses Priority.NORMAL (50).
23
13
  """
24
14
 
25
- def decorator(fn: Callable):
15
+ def decorator(fn):
26
16
  if not hasattr(fn, "hooks_hooks"):
27
17
  fn.hooks_hooks = []
28
18
  fn.hooks_hooks.append((model, event, condition, priority))
@@ -31,7 +21,7 @@ def hook(event: str, *, model: type, condition: Optional[Callable] = None, prior
31
21
  return decorator
32
22
 
33
23
 
34
- def select_related(*related_fields: str):
24
+ def select_related(*related_fields):
35
25
  """
36
26
  Decorator that preloads related fields in-place on `new_records`, before the hook logic runs.
37
27
 
@@ -40,14 +30,11 @@ def select_related(*related_fields: str):
40
30
  - Populates Django's relation cache to avoid extra queries
41
31
  """
42
32
 
43
- def decorator(func: Callable):
44
- # No-op if no fields specified
45
- if not related_fields:
46
- return func
33
+ def decorator(func):
47
34
  sig = inspect.signature(func)
48
35
 
49
36
  @wraps(func)
50
- def wrapper(*args: Any, **kwargs: Any):
37
+ def wrapper(*args, **kwargs):
51
38
  bound = sig.bind_partial(*args, **kwargs)
52
39
  bound.apply_defaults()
53
40
 
@@ -118,7 +105,7 @@ def select_related(*related_fields: str):
118
105
  return decorator
119
106
 
120
107
 
121
- def bulk_hook(model_cls: type, event: str, when: Optional[Callable] = None, priority: Optional[int] = None):
108
+ def bulk_hook(model_cls, event, when=None, priority=None):
122
109
  """
123
110
  Decorator to register a bulk hook for a model.
124
111
 
@@ -128,13 +115,13 @@ def bulk_hook(model_cls: type, event: str, when: Optional[Callable] = None, prio
128
115
  when: Optional condition for when the hook should run
129
116
  priority: Optional priority for hook execution order
130
117
  """
131
- def decorator(func: Callable):
118
+ def decorator(func):
132
119
  # Create a simple handler class for the function
133
120
  class FunctionHandler:
134
121
  def __init__(self):
135
122
  self.func = func
136
123
 
137
- def handle(self, new_instances: list, original_instances: Optional[list]):
124
+ def handle(self, new_instances, original_instances):
138
125
  return self.func(new_instances, original_instances)
139
126
 
140
127
  # Register the hook using the registry
@@ -148,82 +135,3 @@ def bulk_hook(model_cls: type, event: str, when: Optional[Callable] = None, prio
148
135
  )
149
136
  return func
150
137
  return decorator
151
-
152
-
153
- def prefetch_related(*related_fields: str):
154
- """
155
- Decorator that prefetches related collections on `new_records` in-place,
156
- populating Django's prefetch cache to avoid extra queries in hooks.
157
-
158
- - Supports many-to-many and one-to-many relationships
159
- - Preserves instance identity; does not replace objects
160
- - Uses the base manager to avoid recursive hook triggering
161
- """
162
-
163
- def decorator(func: Callable):
164
- # No-op if no fields specified
165
- if not related_fields:
166
- return func
167
-
168
- sig = inspect.signature(func)
169
-
170
- @wraps(func)
171
- def wrapper(*args: Any, **kwargs: Any):
172
- bound = sig.bind_partial(*args, **kwargs)
173
- bound.apply_defaults()
174
-
175
- if "new_records" not in bound.arguments:
176
- raise TypeError(
177
- "@prefetch_related requires a 'new_records' argument in the decorated function"
178
- )
179
-
180
- new_records = bound.arguments["new_records"]
181
-
182
- if not isinstance(new_records, list):
183
- raise TypeError(
184
- f"@prefetch_related expects a list of model instances, got {type(new_records)}"
185
- )
186
-
187
- if not new_records:
188
- return func(*args, **kwargs)
189
-
190
- model_cls = new_records[0].__class__
191
- ids_to_fetch = [obj.pk for obj in new_records if getattr(obj, "pk", None)]
192
-
193
- if ids_to_fetch:
194
- # Validate fields (no dotted notation)
195
- for field in related_fields:
196
- if "." in field:
197
- raise ValueError(
198
- f"@prefetch_related does not support nested fields like '{field}'"
199
- )
200
-
201
- fetched_map = {
202
- obj.pk: obj
203
- for obj in (
204
- model_cls._base_manager.filter(pk__in=ids_to_fetch)
205
- .prefetch_related(*related_fields)
206
- )
207
- }
208
-
209
- for obj in new_records:
210
- preloaded = fetched_map.get(obj.pk)
211
- if preloaded is None:
212
- continue
213
- # Copy prefetch cache entries from the preloaded instance
214
- src_cache = getattr(preloaded, "_prefetched_objects_cache", {})
215
- if not src_cache:
216
- continue
217
- dst_cache = getattr(obj, "_prefetched_objects_cache", None)
218
- if dst_cache is None:
219
- obj._prefetched_objects_cache = {}
220
- dst_cache = obj._prefetched_objects_cache
221
- for field in related_fields:
222
- if field in src_cache and field not in dst_cache:
223
- dst_cache[field] = src_cache[field]
224
-
225
- return func(*bound.args, **bound.kwargs)
226
-
227
- return wrapper
228
-
229
- return decorator
@@ -1,10 +1,8 @@
1
1
  import logging
2
2
 
3
3
  from django.core.exceptions import ValidationError
4
- from django.db import transaction
5
4
 
6
5
  from django_bulk_hooks.registry import get_hooks
7
- from django_bulk_hooks.handler import hook_vars
8
6
 
9
7
  logger = logging.getLogger(__name__)
10
8
 
@@ -12,35 +10,24 @@ logger = logging.getLogger(__name__)
12
10
  def run(model_cls, event, new_records, old_records=None, ctx=None):
13
11
  """
14
12
  Run hooks for a given model, event, and records.
15
-
16
- Production-grade executor:
17
- - Honors bypass_hooks via ctx
18
- - Runs model.clean() before BEFORE_* events for validation
19
- - Executes hooks in registry priority order with condition filtering
20
- - Exposes thread-local context via hook_vars during execution
21
- - AFTER_* timing is configurable via settings.BULK_HOOKS_AFTER_ON_COMMIT (default: False)
22
-
23
- Args:
24
- model_cls: The Django model class
25
- event: The hook event (e.g., 'before_create', 'after_update')
26
- new_records: List of new/updated records
27
- old_records: List of original records (for comparison)
28
- ctx: Optional hook context
29
13
  """
30
14
  if not new_records:
31
15
  return
32
16
 
17
+ # Get hooks for this model and event
33
18
  hooks = get_hooks(model_cls, event)
19
+
34
20
  if not hooks:
35
21
  return
36
22
 
37
- logger.debug(
38
- f"Running {len(hooks)} hooks for {model_cls.__name__}.{event} ({len(new_records)} records)"
39
- )
23
+ import traceback
40
24
 
25
+ stack = traceback.format_stack()
26
+ logger.debug(f"engine.run {model_cls.__name__}.{event} {len(new_records)} records")
27
+
41
28
  # Check if we're in a bypass context
42
- if ctx and hasattr(ctx, "bypass_hooks") and ctx.bypass_hooks:
43
- logger.debug("Hook execution bypassed")
29
+ if ctx and hasattr(ctx, 'bypass_hooks') and ctx.bypass_hooks:
30
+ logger.debug("engine.run bypassed")
44
31
  return
45
32
 
46
33
  # For BEFORE_* events, run model.clean() first for validation
@@ -52,76 +39,36 @@ def run(model_cls, event, new_records, old_records=None, ctx=None):
52
39
  logger.error("Validation failed for %s: %s", instance, e)
53
40
  raise
54
41
 
55
- def _execute():
56
- hook_vars.depth += 1
57
- hook_vars.new = new_records
58
- hook_vars.old = old_records
59
- hook_vars.event = event
60
- hook_vars.model = model_cls
61
-
62
- try:
63
- # Align old_records length with new_records for safe zipping
64
- local_old = old_records or []
65
- if len(local_old) < len(new_records):
66
- local_old = local_old + [None] * (len(new_records) - len(local_old))
67
-
68
- for handler_cls, method_name, condition, priority in hooks:
69
- try:
70
- handler_instance = handler_cls()
71
- func = getattr(handler_instance, method_name)
72
- except Exception as e:
73
- logger.error(
74
- "Failed to instantiate %s.%s: %s",
75
- handler_cls.__name__,
76
- method_name,
77
- e,
78
- )
79
- continue
80
-
81
- # Condition filtering per record
82
- to_process_new = []
83
- to_process_old = []
84
- for new_obj, old_obj in zip(new_records, local_old, strict=True):
85
- if not condition:
86
- to_process_new.append(new_obj)
87
- to_process_old.append(old_obj)
88
- else:
89
- try:
90
- if condition.check(new_obj, old_obj):
91
- to_process_new.append(new_obj)
92
- to_process_old.append(old_obj)
93
- except Exception as e:
94
- logger.error(
95
- "Condition failed for %s.%s: %s",
96
- handler_cls.__name__,
97
- method_name,
98
- e,
99
- )
100
- continue
101
-
102
- if not to_process_new:
103
- continue
104
-
105
- try:
106
- func(
107
- new_records=to_process_new,
108
- old_records=to_process_old
109
- if any(x is not None for x in to_process_old)
110
- else None,
111
- )
112
- except Exception:
113
- logger.exception(
114
- "Error in hook %s.%s", handler_cls.__name__, method_name
115
- )
116
- # Re-raise to ensure proper transactional behavior
117
- raise
118
- finally:
119
- hook_vars.new = None
120
- hook_vars.old = None
121
- hook_vars.event = None
122
- hook_vars.model = None
123
- hook_vars.depth -= 1
124
-
125
- # Execute immediately so AFTER_* runs within the transaction.
126
- # If a hook raises, the transaction is rolled back (Salesforce-style).
127
- _execute()
42
+ # Process hooks
43
+ for handler_cls, method_name, condition, priority in hooks:
44
+ logger.debug(f"Processing {handler_cls.__name__}.{method_name}")
45
+ handler_instance = handler_cls()
46
+ func = getattr(handler_instance, method_name)
47
+
48
+ to_process_new = []
49
+ to_process_old = []
50
+
51
+ for new, original in zip(
52
+ new_records,
53
+ old_records or [None] * len(new_records),
54
+ strict=True,
55
+ ):
56
+ if not condition:
57
+ to_process_new.append(new)
58
+ to_process_old.append(original)
59
+ else:
60
+ condition_result = condition.check(new, original)
61
+ if condition_result:
62
+ to_process_new.append(new)
63
+ to_process_old.append(original)
64
+
65
+ if to_process_new:
66
+ logger.debug(f"Executing {handler_cls.__name__}.{method_name} for {len(to_process_new)} records")
67
+ try:
68
+ func(
69
+ new_records=to_process_new,
70
+ old_records=to_process_old if any(to_process_old) else None,
71
+ )
72
+ except Exception as e:
73
+ logger.debug(f"Hook execution failed: {e}")
74
+ raise
@@ -1,14 +1,17 @@
1
- """
2
- Compatibility layer for priority-related exports.
1
+ from enum import IntEnum
3
2
 
4
- This module re-exports the canonical Priority enum and a DEFAULT_PRIORITY
5
- constant so existing imports like `from django_bulk_hooks.enums import ...`
6
- continue to work without churn. Prefer importing from `priority` in new code.
7
- """
8
3
 
9
- from django_bulk_hooks.priority import Priority
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
10
15
 
11
- # Default priority used when none is specified by the hook decorator
12
- DEFAULT_PRIORITY = Priority.NORMAL
13
16
 
14
- __all__ = ["Priority", "DEFAULT_PRIORITY"]
17
+ DEFAULT_PRIORITY = Priority.NORMAL