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.
Files changed (52) hide show
  1. labtasker/__init__.py +7 -0
  2. labtasker/__main__.py +6 -0
  3. labtasker/api_models.py +205 -0
  4. labtasker/client/__init__.py +0 -0
  5. labtasker/client/cli/__init__.py +26 -0
  6. labtasker/client/cli/cli.py +43 -0
  7. labtasker/client/cli/config.py +80 -0
  8. labtasker/client/cli/loop.py +131 -0
  9. labtasker/client/cli/queue.py +148 -0
  10. labtasker/client/cli/task.py +435 -0
  11. labtasker/client/cli/worker.py +167 -0
  12. labtasker/client/client_api.py +30 -0
  13. labtasker/client/core/__init__.py +0 -0
  14. labtasker/client/core/api.py +400 -0
  15. labtasker/client/core/cli_utils.py +238 -0
  16. labtasker/client/core/cmd_parser/LabCmd.g4 +14 -0
  17. labtasker/client/core/cmd_parser/LabCmd.py +620 -0
  18. labtasker/client/core/cmd_parser/LabCmdLexer.g4 +15 -0
  19. labtasker/client/core/cmd_parser/LabCmdLexer.py +493 -0
  20. labtasker/client/core/cmd_parser/LabCmdListener.py +54 -0
  21. labtasker/client/core/cmd_parser/__init__.py +5 -0
  22. labtasker/client/core/cmd_parser/parser.py +294 -0
  23. labtasker/client/core/config.py +156 -0
  24. labtasker/client/core/context.py +54 -0
  25. labtasker/client/core/exceptions.py +55 -0
  26. labtasker/client/core/heartbeat.py +103 -0
  27. labtasker/client/core/job_runner.py +227 -0
  28. labtasker/client/core/logging.py +91 -0
  29. labtasker/client/core/paths.py +64 -0
  30. labtasker/client/core/plugin_utils.py +72 -0
  31. labtasker/client/templates/labtasker_root/.gitignore +4 -0
  32. labtasker/client/templates/labtasker_root/client.toml +14 -0
  33. labtasker/client/templates/labtasker_root/logs/.gitkeep +1 -0
  34. labtasker/concurrent.py +11 -0
  35. labtasker/constants.py +7 -0
  36. labtasker/filtering.py +82 -0
  37. labtasker/security.py +26 -0
  38. labtasker/server/__init__.py +0 -0
  39. labtasker/server/config.py +59 -0
  40. labtasker/server/database.py +1018 -0
  41. labtasker/server/db_utils.py +91 -0
  42. labtasker/server/dependencies.py +39 -0
  43. labtasker/server/endpoints.py +458 -0
  44. labtasker/server/fsm.py +258 -0
  45. labtasker/server/logging.py +44 -0
  46. labtasker/server/run.py +21 -0
  47. labtasker/utils.py +353 -0
  48. labtasker-0.1.0.dist-info/METADATA +89 -0
  49. labtasker-0.1.0.dist-info/RECORD +52 -0
  50. labtasker-0.1.0.dist-info/WHEEL +5 -0
  51. labtasker-0.1.0.dist-info/entry_points.txt +2 -0
  52. 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