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
labtasker/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from labtasker.client.client_api import *
2
+ from labtasker.client.core.exceptions import *
3
+ from labtasker.filtering import install_traceback_filter
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ install_traceback_filter()
labtasker/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entrypoint for the cli app"""
2
+
3
+ from labtasker.client.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,205 @@
1
+ from datetime import datetime
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr
5
+
6
+ from labtasker.constants import Priority
7
+
8
+
9
+ class BaseApiModel(BaseModel):
10
+ """
11
+ Base API model for all API models.
12
+ """
13
+
14
+ model_config = ConfigDict(populate_by_name=True)
15
+
16
+
17
+ class HealthCheckResponse(BaseApiModel):
18
+ status: str = Field(..., pattern=r"^(healthy|unhealthy)$")
19
+ database: str
20
+
21
+
22
+ class QueueCreateRequest(BaseApiModel):
23
+ queue_name: str = Field(
24
+ ..., pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
25
+ )
26
+ password: SecretStr = Field(..., min_length=1, max_length=100)
27
+ metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
28
+
29
+ def to_request_dict(self):
30
+ """
31
+ Used to form a quest, since password must be revealed
32
+ """
33
+ result = self.model_dump()
34
+ result.update({"password": self.password.get_secret_value()})
35
+ return result
36
+
37
+
38
+ class QueueCreateResponse(BaseApiModel):
39
+ queue_id: str
40
+
41
+
42
+ class QueueGetResponse(BaseApiModel):
43
+ queue_id: str = Field(alias="_id")
44
+ queue_name: str
45
+ created_at: datetime
46
+ last_modified: datetime
47
+ metadata: Dict[str, Any]
48
+
49
+
50
+ class TaskSubmitRequest(BaseApiModel):
51
+ """Task submission request."""
52
+
53
+ task_name: Optional[str] = Field(
54
+ None, pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
55
+ )
56
+ args: Optional[Dict[str, Any]] = Field(default_factory=dict)
57
+ metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
58
+ cmd: Optional[Union[str, List[str]]] = None
59
+ heartbeat_timeout: Optional[float] = None
60
+ task_timeout: Optional[int] = None
61
+ max_retries: int = 3
62
+ priority: int = Priority.MEDIUM
63
+
64
+
65
+ class TaskFetchRequest(BaseApiModel):
66
+ worker_id: Optional[str] = None
67
+ eta_max: Optional[str] = None
68
+ heartbeat_timeout: Optional[float] = None
69
+ start_heartbeat: bool = True
70
+ required_fields: Optional[Dict[str, Any]] = None
71
+ extra_filter: Optional[Dict[str, Any]] = None
72
+
73
+
74
+ class Task(BaseApiModel):
75
+ task_id: str = Field(alias="_id") # Accepts "_id" as an input field
76
+ queue_id: str
77
+ status: str
78
+ task_name: Optional[str]
79
+ created_at: datetime
80
+ start_time: Optional[datetime]
81
+ last_heartbeat: Optional[datetime]
82
+ last_modified: datetime
83
+ heartbeat_timeout: Optional[float]
84
+ task_timeout: Optional[int]
85
+ max_retries: int
86
+ retries: int
87
+ priority: int
88
+ metadata: Dict
89
+ args: Dict
90
+ cmd: Union[str, List[str]]
91
+ summary: Dict
92
+ worker_id: Optional[str]
93
+
94
+
95
+ class TaskUpdateRequest(BaseApiModel):
96
+ """This should be consistent with Task.
97
+ Fields that disallow manual update are commented out.
98
+ """
99
+
100
+ task_id: str = Field(alias="_id") # Accepts "_id" as an input field
101
+ # queue_id: str
102
+ status: Optional[str] = None
103
+ task_name: Optional[str] = Field(
104
+ None, pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
105
+ )
106
+ # created_at: datetime
107
+ # start_time: Optional[datetime]
108
+ # last_heartbeat: Optional[datetime]
109
+ # last_modified: datetime
110
+ heartbeat_timeout: Optional[float] = None
111
+ task_timeout: Optional[int] = None
112
+ max_retries: Optional[int] = None
113
+ retries: Optional[int] = None
114
+ priority: Optional[int] = None
115
+ metadata: Optional[Dict] = None
116
+ args: Optional[Dict] = None
117
+ cmd: Optional[Union[str, List[str]]] = None
118
+ summary: Optional[Dict] = None
119
+ # worker_id: Optional[str]
120
+
121
+
122
+ class TaskFetchResponse(BaseApiModel):
123
+ found: bool = False
124
+ task: Optional[Task] = None
125
+
126
+
127
+ class TaskLsRequest(BaseApiModel):
128
+ offset: int = Field(0, ge=0)
129
+ limit: int = Field(100, ge=0, le=1000)
130
+ task_id: Optional[str] = None
131
+ task_name: Optional[str] = None
132
+ extra_filter: Optional[Dict[str, Any]] = None
133
+
134
+
135
+ class TaskLsResponse(BaseApiModel):
136
+ found: bool = False
137
+ content: List[Task] = Field(default_factory=list)
138
+
139
+
140
+ class TaskSubmitResponse(BaseApiModel):
141
+ task_id: str
142
+
143
+
144
+ class TaskStatusUpdateRequest(BaseApiModel):
145
+ status: str = Field(..., pattern=r"^(success|failed|cancelled)$")
146
+ worker_id: Optional[str] = None
147
+ summary: Optional[Dict[str, Any]] = Field(default_factory=dict)
148
+
149
+
150
+ class WorkerCreateRequest(BaseApiModel):
151
+ worker_name: Optional[str] = None
152
+ metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
153
+ max_retries: Optional[int] = 3
154
+
155
+
156
+ class WorkerCreateResponse(BaseApiModel):
157
+ worker_id: str
158
+
159
+
160
+ class WorkerStatusUpdateRequest(BaseApiModel):
161
+ status: str = Field(..., pattern=r"^(active|suspended|failed)$")
162
+
163
+
164
+ class WorkerLsRequest(BaseApiModel):
165
+ offset: int = Field(0, ge=0)
166
+ limit: int = Field(100, ge=0, le=1000)
167
+ worker_id: Optional[str] = None
168
+ worker_name: Optional[str] = None
169
+ extra_filter: Optional[Dict[str, Any]] = None
170
+
171
+
172
+ class Worker(BaseApiModel):
173
+ worker_id: str = Field(alias="_id")
174
+ queue_id: str
175
+ status: str
176
+ worker_name: Optional[str] = Field(
177
+ None, pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
178
+ )
179
+ metadata: Dict
180
+ retries: int
181
+ max_retries: int
182
+ created_at: datetime
183
+ last_modified: datetime
184
+
185
+
186
+ class WorkerLsResponse(BaseApiModel):
187
+ found: bool = False
188
+ content: List[Worker] = Field(default_factory=list)
189
+
190
+
191
+ class QueueUpdateRequest(BaseApiModel):
192
+ new_queue_name: Optional[str] = Field(
193
+ None, pattern=r"^[a-zA-Z0-9_-]+$", min_length=1, max_length=100
194
+ )
195
+ new_password: Optional[SecretStr] = Field(None, min_length=1, max_length=100)
196
+ metadata_update: Optional[Dict[str, Any]] = Field(default_factory=dict)
197
+
198
+ def to_request_dict(self):
199
+ """
200
+ Used to form a quest, since password must be revealed
201
+ """
202
+ result = self.model_dump()
203
+ if self.new_password:
204
+ result.update({"new_password": self.new_password.get_secret_value()})
205
+ return result
File without changes
@@ -0,0 +1,26 @@
1
+ # 1 level commands
2
+ import labtasker.client.cli.config
3
+ import labtasker.client.cli.loop
4
+
5
+ # multi level commands
6
+ import labtasker.client.cli.queue as queue
7
+ import labtasker.client.cli.task as task
8
+ import labtasker.client.cli.worker as worker
9
+ from labtasker.client.cli.cli import app
10
+ from labtasker.client.core.config import get_client_config
11
+ from labtasker.client.core.logging import stderr_console
12
+ from labtasker.client.core.paths import get_labtasker_client_config_path
13
+ from labtasker.client.core.plugin_utils import load_plugins
14
+
15
+ app.add_typer(queue.app, name="queue", help=queue.__doc__)
16
+ app.add_typer(task.app, name="task", help=task.__doc__)
17
+ app.add_typer(worker.app, name="worker", help=task.__doc__)
18
+
19
+ if get_labtasker_client_config_path().exists():
20
+ load_plugins(group="labtasker.client.cli", config=get_client_config().cli_plugins)
21
+ else:
22
+ stderr_console.print(
23
+ f"[bold orange1]Warning:[/bold orange1] config file not found at {get_labtasker_client_config_path()}. "
24
+ f"[orange1]Skipped plugin loading.[/orange1] "
25
+ f"It is recommended to initialize config via running [orange1]`labtasker config`[/orange1] first."
26
+ )
@@ -0,0 +1,43 @@
1
+ """
2
+ Implements top level cli (mainly callbacks and setup)
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import httpx
8
+ import typer
9
+ from typing_extensions import Annotated
10
+
11
+ from labtasker import __version__
12
+ from labtasker.client.core.api import health_check
13
+ from labtasker.client.core.config import requires_client_config
14
+ from labtasker.client.core.logging import stderr_console, stdout_console
15
+
16
+ app = typer.Typer(pretty_exceptions_show_locals=False)
17
+
18
+
19
+ def version_callback(value: bool):
20
+ if value:
21
+ stdout_console.print(f"Labtasker Version: {__version__}")
22
+ raise typer.Exit()
23
+
24
+
25
+ @app.callback()
26
+ def callback(
27
+ version: Annotated[
28
+ Optional[bool],
29
+ typer.Option(
30
+ ..., "--version", callback=version_callback, help="Print Labtasker version."
31
+ ),
32
+ ] = None,
33
+ ): ...
34
+
35
+
36
+ @app.command()
37
+ @requires_client_config
38
+ def health():
39
+ """Check server connection and server health."""
40
+ try:
41
+ stdout_console.print(health_check())
42
+ except (httpx.ConnectError, httpx.HTTPStatusError) as e:
43
+ stderr_console.print(e)
@@ -0,0 +1,80 @@
1
+ """
2
+ Implements `labtasker config`
3
+ """
4
+
5
+ import tempfile
6
+ from typing import IO, Optional
7
+
8
+ import click
9
+ import pydantic
10
+ import tomlkit
11
+ import tomlkit.exceptions
12
+ import typer
13
+
14
+ from labtasker.client.cli.cli import app
15
+ from labtasker.client.core.config import ClientConfig, init_labtasker_root
16
+ from labtasker.client.core.logging import stderr_console, stdout_console
17
+ from labtasker.client.core.paths import (
18
+ get_labtasker_client_config_path,
19
+ get_labtasker_root,
20
+ )
21
+
22
+
23
+ @app.command()
24
+ def config(
25
+ editor: Optional[str] = typer.Option(
26
+ None,
27
+ help="Editor to use.",
28
+ ),
29
+ ):
30
+ """Configure local client. Run `labtasker config` which opens the configuration file using system configured editor"""
31
+ # 0. Check if labtasker root exists, if not, init
32
+ if not get_labtasker_root().exists():
33
+ typer.confirm(
34
+ "Labtasker root directory not found. Initializing with default template?",
35
+ abort=True,
36
+ )
37
+ init_labtasker_root()
38
+
39
+ # 1. Open editor and edit configuration in a temp file
40
+ with tempfile.NamedTemporaryFile(
41
+ "w+b",
42
+ prefix="labtasker.tmp.",
43
+ suffix=".toml",
44
+ ) as f: # type: IO[bytes]
45
+
46
+ # 1.1 Copy existing config if exists
47
+ if get_labtasker_client_config_path().exists():
48
+ with open(get_labtasker_client_config_path(), "rb") as f_existing:
49
+ f.write(f_existing.read())
50
+
51
+ # 1.2 Edit
52
+ while True:
53
+ try:
54
+ # a. Edit
55
+ f.seek(0)
56
+ click.edit(filename=f.name, editor=editor)
57
+
58
+ # b. Reload and validate
59
+ f.seek(0)
60
+ ClientConfig.model_validate(tomlkit.load(f))
61
+
62
+ f.seek(0)
63
+ updated_content = f.read()
64
+
65
+ break
66
+ except (
67
+ tomlkit.exceptions.ParseError,
68
+ pydantic.ValidationError,
69
+ ) as e:
70
+ stderr_console.print(
71
+ "[bold red]Error:[/bold red] error when parsing config.\n"
72
+ f"Detail: {str(e)}"
73
+ )
74
+ typer.confirm("Continue to edit?", abort=True)
75
+
76
+ # 2. Save
77
+ with open(get_labtasker_client_config_path(), "wb") as f_existing:
78
+ f_existing.write(updated_content)
79
+
80
+ stdout_console.print("[bold green]Configuration updated successfully.[/bold green]")
@@ -0,0 +1,131 @@
1
+ """
2
+ Implements `labtasker loop xxx`
3
+ """
4
+
5
+ import shlex
6
+ import subprocess
7
+ from collections import defaultdict
8
+ from typing import Optional
9
+
10
+ import typer
11
+
12
+ import labtasker
13
+ import labtasker.client.core.context
14
+ from labtasker.client.cli.cli import app
15
+ from labtasker.client.core.cli_utils import (
16
+ cli_utils_decorator,
17
+ eta_max_validation,
18
+ parse_metadata,
19
+ )
20
+ from labtasker.client.core.cmd_parser import cmd_interpolate
21
+ from labtasker.client.core.config import get_client_config
22
+ from labtasker.client.core.exceptions import CmdParserError
23
+ from labtasker.client.core.job_runner import finish
24
+ from labtasker.client.core.job_runner import loop as loop_run
25
+ from labtasker.client.core.logging import logger, stderr_console, stdout_console
26
+ from labtasker.utils import keys_to_query_dict
27
+
28
+
29
+ class InfiniteDefaultDict(defaultdict):
30
+
31
+ def __getitem__(self, key):
32
+ if key not in self:
33
+ self[key] = InfiniteDefaultDict()
34
+ return super().__getitem__(key)
35
+
36
+ def get(self, key, default=None):
37
+ if key not in self:
38
+ self[key] = InfiniteDefaultDict()
39
+ return super().get(key, default)
40
+
41
+
42
+ @app.command()
43
+ @cli_utils_decorator
44
+ def loop(
45
+ cmd: str = typer.Option(
46
+ ...,
47
+ "--cmd",
48
+ "-c",
49
+ help="Command to run. Support argument auto interpolation, formatted like %(arg1).",
50
+ ),
51
+ extra_filter: Optional[str] = typer.Option(
52
+ None,
53
+ help='Optional mongodb filter as a dict string (e.g., \'{"key": "value"}\').',
54
+ ),
55
+ worker_id: Optional[str] = typer.Option(
56
+ None,
57
+ help="Worker ID to run the command under.",
58
+ ),
59
+ eta_max: Optional[str] = typer.Option(
60
+ None,
61
+ callback=eta_max_validation,
62
+ help="Maximum ETA for the task. (e.g. '1h', '1h30m', '50s')",
63
+ ),
64
+ heartbeat_timeout: Optional[float] = typer.Option(
65
+ None,
66
+ help="Heartbeat timeout for the task in seconds.",
67
+ ),
68
+ ):
69
+ """Run the wrapped job command in loop.
70
+ Job command follows a template string syntax: e.g. `python main.py --arg1 %(arg1) --arg2 %(arg2)`.
71
+ The argument inside %(...) will be autofilled by the task args fetched from task queue.
72
+ """
73
+ extra_filter = parse_metadata(extra_filter)
74
+
75
+ if heartbeat_timeout is None:
76
+ heartbeat_timeout = get_client_config().task.heartbeat_interval * 3
77
+
78
+ # Generate required fields dict
79
+ dummy_variable_table = InfiniteDefaultDict()
80
+ try:
81
+ _, queried_keys = cmd_interpolate(cmd, dummy_variable_table)
82
+ except (CmdParserError, KeyError, TypeError) as e:
83
+ raise typer.BadParameter(f"Command error with exception {e}")
84
+
85
+ required_fields = keys_to_query_dict(list(queried_keys))
86
+
87
+ logger.info(f"Got command: {cmd}")
88
+
89
+ @loop_run(
90
+ required_fields=required_fields,
91
+ extra_filter=extra_filter,
92
+ worker_id=worker_id,
93
+ eta_max=eta_max,
94
+ heartbeat_timeout=heartbeat_timeout,
95
+ pass_args_dict=True,
96
+ )
97
+ def run_cmd(args):
98
+ # Interpolate command
99
+ interpolated_cmd, _ = cmd_interpolate(cmd, args)
100
+ logger.info(f"Prepared to run interpolated command: {interpolated_cmd}")
101
+
102
+ with subprocess.Popen(
103
+ shlex.split(interpolated_cmd),
104
+ stdout=subprocess.PIPE,
105
+ stderr=subprocess.PIPE,
106
+ text=True,
107
+ ) as process:
108
+ while True:
109
+ output = process.stdout.readline()
110
+ error = process.stderr.readline()
111
+
112
+ if output:
113
+ stdout_console.print(output.strip())
114
+ if error:
115
+ stderr_console.print(error.strip())
116
+
117
+ # Break loop when process completes and streams are empty
118
+ if process.poll() is not None and not output and not error:
119
+ break
120
+
121
+ process.wait()
122
+ if process.returncode != 0:
123
+ finish("failed")
124
+ else:
125
+ finish("success")
126
+
127
+ logger.info(f"Task {labtasker.client.core.context.task_info().task_id} ended.")
128
+
129
+ run_cmd()
130
+
131
+ logger.info("Loop finished.")
@@ -0,0 +1,148 @@
1
+ """
2
+ Task queue related CRUD operations.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from typing_extensions import Annotated
9
+
10
+ from labtasker.client.core.api import (
11
+ create_queue,
12
+ delete_queue,
13
+ get_queue,
14
+ update_queue,
15
+ )
16
+ from labtasker.client.core.cli_utils import cli_utils_decorator, parse_metadata
17
+ from labtasker.client.core.config import get_client_config
18
+ from labtasker.client.core.logging import stdout_console
19
+
20
+ app = typer.Typer()
21
+
22
+
23
+ @app.command()
24
+ @cli_utils_decorator
25
+ def create(
26
+ queue_name: Annotated[
27
+ str,
28
+ typer.Option(
29
+ prompt=True,
30
+ envvar="QUEUE_NAME",
31
+ help="Queue name for current experiment.",
32
+ ),
33
+ ],
34
+ password: Annotated[
35
+ str,
36
+ typer.Option(
37
+ prompt=True,
38
+ confirmation_prompt=True,
39
+ hide_input=True,
40
+ envvar="PASSWORD",
41
+ help="Password for current queue.",
42
+ ),
43
+ ],
44
+ metadata: Optional[str] = typer.Option(
45
+ None,
46
+ help='Optional metadata as a python dict string (e.g., \'{"key": "value"}\').',
47
+ ),
48
+ ):
49
+ """
50
+ Create a queue.
51
+ """
52
+ metadata = parse_metadata(metadata)
53
+ stdout_console.print(
54
+ create_queue(
55
+ queue_name=queue_name,
56
+ password=password,
57
+ metadata=metadata,
58
+ )
59
+ )
60
+
61
+
62
+ @app.command()
63
+ @cli_utils_decorator
64
+ def create_from_config(
65
+ metadata: Optional[str] = typer.Option(
66
+ None,
67
+ help='Optional metadata as a python dict string (e.g., \'{"key": "value"}\').',
68
+ )
69
+ ):
70
+ """
71
+ Create a queue from config in `.labtasker/client.toml`.
72
+ """
73
+ metadata = parse_metadata(metadata)
74
+ config = get_client_config()
75
+ stdout_console.print(
76
+ create_queue(
77
+ queue_name=config.queue.queue_name,
78
+ password=config.queue.password.get_secret_value(),
79
+ metadata=metadata,
80
+ )
81
+ )
82
+
83
+
84
+ @app.command()
85
+ @cli_utils_decorator
86
+ def get():
87
+ """Get current queue info."""
88
+ stdout_console.print(get_queue())
89
+
90
+
91
+ @app.command()
92
+ @cli_utils_decorator
93
+ def update(
94
+ new_queue_name: Optional[str] = typer.Option(
95
+ None,
96
+ help="New name for the queue.",
97
+ ),
98
+ new_password: Optional[str] = typer.Option(
99
+ None,
100
+ prompt=True,
101
+ confirmation_prompt=True,
102
+ hide_input=True,
103
+ prompt_required=False, # only trigger interactive prompt when `--new-password` is provided
104
+ help="New password for the queue.",
105
+ ),
106
+ metadata: Optional[str] = typer.Option(
107
+ None,
108
+ help='Optional metadata update as a python dict string (e.g., \'{"key": "value"}\').',
109
+ ),
110
+ ):
111
+ """
112
+ Update the current queue.
113
+ If you do not wish to expose password in command (e.g. `labtasker queue update --new-password my-pass --new-queue-name my-name`),
114
+ omit the content of `--new-password` and an interactive prompt will show up (i.e. labtasker queue update --new-password --new-queue-name my-name).
115
+ """
116
+ # Parse metadata
117
+ parsed_metadata = parse_metadata(metadata)
118
+
119
+ # Proceed with the update logic
120
+ stdout_console.print(
121
+ f"Updating queue with:\n"
122
+ f" New Queue Name: {new_queue_name or 'No change'}\n"
123
+ f" New Password: {'******' if new_password else 'No change'}\n"
124
+ f" Metadata: {parsed_metadata or 'No change'}"
125
+ )
126
+
127
+ updated_queue = update_queue(
128
+ new_queue_name=new_queue_name,
129
+ new_password=new_password,
130
+ metadata_update=parsed_metadata,
131
+ )
132
+ stdout_console.print(updated_queue)
133
+
134
+
135
+ @app.command()
136
+ @cli_utils_decorator
137
+ def delete(
138
+ cascade: bool = typer.Option(False, help="Delete all tasks in the queue."),
139
+ yes: bool = typer.Option(False, "--yes", "-y", help="Confirm the operation."),
140
+ ):
141
+ """Delete current queue."""
142
+ if not yes:
143
+ typer.confirm(
144
+ f"Are you sure you want to delete current queue '{get_queue().queue_name}' with cascade={cascade}?",
145
+ abort=True,
146
+ )
147
+ delete_queue(cascade_delete=cascade)
148
+ stdout_console.print("Queue deleted.")