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.
Files changed (57) hide show
  1. {taskbadger-0.4.4 → taskbadger-0.8.0}/PKG-INFO +10 -10
  2. {taskbadger-0.4.4 → taskbadger-0.8.0}/README.md +4 -6
  3. {taskbadger-0.4.4 → taskbadger-0.8.0}/pyproject.toml +11 -6
  4. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/__init__.py +3 -2
  5. taskbadger-0.8.0/taskbadger/cli/__init__.py +3 -0
  6. taskbadger-0.8.0/taskbadger/cli/basics.py +126 -0
  7. taskbadger-0.8.0/taskbadger/cli/list_tasks.py +79 -0
  8. taskbadger-0.8.0/taskbadger/cli/utils.py +46 -0
  9. taskbadger-0.8.0/taskbadger/cli/wrapper.py +82 -0
  10. taskbadger-0.8.0/taskbadger/cli_main.py +85 -0
  11. taskbadger-0.8.0/taskbadger/exceptions.py +26 -0
  12. taskbadger-0.8.0/taskbadger/integrations.py +78 -0
  13. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/__init__.py +1 -1
  14. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_cancel.py +13 -22
  15. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_create.py +14 -23
  16. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_get.py +15 -24
  17. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_list.py +18 -24
  18. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_partial_update.py +15 -23
  19. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/action_update.py +15 -23
  20. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_cancel.py +12 -22
  21. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_create.py +13 -23
  22. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_get.py +14 -24
  23. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_list.py +16 -22
  24. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_partial_update.py +14 -23
  25. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/task_update.py +14 -23
  26. taskbadger-0.8.0/taskbadger/internal/client.py +268 -0
  27. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/errors.py +5 -1
  28. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action.py +4 -3
  29. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_config.py +4 -3
  30. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_request.py +4 -3
  31. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/action_request_config.py +4 -3
  32. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/paginated_task_list.py +4 -3
  33. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_action_request.py +4 -3
  34. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_action_request_config.py +4 -3
  35. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_task_request.py +6 -5
  36. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/patched_task_request_data.py +4 -3
  37. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task.py +6 -5
  38. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_data.py +4 -3
  39. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_request.py +6 -5
  40. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/task_request_data.py +4 -3
  41. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/types.py +5 -5
  42. taskbadger-0.8.0/taskbadger/mug.py +100 -0
  43. taskbadger-0.8.0/taskbadger/process.py +78 -0
  44. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/safe_sdk.py +12 -11
  45. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/sdk.py +61 -64
  46. taskbadger-0.4.4/taskbadger/cli.py +0 -169
  47. taskbadger-0.4.4/taskbadger/exceptions.py +0 -6
  48. taskbadger-0.4.4/taskbadger/integrations.py +0 -41
  49. taskbadger-0.4.4/taskbadger/internal/client.py +0 -64
  50. {taskbadger-0.4.4 → taskbadger-0.8.0}/LICENSE +0 -0
  51. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/config.py +0 -0
  52. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/__init__.py +0 -0
  53. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/action_endpoints/__init__.py +0 -0
  54. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/api/task_endpoints/__init__.py +0 -0
  55. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/__init__.py +0 -0
  56. {taskbadger-0.4.4 → taskbadger-0.8.0}/taskbadger/internal/models/status_enum.py +4 -4
  57. {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.4.4
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: httpx (>=0.15.4,<0.24.0)
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.7.0,<0.8.0)
28
- Requires-Dist: typing-extensions (>=4.6.1,<5.0.0) ; python_version == "3.9"
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
- "*/10%,success,error",
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 "error email to:me@test.com" -- path/to/script.sh
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
- "*/10%,success,error",
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 "error email to:me@test.com" -- path/to/script.sh
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.4.4"
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.15.4,<0.24.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.7.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.6.1", python = "3.9"}
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.13.1"
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.cli:app"
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,3 @@
1
+ from .basics import create, get, update
2
+ from .list_tasks import list_tasks_command
3
+ from .wrapper import run
@@ -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
@@ -1,4 +1,4 @@
1
- """ A client library for accessing Task Badger """
1
+ """ A client library for accessing Taskbadger API """
2
2
  from .client import AuthenticatedClient, Client
3
3
 
4
4
  __all__ = (