labtasker-client 2.0.1__tar.gz → 2.2.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.0.1 → labtasker_client-2.2.0}/.gitignore +4 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/PKG-INFO +4 -3
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/pyproject.toml +5 -4
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/__init__.py +21 -2
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/api.py +101 -3
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/binding.py +7 -1
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/cli.py +146 -7
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/client.py +201 -13
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/command_worker.py +212 -83
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/config.py +10 -1
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/execution.py +7 -6
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/models.py +70 -1
- labtasker_client-2.2.0/src/labtasker/observations.py +160 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/tee.py +20 -2
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/validation.py +39 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/worker.py +132 -46
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/LICENSE +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/__main__.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/command_template.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/errors.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/journal.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/local.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/paths.py +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/py.typed +0 -0
- {labtasker_client-2.0.1 → labtasker_client-2.2.0}/src/labtasker/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labtasker-client
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A small task queue for parallel model inference and evaluation
|
|
5
5
|
Project-URL: Homepage, https://github.com/luocfprime/labtasker
|
|
6
6
|
Project-URL: Repository, https://github.com/luocfprime/labtasker.git
|
|
@@ -8,6 +8,7 @@ Author-email: lcf <luocfprime@gmail.com>
|
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
|
-
Requires-Dist: httpx<1,>=0.28
|
|
12
|
-
Requires-Dist:
|
|
11
|
+
Requires-Dist: httpx[socks]<1,>=0.28
|
|
12
|
+
Requires-Dist: packaging<27,>=24
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.13.5
|
|
13
14
|
Requires-Dist: typer<1,>=0.16
|
|
@@ -1,18 +1,19 @@
|
|
|
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.0
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "A small task queue for parallel model inference and evaluation"
|
|
9
9
|
requires-python = ">=3.11"
|
|
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.28,<1",
|
|
15
|
-
"
|
|
14
|
+
"httpx[socks]>=0.28,<1",
|
|
15
|
+
"packaging>=24,<27",
|
|
16
|
+
"pydantic>=2.13.5,<3",
|
|
16
17
|
"typer>=0.16,<1",
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -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.0
|
|
51
|
+
__version__ = "2.2.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
|
+
)
|
|
@@ -10,13 +10,14 @@ from pydantic import BaseModel
|
|
|
10
10
|
from typer._click.core import Context as ClickContext
|
|
11
11
|
from typer.core import TyperCommand
|
|
12
12
|
|
|
13
|
+
from labtasker import __version__
|
|
13
14
|
from labtasker.client import Client
|
|
14
15
|
from labtasker.command_template import TemplateSyntaxError
|
|
15
16
|
from labtasker.command_worker import run_command_worker
|
|
16
17
|
from labtasker.config import resolve_config
|
|
17
18
|
from labtasker.errors import LabtaskerError
|
|
18
19
|
from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
|
|
19
|
-
from labtasker.validation import RequestValidationError, validate_json_object
|
|
20
|
+
from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
|
|
20
21
|
|
|
21
22
|
T = TypeVar("T")
|
|
22
23
|
app = typer.Typer(
|
|
@@ -44,12 +45,40 @@ config_app = typer.Typer(
|
|
|
44
45
|
no_args_is_help=True,
|
|
45
46
|
rich_markup_mode=None,
|
|
46
47
|
)
|
|
48
|
+
worker_app = typer.Typer(
|
|
49
|
+
help="Inspect online Worker observations.",
|
|
50
|
+
add_completion=False,
|
|
51
|
+
no_args_is_help=True,
|
|
52
|
+
rich_markup_mode=None,
|
|
53
|
+
)
|
|
54
|
+
app.add_typer(worker_app, name="worker")
|
|
47
55
|
app.add_typer(task_app, name="task")
|
|
48
56
|
app.add_typer(queue_app, name="queue")
|
|
49
57
|
app.add_typer(config_app, name="config")
|
|
50
58
|
logger = logging.getLogger("labtasker.cli")
|
|
51
59
|
|
|
52
60
|
|
|
61
|
+
def _version_callback(value: bool) -> None:
|
|
62
|
+
if value:
|
|
63
|
+
typer.echo(f"labtasker-client {__version__}")
|
|
64
|
+
raise typer.Exit()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.callback()
|
|
68
|
+
def main(
|
|
69
|
+
version: Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--version",
|
|
73
|
+
callback=_version_callback,
|
|
74
|
+
is_eager=True,
|
|
75
|
+
help="Show the Client package version and exit.",
|
|
76
|
+
),
|
|
77
|
+
] = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Submit, inspect, and execute Labtasker v2 Tasks."""
|
|
80
|
+
|
|
81
|
+
|
|
53
82
|
class _SeparatedCommand(TyperCommand):
|
|
54
83
|
"""Require the explicit boundary between Worker options and child argv."""
|
|
55
84
|
|
|
@@ -76,6 +105,12 @@ def worker_loop(
|
|
|
76
105
|
str | None,
|
|
77
106
|
typer.Option(help="Queue to claim from; otherwise use Client configuration."),
|
|
78
107
|
] = None,
|
|
108
|
+
max_consecutive_failures: Annotated[
|
|
109
|
+
int,
|
|
110
|
+
typer.Option(
|
|
111
|
+
help="Stop after this many consecutive execution failures (positive integer)."
|
|
112
|
+
),
|
|
113
|
+
] = 5,
|
|
79
114
|
idle_timeout: Annotated[
|
|
80
115
|
float,
|
|
81
116
|
typer.Option(help="Seconds without an eligible Task before normal exit."),
|
|
@@ -115,6 +150,7 @@ def worker_loop(
|
|
|
115
150
|
route=route,
|
|
116
151
|
queue=queue,
|
|
117
152
|
idle_timeout=idle_timeout,
|
|
153
|
+
max_consecutive_failures=max_consecutive_failures,
|
|
118
154
|
force_stop_timeout=force_stop_timeout,
|
|
119
155
|
)
|
|
120
156
|
except (TemplateSyntaxError, RequestValidationError) as error:
|
|
@@ -218,6 +254,10 @@ def task_list(
|
|
|
218
254
|
str | None,
|
|
219
255
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
220
256
|
] = None,
|
|
257
|
+
name_fuzzy: Annotated[
|
|
258
|
+
str | None,
|
|
259
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
260
|
+
] = None,
|
|
221
261
|
filter: Annotated[
|
|
222
262
|
str | None,
|
|
223
263
|
typer.Option(help="Additional Task query expression."),
|
|
@@ -245,7 +285,7 @@ def task_list(
|
|
|
245
285
|
) -> None:
|
|
246
286
|
"""List one page of Tasks and print items plus next_cursor as JSON.
|
|
247
287
|
|
|
248
|
-
--status, --name, and --filter are combined with logical AND.
|
|
288
|
+
--status, --name, --name-fuzzy, and --filter are combined with logical AND.
|
|
249
289
|
Reuse a returned cursor only with the same selectors and ordering.
|
|
250
290
|
|
|
251
291
|
Example:
|
|
@@ -260,6 +300,7 @@ def task_list(
|
|
|
260
300
|
lambda client: client.list_tasks(
|
|
261
301
|
status=status,
|
|
262
302
|
name=name,
|
|
303
|
+
name_fuzzy=name_fuzzy,
|
|
263
304
|
filter=filter,
|
|
264
305
|
order_by=order_by,
|
|
265
306
|
descending=descending,
|
|
@@ -282,10 +323,33 @@ def task_count(
|
|
|
282
323
|
str | None,
|
|
283
324
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
284
325
|
] = None,
|
|
326
|
+
name_fuzzy: Annotated[
|
|
327
|
+
str | None,
|
|
328
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
329
|
+
] = None,
|
|
285
330
|
filter: Annotated[
|
|
286
331
|
str | None,
|
|
287
332
|
typer.Option(help="Additional Task query expression."),
|
|
288
333
|
] = None,
|
|
334
|
+
group_by: Annotated[
|
|
335
|
+
list[str] | None,
|
|
336
|
+
typer.Option(
|
|
337
|
+
"--group-by",
|
|
338
|
+
help=(
|
|
339
|
+
"Comma-separated grouping fields, with no spaces "
|
|
340
|
+
"(for example: routes,status). Supported fields: routes, status. Specify once."
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
] = None,
|
|
344
|
+
limit: Annotated[
|
|
345
|
+
int | None,
|
|
346
|
+
typer.Option(
|
|
347
|
+
min=1, max=1000, help="Maximum groups in this page (default 100); requires --group-by."
|
|
348
|
+
),
|
|
349
|
+
] = None,
|
|
350
|
+
cursor: Annotated[
|
|
351
|
+
str | None, typer.Option(help="Next group page cursor; requires --group-by.")
|
|
352
|
+
] = None,
|
|
289
353
|
queue: Annotated[
|
|
290
354
|
str | None,
|
|
291
355
|
typer.Option(help="Task Queue; otherwise use Client configuration."),
|
|
@@ -299,17 +363,93 @@ def task_count(
|
|
|
299
363
|
labtasker task count --status failed \\
|
|
300
364
|
--filter 'last_error.type == "ValueError"'
|
|
301
365
|
"""
|
|
366
|
+
count_options = _invoke(lambda: _count_options(group_by, {"routes", "status"}, limit, cursor))
|
|
302
367
|
count = _invoke(
|
|
303
368
|
lambda: _with_client(
|
|
304
369
|
lambda client: client.count_tasks(
|
|
305
370
|
status=status,
|
|
306
371
|
name=name,
|
|
372
|
+
name_fuzzy=name_fuzzy,
|
|
307
373
|
filter=filter,
|
|
308
374
|
queue=queue,
|
|
375
|
+
**count_options,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
_write_json({"count": count} if isinstance(count, int) else count)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@worker_app.command("list")
|
|
383
|
+
def worker_list(
|
|
384
|
+
filter: Annotated[str | None, typer.Option(help="Worker filter expression.")] = None,
|
|
385
|
+
limit: Annotated[
|
|
386
|
+
int, typer.Option(min=1, max=1000, help="Maximum Workers in this page.")
|
|
387
|
+
] = 100,
|
|
388
|
+
cursor: Annotated[
|
|
389
|
+
str | None, typer.Option(help="Next cursor from the same Worker query.")
|
|
390
|
+
] = None,
|
|
391
|
+
queue: Annotated[
|
|
392
|
+
str | None, typer.Option(help="Queue; otherwise use Client configuration.")
|
|
393
|
+
] = None,
|
|
394
|
+
) -> None:
|
|
395
|
+
"""List one page of unexpired Worker observations as JSON, ordered by ID."""
|
|
396
|
+
_write_json(
|
|
397
|
+
_invoke(
|
|
398
|
+
lambda: _with_client(
|
|
399
|
+
lambda client: client.list_workers(
|
|
400
|
+
filter=filter, limit=limit, cursor=cursor, queue=queue
|
|
401
|
+
)
|
|
309
402
|
)
|
|
310
403
|
)
|
|
311
404
|
)
|
|
312
|
-
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@worker_app.command("count")
|
|
408
|
+
def worker_count(
|
|
409
|
+
filter: Annotated[str | None, typer.Option(help="Worker filter expression.")] = None,
|
|
410
|
+
group_by: Annotated[
|
|
411
|
+
list[str] | None,
|
|
412
|
+
typer.Option(
|
|
413
|
+
"--group-by",
|
|
414
|
+
help=(
|
|
415
|
+
"Comma-separated grouping fields, with no spaces "
|
|
416
|
+
"(for example: route,status). Supported fields: route, status. Specify once."
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
] = None,
|
|
420
|
+
limit: Annotated[
|
|
421
|
+
int | None,
|
|
422
|
+
typer.Option(
|
|
423
|
+
min=1, max=1000, help="Maximum groups in this page (default 100); requires --group-by."
|
|
424
|
+
),
|
|
425
|
+
] = None,
|
|
426
|
+
cursor: Annotated[
|
|
427
|
+
str | None, typer.Option(help="Next group page cursor; requires --group-by.")
|
|
428
|
+
] = None,
|
|
429
|
+
queue: Annotated[
|
|
430
|
+
str | None, typer.Option(help="Queue; otherwise use Client configuration.")
|
|
431
|
+
] = None,
|
|
432
|
+
) -> None:
|
|
433
|
+
"""Count unexpired Worker observations, optionally grouped, as JSON."""
|
|
434
|
+
options = _invoke(lambda: _count_options(group_by, {"route", "status"}, limit, cursor))
|
|
435
|
+
result = _invoke(
|
|
436
|
+
lambda: _with_client(
|
|
437
|
+
lambda client: client.count_workers(filter=filter, queue=queue, **options)
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
_write_json({"count": result} if isinstance(result, int) else result)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _count_options(
|
|
444
|
+
group_by: list[str] | None, allowed: set[str], limit: int | None, cursor: str | None
|
|
445
|
+
) -> dict[str, Any]:
|
|
446
|
+
if group_by is not None and len(group_by) != 1:
|
|
447
|
+
raise RequestValidationError("Specify --group-by only once, comma-separated with no spaces")
|
|
448
|
+
value = None if group_by is None else group_by[0].split(",")
|
|
449
|
+
fields = validate_grouping(value, allowed, limit, cursor)
|
|
450
|
+
if fields is None:
|
|
451
|
+
return {}
|
|
452
|
+
return {"group_by": fields, "limit": limit, "cursor": cursor}
|
|
313
453
|
|
|
314
454
|
|
|
315
455
|
@task_app.command("update")
|
|
@@ -465,7 +605,7 @@ def _invoke(operation: Callable[[], T]) -> T:
|
|
|
465
605
|
try:
|
|
466
606
|
return operation()
|
|
467
607
|
except LabtaskerError as error:
|
|
468
|
-
_write_json(error.as_envelope()
|
|
608
|
+
_write_json(error.as_envelope())
|
|
469
609
|
raise typer.Exit(1) from error
|
|
470
610
|
except RequestValidationError as error:
|
|
471
611
|
raise typer.BadParameter(str(error)) from error
|
|
@@ -490,17 +630,16 @@ def _json_object(value: str, *, option: str) -> dict[str, Any]:
|
|
|
490
630
|
object_pairs_hook=reject_duplicate_keys,
|
|
491
631
|
)
|
|
492
632
|
return validate_json_object(parsed, field=option)
|
|
493
|
-
except (json.JSONDecodeError, ValueError, RequestValidationError) as error:
|
|
633
|
+
except (json.JSONDecodeError, ValueError, RequestValidationError, RecursionError) as error:
|
|
494
634
|
raise typer.BadParameter(f"{option} must be one strict JSON object: {error}") from error
|
|
495
635
|
|
|
496
636
|
|
|
497
|
-
def _write_json(value: object
|
|
637
|
+
def _write_json(value: object) -> None:
|
|
498
638
|
if isinstance(value, BaseModel):
|
|
499
639
|
value = value.model_dump(mode="json")
|
|
500
640
|
elif isinstance(value, list) and all(isinstance(item, BaseModel) for item in value):
|
|
501
641
|
value = [item.model_dump(mode="json") for item in value]
|
|
502
642
|
typer.echo(
|
|
503
643
|
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
|
504
|
-
err=error,
|
|
505
644
|
nl=False,
|
|
506
645
|
)
|