qbraid-cli 0.8.0.dev0__py3-none-any.whl → 0.8.0.dev3__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.
Potentially problematic release.
This version of qbraid-cli might be problematic. Click here for more details.
- qbraid_cli/_version.py +2 -2
- qbraid_cli/configure/__init__.py +4 -1
- qbraid_cli/configure/actions.py +111 -0
- qbraid_cli/configure/app.py +11 -109
- qbraid_cli/credits/__init__.py +4 -1
- qbraid_cli/credits/app.py +12 -10
- qbraid_cli/devices/__init__.py +4 -1
- qbraid_cli/devices/app.py +43 -35
- qbraid_cli/devices/validation.py +27 -0
- qbraid_cli/envs/__init__.py +4 -1
- qbraid_cli/envs/activate.py +8 -5
- qbraid_cli/envs/app.py +162 -114
- qbraid_cli/envs/create.py +7 -14
- qbraid_cli/envs/data_handling.py +140 -0
- qbraid_cli/exceptions.py +3 -0
- qbraid_cli/handlers.py +4 -1
- qbraid_cli/jobs/__init__.py +4 -1
- qbraid_cli/jobs/app.py +80 -95
- qbraid_cli/jobs/toggle_braket.py +25 -23
- qbraid_cli/jobs/validation.py +74 -0
- qbraid_cli/kernels/__init__.py +4 -1
- qbraid_cli/kernels/app.py +93 -11
- qbraid_cli/main.py +15 -8
- {qbraid_cli-0.8.0.dev0.dist-info → qbraid_cli-0.8.0.dev3.dist-info}/METADATA +58 -42
- qbraid_cli-0.8.0.dev3.dist-info/RECORD +29 -0
- {qbraid_cli-0.8.0.dev0.dist-info → qbraid_cli-0.8.0.dev3.dist-info}/WHEEL +1 -1
- qbraid_cli-0.8.0.dev0.dist-info/RECORD +0 -25
- {qbraid_cli-0.8.0.dev0.dist-info → qbraid_cli-0.8.0.dev3.dist-info}/entry_points.txt +0 -0
- {qbraid_cli-0.8.0.dev0.dist-info → qbraid_cli-0.8.0.dev3.dist-info}/top_level.txt +0 -0
qbraid_cli/envs/app.py
CHANGED
|
@@ -1,85 +1,75 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
1
4
|
"""
|
|
2
5
|
Module defining commands in the 'qbraid envs' namespace.
|
|
3
6
|
|
|
4
7
|
"""
|
|
5
8
|
|
|
6
|
-
import json
|
|
7
9
|
import shutil
|
|
10
|
+
import subprocess
|
|
8
11
|
import sys
|
|
9
12
|
from pathlib import Path
|
|
10
|
-
from typing import
|
|
13
|
+
from typing import Any, Dict, Optional, Tuple
|
|
11
14
|
|
|
12
15
|
import typer
|
|
13
16
|
from rich.console import Console
|
|
14
17
|
|
|
18
|
+
from qbraid_cli.envs.data_handling import installed_envs_data, request_delete_env, validate_env_name
|
|
15
19
|
from qbraid_cli.handlers import QbraidException, run_progress_task
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def installed_envs_data() -> Tuple[Dict[str, Path], Dict[str, str]]:
|
|
21
|
-
"""Gather paths and aliases for all installed qBraid environments."""
|
|
22
|
-
from qbraid.api.system import get_qbraid_envs_paths, is_valid_slug
|
|
23
|
-
|
|
24
|
-
installed = {}
|
|
25
|
-
aliases = {}
|
|
26
|
-
|
|
27
|
-
qbraid_env_paths: List[Path] = get_qbraid_envs_paths()
|
|
28
|
-
|
|
29
|
-
for env_path in qbraid_env_paths:
|
|
30
|
-
for entry in env_path.iterdir():
|
|
31
|
-
if entry.is_dir() and is_valid_slug(entry.name):
|
|
32
|
-
installed[entry.name] = entry
|
|
33
|
-
|
|
34
|
-
if entry.name == "qbraid_000000":
|
|
35
|
-
aliases["default"] = entry.name
|
|
36
|
-
continue
|
|
21
|
+
envs_app = typer.Typer(help="Manage qBraid environments.")
|
|
37
22
|
|
|
38
|
-
state_json_path = entry / "state.json"
|
|
39
|
-
if state_json_path.exists():
|
|
40
|
-
try:
|
|
41
|
-
with open(state_json_path, "r", encoding="utf-8") as f:
|
|
42
|
-
data = json.load(f)
|
|
43
|
-
aliases[data.get("name", entry.name[:-7])] = entry.name
|
|
44
|
-
# pylint: disable-next=broad-exception-caught
|
|
45
|
-
except (json.JSONDecodeError, Exception):
|
|
46
|
-
aliases[entry.name[:-7]] = entry.name
|
|
47
|
-
else:
|
|
48
|
-
aliases[entry.name[:-7]] = entry.name
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
name: str = typer.Option(..., "--name", "-n", help="Name of the environment to create"),
|
|
24
|
+
@envs_app.command(name="create")
|
|
25
|
+
def envs_create( # pylint: disable=too-many-statements
|
|
26
|
+
name: str = typer.Option(
|
|
27
|
+
..., "--name", "-n", help="Name of the environment to create", callback=validate_env_name
|
|
28
|
+
),
|
|
56
29
|
description: Optional[str] = typer.Option(
|
|
57
30
|
None, "--description", "-d", help="Short description of the environment"
|
|
58
31
|
),
|
|
32
|
+
auto_confirm: bool = typer.Option(
|
|
33
|
+
False, "--yes", "-y", help="Automatically answer 'yes' to all prompts"
|
|
34
|
+
),
|
|
59
35
|
) -> None:
|
|
60
|
-
"""Create a new qBraid environment.
|
|
61
|
-
|
|
62
|
-
NOTE: Requires updated API route from https://github.com/qBraid/api/pull/482,
|
|
63
|
-
This command will not work until that PR is merged, and the updates are deployed.
|
|
64
|
-
"""
|
|
65
|
-
from .create import create_qbraid_env
|
|
36
|
+
"""Create a new qBraid environment."""
|
|
37
|
+
from .create import create_qbraid_env_assets, create_venv
|
|
66
38
|
|
|
67
|
-
def request_new_env(req_body: Dict[str, str]) -> str:
|
|
39
|
+
def request_new_env(req_body: Dict[str, str]) -> Dict[str, Any]:
|
|
68
40
|
"""Send request to create new environment and return the slug."""
|
|
69
|
-
from
|
|
41
|
+
from qbraid_core import QbraidSession, RequestsApiError
|
|
70
42
|
|
|
71
43
|
session = QbraidSession()
|
|
72
44
|
|
|
73
45
|
try:
|
|
74
|
-
|
|
46
|
+
env_data = session.post("/environments/create", json=req_body).json()
|
|
75
47
|
except RequestsApiError as err:
|
|
76
48
|
raise QbraidException("Create environment request failed") from err
|
|
77
49
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
50
|
+
if env_data is None or len(env_data) == 0 or env_data.get("slug") is None:
|
|
51
|
+
raise QbraidException(
|
|
52
|
+
"Create environment request responsed with invalid environment data"
|
|
53
|
+
)
|
|
81
54
|
|
|
82
|
-
return
|
|
55
|
+
return env_data
|
|
56
|
+
|
|
57
|
+
def gather_local_data() -> Tuple[Path, str]:
|
|
58
|
+
"""Gather environment data and return the slug."""
|
|
59
|
+
from qbraid_core.services.environments import get_default_envs_paths
|
|
60
|
+
|
|
61
|
+
env_path = get_default_envs_paths()[0]
|
|
62
|
+
|
|
63
|
+
result = subprocess.run(
|
|
64
|
+
[sys.executable, "--version"],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
check=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
python_version = result.stdout or result.stderr
|
|
71
|
+
|
|
72
|
+
return env_path, python_version
|
|
83
73
|
|
|
84
74
|
req_body = {
|
|
85
75
|
"name": name,
|
|
@@ -89,92 +79,148 @@ def envs_create(
|
|
|
89
79
|
"visibility": "private",
|
|
90
80
|
"kernelName": "",
|
|
91
81
|
"prompt": "",
|
|
82
|
+
"origin": "CLI",
|
|
92
83
|
}
|
|
93
|
-
|
|
84
|
+
|
|
85
|
+
environment = run_progress_task(
|
|
94
86
|
request_new_env,
|
|
95
87
|
req_body,
|
|
96
|
-
description="Validating...",
|
|
88
|
+
description="Validating request...",
|
|
97
89
|
error_message="Failed to create qBraid environment",
|
|
98
90
|
)
|
|
99
91
|
|
|
92
|
+
env_path, python_version = run_progress_task(
|
|
93
|
+
gather_local_data,
|
|
94
|
+
description="Solving environment...",
|
|
95
|
+
error_message="Failed to create qBraid environment",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
slug = environment.get("slug")
|
|
99
|
+
display_name = environment.get("displayName")
|
|
100
|
+
prompt = environment.get("prompt")
|
|
101
|
+
description = environment.get("description")
|
|
102
|
+
tags = environment.get("tags")
|
|
103
|
+
kernel_name = environment.get("kernelName")
|
|
104
|
+
|
|
105
|
+
slug_path = env_path / slug
|
|
106
|
+
description = "None" if description == "" else description
|
|
107
|
+
|
|
108
|
+
typer.echo("\n\n## qBraid Metadata ##\n")
|
|
109
|
+
typer.echo(f" name: {display_name}")
|
|
110
|
+
typer.echo(f" description: {description}")
|
|
111
|
+
typer.echo(f" tags: {tags}")
|
|
112
|
+
typer.echo(f" slug: {slug}")
|
|
113
|
+
typer.echo(f" shellPrompt: {prompt}")
|
|
114
|
+
typer.echo(f" kernelName: {kernel_name}")
|
|
115
|
+
|
|
116
|
+
typer.echo("\n\n## Environment Plan ##\n")
|
|
117
|
+
typer.echo(f" location: {slug_path}")
|
|
118
|
+
typer.echo(f" version: {python_version}\n")
|
|
119
|
+
|
|
120
|
+
user_confirmation = auto_confirm or typer.confirm("Proceed", default=True)
|
|
121
|
+
typer.echo("")
|
|
122
|
+
if not user_confirmation:
|
|
123
|
+
request_delete_env(slug)
|
|
124
|
+
typer.echo("qBraidSystemExit: Exiting.")
|
|
125
|
+
raise typer.Exit()
|
|
126
|
+
|
|
100
127
|
run_progress_task(
|
|
101
|
-
|
|
128
|
+
create_qbraid_env_assets,
|
|
102
129
|
slug,
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
prompt,
|
|
131
|
+
kernel_name,
|
|
132
|
+
slug_path,
|
|
133
|
+
description="Generating qBraid assets...",
|
|
134
|
+
error_message="Failed to create qBraid environment",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
run_progress_task(
|
|
138
|
+
create_venv,
|
|
139
|
+
slug_path,
|
|
140
|
+
prompt,
|
|
141
|
+
description="Creating virtual environment...",
|
|
105
142
|
error_message="Failed to create qBraid environment",
|
|
106
143
|
)
|
|
107
144
|
|
|
108
|
-
# TODO: Add the command they can use to activate the environment to end of success message
|
|
109
145
|
console = Console()
|
|
110
146
|
console.print(
|
|
111
147
|
f"\n[bold green]Successfully created qBraid environment: "
|
|
112
148
|
f"[/bold green][bold magenta]{name}[/bold magenta]\n"
|
|
113
149
|
)
|
|
150
|
+
typer.echo("# To activate this environment, use")
|
|
151
|
+
typer.echo("#")
|
|
152
|
+
typer.echo(f"# $ qbraid envs activate {name}")
|
|
153
|
+
typer.echo("#")
|
|
154
|
+
typer.echo("# To deactivate an active environment, use")
|
|
155
|
+
typer.echo("#")
|
|
156
|
+
typer.echo("# $ deactivate")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@envs_app.command(name="remove")
|
|
160
|
+
def envs_remove(
|
|
161
|
+
name: str = typer.Option(..., "-n", "--name", help="Name of the environment to remove"),
|
|
162
|
+
auto_confirm: bool = typer.Option(
|
|
163
|
+
False, "--yes", "-y", help="Automatically answer 'yes' to all prompts"
|
|
164
|
+
),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Delete a qBraid environment."""
|
|
114
167
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def envs_delete(name: str = typer.Argument(..., help="Name of the environment to delete")) -> None:
|
|
118
|
-
"""Delete a qBraid environment.
|
|
119
|
-
|
|
120
|
-
NOTE: Requires updated API route from https://github.com/qBraid/api/pull/482,
|
|
121
|
-
This command will not work until that PR is merged, and the updates are deployed.
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
def request_delete_env(name: str) -> str:
|
|
125
|
-
"""Send request to create new environment and return the slug."""
|
|
126
|
-
from qbraid.api import QbraidSession, RequestsApiError
|
|
127
|
-
|
|
128
|
-
session = QbraidSession()
|
|
129
|
-
|
|
168
|
+
def gather_local_data(env_name: str) -> Tuple[Path, str]:
|
|
169
|
+
"""Get environment path and slug from name (alias)."""
|
|
130
170
|
installed, aliases = installed_envs_data()
|
|
131
|
-
for alias,
|
|
132
|
-
if alias ==
|
|
133
|
-
|
|
134
|
-
path = installed[slug_name]
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
session.delete(f"/environments/{slug}")
|
|
138
|
-
except RequestsApiError as err:
|
|
139
|
-
raise QbraidException("Create environment request failed") from err
|
|
171
|
+
for alias, slug in aliases.items():
|
|
172
|
+
if alias == env_name:
|
|
173
|
+
path = installed[slug]
|
|
140
174
|
|
|
141
|
-
return path
|
|
175
|
+
return path, slug
|
|
142
176
|
|
|
143
177
|
raise QbraidException(f"Environment '{name}' not found.")
|
|
144
178
|
|
|
145
|
-
|
|
146
|
-
request_delete_env,
|
|
147
|
-
name,
|
|
148
|
-
description="Deleting remote environment data...",
|
|
149
|
-
error_message="Failed to delete qBraid environment",
|
|
150
|
-
)
|
|
179
|
+
slug_path, slug = gather_local_data(name)
|
|
151
180
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console = Console()
|
|
160
|
-
console.print(
|
|
161
|
-
f"\n[bold green]Successfully delete qBraid environment: "
|
|
162
|
-
f"[/bold green][bold magenta]{name}[/bold magenta]\n"
|
|
181
|
+
confirmation_message = (
|
|
182
|
+
f"⚠️ Warning: You are about to delete the environment '{name}' "
|
|
183
|
+
f"located at '{slug_path}'.\n"
|
|
184
|
+
"This will remove all local packages and permanently delete all "
|
|
185
|
+
"of its associated qBraid environment metadata.\n"
|
|
186
|
+
"This operation CANNOT be undone.\n\n"
|
|
187
|
+
"Are you sure you want to continue?"
|
|
163
188
|
)
|
|
164
189
|
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
if auto_confirm or typer.confirm(confirmation_message, abort=True):
|
|
191
|
+
typer.echo("")
|
|
192
|
+
run_progress_task(
|
|
193
|
+
request_delete_env,
|
|
194
|
+
slug,
|
|
195
|
+
description="Deleting remote environment data...",
|
|
196
|
+
error_message="Failed to delete qBraid environment",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
run_progress_task(
|
|
200
|
+
shutil.rmtree,
|
|
201
|
+
slug_path,
|
|
202
|
+
description="Deleting local environment...",
|
|
203
|
+
error_message="Failed to delete qBraid environment",
|
|
204
|
+
)
|
|
205
|
+
typer.echo(f"\nEnvironment '{name}' successfully removed.")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@envs_app.command(name="list")
|
|
167
209
|
def envs_list():
|
|
168
210
|
"""List installed qBraid environments."""
|
|
169
211
|
installed, aliases = installed_envs_data()
|
|
170
212
|
|
|
213
|
+
console = Console()
|
|
214
|
+
|
|
171
215
|
if len(installed) == 0:
|
|
172
|
-
print(
|
|
173
|
-
|
|
216
|
+
console.print(
|
|
217
|
+
"No qBraid environments installed.\n\n"
|
|
218
|
+
+ "Use 'qbraid envs create' to create a new environment.",
|
|
219
|
+
style="yellow",
|
|
220
|
+
)
|
|
174
221
|
return
|
|
175
222
|
|
|
176
223
|
alias_path_pairs = [(alias, installed[slug_name]) for alias, slug_name in aliases.items()]
|
|
177
|
-
|
|
178
224
|
sorted_alias_path_pairs = sorted(
|
|
179
225
|
alias_path_pairs,
|
|
180
226
|
key=lambda x: (x[0] != "default", str(x[1]).startswith(str(Path.home())), x[0]),
|
|
@@ -183,21 +229,23 @@ def envs_list():
|
|
|
183
229
|
current_env_path = Path(sys.executable).parent.parent.parent
|
|
184
230
|
|
|
185
231
|
max_alias_length = (
|
|
186
|
-
max(len(alias) for alias,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
max(len(str(path)) for _, path in sorted_alias_path_pairs) if sorted_alias_path_pairs else 0
|
|
232
|
+
max(len(str(alias)) for alias, envpath in sorted_alias_path_pairs)
|
|
233
|
+
if sorted_alias_path_pairs
|
|
234
|
+
else 0
|
|
190
235
|
)
|
|
191
236
|
|
|
192
|
-
print("#
|
|
237
|
+
print("# qbraid environments:")
|
|
193
238
|
print("#")
|
|
194
239
|
print("")
|
|
240
|
+
|
|
195
241
|
for alias, path in sorted_alias_path_pairs:
|
|
196
|
-
mark = "*
|
|
197
|
-
|
|
242
|
+
mark = "*" if path == current_env_path else " "
|
|
243
|
+
# Format each line with spacing based on the longest alias for alignment
|
|
244
|
+
line = f"{alias.ljust(max_alias_length+3)}{mark} {path}" # fix the most optimal spacing
|
|
245
|
+
console.print(line)
|
|
198
246
|
|
|
199
247
|
|
|
200
|
-
@
|
|
248
|
+
@envs_app.command(name="activate")
|
|
201
249
|
def envs_activate(
|
|
202
250
|
name: str = typer.Argument(..., help="Name of the environment. Values from 'qbraid envs list'.")
|
|
203
251
|
):
|
|
@@ -219,4 +267,4 @@ def envs_activate(
|
|
|
219
267
|
|
|
220
268
|
|
|
221
269
|
if __name__ == "__main__":
|
|
222
|
-
|
|
270
|
+
envs_app()
|
qbraid_cli/envs/create.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
1
4
|
"""
|
|
2
5
|
Module supporting 'qbraid envs create' command.
|
|
3
6
|
|
|
@@ -11,11 +14,6 @@ import sys
|
|
|
11
14
|
from typing import Optional
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def create_alias(slug: str) -> str:
|
|
15
|
-
"""Create an alias from a slug."""
|
|
16
|
-
return slug[:-7].replace("_", "-").strip("-")
|
|
17
|
-
|
|
18
|
-
|
|
19
17
|
def replace_str(target: str, replacement: str, file_path: str) -> None:
|
|
20
18
|
"""Replace all instances of string in file"""
|
|
21
19
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
@@ -56,7 +54,7 @@ def update_state_json(
|
|
|
56
54
|
data["install"]["message"] = message
|
|
57
55
|
|
|
58
56
|
if env_name is not None:
|
|
59
|
-
data["name"] = env_name
|
|
57
|
+
data["name"] = env_name
|
|
60
58
|
|
|
61
59
|
# Write updated data back to state.json
|
|
62
60
|
with open(state_json_path, "w", encoding="utf-8") as f:
|
|
@@ -90,22 +88,17 @@ def create_venv(slug_path: str, prompt: str) -> None:
|
|
|
90
88
|
update_state_json(slug_path, 1, 1)
|
|
91
89
|
|
|
92
90
|
|
|
93
|
-
def
|
|
91
|
+
def create_qbraid_env_assets(slug: str, alias: str, kernel_name: str, slug_path: str) -> None:
|
|
94
92
|
"""Create a qBraid environment including python venv, PS1 configs,
|
|
95
93
|
kernel resource files, and qBraid state.json."""
|
|
96
94
|
# pylint: disable-next=import-outside-toplevel
|
|
97
95
|
from jupyter_client.kernelspec import KernelSpecManager
|
|
98
|
-
from qbraid.api.system import get_qbraid_envs_paths
|
|
99
96
|
|
|
100
|
-
alias = create_alias(slug)
|
|
101
|
-
display_name = f"Python 3 [{alias}]"
|
|
102
|
-
envs_dir_path = get_qbraid_envs_paths()[0]
|
|
103
|
-
slug_path = os.path.join(str(envs_dir_path), slug)
|
|
104
97
|
local_resource_dir = os.path.join(slug_path, "kernels", f"python3_{slug}")
|
|
105
98
|
os.makedirs(local_resource_dir, exist_ok=True)
|
|
106
99
|
|
|
107
100
|
# create state.json
|
|
108
|
-
update_state_json(slug_path, 0, 0, env_name=
|
|
101
|
+
update_state_json(slug_path, 0, 0, env_name=alias)
|
|
109
102
|
|
|
110
103
|
# create kernel.json
|
|
111
104
|
kernel_json_path = os.path.join(local_resource_dir, "kernel.json")
|
|
@@ -117,7 +110,7 @@ def create_qbraid_env(slug: str, name: str) -> None:
|
|
|
117
110
|
else:
|
|
118
111
|
python_exec_path = os.path.join(slug_path, "pyenv", "bin", "python")
|
|
119
112
|
kernel_data["argv"][0] = python_exec_path
|
|
120
|
-
kernel_data["display_name"] =
|
|
113
|
+
kernel_data["display_name"] = kernel_name
|
|
121
114
|
with open(kernel_json_path, "w", encoding="utf-8") as file:
|
|
122
115
|
json.dump(kernel_data, file, indent=4)
|
|
123
116
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module for handling data related to qBraid environments.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import keyword
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Tuple
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from qbraid_cli.handlers import QbraidException
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_valid_env_name(env_name: str) -> bool: # pylint: disable=too-many-return-statements
|
|
21
|
+
"""
|
|
22
|
+
Validates a Python virtual environment name against best practices.
|
|
23
|
+
|
|
24
|
+
This function checks if the given environment name is valid based on certain
|
|
25
|
+
criteria, including length, use of special characters, reserved names, and
|
|
26
|
+
operating system-specific restrictions.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
env_name (str): The name of the Python virtual environment to validate.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if the name is valid, False otherwise.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If the environment name is not a string or is empty.
|
|
36
|
+
"""
|
|
37
|
+
# Basic checks for empty names or purely whitespace names
|
|
38
|
+
if not env_name or env_name.isspace():
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
# Check for invalid characters, including shell metacharacters and spaces
|
|
42
|
+
if re.search(r'[<>:"/\\|?*\s&;()$[\]#~!{}]', env_name):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if env_name.startswith("tmp"):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
# Reserved names for Windows (example list, can be expanded)
|
|
49
|
+
reserved_names = [
|
|
50
|
+
"CON",
|
|
51
|
+
"PRN",
|
|
52
|
+
"AUX",
|
|
53
|
+
"NUL",
|
|
54
|
+
"COM1",
|
|
55
|
+
"COM2",
|
|
56
|
+
"COM3",
|
|
57
|
+
"COM4",
|
|
58
|
+
"COM5",
|
|
59
|
+
"COM6",
|
|
60
|
+
"COM7",
|
|
61
|
+
"COM8",
|
|
62
|
+
"COM9",
|
|
63
|
+
"LPT1",
|
|
64
|
+
"LPT2",
|
|
65
|
+
"LPT3",
|
|
66
|
+
"LPT4",
|
|
67
|
+
"LPT5",
|
|
68
|
+
"LPT6",
|
|
69
|
+
"LPT7",
|
|
70
|
+
"LPT8",
|
|
71
|
+
"LPT9",
|
|
72
|
+
]
|
|
73
|
+
if env_name.upper() in reserved_names:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
if len(env_name) > 20:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Check against Python reserved words
|
|
80
|
+
if keyword.iskeyword(env_name):
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Check if it starts with a number, which is not a good practice
|
|
84
|
+
if env_name[0].isdigit():
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def validate_env_name(value: str) -> str:
|
|
91
|
+
"""Validate environment name."""
|
|
92
|
+
if not is_valid_env_name(value):
|
|
93
|
+
raise typer.BadParameter(
|
|
94
|
+
f"Invalid environment name '{value}'. " "Please use a valid Python environment name."
|
|
95
|
+
)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def installed_envs_data() -> Tuple[Dict[str, Path], Dict[str, str]]:
|
|
100
|
+
"""Gather paths and aliases for all installed qBraid environments."""
|
|
101
|
+
from qbraid_core.services.environments.paths import get_default_envs_paths, is_valid_slug
|
|
102
|
+
|
|
103
|
+
installed = {}
|
|
104
|
+
aliases = {}
|
|
105
|
+
|
|
106
|
+
qbraid_env_paths: List[Path] = get_default_envs_paths()
|
|
107
|
+
|
|
108
|
+
for env_path in qbraid_env_paths:
|
|
109
|
+
for entry in env_path.iterdir():
|
|
110
|
+
if entry.is_dir() and is_valid_slug(entry.name):
|
|
111
|
+
installed[entry.name] = entry
|
|
112
|
+
|
|
113
|
+
if entry.name == "qbraid_000000":
|
|
114
|
+
aliases["default"] = entry.name
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
state_json_path = entry / "state.json"
|
|
118
|
+
if state_json_path.exists():
|
|
119
|
+
try:
|
|
120
|
+
with open(state_json_path, "r", encoding="utf-8") as f:
|
|
121
|
+
data = json.load(f)
|
|
122
|
+
aliases[data.get("name", entry.name[:-7])] = entry.name
|
|
123
|
+
# pylint: disable-next=broad-exception-caught
|
|
124
|
+
except (json.JSONDecodeError, Exception):
|
|
125
|
+
aliases[entry.name[:-7]] = entry.name
|
|
126
|
+
else:
|
|
127
|
+
aliases[entry.name[:-7]] = entry.name
|
|
128
|
+
return installed, aliases
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def request_delete_env(slug: str) -> str:
|
|
132
|
+
"""Send request to delete environment given slug."""
|
|
133
|
+
from qbraid_core import QbraidSession, RequestsApiError
|
|
134
|
+
|
|
135
|
+
session = QbraidSession()
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
session.delete(f"/environments/{slug}")
|
|
139
|
+
except RequestsApiError as err:
|
|
140
|
+
raise QbraidException("Delete environment request failed") from err
|
qbraid_cli/exceptions.py
CHANGED
qbraid_cli/handlers.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
1
4
|
"""
|
|
2
5
|
Module providing application support utilities, including abstractions for error handling
|
|
3
6
|
and executing operations with progress tracking within the qBraid CLI.
|
|
@@ -38,7 +41,7 @@ def handle_error(
|
|
|
38
41
|
error_type = error_type or "Error"
|
|
39
42
|
message = message or DEFAULT_ERROR_MESSAGE
|
|
40
43
|
error_prefix = typer.style(f"{error_type}:", fg=typer.colors.RED, bold=True)
|
|
41
|
-
full_message = f"{error_prefix} {message}\n"
|
|
44
|
+
full_message = f"\n{error_prefix} {message}\n"
|
|
42
45
|
if include_traceback:
|
|
43
46
|
tb_string = traceback.format_exc()
|
|
44
47
|
full_message += f"\n{tb_string}"
|