labtasker 0.1.0__py3-none-any.whl
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/__init__.py +7 -0
- labtasker/__main__.py +6 -0
- labtasker/api_models.py +205 -0
- labtasker/client/__init__.py +0 -0
- labtasker/client/cli/__init__.py +26 -0
- labtasker/client/cli/cli.py +43 -0
- labtasker/client/cli/config.py +80 -0
- labtasker/client/cli/loop.py +131 -0
- labtasker/client/cli/queue.py +148 -0
- labtasker/client/cli/task.py +435 -0
- labtasker/client/cli/worker.py +167 -0
- labtasker/client/client_api.py +30 -0
- labtasker/client/core/__init__.py +0 -0
- labtasker/client/core/api.py +400 -0
- labtasker/client/core/cli_utils.py +238 -0
- labtasker/client/core/cmd_parser/LabCmd.g4 +14 -0
- labtasker/client/core/cmd_parser/LabCmd.py +620 -0
- labtasker/client/core/cmd_parser/LabCmdLexer.g4 +15 -0
- labtasker/client/core/cmd_parser/LabCmdLexer.py +493 -0
- labtasker/client/core/cmd_parser/LabCmdListener.py +54 -0
- labtasker/client/core/cmd_parser/__init__.py +5 -0
- labtasker/client/core/cmd_parser/parser.py +294 -0
- labtasker/client/core/config.py +156 -0
- labtasker/client/core/context.py +54 -0
- labtasker/client/core/exceptions.py +55 -0
- labtasker/client/core/heartbeat.py +103 -0
- labtasker/client/core/job_runner.py +227 -0
- labtasker/client/core/logging.py +91 -0
- labtasker/client/core/paths.py +64 -0
- labtasker/client/core/plugin_utils.py +72 -0
- labtasker/client/templates/labtasker_root/.gitignore +4 -0
- labtasker/client/templates/labtasker_root/client.toml +14 -0
- labtasker/client/templates/labtasker_root/logs/.gitkeep +1 -0
- labtasker/concurrent.py +11 -0
- labtasker/constants.py +7 -0
- labtasker/filtering.py +82 -0
- labtasker/security.py +26 -0
- labtasker/server/__init__.py +0 -0
- labtasker/server/config.py +59 -0
- labtasker/server/database.py +1018 -0
- labtasker/server/db_utils.py +91 -0
- labtasker/server/dependencies.py +39 -0
- labtasker/server/endpoints.py +458 -0
- labtasker/server/fsm.py +258 -0
- labtasker/server/logging.py +44 -0
- labtasker/server/run.py +21 -0
- labtasker/utils.py +353 -0
- labtasker-0.1.0.dist-info/METADATA +89 -0
- labtasker-0.1.0.dist-info/RECORD +52 -0
- labtasker-0.1.0.dist-info/WHEEL +5 -0
- labtasker-0.1.0.dist-info/entry_points.txt +2 -0
- labtasker-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task related CRUD operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import tempfile
|
|
7
|
+
from functools import partial
|
|
8
|
+
from typing import Any, Dict, List, Optional, Set
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import rich
|
|
12
|
+
import ruamel.yaml
|
|
13
|
+
import typer
|
|
14
|
+
import yaml
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from starlette.status import HTTP_404_NOT_FOUND
|
|
18
|
+
|
|
19
|
+
from labtasker.api_models import Task, TaskUpdateRequest
|
|
20
|
+
from labtasker.client.core.api import (
|
|
21
|
+
delete_task,
|
|
22
|
+
get_queue,
|
|
23
|
+
ls_tasks,
|
|
24
|
+
report_task_status,
|
|
25
|
+
submit_task,
|
|
26
|
+
update_tasks,
|
|
27
|
+
)
|
|
28
|
+
from labtasker.client.core.cli_utils import (
|
|
29
|
+
LsFmtChoices,
|
|
30
|
+
cli_utils_decorator,
|
|
31
|
+
ls_format_iter,
|
|
32
|
+
pager_iterator,
|
|
33
|
+
parse_metadata,
|
|
34
|
+
)
|
|
35
|
+
from labtasker.client.core.exceptions import LabtaskerHTTPStatusError
|
|
36
|
+
from labtasker.client.core.logging import stderr_console, stdout_console
|
|
37
|
+
|
|
38
|
+
app = typer.Typer()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def commented_seq_from_dict_list(
|
|
42
|
+
entries: List[Dict[str, Any]]
|
|
43
|
+
) -> ruamel.yaml.CommentedSeq:
|
|
44
|
+
return ruamel.yaml.CommentedSeq([ruamel.yaml.CommentedMap(e) for e in entries])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def add_eol_comment(d: ruamel.yaml.CommentedMap, fields: List[str], comment: str):
|
|
48
|
+
"""Add end of line comment at end of fields (in place)"""
|
|
49
|
+
for key in d.keys():
|
|
50
|
+
if key in fields:
|
|
51
|
+
d.yaml_add_eol_comment(comment, key=key, column=50)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def dump_commented_seq(commented_seq, f):
|
|
55
|
+
y = ruamel.yaml.YAML()
|
|
56
|
+
y.indent(mapping=2, sequence=2, offset=0)
|
|
57
|
+
y.dump(commented_seq, f)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def edit_and_reload(f, editor: str):
|
|
61
|
+
click.edit(filename=f.name, editor=editor)
|
|
62
|
+
f.seek(0)
|
|
63
|
+
data = yaml.safe_load(f)
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def diff(
|
|
68
|
+
prev: List[Dict[str, Any]],
|
|
69
|
+
modified: List[Dict[str, Any]],
|
|
70
|
+
readonly_fields: Optional[List[str]] = None,
|
|
71
|
+
) -> List[Dict[str, Any]]:
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
prev:
|
|
76
|
+
modified:
|
|
77
|
+
readonly_fields:
|
|
78
|
+
|
|
79
|
+
Returns: dict storing modified key values
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
readonly_fields = readonly_fields or []
|
|
83
|
+
|
|
84
|
+
updates = []
|
|
85
|
+
for i, new_entry in enumerate(modified):
|
|
86
|
+
u = dict()
|
|
87
|
+
for k, v in new_entry.items():
|
|
88
|
+
if k in readonly_fields:
|
|
89
|
+
# if changed to readonly field, show a warning
|
|
90
|
+
if v != prev[i][k]:
|
|
91
|
+
stderr_console.print(
|
|
92
|
+
f"[bold orange1]Warning:[/bold orange1] Field '{k}' is readonly. You are not supposed to modify it. Your modification to this field will be ignored."
|
|
93
|
+
)
|
|
94
|
+
# the modified field will be ignored by the server
|
|
95
|
+
continue
|
|
96
|
+
elif v != prev[i][k]: # modified
|
|
97
|
+
u[k] = v
|
|
98
|
+
else: # unchanged
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
updates.append(u)
|
|
102
|
+
|
|
103
|
+
return updates
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command()
|
|
107
|
+
@cli_utils_decorator
|
|
108
|
+
def submit(
|
|
109
|
+
task_name: Optional[str] = typer.Option(None, help="Name of the task."),
|
|
110
|
+
args: Optional[str] = typer.Option(
|
|
111
|
+
None,
|
|
112
|
+
help='Arguments for the task as a python dict string (e.g., \'{"key": "value"}\').',
|
|
113
|
+
),
|
|
114
|
+
metadata: Optional[str] = typer.Option(
|
|
115
|
+
None,
|
|
116
|
+
help='Optional metadata as a python dict string (e.g., \'{"key": "value"}\').',
|
|
117
|
+
),
|
|
118
|
+
cmd: Optional[str] = typer.Option(
|
|
119
|
+
None,
|
|
120
|
+
help="Command to execute for the task.",
|
|
121
|
+
),
|
|
122
|
+
heartbeat_timeout: Optional[float] = typer.Option(
|
|
123
|
+
60,
|
|
124
|
+
help="Heartbeat timeout for the task.",
|
|
125
|
+
),
|
|
126
|
+
task_timeout: Optional[int] = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
help="Task execution timeout.",
|
|
129
|
+
),
|
|
130
|
+
max_retries: Optional[int] = typer.Option(
|
|
131
|
+
3,
|
|
132
|
+
help="Maximum number of retries for the task.",
|
|
133
|
+
),
|
|
134
|
+
priority: Optional[int] = typer.Option(
|
|
135
|
+
1,
|
|
136
|
+
help="Priority of the task.",
|
|
137
|
+
),
|
|
138
|
+
):
|
|
139
|
+
"""
|
|
140
|
+
Submit a new task to the queue.
|
|
141
|
+
"""
|
|
142
|
+
args_dict = parse_metadata(args) if args else {}
|
|
143
|
+
metadata_dict = parse_metadata(metadata) if metadata else {}
|
|
144
|
+
|
|
145
|
+
task_id = submit_task(
|
|
146
|
+
task_name=task_name,
|
|
147
|
+
args=args_dict,
|
|
148
|
+
metadata=metadata_dict,
|
|
149
|
+
cmd=cmd,
|
|
150
|
+
heartbeat_timeout=heartbeat_timeout,
|
|
151
|
+
task_timeout=task_timeout,
|
|
152
|
+
max_retries=max_retries,
|
|
153
|
+
priority=priority,
|
|
154
|
+
)
|
|
155
|
+
stdout_console.print(f"Task submitted with ID: {task_id}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
@cli_utils_decorator
|
|
160
|
+
def report(
|
|
161
|
+
task_id: str = typer.Argument(..., help="ID of the task to update."),
|
|
162
|
+
status: str = typer.Argument(
|
|
163
|
+
..., help="New status for the task. One of `success`, `failed`, `cancelled`."
|
|
164
|
+
),
|
|
165
|
+
summary: Optional[str] = typer.Option(
|
|
166
|
+
None,
|
|
167
|
+
help="Summary of the task status.",
|
|
168
|
+
),
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
Report the status of a task.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
summary = parse_metadata(summary)
|
|
175
|
+
report_task_status(task_id=task_id, status=status, summary=summary)
|
|
176
|
+
except ValidationError as e:
|
|
177
|
+
raise typer.BadParameter(e)
|
|
178
|
+
stdout_console.print(f"Task {task_id} status updated to {status}.")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.command()
|
|
182
|
+
@cli_utils_decorator
|
|
183
|
+
def ls(
|
|
184
|
+
task_id: Optional[str] = typer.Option(
|
|
185
|
+
None,
|
|
186
|
+
help="Filter by task ID.",
|
|
187
|
+
),
|
|
188
|
+
task_name: Optional[str] = typer.Option(
|
|
189
|
+
None,
|
|
190
|
+
help="Filter by task name.",
|
|
191
|
+
),
|
|
192
|
+
extra_filter: Optional[str] = typer.Option(
|
|
193
|
+
None,
|
|
194
|
+
"--extra-filter",
|
|
195
|
+
"-f",
|
|
196
|
+
help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
|
|
197
|
+
),
|
|
198
|
+
pager: bool = typer.Option(
|
|
199
|
+
True,
|
|
200
|
+
help="Enable pagination.",
|
|
201
|
+
),
|
|
202
|
+
limit: int = typer.Option(
|
|
203
|
+
100,
|
|
204
|
+
help="Limit the number of tasks returned.",
|
|
205
|
+
),
|
|
206
|
+
offset: int = typer.Option(
|
|
207
|
+
0,
|
|
208
|
+
help="Initial offset for pagination.",
|
|
209
|
+
),
|
|
210
|
+
fmt: LsFmtChoices = typer.Option(
|
|
211
|
+
"yaml",
|
|
212
|
+
help="Output format. One of `yaml`, `jsonl`.",
|
|
213
|
+
),
|
|
214
|
+
):
|
|
215
|
+
"""List tasks in the queue."""
|
|
216
|
+
get_queue() # validate auth and queue existence, prevent err swallowed by pager
|
|
217
|
+
|
|
218
|
+
extra_filter = parse_metadata(extra_filter)
|
|
219
|
+
page_iter = pager_iterator(
|
|
220
|
+
fetch_function=partial(
|
|
221
|
+
ls_tasks,
|
|
222
|
+
task_id=task_id,
|
|
223
|
+
task_name=task_name,
|
|
224
|
+
extra_filter=extra_filter,
|
|
225
|
+
),
|
|
226
|
+
offset=offset,
|
|
227
|
+
limit=limit,
|
|
228
|
+
)
|
|
229
|
+
if pager:
|
|
230
|
+
click.echo_via_pager(
|
|
231
|
+
ls_format_iter[fmt](
|
|
232
|
+
page_iter,
|
|
233
|
+
use_rich=False,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
for item in ls_format_iter[fmt](
|
|
238
|
+
page_iter,
|
|
239
|
+
use_rich=True,
|
|
240
|
+
):
|
|
241
|
+
stdout_console.print(item)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@app.command()
|
|
245
|
+
@cli_utils_decorator
|
|
246
|
+
def update(
|
|
247
|
+
task_id: Optional[str] = typer.Option(
|
|
248
|
+
None,
|
|
249
|
+
help="Filter by task ID.",
|
|
250
|
+
),
|
|
251
|
+
task_name: Optional[str] = typer.Option(
|
|
252
|
+
None,
|
|
253
|
+
help="Filter by task name.",
|
|
254
|
+
),
|
|
255
|
+
extra_filter: Optional[str] = typer.Option(
|
|
256
|
+
None,
|
|
257
|
+
"--extra-filter",
|
|
258
|
+
"-f",
|
|
259
|
+
help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
|
|
260
|
+
),
|
|
261
|
+
update_dict: Optional[str] = typer.Option(
|
|
262
|
+
None,
|
|
263
|
+
"--update",
|
|
264
|
+
"-u",
|
|
265
|
+
help='Optional dict string for updated values of fields (e.g., \'{"task_name": "new_name"}\').',
|
|
266
|
+
),
|
|
267
|
+
offset: int = typer.Option(
|
|
268
|
+
0,
|
|
269
|
+
help="Initial offset for pagination (In case there are too many items for update, only 1000 results starting from offset is displayed. "
|
|
270
|
+
"You would need to adjust offset to apply to other items).",
|
|
271
|
+
),
|
|
272
|
+
reset_pending: bool = typer.Option(
|
|
273
|
+
False,
|
|
274
|
+
help="Reset pending tasks to pending after updating.",
|
|
275
|
+
),
|
|
276
|
+
editor: Optional[str] = typer.Option(
|
|
277
|
+
None,
|
|
278
|
+
help="Editor to use for interactive update.",
|
|
279
|
+
),
|
|
280
|
+
):
|
|
281
|
+
"""Update tasks settings."""
|
|
282
|
+
extra_filter = parse_metadata(extra_filter)
|
|
283
|
+
|
|
284
|
+
# readonly fields
|
|
285
|
+
readonly_fields: Set[str] = (
|
|
286
|
+
Task.model_fields.keys() - TaskUpdateRequest.model_fields.keys() # type: ignore
|
|
287
|
+
)
|
|
288
|
+
readonly_fields.add("task_id")
|
|
289
|
+
|
|
290
|
+
if reset_pending:
|
|
291
|
+
# these fields will be overwritten internally: status: pending, retries: 0
|
|
292
|
+
readonly_fields.add("status")
|
|
293
|
+
readonly_fields.add("retries")
|
|
294
|
+
|
|
295
|
+
update_dict = parse_metadata(update_dict)
|
|
296
|
+
|
|
297
|
+
if not update_dict: # if no update provided, enter interactive mode
|
|
298
|
+
interactive = True
|
|
299
|
+
else:
|
|
300
|
+
interactive = False
|
|
301
|
+
|
|
302
|
+
old_tasks = ls_tasks(
|
|
303
|
+
task_id=task_id,
|
|
304
|
+
task_name=task_name,
|
|
305
|
+
extra_filter=extra_filter,
|
|
306
|
+
limit=1000,
|
|
307
|
+
offset=offset,
|
|
308
|
+
).content
|
|
309
|
+
|
|
310
|
+
task_updates: List[TaskUpdateRequest] = []
|
|
311
|
+
|
|
312
|
+
# Opens a system text editor to allow modification
|
|
313
|
+
if interactive:
|
|
314
|
+
old_tasks_primitive: List[Dict[str, Any]] = [t.model_dump() for t in old_tasks]
|
|
315
|
+
|
|
316
|
+
commented_seq = commented_seq_from_dict_list(old_tasks_primitive)
|
|
317
|
+
|
|
318
|
+
# format: set line break at each entry
|
|
319
|
+
for i in range(len(commented_seq) - 1):
|
|
320
|
+
commented_seq.yaml_set_comment_before_after_key(key=i + 1, before="\n")
|
|
321
|
+
|
|
322
|
+
# add "do not edit" at the end of readonly_fields
|
|
323
|
+
for d in commented_seq:
|
|
324
|
+
add_eol_comment(
|
|
325
|
+
d, fields=list(readonly_fields), comment="Read-only. DO NOT modify!"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# open an editor to allow interaction
|
|
329
|
+
with tempfile.NamedTemporaryFile(mode="w+", suffix=".yaml") as f:
|
|
330
|
+
dump_commented_seq(commented_seq=commented_seq, f=f)
|
|
331
|
+
|
|
332
|
+
while True: # continue to edit until no syntax error
|
|
333
|
+
try:
|
|
334
|
+
modified = edit_and_reload(f=f, editor=editor)
|
|
335
|
+
break # if no error, break
|
|
336
|
+
except yaml.error.YAMLError as e:
|
|
337
|
+
stderr_console.print(
|
|
338
|
+
"[bold red]Error:[/bold red] error when parsing yaml.\n"
|
|
339
|
+
f"Detail: {str(e)}"
|
|
340
|
+
)
|
|
341
|
+
typer.confirm("Continue to edit?", abort=True)
|
|
342
|
+
|
|
343
|
+
# make sure the len match
|
|
344
|
+
if len(modified) != len(old_tasks_primitive):
|
|
345
|
+
stderr_console.print(
|
|
346
|
+
f"[bold red]Error:[/bold red] number of entries do not match. new {len(modified)} != old {len(old_tasks_primitive)}. "
|
|
347
|
+
f"Please check your modification. You should not change the order or make deletions to entries."
|
|
348
|
+
)
|
|
349
|
+
raise typer.Abort()
|
|
350
|
+
|
|
351
|
+
# make sure the order match
|
|
352
|
+
for i, (m, o) in enumerate(zip(modified, old_tasks_primitive)):
|
|
353
|
+
if m["task_id"] != o["task_id"]:
|
|
354
|
+
stderr_console.print(
|
|
355
|
+
f"[bold red]Error:[/bold red] task_id {m['task_id']} should be {o['task_id']} at {i}th entry. "
|
|
356
|
+
"You should not modify task_id or change the order of the entries."
|
|
357
|
+
)
|
|
358
|
+
raise typer.Abort()
|
|
359
|
+
|
|
360
|
+
# get a list of update dict
|
|
361
|
+
updates = diff(
|
|
362
|
+
prev=old_tasks_primitive,
|
|
363
|
+
modified=modified,
|
|
364
|
+
readonly_fields=list(readonly_fields),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
else:
|
|
368
|
+
# populate if not using interactive mode to modify one by one
|
|
369
|
+
updates = [update_dict] * len(old_tasks)
|
|
370
|
+
|
|
371
|
+
for i, ud in enumerate(updates): # ud: update dict list entry
|
|
372
|
+
if not ud: # filter out empty update dict
|
|
373
|
+
continue
|
|
374
|
+
task_updates.append(TaskUpdateRequest(_id=old_tasks[i].task_id, **ud))
|
|
375
|
+
|
|
376
|
+
updated_tasks = update_tasks(task_updates=task_updates, reset_pending=reset_pending)
|
|
377
|
+
|
|
378
|
+
if not typer.confirm(
|
|
379
|
+
f"Total {len(updated_tasks.content)} tasks updated complete. Do you want to see the updated result?"
|
|
380
|
+
):
|
|
381
|
+
raise typer.Exit()
|
|
382
|
+
|
|
383
|
+
# display via pager ---------------------------------------------------------------
|
|
384
|
+
updated_tasks_primitive = [t.model_dump() for t in updated_tasks.content]
|
|
385
|
+
commented_seq = commented_seq_from_dict_list(updated_tasks_primitive)
|
|
386
|
+
|
|
387
|
+
# format: set line break at each entry
|
|
388
|
+
for i in range(len(commented_seq) - 1):
|
|
389
|
+
commented_seq.yaml_set_comment_before_after_key(key=i + 1, before="\n")
|
|
390
|
+
|
|
391
|
+
# add "modified" comment
|
|
392
|
+
for d, ud in zip(commented_seq, updates):
|
|
393
|
+
add_eol_comment(
|
|
394
|
+
d,
|
|
395
|
+
fields=list(ud.keys()),
|
|
396
|
+
comment=f"Modified",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
s = io.StringIO()
|
|
400
|
+
y = ruamel.yaml.YAML()
|
|
401
|
+
y.indent(mapping=2, sequence=2, offset=0)
|
|
402
|
+
y.dump(commented_seq, s)
|
|
403
|
+
|
|
404
|
+
yaml_str = s.getvalue()
|
|
405
|
+
|
|
406
|
+
console = rich.console.Console()
|
|
407
|
+
with console.capture() as capture:
|
|
408
|
+
console.print(Syntax(yaml_str, "yaml"))
|
|
409
|
+
ansi_str = capture.get()
|
|
410
|
+
|
|
411
|
+
click.echo_via_pager(ansi_str)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@app.command()
|
|
415
|
+
@cli_utils_decorator
|
|
416
|
+
def delete(
|
|
417
|
+
task_id: str = typer.Argument(..., help="ID of the task to delete."),
|
|
418
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Confirm the operation."),
|
|
419
|
+
):
|
|
420
|
+
"""
|
|
421
|
+
Delete a task.
|
|
422
|
+
"""
|
|
423
|
+
if not yes:
|
|
424
|
+
typer.confirm(
|
|
425
|
+
f"Are you sure you want to delete task '{task_id}'?",
|
|
426
|
+
abort=True,
|
|
427
|
+
)
|
|
428
|
+
try:
|
|
429
|
+
delete_task(task_id=task_id)
|
|
430
|
+
stdout_console.print(f"Task {task_id} deleted.")
|
|
431
|
+
except LabtaskerHTTPStatusError as e:
|
|
432
|
+
if e.response.status_code == HTTP_404_NOT_FOUND:
|
|
433
|
+
raise typer.BadParameter("Task not found")
|
|
434
|
+
else:
|
|
435
|
+
raise e
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Worker related CRUD operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
from starlette.status import HTTP_404_NOT_FOUND
|
|
12
|
+
|
|
13
|
+
from labtasker.client.core.api import (
|
|
14
|
+
create_worker,
|
|
15
|
+
delete_worker,
|
|
16
|
+
get_queue,
|
|
17
|
+
ls_worker,
|
|
18
|
+
report_worker_status,
|
|
19
|
+
)
|
|
20
|
+
from labtasker.client.core.cli_utils import (
|
|
21
|
+
LsFmtChoices,
|
|
22
|
+
cli_utils_decorator,
|
|
23
|
+
ls_format_iter,
|
|
24
|
+
pager_iterator,
|
|
25
|
+
parse_metadata,
|
|
26
|
+
)
|
|
27
|
+
from labtasker.client.core.exceptions import LabtaskerHTTPStatusError
|
|
28
|
+
from labtasker.client.core.logging import stdout_console
|
|
29
|
+
|
|
30
|
+
app = typer.Typer()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command()
|
|
34
|
+
@cli_utils_decorator
|
|
35
|
+
def create(
|
|
36
|
+
worker_name: Optional[str] = typer.Option(
|
|
37
|
+
None,
|
|
38
|
+
help="Name of the worker.",
|
|
39
|
+
),
|
|
40
|
+
metadata: Optional[str] = typer.Option(
|
|
41
|
+
None,
|
|
42
|
+
help='Optional metadata as a python dict string (e.g., \'{"key": "value"}\').',
|
|
43
|
+
),
|
|
44
|
+
max_retries: Optional[int] = typer.Option(
|
|
45
|
+
3,
|
|
46
|
+
help="Maximum number of retries for the worker.",
|
|
47
|
+
),
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Create a new worker.
|
|
51
|
+
"""
|
|
52
|
+
metadata = parse_metadata(metadata)
|
|
53
|
+
worker_id = create_worker(
|
|
54
|
+
worker_name=worker_name,
|
|
55
|
+
metadata=metadata,
|
|
56
|
+
max_retries=max_retries,
|
|
57
|
+
)
|
|
58
|
+
stdout_console.print(f"Worker created with ID: {worker_id}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
@cli_utils_decorator
|
|
63
|
+
def ls(
|
|
64
|
+
worker_id: Optional[str] = typer.Option(
|
|
65
|
+
None,
|
|
66
|
+
help="Filter by worker ID.",
|
|
67
|
+
),
|
|
68
|
+
worker_name: Optional[str] = typer.Option(
|
|
69
|
+
None,
|
|
70
|
+
help="Filter by worker name.",
|
|
71
|
+
),
|
|
72
|
+
extra_filter: Optional[str] = typer.Option(
|
|
73
|
+
None,
|
|
74
|
+
help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
|
|
75
|
+
),
|
|
76
|
+
pager: bool = typer.Option(
|
|
77
|
+
True,
|
|
78
|
+
help="Enable pagination.",
|
|
79
|
+
),
|
|
80
|
+
limit: int = typer.Option(
|
|
81
|
+
100,
|
|
82
|
+
help="Limit the number of workers returned.",
|
|
83
|
+
),
|
|
84
|
+
offset: int = typer.Option(
|
|
85
|
+
0,
|
|
86
|
+
help="Initial offset for pagination.",
|
|
87
|
+
),
|
|
88
|
+
fmt: LsFmtChoices = typer.Option(
|
|
89
|
+
"yaml",
|
|
90
|
+
help="Output format. One of `yaml`, `jsonl`.",
|
|
91
|
+
),
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
List workers.
|
|
95
|
+
"""
|
|
96
|
+
get_queue() # validate auth and queue existence, prevent err swallowed by pager
|
|
97
|
+
|
|
98
|
+
extra_filter = parse_metadata(extra_filter)
|
|
99
|
+
page_iter = pager_iterator(
|
|
100
|
+
fetch_function=partial(
|
|
101
|
+
ls_worker,
|
|
102
|
+
worker_id=worker_id,
|
|
103
|
+
worker_name=worker_name,
|
|
104
|
+
extra_filter=extra_filter,
|
|
105
|
+
),
|
|
106
|
+
offset=offset,
|
|
107
|
+
limit=limit,
|
|
108
|
+
)
|
|
109
|
+
if pager:
|
|
110
|
+
click.echo_via_pager(
|
|
111
|
+
ls_format_iter[fmt](
|
|
112
|
+
page_iter,
|
|
113
|
+
use_rich=False,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
for item in ls_format_iter[fmt](
|
|
118
|
+
page_iter,
|
|
119
|
+
use_rich=True,
|
|
120
|
+
):
|
|
121
|
+
stdout_console.print(item)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
@cli_utils_decorator
|
|
126
|
+
def report(
|
|
127
|
+
worker_id: str = typer.Argument(..., help="ID of the worker to update."),
|
|
128
|
+
status: str = typer.Argument(
|
|
129
|
+
..., help="New status for the worker. One of `active`, `suspended`, `failed`."
|
|
130
|
+
),
|
|
131
|
+
):
|
|
132
|
+
"""
|
|
133
|
+
Update the status of a worker. Can be used to revive crashed workers or manually suspend active workers.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
report_worker_status(worker_id=worker_id, status=status)
|
|
137
|
+
except ValidationError as e:
|
|
138
|
+
raise typer.BadParameter(e)
|
|
139
|
+
stdout_console.print(f"Worker {worker_id} status updated to {status}.")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
@cli_utils_decorator
|
|
144
|
+
def delete(
|
|
145
|
+
worker_id: str = typer.Argument(..., help="ID of the worker to delete."),
|
|
146
|
+
cascade_update: bool = typer.Option(
|
|
147
|
+
True,
|
|
148
|
+
help="Whether to cascade set the worker id of relevant tasks to None",
|
|
149
|
+
),
|
|
150
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Confirm the operation."),
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Delete a worker by worker_id.
|
|
154
|
+
"""
|
|
155
|
+
if not yes:
|
|
156
|
+
typer.confirm(
|
|
157
|
+
f"Are you sure you want to delete worker '{worker_id}'?",
|
|
158
|
+
abort=True,
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
delete_worker(worker_id=worker_id, cascade_update=cascade_update)
|
|
162
|
+
stdout_console.print(f"Worker {worker_id} deleted.")
|
|
163
|
+
except LabtaskerHTTPStatusError as e:
|
|
164
|
+
if e.response.status_code == HTTP_404_NOT_FOUND:
|
|
165
|
+
raise typer.BadParameter("Worker not found")
|
|
166
|
+
else:
|
|
167
|
+
raise e
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from labtasker.client.core.api import *
|
|
2
|
+
from labtasker.client.core.context import current_task_id, current_worker_id, task_info
|
|
3
|
+
from labtasker.client.core.job_runner import finish, loop
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
# job runner api
|
|
7
|
+
"loop",
|
|
8
|
+
"finish",
|
|
9
|
+
# context api
|
|
10
|
+
"task_info",
|
|
11
|
+
"current_task_id",
|
|
12
|
+
"current_worker_id",
|
|
13
|
+
# http api (you should be careful with these unless you know what you are doing)
|
|
14
|
+
"close_httpx_client",
|
|
15
|
+
"health_check",
|
|
16
|
+
"submit_task",
|
|
17
|
+
"delete_worker",
|
|
18
|
+
"create_queue",
|
|
19
|
+
"create_worker",
|
|
20
|
+
"delete_queue",
|
|
21
|
+
"delete_task",
|
|
22
|
+
"delete_worker",
|
|
23
|
+
"fetch_task",
|
|
24
|
+
"get_queue",
|
|
25
|
+
"health_check",
|
|
26
|
+
"ls_tasks",
|
|
27
|
+
"ls_worker",
|
|
28
|
+
"refresh_task_heartbeat",
|
|
29
|
+
"report_task_status",
|
|
30
|
+
]
|
|
File without changes
|