taskbadger 0.4.4__tar.gz → 0.8.0__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-0.4.4 → taskbadger-0.8.0}/PKG-INFO +10 -10
- {taskbadger-0.4.4 → taskbadger-0.8.0}/README.md +4 -6
- {taskbadger-0.4.4 → taskbadger-0.8.0}/pyproject.toml +11 -6
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/__init__.py +3 -2
- taskbadger-0.8.0/taskbadger/cli/__init__.py +3 -0
- taskbadger-0.8.0/taskbadger/cli/basics.py +126 -0
- taskbadger-0.8.0/taskbadger/cli/list_tasks.py +79 -0
- taskbadger-0.8.0/taskbadger/cli/utils.py +46 -0
- taskbadger-0.8.0/taskbadger/cli/wrapper.py +82 -0
- taskbadger-0.8.0/taskbadger/cli_main.py +85 -0
- taskbadger-0.8.0/taskbadger/exceptions.py +26 -0
- taskbadger-0.8.0/taskbadger/integrations.py +78 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/__init__.py +1 -1
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_cancel.py +13 -22
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_create.py +14 -23
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_get.py +15 -24
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_list.py +18 -24
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_partial_update.py +15 -23
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_update.py +15 -23
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_cancel.py +12 -22
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_create.py +13 -23
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_get.py +14 -24
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_list.py +16 -22
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_partial_update.py +14 -23
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_update.py +14 -23
- taskbadger-0.8.0/taskbadger/internal/client.py +268 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/errors.py +5 -1
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_config.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_request.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_request_config.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/paginated_task_list.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_action_request.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_action_request_config.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_task_request.py +6 -5
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_task_request_data.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task.py +6 -5
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_data.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_request.py +6 -5
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_request_data.py +4 -3
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/types.py +5 -5
- taskbadger-0.8.0/taskbadger/mug.py +100 -0
- taskbadger-0.8.0/taskbadger/process.py +78 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/safe_sdk.py +12 -11
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/sdk.py +61 -64
- taskbadger-0.4.4/taskbadger/cli.py +0 -169
- taskbadger-0.4.4/taskbadger/exceptions.py +0 -6
- taskbadger-0.4.4/taskbadger/integrations.py +0 -41
- taskbadger-0.4.4/taskbadger/internal/client.py +0 -64
- {taskbadger-0.4.4 → taskbadger-0.8.0}/LICENSE +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/config.py +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/__init__.py +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/__init__.py +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/__init__.py +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/__init__.py +0 -0
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/status_enum.py +4 -4
- {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: taskbadger
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: The official Python SDK for Task Badger
|
|
5
5
|
Home-page: https://taskbadger.net/
|
|
6
6
|
License: Apache-2.0
|
|
@@ -19,13 +19,15 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.6
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.7
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Provides-Extra: celery
|
|
22
23
|
Requires-Dist: attrs (>=21.3.0)
|
|
23
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: celery (>=4.0.0,<6.0.0) ; extra == "celery"
|
|
25
|
+
Requires-Dist: httpx (>=0.20.0,<0.25.0)
|
|
24
26
|
Requires-Dist: importlib-metadata (>=1.0,<2.0) ; python_version < "3.8"
|
|
25
27
|
Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
|
|
26
28
|
Requires-Dist: tomlkit (>=0.11.6,<0.12.0)
|
|
27
|
-
Requires-Dist: typer[all] (>=0.
|
|
28
|
-
Requires-Dist: typing-extensions (>=4.
|
|
29
|
+
Requires-Dist: typer[all] (>=0.9.0,<0.10.0)
|
|
30
|
+
Requires-Dist: typing-extensions (>=4.7.1,<5.0.0) ; python_version == "3.9"
|
|
29
31
|
Project-URL: Documentation, https://docs.taskbadger.net/
|
|
30
32
|
Project-URL: Repository, https://github.com/taskbadger/taskbadger-docs
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
@@ -63,7 +65,7 @@ taskbadger.init(
|
|
|
63
65
|
#### API Example
|
|
64
66
|
|
|
65
67
|
```python
|
|
66
|
-
from taskbadger import Task, Action, EmailIntegration
|
|
68
|
+
from taskbadger import Task, Action, EmailIntegration, WebhookIntegration
|
|
67
69
|
|
|
68
70
|
# create a new task with custom data and an action definition
|
|
69
71
|
task = Task.create(
|
|
@@ -72,10 +74,8 @@ task = Task.create(
|
|
|
72
74
|
"custom": "data"
|
|
73
75
|
},
|
|
74
76
|
actions=[
|
|
75
|
-
Action(
|
|
76
|
-
|
|
77
|
-
integration=EmailIntegration(to="me@example.com")
|
|
78
|
-
)
|
|
77
|
+
Action("*/10%,success,error", integration=EmailIntegration(to="me@example.com")),
|
|
78
|
+
Action("cancelled", integration=WebhookIntegration(id="webhook:demo")),
|
|
79
79
|
]
|
|
80
80
|
)
|
|
81
81
|
|
|
@@ -117,7 +117,7 @@ Config written to ~/.config/taskbadger/config
|
|
|
117
117
|
The CLI `run` command executes your command whilst creating and updating a Task Badger task.
|
|
118
118
|
|
|
119
119
|
```shell
|
|
120
|
-
$ taskbadger run "demo task" --action
|
|
120
|
+
$ taskbadger run "demo task" --action error email to:me@test.com -- path/to/script.sh
|
|
121
121
|
|
|
122
122
|
Task created: https://taskbadger.net/public/tasks/xyz/
|
|
123
123
|
```
|
|
@@ -31,7 +31,7 @@ taskbadger.init(
|
|
|
31
31
|
#### API Example
|
|
32
32
|
|
|
33
33
|
```python
|
|
34
|
-
from taskbadger import Task, Action, EmailIntegration
|
|
34
|
+
from taskbadger import Task, Action, EmailIntegration, WebhookIntegration
|
|
35
35
|
|
|
36
36
|
# create a new task with custom data and an action definition
|
|
37
37
|
task = Task.create(
|
|
@@ -40,10 +40,8 @@ task = Task.create(
|
|
|
40
40
|
"custom": "data"
|
|
41
41
|
},
|
|
42
42
|
actions=[
|
|
43
|
-
Action(
|
|
44
|
-
|
|
45
|
-
integration=EmailIntegration(to="me@example.com")
|
|
46
|
-
)
|
|
43
|
+
Action("*/10%,success,error", integration=EmailIntegration(to="me@example.com")),
|
|
44
|
+
Action("cancelled", integration=WebhookIntegration(id="webhook:demo")),
|
|
47
45
|
]
|
|
48
46
|
)
|
|
49
47
|
|
|
@@ -85,7 +83,7 @@ Config written to ~/.config/taskbadger/config
|
|
|
85
83
|
The CLI `run` command executes your command whilst creating and updating a Task Badger task.
|
|
86
84
|
|
|
87
85
|
```shell
|
|
88
|
-
$ taskbadger run "demo task" --action
|
|
86
|
+
$ taskbadger run "demo task" --action error email to:me@test.com -- path/to/script.sh
|
|
89
87
|
|
|
90
88
|
Task created: https://taskbadger.net/public/tasks/xyz/
|
|
91
89
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "taskbadger"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "The official Python SDK for Task Badger"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
|
|
@@ -32,25 +32,30 @@ classifiers = [
|
|
|
32
32
|
|
|
33
33
|
[tool.poetry.dependencies]
|
|
34
34
|
python = "^3.8"
|
|
35
|
-
httpx = ">=0.
|
|
35
|
+
httpx = ">=0.20.0,<0.25.0"
|
|
36
36
|
attrs = ">=21.3.0"
|
|
37
37
|
python-dateutil = "^2.8.0"
|
|
38
|
-
typer = {extras = ["all"], version = "^0.
|
|
38
|
+
typer = {extras = ["all"], version = "^0.9.0"}
|
|
39
39
|
tomlkit = "^0.11.6"
|
|
40
40
|
importlib-metadata = {version = "^1.0", python = "<3.8"}
|
|
41
|
-
typing-extensions = {version = "^4.
|
|
41
|
+
typing-extensions = {version = "^4.7.1", python = "3.9"}
|
|
42
|
+
celery = {version = ">=4.0.0,<6.0.0", optional = true}
|
|
43
|
+
|
|
44
|
+
[tool.poetry.extras]
|
|
45
|
+
celery = ["celery"]
|
|
42
46
|
|
|
43
47
|
[tool.poetry.group.dev.dependencies]
|
|
44
|
-
openapi-python-client = "^0.
|
|
48
|
+
openapi-python-client = "^0.15.1"
|
|
45
49
|
pytest = "^7.2.1"
|
|
46
50
|
isort = "^5.12.0"
|
|
47
51
|
black = "^23.1.0"
|
|
48
52
|
pre-commit = "^3.0.2"
|
|
49
53
|
pytest-httpx = "^0.21.3"
|
|
50
54
|
invoke = "^2.0.0"
|
|
55
|
+
pytest-celery = "^0.0.0"
|
|
51
56
|
|
|
52
57
|
[tool.poetry.scripts]
|
|
53
|
-
taskbadger = "taskbadger.
|
|
58
|
+
taskbadger = "taskbadger.cli_main:app"
|
|
54
59
|
|
|
55
60
|
[build-system]
|
|
56
61
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from .integrations import Action, EmailIntegration
|
|
1
|
+
from .integrations import Action, EmailIntegration, WebhookIntegration
|
|
2
2
|
from .internal.models import StatusEnum
|
|
3
|
+
from .mug import Session
|
|
3
4
|
from .safe_sdk import create_task_safe, update_task_safe
|
|
4
|
-
from .sdk import Task, create_task, get_task, init, update_task
|
|
5
|
+
from .sdk import DefaultMergeStrategy, Task, create_task, get_task, init, update_task
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
import importlib.metadata as importlib_metadata
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich import print
|
|
8
|
+
|
|
9
|
+
from taskbadger import StatusEnum, create_task, get_task, update_task
|
|
10
|
+
from taskbadger.cli.utils import OutputFormat, configure_api, err_console, get_actions, get_metadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get(
|
|
14
|
+
ctx: typer.Context,
|
|
15
|
+
task_id: str = typer.Argument(..., show_default=False, help="The ID of the task."),
|
|
16
|
+
output_format: OutputFormat = typer.Option(OutputFormat.pretty, "--format", "-f", help="Output format"),
|
|
17
|
+
):
|
|
18
|
+
"""Get a task."""
|
|
19
|
+
configure_api(ctx)
|
|
20
|
+
task = get_task(task_id)
|
|
21
|
+
if output_format == OutputFormat.pretty:
|
|
22
|
+
print(f"Task ID: {task.id}")
|
|
23
|
+
print(f"Created: {task.created.isoformat()}")
|
|
24
|
+
print(f"Name: {task.name}")
|
|
25
|
+
print(f"Status: {task.status}")
|
|
26
|
+
print(f"Percent: {task.value_percent}%")
|
|
27
|
+
elif output_format == OutputFormat.json:
|
|
28
|
+
print(json.dumps(task.to_dict(), indent=2))
|
|
29
|
+
elif output_format == OutputFormat.csv:
|
|
30
|
+
writer = csv.writer(sys.stdout)
|
|
31
|
+
writer.writerow("Task ID,Created,Name,Status,Percent".split(","))
|
|
32
|
+
writer.writerow([task.id, task.created.isoformat(), task.name, task.status, str(task.value_percent)])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def create(
|
|
36
|
+
ctx: typer.Context,
|
|
37
|
+
name: str = typer.Argument(..., show_default=False, help="The task name."),
|
|
38
|
+
monitor_id: str = typer.Option(None, help="Associate this task with a monitor."),
|
|
39
|
+
action_def: Tuple[str, str, str] = typer.Option(
|
|
40
|
+
(None, None, None),
|
|
41
|
+
"--action",
|
|
42
|
+
"-a",
|
|
43
|
+
metavar="<trigger integration config>",
|
|
44
|
+
show_default=False,
|
|
45
|
+
help="Action definition e.g. 'success,error email to:me@email.com'",
|
|
46
|
+
),
|
|
47
|
+
status: StatusEnum = typer.Option(StatusEnum.PROCESSING, help="The initial status of the task."),
|
|
48
|
+
value_max: int = typer.Option(100, help="The maximum value for the task."),
|
|
49
|
+
metadata: list[str] = typer.Option(
|
|
50
|
+
None,
|
|
51
|
+
show_default=False,
|
|
52
|
+
help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
|
|
53
|
+
),
|
|
54
|
+
metadata_json: str = typer.Option(
|
|
55
|
+
None, show_default=False, help="Metadata to associate with the task. Must be valid JSON."
|
|
56
|
+
),
|
|
57
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output. Only the Task ID."),
|
|
58
|
+
):
|
|
59
|
+
"""Create a task."""
|
|
60
|
+
configure_api(ctx)
|
|
61
|
+
actions = get_actions(action_def)
|
|
62
|
+
metadata = get_metadata(metadata, metadata_json)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
task = create_task(
|
|
66
|
+
name,
|
|
67
|
+
status=status,
|
|
68
|
+
value_max=value_max,
|
|
69
|
+
data=metadata,
|
|
70
|
+
actions=actions,
|
|
71
|
+
monitor_id=monitor_id,
|
|
72
|
+
)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
err_console.print(f"Error creating task: {e}")
|
|
75
|
+
else:
|
|
76
|
+
if quiet:
|
|
77
|
+
print(task.id)
|
|
78
|
+
else:
|
|
79
|
+
print(f"Task created: {task.public_url}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def update(
|
|
83
|
+
ctx: typer.Context,
|
|
84
|
+
task_id: str = typer.Argument(..., show_default=False, help="The ID of the task to update."),
|
|
85
|
+
name: str = typer.Option(None, show_default=False, help="Update the name of the task."),
|
|
86
|
+
action_def: Tuple[str, str, str] = typer.Option(
|
|
87
|
+
(None, None, None),
|
|
88
|
+
"--action",
|
|
89
|
+
"-a",
|
|
90
|
+
metavar="<trigger integration config>",
|
|
91
|
+
show_default=False,
|
|
92
|
+
help="Action definition e.g. 'success,error email to:me@email.com'",
|
|
93
|
+
),
|
|
94
|
+
status: StatusEnum = typer.Option(StatusEnum.PROCESSING, help="The status of the task."),
|
|
95
|
+
value: int = typer.Option(None, show_default=False, help="The current task value (progress)."),
|
|
96
|
+
value_max: int = typer.Option(None, show_default=False, help="The maximum value for the task."),
|
|
97
|
+
metadata: list[str] = typer.Option(
|
|
98
|
+
None,
|
|
99
|
+
show_default=False,
|
|
100
|
+
help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
|
|
101
|
+
),
|
|
102
|
+
metadata_json: str = typer.Option(
|
|
103
|
+
None, show_default=False, help="Metadata to associate with the task. Must be valid JSON."
|
|
104
|
+
),
|
|
105
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="No output."),
|
|
106
|
+
):
|
|
107
|
+
"""Update a task."""
|
|
108
|
+
configure_api(ctx)
|
|
109
|
+
actions = get_actions(action_def)
|
|
110
|
+
metadata = get_metadata(metadata, metadata_json)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
task = update_task(
|
|
114
|
+
task_id,
|
|
115
|
+
name=name,
|
|
116
|
+
status=status,
|
|
117
|
+
value=value,
|
|
118
|
+
value_max=value_max,
|
|
119
|
+
data=metadata,
|
|
120
|
+
actions=actions,
|
|
121
|
+
)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
err_console.print(f"Error creating task: {e}")
|
|
124
|
+
else:
|
|
125
|
+
if not quiet:
|
|
126
|
+
print(f"Task updated: {task.public_url}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from urllib.parse import parse_qs, urlparse
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich import print
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from taskbadger.cli.utils import OutputFormat, configure_api
|
|
12
|
+
from taskbadger.sdk import list_tasks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_tasks_command(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
output_format: OutputFormat = typer.Option(OutputFormat.pretty, "--format", "-f", help="Output format"),
|
|
18
|
+
limit: int = typer.Option(100, help="Limit the number of results."),
|
|
19
|
+
start_token: str = typer.Option(None, show_default=False, help="Start token."),
|
|
20
|
+
):
|
|
21
|
+
"""List tasks."""
|
|
22
|
+
configure_api(ctx)
|
|
23
|
+
tasks = list_tasks(page_size=limit, cursor=start_token)
|
|
24
|
+
render(output_format, ctx, tasks)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render(format_: OutputFormat, ctx, result):
|
|
28
|
+
if format_ == OutputFormat.pretty:
|
|
29
|
+
_render_pretty(ctx, result)
|
|
30
|
+
elif format_ == OutputFormat.json:
|
|
31
|
+
_render_json(ctx, result)
|
|
32
|
+
elif format_ == OutputFormat.csv:
|
|
33
|
+
_render_csv(ctx, result)
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Unknown format: {format_}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _render_pretty(ctx, result):
|
|
39
|
+
table = Table(
|
|
40
|
+
title=f"Project: {ctx.meta['tb_config'].project_slug}, Organization: {ctx.meta['tb_config'].organization_slug}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
table.add_column("Task ID", no_wrap=True)
|
|
44
|
+
table.add_column("Created", no_wrap=True)
|
|
45
|
+
table.add_column("Name")
|
|
46
|
+
table.add_column("Status", no_wrap=True)
|
|
47
|
+
table.add_column("Percent", no_wrap=True)
|
|
48
|
+
|
|
49
|
+
for task in result.results:
|
|
50
|
+
table.add_row(task.id, task.created.isoformat(), task.name, task.status, str(task.value_percent))
|
|
51
|
+
Console().print(table)
|
|
52
|
+
|
|
53
|
+
cursor = _get_cursor(result.next_)
|
|
54
|
+
if cursor:
|
|
55
|
+
print(f"Next page token: [bold green]'{cursor}'[/]")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _render_csv(ctx, result):
|
|
59
|
+
writer = csv.writer(sys.stdout)
|
|
60
|
+
writer.writerow("Task ID,Created,Name,Status,Percent".split(","))
|
|
61
|
+
for task in result.results:
|
|
62
|
+
writer.writerow([task.id, task.created.isoformat(), task.name, task.status, str(task.value_percent)])
|
|
63
|
+
|
|
64
|
+
cursor = _get_cursor(result.next_)
|
|
65
|
+
if cursor:
|
|
66
|
+
print(f"next_token,{cursor}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _render_json(ctx, result):
|
|
70
|
+
obj = result.to_dict()
|
|
71
|
+
ret = {"next_token": _get_cursor(result.next_), "results": obj["results"]}
|
|
72
|
+
print(json.dumps(ret, indent=2))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_cursor(url):
|
|
76
|
+
if url:
|
|
77
|
+
qs = urlparse(url).query
|
|
78
|
+
query = parse_qs(qs)
|
|
79
|
+
return query["cursor"][0]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich import print
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from taskbadger import Action, integrations
|
|
9
|
+
from taskbadger.exceptions import ConfigurationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def configure_api(ctx):
|
|
13
|
+
config = ctx.meta["tb_config"]
|
|
14
|
+
try:
|
|
15
|
+
config.init_api()
|
|
16
|
+
except ConfigurationError as e:
|
|
17
|
+
print(f"[red]{str(e)}[/red]")
|
|
18
|
+
raise typer.Exit(code=1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
err_console = Console(stderr=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_actions(action_def: tuple[str, str, str]) -> list[Action]:
|
|
25
|
+
if any(action_def):
|
|
26
|
+
trigger, integration, config = action_def
|
|
27
|
+
return [Action(trigger, integrations.from_config(integration, config))]
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_metadata(metadata_kv: list[str], metadata_json: str) -> dict:
|
|
32
|
+
metadata = {}
|
|
33
|
+
for kv in metadata_kv:
|
|
34
|
+
k, v = kv.strip().split("=", 1)
|
|
35
|
+
metadata[k] = v
|
|
36
|
+
|
|
37
|
+
if metadata_json:
|
|
38
|
+
metadata.update(json.loads(metadata_json))
|
|
39
|
+
|
|
40
|
+
return metadata
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OutputFormat(str, Enum):
|
|
44
|
+
pretty = "pretty"
|
|
45
|
+
json = "json"
|
|
46
|
+
csv = "csv"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
|
|
6
|
+
from taskbadger import DefaultMergeStrategy, Session, StatusEnum, Task
|
|
7
|
+
from taskbadger.cli.utils import configure_api, err_console, get_actions
|
|
8
|
+
from taskbadger.process import ProcessRunner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
ctx: typer.Context,
|
|
13
|
+
name: str = typer.Argument(..., show_default=False, help="The task name"),
|
|
14
|
+
monitor_id: str = typer.Option(None, help="Associate this task with a monitor."),
|
|
15
|
+
update_frequency: int = typer.Option(5, metavar="SECONDS", min=5, max=300, help="Seconds between updates."),
|
|
16
|
+
action_def: Tuple[str, str, str] = typer.Option(
|
|
17
|
+
(None, None, None),
|
|
18
|
+
"--action",
|
|
19
|
+
"-a",
|
|
20
|
+
metavar="<trigger integration config>",
|
|
21
|
+
show_default=False,
|
|
22
|
+
help="Action definition e.g. 'success,error email to:me@email.com'",
|
|
23
|
+
),
|
|
24
|
+
capture_output: bool = typer.Option(False, help="Capture stdout and stderr."),
|
|
25
|
+
):
|
|
26
|
+
"""Execute a command using the CLI and create a Task to track its outcome.
|
|
27
|
+
|
|
28
|
+
This command makes it easy to track a process's outcome using Task Badger.
|
|
29
|
+
|
|
30
|
+
It will create a Task prior to executing your command and will update
|
|
31
|
+
the Task status when you command exits.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
|
|
35
|
+
[on black]taskbadger run 'my task' -- ./my-script.sh arg -v[/]
|
|
36
|
+
"""
|
|
37
|
+
configure_api(ctx)
|
|
38
|
+
actions = get_actions(action_def)
|
|
39
|
+
stale_timeout = update_frequency * 2
|
|
40
|
+
with Session():
|
|
41
|
+
try:
|
|
42
|
+
task = Task.create(
|
|
43
|
+
name,
|
|
44
|
+
status=StatusEnum.PROCESSING,
|
|
45
|
+
stale_timeout=stale_timeout,
|
|
46
|
+
actions=actions,
|
|
47
|
+
monitor_id=monitor_id,
|
|
48
|
+
)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
err_console.print(f"Error creating task: {e}")
|
|
51
|
+
task = None
|
|
52
|
+
else:
|
|
53
|
+
print(f"Task created: {task.public_url}")
|
|
54
|
+
env = {"TASKBADGER_TASK_ID": task.id} if task else None
|
|
55
|
+
try:
|
|
56
|
+
process = ProcessRunner(ctx.args, env, capture_output=capture_output, update_frequency=update_frequency)
|
|
57
|
+
for output in process.run():
|
|
58
|
+
_update_task(task, **(output or {}))
|
|
59
|
+
except Exception as e:
|
|
60
|
+
_update_task(task, exception=str(e))
|
|
61
|
+
raise typer.Exit(1)
|
|
62
|
+
|
|
63
|
+
if task:
|
|
64
|
+
if process.returncode == 0:
|
|
65
|
+
task.success(value=100)
|
|
66
|
+
else:
|
|
67
|
+
_update_task(task, status=StatusEnum.ERROR, return_code=process.returncode)
|
|
68
|
+
|
|
69
|
+
if process.returncode != 0:
|
|
70
|
+
raise typer.Exit(process.returncode)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _update_task(task, status=None, **data_kwargs):
|
|
74
|
+
"""Update the task and merge the data"""
|
|
75
|
+
if not task:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
merge_strategy = DefaultMergeStrategy(append_keys=("stdout", "stderr"))
|
|
79
|
+
try:
|
|
80
|
+
task.update(status=status, data=data_kwargs or None, data_merge_strategy=merge_strategy)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
err_console.print(f"Error updating task status: {e!r}")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich import print
|
|
5
|
+
|
|
6
|
+
from taskbadger import __version__
|
|
7
|
+
from taskbadger.cli import create, get, list_tasks_command, run, update
|
|
8
|
+
from taskbadger.config import get_config, write_config
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
rich_markup_mode="rich",
|
|
12
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": False})(run)
|
|
17
|
+
app.command(context_settings={"ignore_unknown_options": False})(get)
|
|
18
|
+
app.command(context_settings={"ignore_unknown_options": False})(create)
|
|
19
|
+
app.command(context_settings={"ignore_unknown_options": False})(update)
|
|
20
|
+
app.command("list", context_settings={"ignore_unknown_options": False})(list_tasks_command)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def version_callback(value: bool):
|
|
24
|
+
if value:
|
|
25
|
+
print(f"Task Badger CLI Version: {__version__}")
|
|
26
|
+
raise typer.Exit()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def configure(ctx: typer.Context):
|
|
31
|
+
"""Update CLI configuration."""
|
|
32
|
+
config = ctx.meta["tb_config"]
|
|
33
|
+
config.organization_slug = typer.prompt(f"Organization slug", default=config.organization_slug)
|
|
34
|
+
config.project_slug = typer.prompt(f"Project slug", default=config.project_slug)
|
|
35
|
+
config.token = typer.prompt(f"API Key", default=config.token)
|
|
36
|
+
path = write_config(config)
|
|
37
|
+
print(f"Config written to [green]{path}[/green]")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def docs():
|
|
42
|
+
"""Open Task Badger docs in a browser."""
|
|
43
|
+
typer.launch("https://docs.taskbadger.net")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command()
|
|
47
|
+
def info(ctx: typer.Context):
|
|
48
|
+
"""Show CLI configuration."""
|
|
49
|
+
config = ctx.meta["tb_config"]
|
|
50
|
+
print(str(config))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.callback()
|
|
54
|
+
def main(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
org: Optional[str] = typer.Option(
|
|
57
|
+
None,
|
|
58
|
+
"--org",
|
|
59
|
+
"-o",
|
|
60
|
+
metavar="TASKBADGER_ORG",
|
|
61
|
+
show_default=False,
|
|
62
|
+
help="Organization Slug. This will override values from the config file and environment variables.",
|
|
63
|
+
),
|
|
64
|
+
project: Optional[str] = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
"--project",
|
|
67
|
+
"-p",
|
|
68
|
+
show_envvar=False,
|
|
69
|
+
metavar="TASKBADGER_PROJECT",
|
|
70
|
+
show_default=False,
|
|
71
|
+
help="Project Slug. This will override values from the config file and environment variables.",
|
|
72
|
+
),
|
|
73
|
+
version: Optional[bool] = typer.Option( # noqa
|
|
74
|
+
None, "--version", callback=version_callback, is_eager=True, help="Show CLI Version"
|
|
75
|
+
),
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Task Badger CLI
|
|
79
|
+
"""
|
|
80
|
+
config = get_config(org=org, project=project)
|
|
81
|
+
ctx.meta["tb_config"] = config
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
app()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class ConfigurationError(Exception):
|
|
2
|
+
def __init__(self, **kwargs):
|
|
3
|
+
self.missing = [name for name, arg in kwargs.items() if arg is None]
|
|
4
|
+
|
|
5
|
+
def __str__(self):
|
|
6
|
+
return f"Missing configuration parameters: {', '.join(self.missing)}"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TaskbadgerException(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Unauthorized(TaskbadgerException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UnexpectedStatus(TaskbadgerException):
|
|
18
|
+
def __init__(self, status_code: int, content: bytes):
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
self.content = content
|
|
21
|
+
|
|
22
|
+
super().__init__(f"Unexpected status code: {status_code}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ServerError(UnexpectedStatus):
|
|
26
|
+
pass
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from taskbadger.exceptions import TaskbadgerException
|
|
5
|
+
from taskbadger.internal.models import ActionRequest, ActionRequestConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def from_config(integration_id: str, config: str):
|
|
9
|
+
cls = integration_from_id(integration_id)
|
|
10
|
+
return cls.from_config_string(integration_id, config)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Integration:
|
|
14
|
+
type: str
|
|
15
|
+
id: str
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
if not self.id.startswith(self.type):
|
|
19
|
+
raise TaskbadgerException(f"Expected integration ID '{self.id}' to start with '{self.type}'")
|
|
20
|
+
|
|
21
|
+
def request_config(self):
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_config_string(cls, integration_id, config):
|
|
26
|
+
kwargs = {"id": integration_id}
|
|
27
|
+
if config:
|
|
28
|
+
# convert config string to dict
|
|
29
|
+
# "to:me@me.com,from:you@you.com"
|
|
30
|
+
split_ = [tuple(item.split(":", 1)) for item in config.split(",")]
|
|
31
|
+
kwargs.update(dict(split_))
|
|
32
|
+
return cls(**kwargs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass
|
|
36
|
+
class Action:
|
|
37
|
+
trigger: str
|
|
38
|
+
integration: Integration
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
return ActionRequest(self.trigger, self.integration.id, self.integration.request_config()).to_dict()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclasses.dataclass
|
|
45
|
+
class EmailIntegration(Integration):
|
|
46
|
+
type = "email"
|
|
47
|
+
to: str # custom type
|
|
48
|
+
id: str = "email"
|
|
49
|
+
|
|
50
|
+
def request_config(self) -> ActionRequestConfig:
|
|
51
|
+
return ActionRequestConfig.from_dict({"to": self.to})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass
|
|
55
|
+
class WebhookIntegration(Integration):
|
|
56
|
+
type = "webhook"
|
|
57
|
+
id: str
|
|
58
|
+
|
|
59
|
+
def request_config(self) -> ActionRequestConfig:
|
|
60
|
+
return ActionRequestConfig.from_dict({})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
ALL = [EmailIntegration, WebhookIntegration]
|
|
64
|
+
BY_TYPE = {cls.type: cls for cls in ALL}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def integration_from_id(integration_id):
|
|
68
|
+
type_, _ = get_type_id(integration_id)
|
|
69
|
+
try:
|
|
70
|
+
return BY_TYPE[type_]
|
|
71
|
+
except KeyError:
|
|
72
|
+
raise TaskbadgerException(f"Unknown integration type: '{type_}'")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_type_id(integration_id: str):
|
|
76
|
+
if ":" in integration_id:
|
|
77
|
+
return integration_id.split(":", 1)
|
|
78
|
+
return integration_id, None
|