taskbadger 2.0.0__tar.gz → 2.1.0a2__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.
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/PKG-INFO +60 -1
- taskbadger-2.1.0a2/README.md +100 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/pyproject.toml +5 -1
- taskbadger-2.1.0a2/taskbadger/_integrations.py +110 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/celery.py +7 -62
- taskbadger-2.1.0a2/taskbadger/procrastinate.py +335 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/systems/celery.py +9 -27
- taskbadger-2.1.0a2/taskbadger/systems/procrastinate.py +51 -0
- taskbadger-2.0.0/README.md +0 -43
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/.gitignore +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/LICENSE +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli/basics.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli/list_tasks.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli/utils.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli/wrapper.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/cli_main.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/config.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/decorators.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/exceptions.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/integrations.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_cancel.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_create.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_get.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_list.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_partial_update.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_update.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_cancel.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_create.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_get.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_list.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_partial_update.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_update.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/client.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/errors.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/action.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/action_request.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/paginated_task_list.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/patched_action_request.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/patched_task_request.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/patched_task_request_tags.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/status_enum.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/task.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/task_request.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/task_request_tags.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/task_tags.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/py.typed +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/types.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/mug.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/process.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/safe_sdk.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/sdk.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/systems/__init__.py +0 -0
- {taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: taskbadger
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0a2
|
|
4
4
|
Summary: The official Python SDK for Task Badger
|
|
5
5
|
Project-URL: Changelog, https://github.com/taskbadger/taskbadger-python/releases
|
|
6
6
|
Project-URL: homepage, https://taskbadger.net/
|
|
@@ -28,6 +28,8 @@ Requires-Dist: celery<6.0.0,>=4.0.0; extra == 'celery'
|
|
|
28
28
|
Provides-Extra: cli
|
|
29
29
|
Requires-Dist: rich>=13.0; extra == 'cli'
|
|
30
30
|
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
31
|
+
Provides-Extra: procrastinate
|
|
32
|
+
Requires-Dist: procrastinate>=3.0; extra == 'procrastinate'
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
|
|
33
35
|
# Task Badger Python Client
|
|
@@ -73,3 +75,60 @@ taskbadger.init(
|
|
|
73
75
|
$ export TASKBADGER_API_KEY=***
|
|
74
76
|
$ taskbadger run "nightly-backup" -- ./backup.sh
|
|
75
77
|
```
|
|
78
|
+
|
|
79
|
+
### Procrastinate Integration
|
|
80
|
+
|
|
81
|
+
The SDK includes optional support for the [Procrastinate](https://procrastinate.readthedocs.io/) task queue.
|
|
82
|
+
|
|
83
|
+
Install with the extra:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install 'taskbadger[procrastinate]'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Opt a single task into tracking with the `track` decorator:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import procrastinate
|
|
93
|
+
from taskbadger.procrastinate import track, current_task
|
|
94
|
+
|
|
95
|
+
app = procrastinate.App(connector=...)
|
|
96
|
+
|
|
97
|
+
@track
|
|
98
|
+
@app.task(queue="default")
|
|
99
|
+
async def add(a, b):
|
|
100
|
+
return a + b
|
|
101
|
+
|
|
102
|
+
@track(name="report", value_max=100, tags={"env": "prod"})
|
|
103
|
+
@app.task
|
|
104
|
+
async def report(rows):
|
|
105
|
+
tb = current_task()
|
|
106
|
+
for i, row in enumerate(rows):
|
|
107
|
+
await process(row)
|
|
108
|
+
if i % 10 == 0:
|
|
109
|
+
tb.update(value=i)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To auto-track every task on an App, register the system integration:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
import taskbadger
|
|
116
|
+
from taskbadger.systems.procrastinate import ProcrastinateSystemIntegration
|
|
117
|
+
|
|
118
|
+
taskbadger.init(
|
|
119
|
+
token="***",
|
|
120
|
+
systems=[ProcrastinateSystemIntegration(
|
|
121
|
+
app=app,
|
|
122
|
+
auto_track_tasks=True,
|
|
123
|
+
includes=[r"myapp\..*"],
|
|
124
|
+
excludes=[r"myapp\.cleanup\..*"],
|
|
125
|
+
record_task_args=True,
|
|
126
|
+
)],
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Known limitations
|
|
131
|
+
|
|
132
|
+
- **`task.configure(...).defer(...)` is not tracked.** Procrastinate's `configure()` returns a separate `JobDeferrer` whose methods bypass our wrapper. Use `task.defer(...)` directly for tracked deferrals. Tasks deferred via `configure().defer()` will run normally but will not appear in TaskBadger.
|
|
133
|
+
- **`task.batch_defer*` is not tracked.** Same reason as `configure().defer()`.
|
|
134
|
+
- **Tasks added via `app.add_tasks_from(blueprint)` after `ProcrastinateSystemIntegration` is constructed are not auto-instrumented.** Construct the integration after all blueprints are registered, or apply `@track` to those tasks explicitly.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Task Badger Python Client
|
|
2
|
+
|
|
3
|
+
This is the official Python SDK for [Task Badger](https://taskbadger.net/).
|
|
4
|
+
|
|
5
|
+
For full documentation go to https://docs.taskbadger.net/python/.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
### Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install taskbadger
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
To use the `taskbadger` command-line tool, install the `cli` extra:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install 'taskbadger[cli]'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Client Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import taskbadger
|
|
29
|
+
from taskbadger.systems import CelerySystemIntegration
|
|
30
|
+
|
|
31
|
+
taskbadger.init(
|
|
32
|
+
token="***",
|
|
33
|
+
systems=[CelerySystemIntegration()],
|
|
34
|
+
tags={"environment": "production"}
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### CLI Usage
|
|
39
|
+
|
|
40
|
+
```shell
|
|
41
|
+
$ export TASKBADGER_API_KEY=***
|
|
42
|
+
$ taskbadger run "nightly-backup" -- ./backup.sh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Procrastinate Integration
|
|
46
|
+
|
|
47
|
+
The SDK includes optional support for the [Procrastinate](https://procrastinate.readthedocs.io/) task queue.
|
|
48
|
+
|
|
49
|
+
Install with the extra:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install 'taskbadger[procrastinate]'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Opt a single task into tracking with the `track` decorator:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import procrastinate
|
|
59
|
+
from taskbadger.procrastinate import track, current_task
|
|
60
|
+
|
|
61
|
+
app = procrastinate.App(connector=...)
|
|
62
|
+
|
|
63
|
+
@track
|
|
64
|
+
@app.task(queue="default")
|
|
65
|
+
async def add(a, b):
|
|
66
|
+
return a + b
|
|
67
|
+
|
|
68
|
+
@track(name="report", value_max=100, tags={"env": "prod"})
|
|
69
|
+
@app.task
|
|
70
|
+
async def report(rows):
|
|
71
|
+
tb = current_task()
|
|
72
|
+
for i, row in enumerate(rows):
|
|
73
|
+
await process(row)
|
|
74
|
+
if i % 10 == 0:
|
|
75
|
+
tb.update(value=i)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To auto-track every task on an App, register the system integration:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import taskbadger
|
|
82
|
+
from taskbadger.systems.procrastinate import ProcrastinateSystemIntegration
|
|
83
|
+
|
|
84
|
+
taskbadger.init(
|
|
85
|
+
token="***",
|
|
86
|
+
systems=[ProcrastinateSystemIntegration(
|
|
87
|
+
app=app,
|
|
88
|
+
auto_track_tasks=True,
|
|
89
|
+
includes=[r"myapp\..*"],
|
|
90
|
+
excludes=[r"myapp\.cleanup\..*"],
|
|
91
|
+
record_task_args=True,
|
|
92
|
+
)],
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Known limitations
|
|
97
|
+
|
|
98
|
+
- **`task.configure(...).defer(...)` is not tracked.** Procrastinate's `configure()` returns a separate `JobDeferrer` whose methods bypass our wrapper. Use `task.defer(...)` directly for tracked deferrals. Tasks deferred via `configure().defer()` will run normally but will not appear in TaskBadger.
|
|
99
|
+
- **`task.batch_defer*` is not tracked.** Same reason as `configure().defer()`.
|
|
100
|
+
- **Tasks added via `app.add_tasks_from(blueprint)` after `ProcrastinateSystemIntegration` is constructed are not auto-instrumented.** Construct the integration after all blueprints are registered, or apply `@track` to those tasks explicitly.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "taskbadger"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.0a2"
|
|
4
4
|
description = "The official Python SDK for Task Badger"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
authors = []
|
|
@@ -44,6 +44,9 @@ cli = [
|
|
|
44
44
|
"typer >=0.12",
|
|
45
45
|
"rich >=13.0",
|
|
46
46
|
]
|
|
47
|
+
procrastinate = [
|
|
48
|
+
"procrastinate>=3.0",
|
|
49
|
+
]
|
|
47
50
|
|
|
48
51
|
[tool.uv]
|
|
49
52
|
package = true
|
|
@@ -58,6 +61,7 @@ documentation = "https://docs.taskbadger.net/"
|
|
|
58
61
|
[dependency-groups]
|
|
59
62
|
dev = [
|
|
60
63
|
"pre-commit",
|
|
64
|
+
"procrastinate",
|
|
61
65
|
"pytest",
|
|
62
66
|
"pytest-httpx",
|
|
63
67
|
"invoke",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Shared internals for taskbadger's optional system integrations
|
|
2
|
+
(Celery, Procrastinate). Not part of the public API.
|
|
3
|
+
|
|
4
|
+
A single module-level ``TaskCache`` (``task_cache``) is shared across all
|
|
5
|
+
integrations; task ids are UUIDs so cross-integration key collisions are not
|
|
6
|
+
a concern. ``BaseSystemIntegration`` provides the common ctor/include-exclude
|
|
7
|
+
shape; subclasses override ``track_task`` if they need to filter additional
|
|
8
|
+
task names (e.g. Procrastinate built-ins).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import collections
|
|
14
|
+
import logging
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from . import sdk
|
|
18
|
+
from .internal.models import StatusEnum
|
|
19
|
+
from .systems import System
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("taskbadger")
|
|
22
|
+
|
|
23
|
+
TERMINAL_STATES = {
|
|
24
|
+
StatusEnum.SUCCESS,
|
|
25
|
+
StatusEnum.ERROR,
|
|
26
|
+
StatusEnum.CANCELLED,
|
|
27
|
+
StatusEnum.STALE,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TaskCache:
|
|
32
|
+
"""Bounded LRU-ish cache for TaskBadger Task objects.
|
|
33
|
+
|
|
34
|
+
Keys are arbitrary hashable values chosen by the caller (typically the
|
|
35
|
+
task id). Auto-prunes on ``set`` when ``maxsize`` is exceeded.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, maxsize: int = 128):
|
|
39
|
+
self.cache: collections.OrderedDict = collections.OrderedDict()
|
|
40
|
+
self.maxsize = maxsize
|
|
41
|
+
|
|
42
|
+
def set(self, key, value) -> None:
|
|
43
|
+
self.cache[key] = value
|
|
44
|
+
if len(self.cache) > self.maxsize:
|
|
45
|
+
self.cache.popitem(last=False)
|
|
46
|
+
|
|
47
|
+
def get(self, key):
|
|
48
|
+
return self.cache.get(key)
|
|
49
|
+
|
|
50
|
+
def unset(self, key) -> None:
|
|
51
|
+
self.cache.pop(key, None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
task_cache = TaskCache()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def safe_get_task(task_id: str):
|
|
58
|
+
"""Cache-aware ``get_task``: returns the cached entry if present, otherwise
|
|
59
|
+
fetches from the API and caches the result. Errors are logged and swallowed
|
|
60
|
+
(returns ``None``). ``None`` results are not cached.
|
|
61
|
+
"""
|
|
62
|
+
cached = task_cache.get(task_id)
|
|
63
|
+
if cached is not None:
|
|
64
|
+
return cached
|
|
65
|
+
try:
|
|
66
|
+
task = sdk.get_task(task_id)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
log.warning("Error fetching task '%s': %s", task_id, e)
|
|
69
|
+
return None
|
|
70
|
+
task_cache.set(task_id, task)
|
|
71
|
+
return task
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def match_task_name(task_name: str, includes, excludes) -> bool:
|
|
75
|
+
"""Return True if ``task_name`` should be tracked under the given rules.
|
|
76
|
+
|
|
77
|
+
Excludes win over includes. Both lists contain regex strings matched with
|
|
78
|
+
``re.fullmatch``. ``None`` means "no rule".
|
|
79
|
+
"""
|
|
80
|
+
if excludes:
|
|
81
|
+
for exclude in excludes:
|
|
82
|
+
if re.fullmatch(exclude, task_name):
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
if includes:
|
|
86
|
+
for include in includes:
|
|
87
|
+
if re.fullmatch(include, task_name):
|
|
88
|
+
return True
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class BaseSystemIntegration(System):
|
|
95
|
+
"""Common ctor + ``track_task`` body for system integrations.
|
|
96
|
+
|
|
97
|
+
Subclasses set ``identifier`` and may override ``track_task`` to add
|
|
98
|
+
additional filtering (e.g. skipping built-in tasks).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, auto_track_tasks=True, includes=None, excludes=None, record_task_args=False):
|
|
102
|
+
self.auto_track_tasks = auto_track_tasks
|
|
103
|
+
self.includes = includes
|
|
104
|
+
self.excludes = excludes
|
|
105
|
+
self.record_task_args = record_task_args
|
|
106
|
+
|
|
107
|
+
def track_task(self, task_name: str) -> bool:
|
|
108
|
+
if not self.auto_track_tasks:
|
|
109
|
+
return False
|
|
110
|
+
return match_task_name(task_name, self.includes, self.excludes)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import collections
|
|
2
1
|
import functools
|
|
3
2
|
import json
|
|
4
3
|
import logging
|
|
@@ -13,66 +12,21 @@ from celery.signals import (
|
|
|
13
12
|
)
|
|
14
13
|
from kombu import serialization
|
|
15
14
|
|
|
15
|
+
from . import sdk
|
|
16
|
+
from ._integrations import TERMINAL_STATES, safe_get_task, task_cache
|
|
16
17
|
from .internal.models import StatusEnum
|
|
17
18
|
from .mug import Badger
|
|
18
19
|
from .safe_sdk import create_task_safe, update_task_safe
|
|
19
|
-
from .sdk import DefaultMergeStrategy
|
|
20
|
+
from .sdk import DefaultMergeStrategy
|
|
20
21
|
|
|
21
22
|
KWARG_PREFIX = "taskbadger_"
|
|
22
23
|
TB_KWARGS_ARG = f"{KWARG_PREFIX}kwargs"
|
|
23
24
|
IGNORE_ARGS = {TB_KWARGS_ARG, f"{KWARG_PREFIX}task", f"{KWARG_PREFIX}task_id", f"{KWARG_PREFIX}record_task_args"}
|
|
24
25
|
TB_TASK_ID = f"{KWARG_PREFIX}task_id"
|
|
25
26
|
|
|
26
|
-
TERMINAL_STATES = {
|
|
27
|
-
StatusEnum.SUCCESS,
|
|
28
|
-
StatusEnum.ERROR,
|
|
29
|
-
StatusEnum.CANCELLED,
|
|
30
|
-
StatusEnum.STALE,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
log = logging.getLogger("taskbadger")
|
|
34
28
|
|
|
35
29
|
|
|
36
|
-
class Cache:
|
|
37
|
-
def __init__(self, maxsize=128):
|
|
38
|
-
self.cache = collections.OrderedDict()
|
|
39
|
-
self.maxsize = maxsize
|
|
40
|
-
|
|
41
|
-
def set(self, key, value):
|
|
42
|
-
self.cache[key] = value
|
|
43
|
-
|
|
44
|
-
def unset(self, key):
|
|
45
|
-
self.cache.pop(key, None)
|
|
46
|
-
|
|
47
|
-
def get(self, key):
|
|
48
|
-
return self.cache.get(key)
|
|
49
|
-
|
|
50
|
-
def prune(self):
|
|
51
|
-
if len(self.cache) > self.maxsize:
|
|
52
|
-
self.cache.popitem(last=False)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def cached(cache_none=True, maxsize=128):
|
|
56
|
-
cache = Cache(maxsize=maxsize)
|
|
57
|
-
|
|
58
|
-
def _wrapper(func):
|
|
59
|
-
@functools.wraps(func)
|
|
60
|
-
def _inner(*args, **kwargs):
|
|
61
|
-
key = args + tuple(sorted(kwargs.items()))
|
|
62
|
-
if key in cache.cache:
|
|
63
|
-
return cache.get(key)
|
|
64
|
-
|
|
65
|
-
result = func(*args, **kwargs)
|
|
66
|
-
if result is not None or cache_none:
|
|
67
|
-
cache.set(key, result)
|
|
68
|
-
return result
|
|
69
|
-
|
|
70
|
-
_inner.cache = cache
|
|
71
|
-
return _inner
|
|
72
|
-
|
|
73
|
-
return _wrapper
|
|
74
|
-
|
|
75
|
-
|
|
76
30
|
class Task(celery.Task):
|
|
77
31
|
"""A Celery Task that tracks itself with TaskBadger.
|
|
78
32
|
|
|
@@ -135,7 +89,7 @@ class Task(celery.Task):
|
|
|
135
89
|
tb_task_id = info.get(TB_TASK_ID) if isinstance(info, dict) else None
|
|
136
90
|
setattr(result, TB_TASK_ID, tb_task_id)
|
|
137
91
|
|
|
138
|
-
_get_task = functools.partial(get_task, tb_task_id) if tb_task_id else lambda: None
|
|
92
|
+
_get_task = functools.partial(sdk.get_task, tb_task_id) if tb_task_id else lambda: None
|
|
139
93
|
setattr(result, "get_taskbadger_task", _get_task)
|
|
140
94
|
|
|
141
95
|
return result
|
|
@@ -292,7 +246,7 @@ def _maybe_create_task(signal_sender):
|
|
|
292
246
|
if task:
|
|
293
247
|
# Store the task ID in the request so _update_task can find it
|
|
294
248
|
signal_sender.request.update({TB_TASK_ID: task.id})
|
|
295
|
-
|
|
249
|
+
task_cache.set(task.id, task)
|
|
296
250
|
|
|
297
251
|
|
|
298
252
|
@task_prerun.connect
|
|
@@ -344,7 +298,7 @@ def _update_task(signal_sender, status, einfo=None):
|
|
|
344
298
|
data = DefaultMergeStrategy().merge(task.data, {"exception": str(einfo)})
|
|
345
299
|
task = update_task_safe(task.id, status=status, data=data)
|
|
346
300
|
if task:
|
|
347
|
-
|
|
301
|
+
task_cache.set(task_id, task)
|
|
348
302
|
|
|
349
303
|
|
|
350
304
|
def enter_session():
|
|
@@ -364,22 +318,13 @@ def exit_session(signal_sender):
|
|
|
364
318
|
if not task_id or not Badger.is_configured():
|
|
365
319
|
return
|
|
366
320
|
|
|
367
|
-
|
|
368
|
-
safe_get_task.cache.prune()
|
|
321
|
+
task_cache.unset(task_id)
|
|
369
322
|
|
|
370
323
|
session = Badger.current.session()
|
|
371
324
|
if session.client:
|
|
372
325
|
session.__exit__()
|
|
373
326
|
|
|
374
327
|
|
|
375
|
-
@cached(cache_none=False)
|
|
376
|
-
def safe_get_task(task_id: str):
|
|
377
|
-
try:
|
|
378
|
-
return get_task(task_id)
|
|
379
|
-
except Exception as e:
|
|
380
|
-
log.warning("Error fetching task '%s': %s", task_id, e)
|
|
381
|
-
|
|
382
|
-
|
|
383
328
|
def _get_taskbadger_task_id(request):
|
|
384
329
|
if not request:
|
|
385
330
|
return
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""TaskBadger integration for the Procrastinate task queue.
|
|
2
|
+
|
|
3
|
+
This module is opt-in. Users install Procrastinate themselves (or via the
|
|
4
|
+
``taskbadger[procrastinate]`` extra) and import from here.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
- ``track``: decorator to opt a single task into TaskBadger tracking.
|
|
8
|
+
- ``current_task()``: accessor for the TaskBadger task associated with the
|
|
9
|
+
currently-running Procrastinate job (returns ``None`` if not tracked).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import inspect
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from contextvars import ContextVar
|
|
19
|
+
|
|
20
|
+
from ._integrations import TERMINAL_STATES, safe_get_task, task_cache
|
|
21
|
+
from .internal.models import StatusEnum
|
|
22
|
+
from .mug import Badger
|
|
23
|
+
from .safe_sdk import create_task_safe, update_task_safe
|
|
24
|
+
from .sdk import DefaultMergeStrategy
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger("taskbadger")
|
|
27
|
+
|
|
28
|
+
# Reserved key used to smuggle the TaskBadger task id through Procrastinate's
|
|
29
|
+
# task_kwargs from the deferring process to the worker. Stripped before the
|
|
30
|
+
# user function is called.
|
|
31
|
+
TB_TASK_ID_KWARG = "__taskbadger_task_id__"
|
|
32
|
+
|
|
33
|
+
# Sentinel attribute names set on a Procrastinate Task object once it has been
|
|
34
|
+
# instrumented. Used to make instrumentation idempotent.
|
|
35
|
+
_INSTRUMENTED_ATTR = "_taskbadger_instrumented"
|
|
36
|
+
_MANUAL_ATTR = "_taskbadger_manual"
|
|
37
|
+
_OPTS_ATTR = "_taskbadger_opts"
|
|
38
|
+
|
|
39
|
+
_current_tb_task_id: ContextVar[str | None] = ContextVar("_current_tb_task_id", default=None)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _instrument_task(task, system=None, manual=False, opts=None):
|
|
43
|
+
"""Wrap a Procrastinate Task's ``func`` so the worker side updates TaskBadger.
|
|
44
|
+
|
|
45
|
+
Idempotent: a second call on the same task is a no-op (but ``manual`` and
|
|
46
|
+
``opts`` will be merged onto the existing attributes if provided).
|
|
47
|
+
"""
|
|
48
|
+
if opts is not None:
|
|
49
|
+
existing_opts = getattr(task, _OPTS_ATTR, {}) or {}
|
|
50
|
+
merged = {**existing_opts, **opts}
|
|
51
|
+
setattr(task, _OPTS_ATTR, merged)
|
|
52
|
+
elif not hasattr(task, _OPTS_ATTR):
|
|
53
|
+
setattr(task, _OPTS_ATTR, {})
|
|
54
|
+
|
|
55
|
+
if manual:
|
|
56
|
+
setattr(task, _MANUAL_ATTR, True)
|
|
57
|
+
|
|
58
|
+
if getattr(task, _INSTRUMENTED_ATTR, False):
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
original_func = task.func
|
|
62
|
+
is_async = inspect.iscoroutinefunction(original_func)
|
|
63
|
+
|
|
64
|
+
# pass_context=True works transparently: Procrastinate passes the context
|
|
65
|
+
# object as the first positional arg; our *args/**kwargs wrapper forwards it.
|
|
66
|
+
if is_async:
|
|
67
|
+
|
|
68
|
+
@functools.wraps(original_func)
|
|
69
|
+
async def wrapped(*args, **kwargs):
|
|
70
|
+
tb_id = kwargs.pop(TB_TASK_ID_KWARG, None)
|
|
71
|
+
if tb_id is None:
|
|
72
|
+
return await original_func(*args, **kwargs)
|
|
73
|
+
token = _current_tb_task_id.set(tb_id)
|
|
74
|
+
try:
|
|
75
|
+
_update_status(tb_id, StatusEnum.PROCESSING)
|
|
76
|
+
try:
|
|
77
|
+
result = await original_func(*args, **kwargs)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
_update_status(tb_id, StatusEnum.ERROR, exception=exc)
|
|
80
|
+
raise
|
|
81
|
+
_update_status(tb_id, StatusEnum.SUCCESS)
|
|
82
|
+
return result
|
|
83
|
+
finally:
|
|
84
|
+
_current_tb_task_id.reset(token)
|
|
85
|
+
else:
|
|
86
|
+
|
|
87
|
+
@functools.wraps(original_func)
|
|
88
|
+
def wrapped(*args, **kwargs):
|
|
89
|
+
tb_id = kwargs.pop(TB_TASK_ID_KWARG, None)
|
|
90
|
+
if tb_id is None:
|
|
91
|
+
return original_func(*args, **kwargs)
|
|
92
|
+
token = _current_tb_task_id.set(tb_id)
|
|
93
|
+
try:
|
|
94
|
+
_update_status(tb_id, StatusEnum.PROCESSING)
|
|
95
|
+
try:
|
|
96
|
+
result = original_func(*args, **kwargs)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
_update_status(tb_id, StatusEnum.ERROR, exception=exc)
|
|
99
|
+
raise
|
|
100
|
+
_update_status(tb_id, StatusEnum.SUCCESS)
|
|
101
|
+
return result
|
|
102
|
+
finally:
|
|
103
|
+
_current_tb_task_id.reset(token)
|
|
104
|
+
|
|
105
|
+
_wrap_defer(task)
|
|
106
|
+
task.func = wrapped
|
|
107
|
+
setattr(task, _INSTRUMENTED_ATTR, True)
|
|
108
|
+
setattr(task, "_taskbadger_system", system)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _update_status(tb_id, status, exception=None):
|
|
112
|
+
"""Update the TaskBadger task to ``status``. Skips if already terminal."""
|
|
113
|
+
if not Badger.is_configured():
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if exception is not None or status in TERMINAL_STATES:
|
|
117
|
+
# Bypass the cache for the terminal-state check: the user may have
|
|
118
|
+
# updated the task to a terminal state via the regular SDK during
|
|
119
|
+
# the body, which wouldn't be reflected in our local cache.
|
|
120
|
+
task_cache.unset(tb_id)
|
|
121
|
+
current = safe_get_task(tb_id)
|
|
122
|
+
if current is not None and current.status in TERMINAL_STATES:
|
|
123
|
+
return
|
|
124
|
+
data = None
|
|
125
|
+
if exception is not None and current is not None:
|
|
126
|
+
base = dict(current.data) if current.data else None
|
|
127
|
+
data = DefaultMergeStrategy().merge(base, {"exception": str(exception)})
|
|
128
|
+
if data is not None:
|
|
129
|
+
updated = update_task_safe(tb_id, status=status, data=data)
|
|
130
|
+
else:
|
|
131
|
+
updated = update_task_safe(tb_id, status=status)
|
|
132
|
+
else:
|
|
133
|
+
updated = update_task_safe(tb_id, status=status)
|
|
134
|
+
|
|
135
|
+
if updated is not None:
|
|
136
|
+
task_cache.set(tb_id, updated)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _wrap_defer(task):
|
|
140
|
+
"""Wrap ``task.defer`` and ``task.defer_async`` so they create a TaskBadger
|
|
141
|
+
task in PENDING state and inject its id into the job's task_kwargs.
|
|
142
|
+
|
|
143
|
+
Not idempotent on its own — the caller (``_instrument_task``) gates this
|
|
144
|
+
via ``_INSTRUMENTED_ATTR`` so each task is wrapped at most once.
|
|
145
|
+
"""
|
|
146
|
+
original_defer = task.defer
|
|
147
|
+
original_defer_async = task.defer_async
|
|
148
|
+
|
|
149
|
+
@functools.wraps(original_defer)
|
|
150
|
+
def defer(**kwargs):
|
|
151
|
+
kwargs = _maybe_create_pending(task, kwargs)
|
|
152
|
+
return original_defer(**kwargs)
|
|
153
|
+
|
|
154
|
+
@functools.wraps(original_defer_async)
|
|
155
|
+
async def defer_async(**kwargs):
|
|
156
|
+
kwargs = _maybe_create_pending(task, kwargs)
|
|
157
|
+
return await original_defer_async(**kwargs)
|
|
158
|
+
|
|
159
|
+
task.defer = defer
|
|
160
|
+
task.defer_async = defer_async
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _create_pending_task(task, task_kwargs):
|
|
164
|
+
"""Create a PENDING TaskBadger task for ``task`` if it should be tracked.
|
|
165
|
+
|
|
166
|
+
Returns the created TaskBadger task, or ``None`` if Badger isn't
|
|
167
|
+
configured, the task isn't tracked (neither manual nor auto), or the
|
|
168
|
+
create call failed. ``task_kwargs`` is used only for the
|
|
169
|
+
``record_task_args`` data capture.
|
|
170
|
+
"""
|
|
171
|
+
if not Badger.is_configured():
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
system = getattr(task, "_taskbadger_system", None)
|
|
175
|
+
manual = getattr(task, _MANUAL_ATTR, False)
|
|
176
|
+
auto = bool(system) and system.track_task(task.name)
|
|
177
|
+
if not manual and not auto:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
opts = dict(getattr(task, _OPTS_ATTR, {}) or {})
|
|
181
|
+
name = opts.pop("name", None) or task.name
|
|
182
|
+
create_kwargs = {"status": StatusEnum.PENDING}
|
|
183
|
+
for key in ("value_max", "tags"):
|
|
184
|
+
if key in opts and opts[key] is not None:
|
|
185
|
+
create_kwargs[key] = opts[key]
|
|
186
|
+
|
|
187
|
+
data = dict(opts.get("data") or {})
|
|
188
|
+
|
|
189
|
+
record_args = opts.get("record_task_args")
|
|
190
|
+
if record_args is None:
|
|
191
|
+
record_args = bool(system) and system.record_task_args
|
|
192
|
+
if record_args:
|
|
193
|
+
data["procrastinate_task_kwargs"] = _serialize_kwargs(task_kwargs)
|
|
194
|
+
|
|
195
|
+
if data:
|
|
196
|
+
create_kwargs["data"] = data
|
|
197
|
+
|
|
198
|
+
return create_task_safe(name, **create_kwargs)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _maybe_create_pending(task, kwargs):
|
|
202
|
+
"""Decide whether to track this defer, and if so create the TaskBadger
|
|
203
|
+
task and inject its id into ``kwargs``. Always returns the kwargs dict."""
|
|
204
|
+
tb_task = _create_pending_task(task, kwargs)
|
|
205
|
+
if tb_task is None:
|
|
206
|
+
return kwargs
|
|
207
|
+
|
|
208
|
+
new_kwargs = dict(kwargs)
|
|
209
|
+
new_kwargs[TB_TASK_ID_KWARG] = tb_task.id
|
|
210
|
+
return new_kwargs
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _serialize_kwargs(kwargs):
|
|
214
|
+
"""Return a JSON-roundtrippable copy of the defer kwargs.
|
|
215
|
+
|
|
216
|
+
Procrastinate already requires kwargs be JSON-serializable, so a json
|
|
217
|
+
dumps/loads roundtrip is safe. Non-serializable values are dropped with
|
|
218
|
+
a warning."""
|
|
219
|
+
try:
|
|
220
|
+
return json.loads(json.dumps(kwargs))
|
|
221
|
+
except (TypeError, ValueError) as e:
|
|
222
|
+
log.warning("Error serializing task arguments: %s", e)
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_TRACK_OPT_KEYS = ("name", "value_max", "tags", "data", "record_task_args")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def track(original_task=None, **opts):
|
|
230
|
+
"""Opt a Procrastinate task into TaskBadger tracking.
|
|
231
|
+
|
|
232
|
+
Usage:
|
|
233
|
+
|
|
234
|
+
@track
|
|
235
|
+
@app.task(...)
|
|
236
|
+
def my_task(...): ...
|
|
237
|
+
|
|
238
|
+
@track(name="custom", value_max=100, tags={"env": "prod"})
|
|
239
|
+
@app.task(...)
|
|
240
|
+
async def big_job(...): ...
|
|
241
|
+
|
|
242
|
+
Accepted keyword options (all optional):
|
|
243
|
+
name: TaskBadger task name (defaults to the Procrastinate task's name).
|
|
244
|
+
value_max: Maximum value for the TaskBadger task.
|
|
245
|
+
tags: Dict of tags applied to the TaskBadger task.
|
|
246
|
+
data: Dict of initial data merged into the TaskBadger task.
|
|
247
|
+
record_task_args: If True, serialize the Procrastinate job kwargs and
|
|
248
|
+
store them under ``data["procrastinate_task_kwargs"]``. Defaults to
|
|
249
|
+
``None`` meaning "inherit from system integration if any, else False".
|
|
250
|
+
"""
|
|
251
|
+
unknown = set(opts) - set(_TRACK_OPT_KEYS)
|
|
252
|
+
if unknown:
|
|
253
|
+
raise TypeError(f"track() got unexpected keyword arguments: {sorted(unknown)}")
|
|
254
|
+
|
|
255
|
+
def wrap(task):
|
|
256
|
+
_instrument_task(task, system=None, manual=True, opts=opts)
|
|
257
|
+
return task
|
|
258
|
+
|
|
259
|
+
if original_task is None:
|
|
260
|
+
return wrap
|
|
261
|
+
return wrap(original_task)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def current_task():
|
|
265
|
+
"""Return the TaskBadger Task for the currently-running Procrastinate job.
|
|
266
|
+
|
|
267
|
+
Returns ``None`` outside of a tracked task or if the task can't be fetched.
|
|
268
|
+
Result is cached for the lifetime of the worker process via an LRU.
|
|
269
|
+
"""
|
|
270
|
+
tb_id = _current_tb_task_id.get()
|
|
271
|
+
if tb_id is None:
|
|
272
|
+
return None
|
|
273
|
+
return safe_get_task(tb_id)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _patch_job_manager(app, system):
|
|
277
|
+
"""Patch ``app.job_manager.defer_periodic_job`` so periodic tasks are tracked.
|
|
278
|
+
|
|
279
|
+
Procrastinate's ``PeriodicDeferrer`` enqueues jobs by calling
|
|
280
|
+
``job_manager.defer_periodic_job(job=..., ...)`` directly, bypassing
|
|
281
|
+
``task.defer``/``defer_async``. Without this hook, ``@app.periodic`` tasks
|
|
282
|
+
would never get a PENDING TaskBadger task created at enqueue time.
|
|
283
|
+
|
|
284
|
+
Idempotent: a second call updates the system reference but doesn't
|
|
285
|
+
re-wrap.
|
|
286
|
+
"""
|
|
287
|
+
jm = app.job_manager
|
|
288
|
+
if not getattr(jm, "_taskbadger_original_defer_periodic_job", None):
|
|
289
|
+
original = jm.defer_periodic_job
|
|
290
|
+
jm._taskbadger_original_defer_periodic_job = original
|
|
291
|
+
|
|
292
|
+
@functools.wraps(original)
|
|
293
|
+
async def patched(*, job, periodic_id, defer_timestamp):
|
|
294
|
+
task = app.tasks.get(job.task_name)
|
|
295
|
+
if task is not None:
|
|
296
|
+
tb_task = _create_pending_task(task, job.task_kwargs)
|
|
297
|
+
if tb_task is not None:
|
|
298
|
+
new_kwargs = {**job.task_kwargs, TB_TASK_ID_KWARG: tb_task.id}
|
|
299
|
+
job = job.evolve(task_kwargs=new_kwargs)
|
|
300
|
+
return await jm._taskbadger_original_defer_periodic_job(
|
|
301
|
+
job=job, periodic_id=periodic_id, defer_timestamp=defer_timestamp
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
jm.defer_periodic_job = patched
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _patch_app_task(app, system):
|
|
308
|
+
"""Replace ``app.task`` with a wrapper that instruments newly-registered
|
|
309
|
+
tasks under the supplied ``system``. Idempotent — a second call replaces
|
|
310
|
+
the wrapper but keeps the same original task method."""
|
|
311
|
+
original = getattr(app, "_taskbadger_original_task", None) or app.task
|
|
312
|
+
if not getattr(app, "_taskbadger_original_task", None):
|
|
313
|
+
app._taskbadger_original_task = original
|
|
314
|
+
|
|
315
|
+
@functools.wraps(original)
|
|
316
|
+
def patched(*args, **kwargs):
|
|
317
|
+
task = original(*args, **kwargs)
|
|
318
|
+
# ``original`` may return the Task directly or a decorator depending on
|
|
319
|
+
# call form. Procrastinate's ``app.task`` always returns a decorator
|
|
320
|
+
# when called with arguments and the Task when called bare.
|
|
321
|
+
if callable(task) and not hasattr(task, "name"):
|
|
322
|
+
# decorator form: wrap the returned decorator
|
|
323
|
+
inner_decorator = task
|
|
324
|
+
|
|
325
|
+
@functools.wraps(inner_decorator)
|
|
326
|
+
def outer(func):
|
|
327
|
+
t = inner_decorator(func)
|
|
328
|
+
_instrument_task(t, system=system)
|
|
329
|
+
return t
|
|
330
|
+
|
|
331
|
+
return outer
|
|
332
|
+
_instrument_task(task, system=system)
|
|
333
|
+
return task
|
|
334
|
+
|
|
335
|
+
app.task = patched
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
from taskbadger._integrations import BaseSystemIntegration
|
|
2
2
|
|
|
3
|
-
from taskbadger.systems import System
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
class CelerySystemIntegration(System):
|
|
4
|
+
class CelerySystemIntegration(BaseSystemIntegration):
|
|
7
5
|
identifier = "celery"
|
|
8
6
|
|
|
9
7
|
def __init__(self, auto_track_tasks=True, includes=None, excludes=None, record_task_args=False):
|
|
@@ -18,29 +16,13 @@ class CelerySystemIntegration(System):
|
|
|
18
16
|
the full task name or a regular expression. Exclusions take precedence over inclusions.
|
|
19
17
|
record_task_args: Record the arguments passed to each task.
|
|
20
18
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
super().__init__(
|
|
20
|
+
auto_track_tasks=auto_track_tasks,
|
|
21
|
+
includes=includes,
|
|
22
|
+
excludes=excludes,
|
|
23
|
+
record_task_args=record_task_args,
|
|
24
|
+
)
|
|
25
25
|
|
|
26
26
|
if auto_track_tasks:
|
|
27
27
|
# Importing this here ensures that the Celery signal handlers are registered
|
|
28
|
-
import taskbadger.celery # noqa
|
|
29
|
-
|
|
30
|
-
def track_task(self, task_name):
|
|
31
|
-
if not self.auto_track_tasks:
|
|
32
|
-
return False
|
|
33
|
-
|
|
34
|
-
if self.excludes:
|
|
35
|
-
for exclude in self.excludes:
|
|
36
|
-
if re.fullmatch(exclude, task_name):
|
|
37
|
-
return False
|
|
38
|
-
|
|
39
|
-
if self.includes:
|
|
40
|
-
for include in self.includes:
|
|
41
|
-
if re.fullmatch(include, task_name):
|
|
42
|
-
break
|
|
43
|
-
else:
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
return True
|
|
28
|
+
import taskbadger.celery # noqa: F401
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""ProcrastinateSystemIntegration — auto-track tasks on a Procrastinate App."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from taskbadger._integrations import BaseSystemIntegration
|
|
6
|
+
from taskbadger.procrastinate import _instrument_task, _patch_app_task, _patch_job_manager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProcrastinateSystemIntegration(BaseSystemIntegration):
|
|
10
|
+
identifier = "procrastinate"
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
app,
|
|
15
|
+
auto_track_tasks=True,
|
|
16
|
+
includes=None,
|
|
17
|
+
excludes=None,
|
|
18
|
+
record_task_args=False,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Args:
|
|
22
|
+
app: The ``procrastinate.App`` instance to instrument.
|
|
23
|
+
auto_track_tasks: Track all tasks regardless of whether they use
|
|
24
|
+
the ``@taskbadger.procrastinate.track`` decorator.
|
|
25
|
+
includes: List of task names to include in auto-tracking. Each
|
|
26
|
+
entry can be a full name or a regex (matched with
|
|
27
|
+
``re.fullmatch``).
|
|
28
|
+
excludes: List of task names to exclude. Same semantics as
|
|
29
|
+
``includes``. Exclusions take precedence.
|
|
30
|
+
record_task_args: Record the task's defer kwargs into the
|
|
31
|
+
TaskBadger task's ``data`` under ``procrastinate_task_kwargs``.
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(
|
|
34
|
+
auto_track_tasks=auto_track_tasks,
|
|
35
|
+
includes=includes,
|
|
36
|
+
excludes=excludes,
|
|
37
|
+
record_task_args=record_task_args,
|
|
38
|
+
)
|
|
39
|
+
self.app = app
|
|
40
|
+
|
|
41
|
+
for task in list(app.tasks.values()):
|
|
42
|
+
_instrument_task(task, system=self)
|
|
43
|
+
_patch_app_task(app, system=self)
|
|
44
|
+
_patch_job_manager(app, system=self)
|
|
45
|
+
|
|
46
|
+
def track_task(self, task_name):
|
|
47
|
+
# Never auto-track Procrastinate's built-in housekeeping tasks
|
|
48
|
+
# (e.g. ``builtin:procrastinate.builtin_tasks.remove_old_jobs``).
|
|
49
|
+
if task_name.startswith("builtin:") or task_name.startswith("procrastinate."):
|
|
50
|
+
return False
|
|
51
|
+
return super().track_task(task_name)
|
taskbadger-2.0.0/README.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# Task Badger Python Client
|
|
2
|
-
|
|
3
|
-
This is the official Python SDK for [Task Badger](https://taskbadger.net/).
|
|
4
|
-
|
|
5
|
-
For full documentation go to https://docs.taskbadger.net/python/.
|
|
6
|
-
|
|
7
|
-

|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Getting Started
|
|
12
|
-
|
|
13
|
-
### Install
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pip install taskbadger
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
To use the `taskbadger` command-line tool, install the `cli` extra:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
pip install 'taskbadger[cli]'
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
### Client Usage
|
|
26
|
-
|
|
27
|
-
```python
|
|
28
|
-
import taskbadger
|
|
29
|
-
from taskbadger.systems import CelerySystemIntegration
|
|
30
|
-
|
|
31
|
-
taskbadger.init(
|
|
32
|
-
token="***",
|
|
33
|
-
systems=[CelerySystemIntegration()],
|
|
34
|
-
tags={"environment": "production"}
|
|
35
|
-
)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### CLI Usage
|
|
39
|
-
|
|
40
|
-
```shell
|
|
41
|
-
$ export TASKBADGER_API_KEY=***
|
|
42
|
-
$ taskbadger run "nightly-backup" -- ./backup.sh
|
|
43
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/__init__.py
RENAMED
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_cancel.py
RENAMED
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_create.py
RENAMED
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_get.py
RENAMED
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_list.py
RENAMED
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/action_endpoints/action_update.py
RENAMED
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_cancel.py
RENAMED
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_create.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/api/task_endpoints/task_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/patched_action_request.py
RENAMED
|
File without changes
|
|
File without changes
|
{taskbadger-2.0.0 → taskbadger-2.1.0a2}/taskbadger/internal/models/patched_task_request_tags.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|