labtasker-client 2.1.0__tar.gz → 2.3.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.
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/.gitignore +4 -0
- labtasker_client-2.3.0/PKG-INFO +17 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/pyproject.toml +10 -6
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/__init__.py +21 -2
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/api.py +101 -3
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/binding.py +7 -1
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/cli.py +127 -8
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/client.py +201 -13
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/command_worker.py +212 -83
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/config.py +16 -2
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/execution.py +7 -6
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/journal.py +3 -1
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/models.py +73 -2
- labtasker_client-2.3.0/src/labtasker/observations.py +160 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/tee.py +20 -2
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/validation.py +39 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/worker.py +132 -46
- labtasker_client-2.1.0/PKG-INFO +0 -13
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/LICENSE +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/__main__.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/command_template.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/errors.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/local.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/paths.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/py.typed +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.3.0}/src/labtasker/types.py +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: labtasker-client
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: A small task queue for parallel model inference and evaluation
|
|
5
|
+
Project-URL: Homepage, https://github.com/luocfprime/labtasker
|
|
6
|
+
Project-URL: Repository, https://github.com/luocfprime/labtasker.git
|
|
7
|
+
Author-email: lcf <luocfprime@gmail.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: httpx[socks]<1,>=0.25.2
|
|
12
|
+
Requires-Dist: packaging>=21.3
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.12; python_version >= '3.14'
|
|
14
|
+
Requires-Dist: pydantic<3,>=2.5.3; python_version < '3.13'
|
|
15
|
+
Requires-Dist: pydantic<3,>=2.9; python_version >= '3.13' and python_version < '3.14'
|
|
16
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
17
|
+
Requires-Dist: typer<1,>=0.19
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling>=1.
|
|
2
|
+
requires = ["hatchling>=1.32.0"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labtasker-client"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "A small task queue for parallel model inference and evaluation"
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
12
|
authors = [{ name = "lcf", email = "luocfprime@gmail.com" }]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"httpx>=0.
|
|
15
|
-
"
|
|
16
|
-
"
|
|
14
|
+
"httpx[socks]>=0.25.2,<1",
|
|
15
|
+
"packaging>=21.3",
|
|
16
|
+
"pydantic>=2.5.3,<3; python_version < '3.13'",
|
|
17
|
+
"pydantic>=2.9,<3; python_version >= '3.13' and python_version < '3.14'",
|
|
18
|
+
"pydantic>=2.12,<3; python_version >= '3.14'",
|
|
19
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
20
|
+
"typer>=0.19,<1",
|
|
17
21
|
]
|
|
18
22
|
|
|
19
23
|
[project.urls]
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
from labtasker.api import (
|
|
4
4
|
cancel_task,
|
|
5
5
|
count_tasks,
|
|
6
|
+
count_workers,
|
|
6
7
|
create_queue,
|
|
7
8
|
delete_queue,
|
|
8
9
|
delete_task,
|
|
9
10
|
get_task,
|
|
10
11
|
list_queues,
|
|
11
12
|
list_tasks,
|
|
13
|
+
list_workers,
|
|
12
14
|
requeue_task,
|
|
13
15
|
submit_task,
|
|
14
16
|
update_task,
|
|
@@ -31,18 +33,31 @@ from labtasker.execution import (
|
|
|
31
33
|
set_force_stop_timeout,
|
|
32
34
|
task_info,
|
|
33
35
|
)
|
|
34
|
-
from labtasker.models import
|
|
36
|
+
from labtasker.models import (
|
|
37
|
+
BulkUpdateResult,
|
|
38
|
+
CountGroup,
|
|
39
|
+
GroupCountPage,
|
|
40
|
+
LastError,
|
|
41
|
+
Queue,
|
|
42
|
+
Task,
|
|
43
|
+
TaskInfo,
|
|
44
|
+
TaskPage,
|
|
45
|
+
WorkerObservation,
|
|
46
|
+
WorkerPage,
|
|
47
|
+
)
|
|
35
48
|
from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
|
|
36
49
|
from labtasker.worker import loop
|
|
37
50
|
|
|
38
|
-
__version__ = "2.
|
|
51
|
+
__version__ = "2.3.0"
|
|
39
52
|
|
|
40
53
|
__all__ = [
|
|
41
54
|
"APIError",
|
|
42
55
|
"BulkUpdateResult",
|
|
43
56
|
"Client",
|
|
44
57
|
"ConfigError",
|
|
58
|
+
"CountGroup",
|
|
45
59
|
"FatalWorkerError",
|
|
60
|
+
"GroupCountPage",
|
|
46
61
|
"JSONValue",
|
|
47
62
|
"LabtaskerError",
|
|
48
63
|
"LastError",
|
|
@@ -57,9 +72,12 @@ __all__ = [
|
|
|
57
72
|
"TaskUpdate",
|
|
58
73
|
"TransientError",
|
|
59
74
|
"TransportError",
|
|
75
|
+
"WorkerObservation",
|
|
76
|
+
"WorkerPage",
|
|
60
77
|
"cancel_task",
|
|
61
78
|
"cancellation_requested",
|
|
62
79
|
"count_tasks",
|
|
80
|
+
"count_workers",
|
|
63
81
|
"create_queue",
|
|
64
82
|
"delete_queue",
|
|
65
83
|
"delete_task",
|
|
@@ -67,6 +85,7 @@ __all__ = [
|
|
|
67
85
|
"get_task",
|
|
68
86
|
"list_queues",
|
|
69
87
|
"list_tasks",
|
|
88
|
+
"list_workers",
|
|
70
89
|
"loop",
|
|
71
90
|
"requeue_task",
|
|
72
91
|
"set_force_stop_timeout",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Sequence
|
|
3
4
|
from threading import Lock
|
|
5
|
+
from typing import overload
|
|
4
6
|
|
|
5
7
|
from labtasker.client import Client
|
|
6
|
-
from labtasker.models import BulkUpdateResult, Queue, Task, TaskPage
|
|
8
|
+
from labtasker.models import BulkUpdateResult, GroupCountPage, Queue, Task, TaskPage, WorkerPage
|
|
7
9
|
from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
|
|
8
10
|
|
|
9
11
|
_default_client: Client | None = None
|
|
@@ -41,6 +43,7 @@ def list_tasks(
|
|
|
41
43
|
*,
|
|
42
44
|
status: TaskStatus | None = None,
|
|
43
45
|
name: str | None = None,
|
|
46
|
+
name_fuzzy: str | None = None,
|
|
44
47
|
filter: str | None = None,
|
|
45
48
|
order_by: TaskOrderField = "created_at",
|
|
46
49
|
descending: bool = True,
|
|
@@ -51,6 +54,7 @@ def list_tasks(
|
|
|
51
54
|
return _client().list_tasks(
|
|
52
55
|
status=status,
|
|
53
56
|
name=name,
|
|
57
|
+
name_fuzzy=name_fuzzy,
|
|
54
58
|
filter=filter,
|
|
55
59
|
order_by=order_by,
|
|
56
60
|
descending=descending,
|
|
@@ -60,14 +64,108 @@ def list_tasks(
|
|
|
60
64
|
)
|
|
61
65
|
|
|
62
66
|
|
|
67
|
+
@overload
|
|
63
68
|
def count_tasks(
|
|
64
69
|
*,
|
|
65
70
|
status: TaskStatus | None = None,
|
|
66
71
|
name: str | None = None,
|
|
72
|
+
name_fuzzy: str | None = None,
|
|
67
73
|
filter: str | None = None,
|
|
68
74
|
queue: str | None = None,
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
group_by: None = None,
|
|
76
|
+
limit: int | None = None,
|
|
77
|
+
cursor: str | None = None,
|
|
78
|
+
) -> int: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def count_tasks(
|
|
83
|
+
*,
|
|
84
|
+
status: TaskStatus | None = None,
|
|
85
|
+
name: str | None = None,
|
|
86
|
+
name_fuzzy: str | None = None,
|
|
87
|
+
filter: str | None = None,
|
|
88
|
+
queue: str | None = None,
|
|
89
|
+
group_by: Sequence[str],
|
|
90
|
+
limit: int | None = None,
|
|
91
|
+
cursor: str | None = None,
|
|
92
|
+
) -> GroupCountPage: ...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def count_tasks(
|
|
96
|
+
*,
|
|
97
|
+
status: TaskStatus | None = None,
|
|
98
|
+
name: str | None = None,
|
|
99
|
+
name_fuzzy: str | None = None,
|
|
100
|
+
filter: str | None = None,
|
|
101
|
+
queue: str | None = None,
|
|
102
|
+
group_by: Sequence[str] | None = None,
|
|
103
|
+
limit: int | None = None,
|
|
104
|
+
cursor: str | None = None,
|
|
105
|
+
) -> int | GroupCountPage:
|
|
106
|
+
if group_by is None:
|
|
107
|
+
if limit is not None or cursor is not None:
|
|
108
|
+
from labtasker.validation import validate_grouping
|
|
109
|
+
|
|
110
|
+
validate_grouping(None, {"routes", "status"}, limit, cursor)
|
|
111
|
+
return _client().count_tasks(
|
|
112
|
+
status=status, name=name, name_fuzzy=name_fuzzy, filter=filter, queue=queue
|
|
113
|
+
)
|
|
114
|
+
return _client().count_tasks(
|
|
115
|
+
status=status,
|
|
116
|
+
name=name,
|
|
117
|
+
name_fuzzy=name_fuzzy,
|
|
118
|
+
filter=filter,
|
|
119
|
+
queue=queue,
|
|
120
|
+
group_by=group_by,
|
|
121
|
+
limit=limit,
|
|
122
|
+
cursor=cursor,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def list_workers(
|
|
127
|
+
*,
|
|
128
|
+
filter: str | None = None,
|
|
129
|
+
limit: int = 100,
|
|
130
|
+
cursor: str | None = None,
|
|
131
|
+
queue: str | None = None,
|
|
132
|
+
) -> WorkerPage:
|
|
133
|
+
return _client().list_workers(filter=filter, limit=limit, cursor=cursor, queue=queue)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@overload
|
|
137
|
+
def count_workers(
|
|
138
|
+
*,
|
|
139
|
+
filter: str | None = None,
|
|
140
|
+
group_by: None = None,
|
|
141
|
+
limit: int | None = None,
|
|
142
|
+
cursor: str | None = None,
|
|
143
|
+
queue: str | None = None,
|
|
144
|
+
) -> int: ...
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@overload
|
|
148
|
+
def count_workers(
|
|
149
|
+
*,
|
|
150
|
+
filter: str | None = None,
|
|
151
|
+
group_by: Sequence[str],
|
|
152
|
+
limit: int | None = None,
|
|
153
|
+
cursor: str | None = None,
|
|
154
|
+
queue: str | None = None,
|
|
155
|
+
) -> GroupCountPage: ...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def count_workers(
|
|
159
|
+
*,
|
|
160
|
+
filter: str | None = None,
|
|
161
|
+
group_by: Sequence[str] | None = None,
|
|
162
|
+
limit: int | None = None,
|
|
163
|
+
cursor: str | None = None,
|
|
164
|
+
queue: str | None = None,
|
|
165
|
+
) -> int | GroupCountPage:
|
|
166
|
+
return _client().count_workers(
|
|
167
|
+
filter=filter, group_by=group_by, limit=limit, cursor=cursor, queue=queue
|
|
168
|
+
)
|
|
71
169
|
|
|
72
170
|
|
|
73
171
|
def update_task(
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
6
7
|
from typing import Any, TypeVar, get_type_hints, overload
|
|
7
8
|
|
|
8
9
|
from pydantic import TypeAdapter, ValidationError
|
|
@@ -185,4 +186,9 @@ def _validate_resolver(resolver: object, name: str) -> None:
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
def _is_async_callable(value: object) -> bool:
|
|
188
|
-
|
|
189
|
+
while isinstance(value, partial):
|
|
190
|
+
value = value.func
|
|
191
|
+
return any(
|
|
192
|
+
inspect.iscoroutinefunction(candidate) or inspect.isasyncgenfunction(candidate)
|
|
193
|
+
for candidate in (value, type(value).__call__)
|
|
194
|
+
)
|
|
@@ -7,7 +7,6 @@ from typing import Annotated, Any, TypeVar, cast
|
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from pydantic import BaseModel
|
|
10
|
-
from typer._click.core import Context as ClickContext
|
|
11
10
|
from typer.core import TyperCommand
|
|
12
11
|
|
|
13
12
|
from labtasker import __version__
|
|
@@ -17,7 +16,7 @@ from labtasker.command_worker import run_command_worker
|
|
|
17
16
|
from labtasker.config import resolve_config
|
|
18
17
|
from labtasker.errors import LabtaskerError
|
|
19
18
|
from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
|
|
20
|
-
from labtasker.validation import RequestValidationError, validate_json_object
|
|
19
|
+
from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
|
|
21
20
|
|
|
22
21
|
T = TypeVar("T")
|
|
23
22
|
app = typer.Typer(
|
|
@@ -45,6 +44,13 @@ config_app = typer.Typer(
|
|
|
45
44
|
no_args_is_help=True,
|
|
46
45
|
rich_markup_mode=None,
|
|
47
46
|
)
|
|
47
|
+
worker_app = typer.Typer(
|
|
48
|
+
help="Inspect online Worker observations.",
|
|
49
|
+
add_completion=False,
|
|
50
|
+
no_args_is_help=True,
|
|
51
|
+
rich_markup_mode=None,
|
|
52
|
+
)
|
|
53
|
+
app.add_typer(worker_app, name="worker")
|
|
48
54
|
app.add_typer(task_app, name="task")
|
|
49
55
|
app.add_typer(queue_app, name="queue")
|
|
50
56
|
app.add_typer(config_app, name="config")
|
|
@@ -75,10 +81,12 @@ def main(
|
|
|
75
81
|
class _SeparatedCommand(TyperCommand):
|
|
76
82
|
"""Require the explicit boundary between Worker options and child argv."""
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
# Typer 0.26 vendored Click, so its internal Context type differs from the
|
|
85
|
+
# public typer.Context used by earlier supported releases.
|
|
86
|
+
def collect_usage_pieces(self, ctx: Any) -> list[str]:
|
|
79
87
|
return [*super().collect_usage_pieces(ctx), "--", "COMMAND", "[ARG...]"]
|
|
80
88
|
|
|
81
|
-
def parse_args(self, ctx:
|
|
89
|
+
def parse_args(self, ctx: Any, args: list[str]) -> list[str]:
|
|
82
90
|
ctx.meta["labtasker_command_separator"] = "--" in args
|
|
83
91
|
return super().parse_args(ctx, args)
|
|
84
92
|
|
|
@@ -98,6 +106,12 @@ def worker_loop(
|
|
|
98
106
|
str | None,
|
|
99
107
|
typer.Option(help="Queue to claim from; otherwise use Client configuration."),
|
|
100
108
|
] = None,
|
|
109
|
+
max_consecutive_failures: Annotated[
|
|
110
|
+
int,
|
|
111
|
+
typer.Option(
|
|
112
|
+
help="Stop after this many consecutive execution failures (positive integer)."
|
|
113
|
+
),
|
|
114
|
+
] = 5,
|
|
101
115
|
idle_timeout: Annotated[
|
|
102
116
|
float,
|
|
103
117
|
typer.Option(help="Seconds without an eligible Task before normal exit."),
|
|
@@ -137,6 +151,7 @@ def worker_loop(
|
|
|
137
151
|
route=route,
|
|
138
152
|
queue=queue,
|
|
139
153
|
idle_timeout=idle_timeout,
|
|
154
|
+
max_consecutive_failures=max_consecutive_failures,
|
|
140
155
|
force_stop_timeout=force_stop_timeout,
|
|
141
156
|
)
|
|
142
157
|
except (TemplateSyntaxError, RequestValidationError) as error:
|
|
@@ -158,7 +173,7 @@ def worker_loop(
|
|
|
158
173
|
def task_submit(
|
|
159
174
|
args: Annotated[
|
|
160
175
|
str,
|
|
161
|
-
typer.Option(help="Task arguments as one strict JSON object."),
|
|
176
|
+
typer.Option(metavar="<str>", help="Task arguments as one strict JSON object."),
|
|
162
177
|
] = "{}",
|
|
163
178
|
name: Annotated[
|
|
164
179
|
str | None,
|
|
@@ -240,6 +255,10 @@ def task_list(
|
|
|
240
255
|
str | None,
|
|
241
256
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
242
257
|
] = None,
|
|
258
|
+
name_fuzzy: Annotated[
|
|
259
|
+
str | None,
|
|
260
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
261
|
+
] = None,
|
|
243
262
|
filter: Annotated[
|
|
244
263
|
str | None,
|
|
245
264
|
typer.Option(help="Additional Task query expression."),
|
|
@@ -267,7 +286,7 @@ def task_list(
|
|
|
267
286
|
) -> None:
|
|
268
287
|
"""List one page of Tasks and print items plus next_cursor as JSON.
|
|
269
288
|
|
|
270
|
-
--status, --name, and --filter are combined with logical AND.
|
|
289
|
+
--status, --name, --name-fuzzy, and --filter are combined with logical AND.
|
|
271
290
|
Reuse a returned cursor only with the same selectors and ordering.
|
|
272
291
|
|
|
273
292
|
Example:
|
|
@@ -282,6 +301,7 @@ def task_list(
|
|
|
282
301
|
lambda client: client.list_tasks(
|
|
283
302
|
status=status,
|
|
284
303
|
name=name,
|
|
304
|
+
name_fuzzy=name_fuzzy,
|
|
285
305
|
filter=filter,
|
|
286
306
|
order_by=order_by,
|
|
287
307
|
descending=descending,
|
|
@@ -304,10 +324,33 @@ def task_count(
|
|
|
304
324
|
str | None,
|
|
305
325
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
306
326
|
] = None,
|
|
327
|
+
name_fuzzy: Annotated[
|
|
328
|
+
str | None,
|
|
329
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
330
|
+
] = None,
|
|
307
331
|
filter: Annotated[
|
|
308
332
|
str | None,
|
|
309
333
|
typer.Option(help="Additional Task query expression."),
|
|
310
334
|
] = None,
|
|
335
|
+
group_by: Annotated[
|
|
336
|
+
list[str] | None,
|
|
337
|
+
typer.Option(
|
|
338
|
+
"--group-by",
|
|
339
|
+
help=(
|
|
340
|
+
"Comma-separated grouping fields, with no spaces "
|
|
341
|
+
"(for example: routes,status). Supported fields: routes, status. Specify once."
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
] = None,
|
|
345
|
+
limit: Annotated[
|
|
346
|
+
int | None,
|
|
347
|
+
typer.Option(
|
|
348
|
+
min=1, max=1000, help="Maximum groups in this page (default 100); requires --group-by."
|
|
349
|
+
),
|
|
350
|
+
] = None,
|
|
351
|
+
cursor: Annotated[
|
|
352
|
+
str | None, typer.Option(help="Next group page cursor; requires --group-by.")
|
|
353
|
+
] = None,
|
|
311
354
|
queue: Annotated[
|
|
312
355
|
str | None,
|
|
313
356
|
typer.Option(help="Task Queue; otherwise use Client configuration."),
|
|
@@ -321,17 +364,93 @@ def task_count(
|
|
|
321
364
|
labtasker task count --status failed \\
|
|
322
365
|
--filter 'last_error.type == "ValueError"'
|
|
323
366
|
"""
|
|
367
|
+
count_options = _invoke(lambda: _count_options(group_by, {"routes", "status"}, limit, cursor))
|
|
324
368
|
count = _invoke(
|
|
325
369
|
lambda: _with_client(
|
|
326
370
|
lambda client: client.count_tasks(
|
|
327
371
|
status=status,
|
|
328
372
|
name=name,
|
|
373
|
+
name_fuzzy=name_fuzzy,
|
|
329
374
|
filter=filter,
|
|
330
375
|
queue=queue,
|
|
376
|
+
**count_options,
|
|
331
377
|
)
|
|
332
378
|
)
|
|
333
379
|
)
|
|
334
|
-
_write_json({"count": count})
|
|
380
|
+
_write_json({"count": count} if isinstance(count, int) else count)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@worker_app.command("list")
|
|
384
|
+
def worker_list(
|
|
385
|
+
filter: Annotated[str | None, typer.Option(help="Worker filter expression.")] = None,
|
|
386
|
+
limit: Annotated[
|
|
387
|
+
int, typer.Option(min=1, max=1000, help="Maximum Workers in this page.")
|
|
388
|
+
] = 100,
|
|
389
|
+
cursor: Annotated[
|
|
390
|
+
str | None, typer.Option(help="Next cursor from the same Worker query.")
|
|
391
|
+
] = None,
|
|
392
|
+
queue: Annotated[
|
|
393
|
+
str | None, typer.Option(help="Queue; otherwise use Client configuration.")
|
|
394
|
+
] = None,
|
|
395
|
+
) -> None:
|
|
396
|
+
"""List one page of unexpired Worker observations as JSON, ordered by ID."""
|
|
397
|
+
_write_json(
|
|
398
|
+
_invoke(
|
|
399
|
+
lambda: _with_client(
|
|
400
|
+
lambda client: client.list_workers(
|
|
401
|
+
filter=filter, limit=limit, cursor=cursor, queue=queue
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@worker_app.command("count")
|
|
409
|
+
def worker_count(
|
|
410
|
+
filter: Annotated[str | None, typer.Option(help="Worker filter expression.")] = None,
|
|
411
|
+
group_by: Annotated[
|
|
412
|
+
list[str] | None,
|
|
413
|
+
typer.Option(
|
|
414
|
+
"--group-by",
|
|
415
|
+
help=(
|
|
416
|
+
"Comma-separated grouping fields, with no spaces "
|
|
417
|
+
"(for example: route,status). Supported fields: route, status. Specify once."
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
] = None,
|
|
421
|
+
limit: Annotated[
|
|
422
|
+
int | None,
|
|
423
|
+
typer.Option(
|
|
424
|
+
min=1, max=1000, help="Maximum groups in this page (default 100); requires --group-by."
|
|
425
|
+
),
|
|
426
|
+
] = None,
|
|
427
|
+
cursor: Annotated[
|
|
428
|
+
str | None, typer.Option(help="Next group page cursor; requires --group-by.")
|
|
429
|
+
] = None,
|
|
430
|
+
queue: Annotated[
|
|
431
|
+
str | None, typer.Option(help="Queue; otherwise use Client configuration.")
|
|
432
|
+
] = None,
|
|
433
|
+
) -> None:
|
|
434
|
+
"""Count unexpired Worker observations, optionally grouped, as JSON."""
|
|
435
|
+
options = _invoke(lambda: _count_options(group_by, {"route", "status"}, limit, cursor))
|
|
436
|
+
result = _invoke(
|
|
437
|
+
lambda: _with_client(
|
|
438
|
+
lambda client: client.count_workers(filter=filter, queue=queue, **options)
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
_write_json({"count": result} if isinstance(result, int) else result)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _count_options(
|
|
445
|
+
group_by: list[str] | None, allowed: set[str], limit: int | None, cursor: str | None
|
|
446
|
+
) -> dict[str, Any]:
|
|
447
|
+
if group_by is not None and len(group_by) != 1:
|
|
448
|
+
raise RequestValidationError("Specify --group-by only once, comma-separated with no spaces")
|
|
449
|
+
value = None if group_by is None else group_by[0].split(",")
|
|
450
|
+
fields = validate_grouping(value, allowed, limit, cursor)
|
|
451
|
+
if fields is None:
|
|
452
|
+
return {}
|
|
453
|
+
return {"group_by": fields, "limit": limit, "cursor": cursor}
|
|
335
454
|
|
|
336
455
|
|
|
337
456
|
@task_app.command("update")
|
|
@@ -512,7 +631,7 @@ def _json_object(value: str, *, option: str) -> dict[str, Any]:
|
|
|
512
631
|
object_pairs_hook=reject_duplicate_keys,
|
|
513
632
|
)
|
|
514
633
|
return validate_json_object(parsed, field=option)
|
|
515
|
-
except (json.JSONDecodeError, ValueError, RequestValidationError) as error:
|
|
634
|
+
except (json.JSONDecodeError, ValueError, RequestValidationError, RecursionError) as error:
|
|
516
635
|
raise typer.BadParameter(f"{option} must be one strict JSON object: {error}") from error
|
|
517
636
|
|
|
518
637
|
|