taskbadger 1.3.4__tar.gz → 1.5.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 (69) hide show
  1. taskbadger-1.5.0/.gitignore +25 -0
  2. {taskbadger-1.3.4 → taskbadger-1.5.0}/PKG-INFO +16 -22
  3. taskbadger-1.5.0/pyproject.toml +100 -0
  4. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/__init__.py +18 -0
  5. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/celery.py +32 -3
  6. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli/__init__.py +2 -0
  7. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli/basics.py +50 -9
  8. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli/list_tasks.py +16 -2
  9. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli/utils.py +1 -1
  10. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli/wrapper.py +15 -5
  11. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/cli_main.py +8 -4
  12. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/config.py +26 -9
  13. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/decorators.py +19 -3
  14. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/exceptions.py +4 -0
  15. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/integrations.py +7 -7
  16. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/__init__.py +2 -1
  17. taskbadger-1.5.0/taskbadger/internal/api/__init__.py +1 -0
  18. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_cancel.py +13 -17
  19. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_create.py +27 -26
  20. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_get.py +17 -21
  21. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_list.py +17 -21
  22. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_partial_update.py +37 -36
  23. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/action_update.py +37 -36
  24. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_cancel.py +13 -16
  25. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_create.py +37 -35
  26. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_get.py +17 -20
  27. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_list.py +26 -28
  28. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_partial_update.py +37 -35
  29. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/task_update.py +37 -35
  30. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/client.py +21 -21
  31. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/errors.py +4 -2
  32. taskbadger-1.5.0/taskbadger/internal/models/__init__.py +27 -0
  33. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/models/action.py +15 -23
  34. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/models/action_request.py +12 -23
  35. taskbadger-1.5.0/taskbadger/internal/models/paginated_task_list.py +114 -0
  36. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/models/patched_action_request.py +12 -23
  37. taskbadger-1.5.0/taskbadger/internal/models/patched_task_request.py +249 -0
  38. taskbadger-1.5.0/taskbadger/internal/models/patched_task_request_tags.py +43 -0
  39. taskbadger-1.5.0/taskbadger/internal/models/task.py +320 -0
  40. taskbadger-1.5.0/taskbadger/internal/models/task_request.py +250 -0
  41. taskbadger-1.5.0/taskbadger/internal/models/task_request_tags.py +43 -0
  42. taskbadger-1.5.0/taskbadger/internal/models/task_tags.py +43 -0
  43. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/types.py +6 -4
  44. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/mug.py +27 -9
  45. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/sdk.py +100 -37
  46. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/systems/__init__.py +1 -1
  47. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/systems/celery.py +3 -1
  48. taskbadger-1.5.0/taskbadger/utils.py +15 -0
  49. taskbadger-1.3.4/pyproject.toml +0 -87
  50. taskbadger-1.3.4/taskbadger/internal/api/__init__.py +0 -1
  51. taskbadger-1.3.4/taskbadger/internal/models/__init__.py +0 -33
  52. taskbadger-1.3.4/taskbadger/internal/models/action_config.py +0 -44
  53. taskbadger-1.3.4/taskbadger/internal/models/action_request_config.py +0 -44
  54. taskbadger-1.3.4/taskbadger/internal/models/paginated_task_list.py +0 -91
  55. taskbadger-1.3.4/taskbadger/internal/models/patched_action_request_config.py +0 -44
  56. taskbadger-1.3.4/taskbadger/internal/models/patched_task_request.py +0 -173
  57. taskbadger-1.3.4/taskbadger/internal/models/patched_task_request_data.py +0 -44
  58. taskbadger-1.3.4/taskbadger/internal/models/task.py +0 -233
  59. taskbadger-1.3.4/taskbadger/internal/models/task_data.py +0 -44
  60. taskbadger-1.3.4/taskbadger/internal/models/task_request.py +0 -175
  61. taskbadger-1.3.4/taskbadger/internal/models/task_request_data.py +0 -44
  62. {taskbadger-1.3.4 → taskbadger-1.5.0}/LICENSE +0 -0
  63. {taskbadger-1.3.4 → taskbadger-1.5.0}/README.md +0 -0
  64. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/action_endpoints/__init__.py +0 -0
  65. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/api/task_endpoints/__init__.py +0 -0
  66. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/models/status_enum.py +0 -0
  67. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/internal/py.typed +0 -0
  68. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/process.py +0 -0
  69. {taskbadger-1.3.4 → taskbadger-1.5.0}/taskbadger/safe_sdk.py +0 -0
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ .pytest_cache/
6
+
7
+ # pyenv
8
+ .python-version
9
+
10
+ # Environments
11
+ .env
12
+ .venv
13
+ .envrc
14
+ .env.integration
15
+
16
+ # mypy
17
+ .mypy_cache/
18
+ .dmypy.json
19
+ dmypy.json
20
+
21
+ # JetBrains
22
+ .idea/
23
+
24
+ /coverage.xml
25
+ /.coverage
@@ -1,37 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: taskbadger
3
- Version: 1.3.4
3
+ Version: 1.5.0
4
4
  Summary: The official Python SDK for Task Badger
5
- Home-page: https://taskbadger.net/
6
- License: Apache-2.0
7
- Requires-Python: >=3.8,<4.0
5
+ Project-URL: Changelog, https://github.com/taskbadger/taskbadger-python/releases
6
+ Project-URL: homepage, https://taskbadger.net/
7
+ Project-URL: repository, https://github.com/taskbadger/taskbadger-python
8
+ Project-URL: documentation, https://docs.taskbadger.net/
9
+ License-File: LICENSE
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Environment :: Web Environment
10
12
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: Apache Software License
12
13
  Classifier: Operating System :: OS Independent
13
14
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
15
  Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.6
21
- Classifier: Programming Language :: Python :: 3.7
22
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: attrs>=21.3.0
22
+ Requires-Dist: httpx<0.28.0,>=0.20.0
23
+ Requires-Dist: importlib-metadata>=1.0; python_version < '3.8'
24
+ Requires-Dist: python-dateutil>=2.8.0
25
+ Requires-Dist: tomlkit>=0.12.5
26
+ Requires-Dist: typer[all]<0.10.0
27
+ Requires-Dist: typing-extensions>=4.7.1; python_version <= '3.9'
23
28
  Provides-Extra: celery
24
- Requires-Dist: attrs (>=21.3.0)
25
- Requires-Dist: celery (>=4.0.0,<6.0.0) ; extra == "celery"
26
- Requires-Dist: httpx (>=0.20.0,<0.28.0)
27
- Requires-Dist: importlib-metadata (>=1.0,<2.0) ; python_version < "3.8"
28
- Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
29
- Requires-Dist: tomlkit (>=0.12.5,<0.13.0)
30
- Requires-Dist: typer[all] (<0.10.0)
31
- Requires-Dist: typing-extensions (>=4.7.1,<5.0.0) ; python_version == "3.9"
32
- Project-URL: Changelog, https://github.com/taskbadger/taskbadger-python/releases
33
- Project-URL: Documentation, https://docs.taskbadger.net/
34
- Project-URL: Repository, https://github.com/taskbadger/taskbadger-python
29
+ Requires-Dist: celery<6.0.0,>=4.0.0; extra == 'celery'
35
30
  Description-Content-Type: text/markdown
36
31
 
37
32
  # Task Badger Python Client
@@ -143,4 +138,3 @@ $ taskbadger run "demo task" --action error email to:me@test.com -- path/to/scri
143
138
 
144
139
  Task created: https://taskbadger.net/public/tasks/xyz/
145
140
  ```
146
-
@@ -0,0 +1,100 @@
1
+ [project]
2
+ name = "taskbadger"
3
+ version = "1.5.0"
4
+ description = "The official Python SDK for Task Badger"
5
+ requires-python = ">=3.9"
6
+ authors = []
7
+ readme = "README.md"
8
+ classifiers = [
9
+ "Development Status :: 4 - Beta",
10
+ "Environment :: Web Environment",
11
+ "Intended Audience :: Developers",
12
+ "Operating System :: OS Independent",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3.9",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Software Development :: Libraries :: Python Modules",
19
+ ]
20
+
21
+ dependencies = [
22
+ "httpx >=0.20.0,<0.28.0",
23
+ "attrs >=21.3.0",
24
+ "python-dateutil >=2.8.0",
25
+ "typer[all] <0.10.0",
26
+ "tomlkit >=0.12.5",
27
+ "importlib-metadata >=1.0; python_version < '3.8'",
28
+ "typing-extensions >=4.7.1; python_version <= '3.9'",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.sdist]
36
+ include = [
37
+ "taskbadger",
38
+ "taskbadger/internal/py.typed",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ celery = [
43
+ "celery>=4.0.0,<6.0.0",
44
+ ]
45
+
46
+ [tool.uv]
47
+ package = true
48
+
49
+ [project.urls]
50
+ "Changelog" = "https://github.com/taskbadger/taskbadger-python/releases"
51
+ homepage = "https://taskbadger.net/"
52
+ repository = "https://github.com/taskbadger/taskbadger-python"
53
+ documentation = "https://docs.taskbadger.net/"
54
+
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "black",
59
+ "isort",
60
+ "pre-commit",
61
+ "pytest",
62
+ "pytest-httpx",
63
+ "invoke",
64
+ "pytest-celery",
65
+ "redis",
66
+ "openapi-python-client",
67
+ ]
68
+
69
+ [project.scripts]
70
+ taskbadger = "taskbadger.cli_main:app"
71
+
72
+ [tool.pytest.ini_options]
73
+ # don't run integration tests unless specifically requested
74
+ norecursedirs = ".* integration_tests"
75
+
76
+
77
+ [tool.ruff]
78
+ exclude = [
79
+ ".venv",
80
+ ".git",
81
+ ".ruff_cache",
82
+ ]
83
+ line-length = 120
84
+ indent-width = 4
85
+ target-version = "py39"
86
+
87
+ [tool.ruff.lint]
88
+ select = ["E", "F", "I", "UP", "DJ", "PT"]
89
+ fixable = ["ALL"]
90
+ unfixable = []
91
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
92
+
93
+ [tool.ruff.format]
94
+ quote-style = "double"
95
+ indent-style = "space"
96
+ skip-magic-trailing-comma = false
97
+ line-ending = "auto"
98
+
99
+ [tool.ruff.lint.per-file-ignores]
100
+ "taskbadger/internal/*" = ["E501"]
@@ -5,6 +5,24 @@ from .mug import Badger, Session
5
5
  from .safe_sdk import create_task_safe, update_task_safe
6
6
  from .sdk import DefaultMergeStrategy, Task, create_task, get_task, init, update_task
7
7
 
8
+ __all__ = [
9
+ "track",
10
+ "Action",
11
+ "EmailIntegration",
12
+ "WebhookIntegration",
13
+ "StatusEnum",
14
+ "Badger",
15
+ "Session",
16
+ "create_task_safe",
17
+ "update_task_safe",
18
+ "DefaultMergeStrategy",
19
+ "Task",
20
+ "create_task",
21
+ "get_task",
22
+ "init",
23
+ "update_task",
24
+ ]
25
+
8
26
  try:
9
27
  import importlib.metadata as importlib_metadata
10
28
  except ModuleNotFoundError:
@@ -1,9 +1,17 @@
1
1
  import collections
2
2
  import functools
3
+ import json
3
4
  import logging
4
5
 
5
6
  import celery
6
- from celery.signals import before_task_publish, task_failure, task_prerun, task_retry, task_success
7
+ from celery.signals import (
8
+ before_task_publish,
9
+ task_failure,
10
+ task_prerun,
11
+ task_retry,
12
+ task_success,
13
+ )
14
+ from kombu import serialization
7
15
 
8
16
  from .internal.models import StatusEnum
9
17
  from .mug import Badger
@@ -12,10 +20,15 @@ from .sdk import DefaultMergeStrategy, get_task
12
20
 
13
21
  KWARG_PREFIX = "taskbadger_"
14
22
  TB_KWARGS_ARG = f"{KWARG_PREFIX}kwargs"
15
- IGNORE_ARGS = {TB_KWARGS_ARG, f"{KWARG_PREFIX}task", f"{KWARG_PREFIX}task_id"}
23
+ IGNORE_ARGS = {TB_KWARGS_ARG, f"{KWARG_PREFIX}task", f"{KWARG_PREFIX}task_id", f"{KWARG_PREFIX}record_task_args"}
16
24
  TB_TASK_ID = f"{KWARG_PREFIX}task_id"
17
25
 
18
- TERMINAL_STATES = {StatusEnum.SUCCESS, StatusEnum.ERROR, StatusEnum.CANCELLED, StatusEnum.STALE}
26
+ TERMINAL_STATES = {
27
+ StatusEnum.SUCCESS,
28
+ StatusEnum.ERROR,
29
+ StatusEnum.CANCELLED,
30
+ StatusEnum.STALE,
31
+ }
19
32
 
20
33
  log = logging.getLogger("taskbadger")
21
34
 
@@ -113,6 +126,8 @@ class Task(celery.Task):
113
126
  if Badger.is_configured():
114
127
  headers["taskbadger_track"] = True
115
128
  headers[TB_KWARGS_ARG] = tb_kwargs
129
+ if "record_task_args" in tb_kwargs:
130
+ headers["taskbadger_record_task_args"] = tb_kwargs.pop("record_task_args")
116
131
 
117
132
  result = super().apply_async(*args, **kwargs)
118
133
 
@@ -176,6 +191,20 @@ def task_publish_handler(sender=None, headers=None, body=None, **kwargs):
176
191
  kwargs["status"] = StatusEnum.PENDING
177
192
  name = kwargs.pop("name", headers["task"])
178
193
 
194
+ global_record_task_args = celery_system and celery_system.record_task_args
195
+ if headers.get("taskbadger_record_task_args", global_record_task_args):
196
+ data = {
197
+ "celery_task_args": body[0],
198
+ "celery_task_kwargs": body[1],
199
+ }
200
+ try:
201
+ _, _, value = serialization.dumps(data, serializer="json")
202
+ data = json.loads(value)
203
+ except Exception:
204
+ log.error("Error serializing task arguments for task '%s'", name)
205
+ else:
206
+ kwargs.setdefault("data", {}).update(data)
207
+
179
208
  task = create_task_safe(name, **kwargs)
180
209
  if task:
181
210
  meta = {TB_TASK_ID: task.id}
@@ -1,3 +1,5 @@
1
1
  from .basics import create, get, update
2
2
  from .list_tasks import list_tasks_command
3
3
  from .wrapper import run
4
+
5
+ __all__ = ["create", "get", "update", "list_tasks_command", "run"]
@@ -1,13 +1,18 @@
1
1
  import csv
2
2
  import json
3
3
  import sys
4
- from typing import Tuple
5
4
 
6
5
  import typer
7
6
  from rich import print
8
7
 
9
8
  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
9
+ from taskbadger.cli.utils import (
10
+ OutputFormat,
11
+ configure_api,
12
+ err_console,
13
+ get_actions,
14
+ merge_kv_json,
15
+ )
11
16
 
12
17
 
13
18
  def get(
@@ -29,14 +34,22 @@ def get(
29
34
  elif output_format == OutputFormat.csv:
30
35
  writer = csv.writer(sys.stdout)
31
36
  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)])
37
+ writer.writerow(
38
+ [
39
+ task.id,
40
+ task.created.isoformat(),
41
+ task.name,
42
+ task.status,
43
+ str(task.value_percent),
44
+ ]
45
+ )
33
46
 
34
47
 
35
48
  def create(
36
49
  ctx: typer.Context,
37
50
  name: str = typer.Argument(..., show_default=False, help="The task name."),
38
51
  monitor_id: str = typer.Option(None, help="Associate this task with a monitor."),
39
- action_def: Tuple[str, str, str] = typer.Option(
52
+ action_def: tuple[str, str, str] = typer.Option(
40
53
  (None, None, None),
41
54
  "--action",
42
55
  "-a",
@@ -52,14 +65,27 @@ def create(
52
65
  help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
53
66
  ),
54
67
  metadata_json: str = typer.Option(
55
- None, show_default=False, help="Metadata to associate with the task. Must be valid JSON."
68
+ None,
69
+ show_default=False,
70
+ help="Metadata to associate with the task. Must be valid JSON.",
71
+ ),
72
+ tag: list[str] = typer.Option(
73
+ None,
74
+ show_default=False,
75
+ help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
76
+ ),
77
+ tags_json: str = typer.Option(
78
+ None,
79
+ show_default=False,
80
+ help="Tags to associate with the task. Must be valid JSON mapping name -> value.",
56
81
  ),
57
82
  quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output. Only the Task ID."),
58
83
  ):
59
84
  """Create a task."""
60
85
  configure_api(ctx)
61
86
  actions = get_actions(action_def)
62
- metadata = get_metadata(metadata, metadata_json)
87
+ metadata = merge_kv_json(metadata, metadata_json)
88
+ tags = merge_kv_json(tag, tags_json)
63
89
 
64
90
  try:
65
91
  task = create_task(
@@ -69,6 +95,7 @@ def create(
69
95
  data=metadata,
70
96
  actions=actions,
71
97
  monitor_id=monitor_id,
98
+ tags=tags,
72
99
  )
73
100
  except Exception as e:
74
101
  err_console.print(f"Error creating task: {e}")
@@ -83,7 +110,7 @@ def update(
83
110
  ctx: typer.Context,
84
111
  task_id: str = typer.Argument(..., show_default=False, help="The ID of the task to update."),
85
112
  name: str = typer.Option(None, show_default=False, help="Update the name of the task."),
86
- action_def: Tuple[str, str, str] = typer.Option(
113
+ action_def: tuple[str, str, str] = typer.Option(
87
114
  (None, None, None),
88
115
  "--action",
89
116
  "-a",
@@ -100,14 +127,27 @@ def update(
100
127
  help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
101
128
  ),
102
129
  metadata_json: str = typer.Option(
103
- None, show_default=False, help="Metadata to associate with the task. Must be valid JSON."
130
+ None,
131
+ show_default=False,
132
+ help="Metadata to associate with the task. Must be valid JSON.",
133
+ ),
134
+ tag: list[str] = typer.Option(
135
+ None,
136
+ show_default=False,
137
+ help="Metadata 'key=value' pair to associate with the task. Can be specified multiple times.",
138
+ ),
139
+ tags_json: str = typer.Option(
140
+ None,
141
+ show_default=False,
142
+ help="Tags to associate with the task. Must be valid JSON mapping name -> value.",
104
143
  ),
105
144
  quiet: bool = typer.Option(False, "--quiet", "-q", help="No output."),
106
145
  ):
107
146
  """Update a task."""
108
147
  configure_api(ctx)
109
148
  actions = get_actions(action_def)
110
- metadata = get_metadata(metadata, metadata_json)
149
+ metadata = merge_kv_json(metadata, metadata_json)
150
+ tags = merge_kv_json(tag, tags_json)
111
151
 
112
152
  try:
113
153
  task = update_task(
@@ -118,6 +158,7 @@ def update(
118
158
  value_max=value_max,
119
159
  data=metadata,
120
160
  actions=actions,
161
+ tags=tags,
121
162
  )
122
163
  except Exception as e:
123
164
  err_console.print(f"Error creating task: {e}")
@@ -47,7 +47,13 @@ def _render_pretty(ctx, result):
47
47
  table.add_column("Percent", no_wrap=True)
48
48
 
49
49
  for task in result.results:
50
- table.add_row(task.id, task.created.isoformat(), task.name, task.status, str(task.value_percent))
50
+ table.add_row(
51
+ task.id,
52
+ task.created.isoformat(),
53
+ task.name,
54
+ task.status,
55
+ str(task.value_percent),
56
+ )
51
57
  Console().print(table)
52
58
 
53
59
  cursor = _get_cursor(result.next_)
@@ -59,7 +65,15 @@ def _render_csv(ctx, result):
59
65
  writer = csv.writer(sys.stdout)
60
66
  writer.writerow("Task ID,Created,Name,Status,Percent".split(","))
61
67
  for task in result.results:
62
- writer.writerow([task.id, task.created.isoformat(), task.name, task.status, str(task.value_percent)])
68
+ writer.writerow(
69
+ [
70
+ task.id,
71
+ task.created.isoformat(),
72
+ task.name,
73
+ task.status,
74
+ str(task.value_percent),
75
+ ]
76
+ )
63
77
 
64
78
  cursor = _get_cursor(result.next_)
65
79
  if cursor:
@@ -28,7 +28,7 @@ def get_actions(action_def: tuple[str, str, str]) -> list[Action]:
28
28
  return []
29
29
 
30
30
 
31
- def get_metadata(metadata_kv: list[str], metadata_json: str) -> dict:
31
+ def merge_kv_json(metadata_kv: list[str], metadata_json: str) -> dict:
32
32
  metadata = {}
33
33
  for kv in metadata_kv:
34
34
  k, v = kv.strip().split("=", 1)
@@ -1,10 +1,8 @@
1
- from typing import Tuple
2
-
3
1
  import typer
4
2
  from rich import print
5
3
 
6
4
  from taskbadger import DefaultMergeStrategy, Session, StatusEnum, Task
7
- from taskbadger.cli.utils import configure_api, err_console, get_actions
5
+ from taskbadger.cli.utils import configure_api, err_console, get_actions, merge_kv_json
8
6
  from taskbadger.process import ProcessRunner
9
7
 
10
8
 
@@ -13,7 +11,7 @@ def run(
13
11
  name: str = typer.Argument(..., show_default=False, help="The task name"),
14
12
  monitor_id: str = typer.Option(None, help="Associate this task with a monitor."),
15
13
  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(
14
+ action_def: tuple[str, str, str] = typer.Option(
17
15
  (None, None, None),
18
16
  "--action",
19
17
  "-a",
@@ -21,6 +19,11 @@ def run(
21
19
  show_default=False,
22
20
  help="Action definition e.g. 'success,error email to:me@email.com'",
23
21
  ),
22
+ tag: list[str] = typer.Option(
23
+ None,
24
+ show_default=False,
25
+ help="Tags: 'name=value' pair to associate with the task. Can be specified multiple times.",
26
+ ),
24
27
  capture_output: bool = typer.Option(False, help="Capture stdout and stderr."),
25
28
  ):
26
29
  """Execute a command using the CLI and create a Task to track its outcome.
@@ -36,6 +39,7 @@ def run(
36
39
  """
37
40
  configure_api(ctx)
38
41
  actions = get_actions(action_def)
42
+ tags = merge_kv_json(tag, "")
39
43
  stale_timeout = update_frequency * 2
40
44
  with Session():
41
45
  try:
@@ -45,6 +49,7 @@ def run(
45
49
  stale_timeout=stale_timeout,
46
50
  actions=actions,
47
51
  monitor_id=monitor_id,
52
+ tags=tags,
48
53
  )
49
54
  except Exception as e:
50
55
  err_console.print(f"Error creating task: {e}")
@@ -53,7 +58,12 @@ def run(
53
58
  print(f"Task created: {task.public_url}")
54
59
  env = {"TASKBADGER_TASK_ID": task.id} if task else None
55
60
  try:
56
- process = ProcessRunner(ctx.args, env, capture_output=capture_output, update_frequency=update_frequency)
61
+ process = ProcessRunner(
62
+ ctx.args,
63
+ env,
64
+ capture_output=capture_output,
65
+ update_frequency=update_frequency,
66
+ )
57
67
  for output in process.run():
58
68
  _update_task(task, **(output or {}))
59
69
  except Exception as e:
@@ -30,9 +30,9 @@ def version_callback(value: bool):
30
30
  def configure(ctx: typer.Context):
31
31
  """Update CLI configuration."""
32
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)
33
+ config.organization_slug = typer.prompt("Organization slug", default=config.organization_slug)
34
+ config.project_slug = typer.prompt("Project slug", default=config.project_slug)
35
+ config.token = typer.prompt("API Key", default=config.token)
36
36
  path = write_config(config)
37
37
  print(f"Config written to [green]{path}[/green]")
38
38
 
@@ -71,7 +71,11 @@ def main(
71
71
  help="Project Slug. This will override values from the config file and environment variables.",
72
72
  ),
73
73
  version: Optional[bool] = typer.Option( # noqa
74
- None, "--version", callback=version_callback, is_eager=True, help="Show CLI Version"
74
+ None,
75
+ "--version",
76
+ callback=version_callback,
77
+ is_eager=True,
78
+ help="Show CLI Version",
75
79
  ),
76
80
  ):
77
81
  """
@@ -1,6 +1,6 @@
1
1
  import dataclasses
2
- import inspect
3
2
  import os
3
+ import textwrap
4
4
  from pathlib import Path
5
5
 
6
6
  import tomlkit
@@ -18,6 +18,7 @@ class Config:
18
18
  organization_slug: str = None
19
19
  project_slug: str = None
20
20
  host: str = _TB_HOST
21
+ tags: dict = None
21
22
 
22
23
  def is_valid(self):
23
24
  return bool(self.token and self.organization_slug and self.project_slug)
@@ -51,18 +52,26 @@ class Config:
51
52
  organization_slug=overrides.get("org") or _from_env("ORG", defaults.get("org")),
52
53
  project_slug=overrides.get("project") or _from_env("PROJECT", defaults.get("project")),
53
54
  host=overrides.get("host") or auth.get("host"),
55
+ tags=config_dict.get("tags", {}),
54
56
  )
55
57
 
56
58
  def __str__(self):
57
59
  host = ""
58
- if self.host != _TB_HOST:
59
- host = f"\n Host: {self.host}"
60
- return inspect.cleandoc(
61
- f"""
62
- Organization slug: {self.organization_slug or '-'}
63
- Project slug: {self.project_slug or '-'}
64
- Auth token: {self.token or '-'}{host}
60
+ if self.host and self.host != _TB_HOST:
61
+ host = f"Host: {self.host or '-'}\n"
62
+ tags = ""
63
+ if self.tags:
64
+ tags = "Tags:\n " + "\n ".join(f"{k}: {v}" for k, v in self.tags.items())
65
+ return (
66
+ textwrap.dedent(
67
+ f"""
68
+ Organization slug: {self.organization_slug or "-"}
69
+ Project slug: {self.project_slug or "-"}
70
+ Auth token: {self.token or "-"}
65
71
  """
72
+ )
73
+ + host
74
+ + tags
66
75
  )
67
76
 
68
77
 
@@ -73,9 +82,17 @@ def _from_env(name, default=None, prefix="TASKBADGER_"):
73
82
  def write_config(config):
74
83
  doc = document()
75
84
 
76
- doc.add("defaults", table().add("org", config.organization_slug).add("project", config.project_slug))
85
+ doc.add(
86
+ "defaults",
87
+ table().add("org", config.organization_slug).add("project", config.project_slug),
88
+ )
77
89
 
78
90
  doc.add("auth", table().add("token", config.token))
91
+ if config.tags:
92
+ tags = table()
93
+ for key, value in config.tags.items():
94
+ tags.add(key, value)
95
+ doc.add("tags", tags)
79
96
 
80
97
  config_path = _get_config_path()
81
98
  if not config_path.parent.exists():
@@ -8,7 +8,14 @@ from .sdk import StatusEnum
8
8
  log = logging.getLogger("taskbadger")
9
9
 
10
10
 
11
- def track(func=None, *, name: str = None, monitor_id: str = None, max_runtime: int = None, **kwargs):
11
+ def track(
12
+ func=None,
13
+ *,
14
+ name: str = None,
15
+ monitor_id: str = None,
16
+ max_runtime: int = None,
17
+ **task_kwargs,
18
+ ):
12
19
  """
13
20
  Decorator to track a function as a task.
14
21
 
@@ -39,12 +46,21 @@ def track(func=None, *, name: str = None, monitor_id: str = None, max_runtime: i
39
46
  @Session()
40
47
  def _inner(*args, **kwargs):
41
48
  task = create_task_safe(
42
- task_name, status=StatusEnum.PROCESSING, max_runtime=max_runtime, monitor_id=monitor_id, **kwargs
49
+ task_name,
50
+ status=StatusEnum.PROCESSING,
51
+ max_runtime=max_runtime,
52
+ monitor_id=monitor_id,
53
+ **task_kwargs,
43
54
  )
44
55
  try:
45
56
  result = func(*args, **kwargs)
46
57
  except Exception as e:
47
- _update_task(task, status=StatusEnum.ERROR, data={"exception": str(e)}, data_merge_strategy="default")
58
+ _update_task(
59
+ task,
60
+ status=StatusEnum.ERROR,
61
+ data={"exception": str(e)},
62
+ data_merge_strategy="default",
63
+ )
48
64
  raise
49
65
 
50
66
  _update_task(task, status=StatusEnum.SUCCESS)
@@ -1,4 +1,8 @@
1
1
  class ConfigurationError(Exception):
2
+ pass
3
+
4
+
5
+ class MissingConfiguration(ConfigurationError):
2
6
  def __init__(self, **kwargs):
3
7
  self.missing = [name for name, arg in kwargs.items() if arg is None]
4
8