Skip to content

Commit 8a1b778

Browse files
committed
feat(procrastinate): @track decorator with bare and parameterized forms
1 parent 06a8176 commit 8a1b778

2 files changed

Lines changed: 106 additions & 1 deletion

File tree

taskbadger/procrastinate.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,41 @@ def _maybe_create_pending(task, kwargs):
222222
new_kwargs = dict(kwargs)
223223
new_kwargs[TB_TASK_ID_KWARG] = tb_task.id
224224
return new_kwargs
225+
226+
227+
_TRACK_OPT_KEYS = ("name", "value_max", "tags", "data", "record_task_args")
228+
229+
230+
def track(original_task=None, **opts):
231+
"""Opt a Procrastinate task into TaskBadger tracking.
232+
233+
Usage:
234+
235+
@track
236+
@app.task(...)
237+
def my_task(...): ...
238+
239+
@track(name="custom", value_max=100, tags={"env": "prod"})
240+
@app.task(...)
241+
async def big_job(...): ...
242+
243+
Accepted keyword options (all optional):
244+
name: TaskBadger task name (defaults to the Procrastinate task's name).
245+
value_max: Maximum value for the TaskBadger task.
246+
tags: Dict of tags applied to the TaskBadger task.
247+
data: Dict of initial data merged into the TaskBadger task.
248+
record_task_args: If True, serialize the Procrastinate job kwargs and
249+
store them under ``data["procrastinate_task_kwargs"]``. Defaults to
250+
``None`` meaning "inherit from system integration if any, else False".
251+
"""
252+
unknown = set(opts) - set(_TRACK_OPT_KEYS)
253+
if unknown:
254+
raise TypeError(f"track() got unexpected keyword arguments: {sorted(unknown)}")
255+
256+
def wrap(task):
257+
_instrument_task(task, system=None, manual=True, opts=opts)
258+
return task
259+
260+
if original_task is None:
261+
return wrap
262+
return wrap(original_task)

tests/test_procrastinate.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from procrastinate import testing
88

99
from taskbadger import StatusEnum
10-
from taskbadger.procrastinate import TB_TASK_ID_KWARG, _instrument_task
10+
from taskbadger.procrastinate import TB_TASK_ID_KWARG, _instrument_task, track
1111
from tests.utils import task_for_test
1212

1313

@@ -181,3 +181,70 @@ def add6(a, b):
181181
create.assert_called_once()
182182
statuses = [c.kwargs["status"] for c in update.call_args_list]
183183
assert statuses == [StatusEnum.PROCESSING, StatusEnum.SUCCESS]
184+
185+
186+
@pytest.mark.usefixtures("_bind_settings")
187+
def test_track_bare_form(app):
188+
@track
189+
@app.task(name="bare")
190+
def bare(a):
191+
return a
192+
193+
tb = task_for_test()
194+
with mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb):
195+
bare.defer(a=1)
196+
197+
assert getattr(bare, "_taskbadger_manual") is True
198+
# Inspect the actual Procrastinate job - jobs is a dict keyed by int, kwargs under "args"
199+
jobs = list(app.connector.jobs.values())
200+
assert jobs[0]["args"][TB_TASK_ID_KWARG] == tb.id
201+
202+
203+
@pytest.mark.usefixtures("_bind_settings")
204+
def test_track_parameterized(app):
205+
@track(name="custom", value_max=10, tags={"env": "test"}, data={"k": "v"})
206+
@app.task(name="raw_name")
207+
def raw(a):
208+
return a
209+
210+
tb = task_for_test()
211+
with mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create:
212+
raw.defer(a=1)
213+
214+
create.assert_called_once()
215+
assert create.call_args.args == ("custom",)
216+
assert create.call_args.kwargs == {
217+
"status": StatusEnum.PENDING,
218+
"value_max": 10,
219+
"tags": {"env": "test"},
220+
"data": {"k": "v"},
221+
}
222+
223+
224+
@pytest.mark.usefixtures("_bind_settings")
225+
def test_track_idempotent(app):
226+
@track
227+
@track
228+
@app.task(name="dup")
229+
def dup(a):
230+
return a
231+
232+
# Two @track applications must not double-wrap; defer once still creates one
233+
# PENDING task and injects one id.
234+
tb = task_for_test()
235+
with mock.patch("taskbadger.procrastinate.create_task_safe", return_value=tb) as create:
236+
dup.defer(a=1)
237+
assert create.call_count == 1
238+
jobs = list(app.connector.jobs.values())
239+
args = jobs[0]["args"]
240+
# Reserved key appears exactly once
241+
assert list(args).count(TB_TASK_ID_KWARG) == 1
242+
243+
244+
def test_track_unknown_opt_raises(app):
245+
@app.task(name="bad")
246+
def bad():
247+
pass
248+
249+
with pytest.raises(TypeError, match="unexpected keyword"):
250+
track(name="x", does_not_exist=True)(bad)

0 commit comments

Comments
 (0)