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/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 Dict, List, Optional, Tuple
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
- app = typer.Typer(help="Manage qBraid environments.")
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
- return installed, aliases
51
-
52
-
53
- @app.command(name="create")
54
- def envs_create(
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 qbraid.api import QbraidSession, RequestsApiError
41
+ from qbraid_core import QbraidSession, RequestsApiError
70
42
 
71
43
  session = QbraidSession()
72
44
 
73
45
  try:
74
- resp = session.post("/environments/create", json=req_body).json()
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
- slug = resp.get("slug")
79
- if slug is None:
80
- raise QbraidException(f"Create environment request returned invalid slug, {slug}")
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 slug
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
- slug = run_progress_task(
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
- create_qbraid_env,
128
+ create_qbraid_env_assets,
102
129
  slug,
103
- name,
104
- description="Creating qBraid environment...",
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
- @app.command(name="delete")
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, slug_name in aliases.items():
132
- if alias == name:
133
- slug = slug_name
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
- path = run_progress_task(
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
- run_progress_task(
153
- shutil.rmtree,
154
- path,
155
- description="Deleting local environment...",
156
- error_message="Failed to delete qBraid environment",
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
- @app.command(name="list")
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("No qBraid environments installed.")
173
- print("\nUse 'qbraid envs create' to create a new environment.")
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, _ in sorted_alias_path_pairs) if sorted_alias_path_pairs else 0
187
- )
188
- max_path_length = (
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("# installed environments:")
237
+ print("# qbraid environments:")
193
238
  print("#")
194
239
  print("")
240
+
195
241
  for alias, path in sorted_alias_path_pairs:
196
- mark = "* " if path == current_env_path else " "
197
- print(f"{alias.ljust(max_alias_length + 11)}{mark}{str(path).ljust(max_path_length)}")
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
- @app.command(name="activate")
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
- app()
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.lower()
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 create_qbraid_env(slug: str, name: str) -> None:
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=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"] = 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
@@ -1,3 +1,6 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
1
4
  """
2
5
  Module defining custom exceptions for the qBraid CLI.
3
6
 
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}"
@@ -1,6 +1,9 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
1
4
  """
2
5
  Module defining the qbraid jobs namespace
3
6
 
4
7
  """
5
8
 
6
- from .app import app
9
+ from .app import jobs_app