labtasker-client 2.1.0__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.1.0 → labtasker_client-2.2.0}/.gitignore +4 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/PKG-INFO +4 -3
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/pyproject.toml +5 -4
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/__init__.py +21 -2
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/api.py +101 -3
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/binding.py +7 -1
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/cli.py +122 -4
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/client.py +201 -13
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/command_worker.py +212 -83
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/config.py +10 -1
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/execution.py +7 -6
- {labtasker_client-2.1.0 → 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.1.0 → labtasker_client-2.2.0}/src/labtasker/tee.py +20 -2
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/validation.py +39 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/worker.py +132 -46
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/LICENSE +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/__main__.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/command_template.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/errors.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/journal.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/local.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/paths.py +0 -0
- {labtasker_client-2.1.0 → labtasker_client-2.2.0}/src/labtasker/py.typed +0 -0
- {labtasker_client-2.1.0 → 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.
|
|
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.
|
|
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.
|
|
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
|
+
)
|
|
@@ -17,7 +17,7 @@ from labtasker.command_worker import run_command_worker
|
|
|
17
17
|
from labtasker.config import resolve_config
|
|
18
18
|
from labtasker.errors import LabtaskerError
|
|
19
19
|
from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
|
|
20
|
-
from labtasker.validation import RequestValidationError, validate_json_object
|
|
20
|
+
from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
|
|
21
21
|
|
|
22
22
|
T = TypeVar("T")
|
|
23
23
|
app = typer.Typer(
|
|
@@ -45,6 +45,13 @@ config_app = typer.Typer(
|
|
|
45
45
|
no_args_is_help=True,
|
|
46
46
|
rich_markup_mode=None,
|
|
47
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")
|
|
48
55
|
app.add_typer(task_app, name="task")
|
|
49
56
|
app.add_typer(queue_app, name="queue")
|
|
50
57
|
app.add_typer(config_app, name="config")
|
|
@@ -98,6 +105,12 @@ def worker_loop(
|
|
|
98
105
|
str | None,
|
|
99
106
|
typer.Option(help="Queue to claim from; otherwise use Client configuration."),
|
|
100
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,
|
|
101
114
|
idle_timeout: Annotated[
|
|
102
115
|
float,
|
|
103
116
|
typer.Option(help="Seconds without an eligible Task before normal exit."),
|
|
@@ -137,6 +150,7 @@ def worker_loop(
|
|
|
137
150
|
route=route,
|
|
138
151
|
queue=queue,
|
|
139
152
|
idle_timeout=idle_timeout,
|
|
153
|
+
max_consecutive_failures=max_consecutive_failures,
|
|
140
154
|
force_stop_timeout=force_stop_timeout,
|
|
141
155
|
)
|
|
142
156
|
except (TemplateSyntaxError, RequestValidationError) as error:
|
|
@@ -240,6 +254,10 @@ def task_list(
|
|
|
240
254
|
str | None,
|
|
241
255
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
242
256
|
] = None,
|
|
257
|
+
name_fuzzy: Annotated[
|
|
258
|
+
str | None,
|
|
259
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
260
|
+
] = None,
|
|
243
261
|
filter: Annotated[
|
|
244
262
|
str | None,
|
|
245
263
|
typer.Option(help="Additional Task query expression."),
|
|
@@ -267,7 +285,7 @@ def task_list(
|
|
|
267
285
|
) -> None:
|
|
268
286
|
"""List one page of Tasks and print items plus next_cursor as JSON.
|
|
269
287
|
|
|
270
|
-
--status, --name, and --filter are combined with logical AND.
|
|
288
|
+
--status, --name, --name-fuzzy, and --filter are combined with logical AND.
|
|
271
289
|
Reuse a returned cursor only with the same selectors and ordering.
|
|
272
290
|
|
|
273
291
|
Example:
|
|
@@ -282,6 +300,7 @@ def task_list(
|
|
|
282
300
|
lambda client: client.list_tasks(
|
|
283
301
|
status=status,
|
|
284
302
|
name=name,
|
|
303
|
+
name_fuzzy=name_fuzzy,
|
|
285
304
|
filter=filter,
|
|
286
305
|
order_by=order_by,
|
|
287
306
|
descending=descending,
|
|
@@ -304,10 +323,33 @@ def task_count(
|
|
|
304
323
|
str | None,
|
|
305
324
|
typer.Option(help="Select an exact Task name; empty string is valid."),
|
|
306
325
|
] = None,
|
|
326
|
+
name_fuzzy: Annotated[
|
|
327
|
+
str | None,
|
|
328
|
+
typer.Option(help="Case-insensitive subsequence search; every word must match."),
|
|
329
|
+
] = None,
|
|
307
330
|
filter: Annotated[
|
|
308
331
|
str | None,
|
|
309
332
|
typer.Option(help="Additional Task query expression."),
|
|
310
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,
|
|
311
353
|
queue: Annotated[
|
|
312
354
|
str | None,
|
|
313
355
|
typer.Option(help="Task Queue; otherwise use Client configuration."),
|
|
@@ -321,17 +363,93 @@ def task_count(
|
|
|
321
363
|
labtasker task count --status failed \\
|
|
322
364
|
--filter 'last_error.type == "ValueError"'
|
|
323
365
|
"""
|
|
366
|
+
count_options = _invoke(lambda: _count_options(group_by, {"routes", "status"}, limit, cursor))
|
|
324
367
|
count = _invoke(
|
|
325
368
|
lambda: _with_client(
|
|
326
369
|
lambda client: client.count_tasks(
|
|
327
370
|
status=status,
|
|
328
371
|
name=name,
|
|
372
|
+
name_fuzzy=name_fuzzy,
|
|
329
373
|
filter=filter,
|
|
330
374
|
queue=queue,
|
|
375
|
+
**count_options,
|
|
331
376
|
)
|
|
332
377
|
)
|
|
333
378
|
)
|
|
334
|
-
_write_json({"count": count})
|
|
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
|
+
)
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
)
|
|
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}
|
|
335
453
|
|
|
336
454
|
|
|
337
455
|
@task_app.command("update")
|
|
@@ -512,7 +630,7 @@ def _json_object(value: str, *, option: str) -> dict[str, Any]:
|
|
|
512
630
|
object_pairs_hook=reject_duplicate_keys,
|
|
513
631
|
)
|
|
514
632
|
return validate_json_object(parsed, field=option)
|
|
515
|
-
except (json.JSONDecodeError, ValueError, RequestValidationError) as error:
|
|
633
|
+
except (json.JSONDecodeError, ValueError, RequestValidationError, RecursionError) as error:
|
|
516
634
|
raise typer.BadParameter(f"{option} must be one strict JSON object: {error}") from error
|
|
517
635
|
|
|
518
636
|
|