qbraid-cli 0.8.0.dev1__py3-none-any.whl → 0.9.5__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.

Files changed (45) hide show
  1. qbraid_cli/_version.py +2 -2
  2. qbraid_cli/account/__init__.py +11 -0
  3. qbraid_cli/account/app.py +65 -0
  4. qbraid_cli/admin/__init__.py +11 -0
  5. qbraid_cli/admin/app.py +59 -0
  6. qbraid_cli/admin/headers.py +235 -0
  7. qbraid_cli/admin/validation.py +32 -0
  8. qbraid_cli/chat/__init__.py +11 -0
  9. qbraid_cli/chat/app.py +76 -0
  10. qbraid_cli/configure/__init__.py +6 -1
  11. qbraid_cli/configure/actions.py +111 -0
  12. qbraid_cli/configure/app.py +34 -108
  13. qbraid_cli/devices/__init__.py +6 -1
  14. qbraid_cli/devices/app.py +43 -35
  15. qbraid_cli/devices/validation.py +26 -0
  16. qbraid_cli/envs/__init__.py +6 -1
  17. qbraid_cli/envs/activate.py +8 -5
  18. qbraid_cli/envs/app.py +116 -197
  19. qbraid_cli/envs/create.py +13 -109
  20. qbraid_cli/envs/data_handling.py +46 -0
  21. qbraid_cli/exceptions.py +3 -0
  22. qbraid_cli/files/__init__.py +11 -0
  23. qbraid_cli/files/app.py +118 -0
  24. qbraid_cli/handlers.py +46 -12
  25. qbraid_cli/jobs/__init__.py +6 -1
  26. qbraid_cli/jobs/app.py +76 -93
  27. qbraid_cli/jobs/toggle_braket.py +68 -74
  28. qbraid_cli/jobs/validation.py +94 -0
  29. qbraid_cli/kernels/__init__.py +6 -1
  30. qbraid_cli/kernels/app.py +74 -17
  31. qbraid_cli/main.py +66 -14
  32. qbraid_cli/pip/__init__.py +11 -0
  33. qbraid_cli/pip/app.py +50 -0
  34. qbraid_cli/pip/hooks.py +74 -0
  35. qbraid_cli/py.typed +0 -0
  36. qbraid_cli-0.9.5.dist-info/LICENSE +41 -0
  37. qbraid_cli-0.9.5.dist-info/METADATA +179 -0
  38. qbraid_cli-0.9.5.dist-info/RECORD +42 -0
  39. {qbraid_cli-0.8.0.dev1.dist-info → qbraid_cli-0.9.5.dist-info}/WHEEL +1 -1
  40. qbraid_cli/credits/__init__.py +0 -6
  41. qbraid_cli/credits/app.py +0 -30
  42. qbraid_cli-0.8.0.dev1.dist-info/METADATA +0 -136
  43. qbraid_cli-0.8.0.dev1.dist-info/RECORD +0 -25
  44. {qbraid_cli-0.8.0.dev1.dist-info → qbraid_cli-0.9.5.dist-info}/entry_points.txt +0 -0
  45. {qbraid_cli-0.8.0.dev1.dist-info → qbraid_cli-0.9.5.dist-info}/top_level.txt +0 -0
qbraid_cli/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.8.0.dev1'
16
- __version_tuple__ = version_tuple = (0, 8, 0, 'dev1')
15
+ __version__ = version = '0.9.5'
16
+ __version_tuple__ = version_tuple = (0, 9, 5)
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining the qbraid account namespace
6
+
7
+ """
8
+
9
+ from .app import account_app
10
+
11
+ __all__ = ["account_app"]
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining commands in the 'qbraid user' namespace.
6
+
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ import rich
12
+ import typer
13
+
14
+ from qbraid_cli.handlers import run_progress_task
15
+
16
+ account_app = typer.Typer(help="Manage qBraid account.", no_args_is_help=True)
17
+
18
+
19
+ @account_app.command(name="credits")
20
+ def account_credits():
21
+ """Get number of qBraid credits remaining."""
22
+
23
+ def get_credits() -> float:
24
+ from qbraid_core import QbraidClient
25
+
26
+ client = QbraidClient()
27
+ return client.user_credits_value()
28
+
29
+ qbraid_credits: float = run_progress_task(get_credits)
30
+ typer.secho(
31
+ f"{typer.style('qBraid credits remaining:')} "
32
+ f"{typer.style(f'{qbraid_credits:.4f}', fg=typer.colors.MAGENTA, bold=True)}",
33
+ nl=True, # Ensure a newline after output (default is True)
34
+ )
35
+ rich.print("\nFor more information, visit: https://docs.qbraid.com/home/pricing#credits")
36
+
37
+
38
+ @account_app.command(name="info")
39
+ def account_info():
40
+ """Get qBraid account (user) metadata."""
41
+
42
+ def get_user() -> dict[str, Any]:
43
+ from qbraid_core import QbraidSession
44
+
45
+ session = QbraidSession()
46
+ user = session.get_user()
47
+ personal_info: dict = user.get("personalInformation", {})
48
+ metadata = {
49
+ "_id": user.get("_id"),
50
+ "userName": user.get("userName"),
51
+ "email": user.get("email"),
52
+ "joinedDate": user.get("createdAt", "Unknown"),
53
+ "activePlan": user.get("activePlan", "") or "Free",
54
+ "organization": personal_info.get("organization", "") or "qbraid",
55
+ "role": personal_info.get("role", "") or "guest",
56
+ }
57
+
58
+ return metadata
59
+
60
+ info = run_progress_task(get_user)
61
+ rich.print(info)
62
+
63
+
64
+ if __name__ == "__main__":
65
+ account_app()
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining the qbraid admin namespace
6
+
7
+ """
8
+
9
+ from .app import admin_app
10
+
11
+ __all__ = ["admin_app"]
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining commands in the 'qbraid admin' namespace.
6
+
7
+ """
8
+
9
+ from typing import Optional
10
+
11
+ import typer
12
+
13
+ from qbraid_cli.admin.headers import check_and_fix_headers
14
+ from qbraid_cli.admin.validation import validate_header_type, validate_paths_exist
15
+
16
+ admin_app = typer.Typer(
17
+ help="CI/CD commands for qBraid maintainers.",
18
+ pretty_exceptions_show_locals=False,
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ @admin_app.command(name="headers")
24
+ def admin_headers(
25
+ src_paths: list[str] = typer.Argument(
26
+ ..., help="Source file or directory paths to verify.", callback=validate_paths_exist
27
+ ),
28
+ header_type: str = typer.Option(
29
+ "default",
30
+ "--type",
31
+ "-t",
32
+ help="Type of header to use ('default' or 'gpl').",
33
+ callback=validate_header_type,
34
+ ),
35
+ skip_files: list[str] = typer.Option(
36
+ [], "--skip", "-s", help="Files to skip during verification.", callback=validate_paths_exist
37
+ ),
38
+ fix: bool = typer.Option(
39
+ False, "--fix", "-f", help="Whether to fix the headers instead of just verifying."
40
+ ),
41
+ project_name: Optional[str] = typer.Option(
42
+ "the qBraid-SDK", "--project", "-p", help="Name of the project to use in the header."
43
+ ),
44
+ ):
45
+ """
46
+ Verifies and optionally fixes qBraid headers in specified files and directories.
47
+
48
+ """
49
+ check_and_fix_headers(
50
+ src_paths,
51
+ header_type=header_type,
52
+ skip_files=skip_files,
53
+ fix=fix,
54
+ project_name=project_name,
55
+ )
56
+
57
+
58
+ if __name__ == "__main__":
59
+ admin_app()
@@ -0,0 +1,235 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Script to verify qBraid copyright file headers
6
+
7
+ """
8
+
9
+ import datetime
10
+ import os
11
+ from typing import Optional
12
+
13
+ import typer
14
+ from rich.console import Console
15
+
16
+ from qbraid_cli.handlers import handle_error
17
+
18
+ # pylint: disable=too-many-branches,too-many-statements
19
+ CURR_YEAR = datetime.datetime.now().year
20
+ PREV_YEAR = CURR_YEAR - 1
21
+ VALID_EXTS = (".py", ".js", ".ts")
22
+
23
+
24
+ DEFAULT_HEADER = f"""# Copyright (c) {str(CURR_YEAR)}, qBraid Development Team
25
+ # All rights reserved.
26
+ """
27
+
28
+ DEFAULT_HEADER_GPL = (
29
+ f"""# Copyright (C) {str(CURR_YEAR)} qBraid"""
30
+ + """#
31
+ # This file is part of {project_name}
32
+ #
33
+ # {project_name_start} is free software released under the GNU General Public License v3
34
+ # or later. You can redistribute and/or modify it under the terms of the GPL v3.
35
+ # See the LICENSE file in the project root or <https://www.gnu.org/licenses/gpl-3.0.html>.
36
+ #
37
+ # THERE IS NO WARRANTY for {project_name}, as per Section 15 of the GPL v3.
38
+ """
39
+ )
40
+
41
+ HEADER_TYPES = {
42
+ "default": DEFAULT_HEADER,
43
+ "gpl": DEFAULT_HEADER_GPL,
44
+ }
45
+
46
+
47
+ def get_formatted_header(header_type: str, project_name: str) -> str:
48
+ """Get the formatted header based on the header type
49
+
50
+ Args:
51
+ header_type (str): The type of header to use.
52
+ project_name (str): The name of the project to use in the header.
53
+
54
+ Returns:
55
+ str: The formatted header
56
+ """
57
+
58
+ header = HEADER_TYPES[header_type]
59
+ if header_type == "gpl":
60
+ return header.format(
61
+ project_name=project_name, project_name_start=project_name[0].upper() + project_name[1:]
62
+ )
63
+ return header
64
+
65
+
66
+ def check_and_fix_headers(
67
+ src_paths: list[str],
68
+ header_type: str = "default",
69
+ skip_files: Optional[list[str]] = None,
70
+ fix: bool = False,
71
+ project_name: Optional[str] = None,
72
+ ) -> None:
73
+ """Script to add or verify qBraid copyright file headers"""
74
+ try:
75
+ header = get_formatted_header(header_type, project_name)
76
+ except KeyError:
77
+ handle_error(
78
+ error_type="ValueError",
79
+ message=(
80
+ f"Invalid header type: {HEADER_TYPES}. Expected one of {list(HEADER_TYPES.keys())}"
81
+ ),
82
+ )
83
+
84
+ for path in src_paths:
85
+ if not os.path.exists(path):
86
+ handle_error(error_type="FileNotFoundError", message=f"Path '{path}' does not exist.")
87
+
88
+ header_prev_year = header.replace(str(CURR_YEAR), str(PREV_YEAR))
89
+
90
+ skip_files = skip_files or []
91
+
92
+ failed_headers = []
93
+ fixed_headers = []
94
+
95
+ console = Console()
96
+
97
+ def should_skip(file_path: str, content: str) -> bool:
98
+ if file_path in skip_files:
99
+ return True
100
+
101
+ if os.path.basename(file_path) == "__init__.py":
102
+ return not content.strip()
103
+
104
+ skip_header_tag = "# qbraid: skip-header"
105
+ line_number = 0
106
+
107
+ for line in content.splitlines():
108
+ line_number += 1
109
+ if 5 <= line_number <= 30 and skip_header_tag in line:
110
+ return True
111
+ if line_number > 30:
112
+ break
113
+
114
+ return False
115
+
116
+ def replace_or_add_header(file_path: str, fix: bool = False) -> None:
117
+ with open(file_path, "r", encoding="ISO-8859-1") as f:
118
+ content = f.read()
119
+
120
+ # Get the file extension
121
+ file_ext = os.path.splitext(file_path)[1]
122
+
123
+ if file_ext == ".py":
124
+ comment_marker = "#"
125
+ elif file_ext in (".js", ".ts"):
126
+ comment_marker = "//"
127
+
128
+ # This finds the start of the actual content after skipping initial whitespace and comments.
129
+ lines = content.splitlines()
130
+ first_non_comment_line_index = next(
131
+ (i for i, line in enumerate(lines) if not line.strip().startswith(comment_marker)), None
132
+ )
133
+
134
+ # Prepare the content by stripping leading and trailing whitespace and separating into lines
135
+ actual_content = (
136
+ "\n".join(lines[first_non_comment_line_index:]).strip()
137
+ if first_non_comment_line_index is not None
138
+ else ""
139
+ )
140
+
141
+ updated_header = header.replace("#", comment_marker)
142
+ updated_prev_header = header_prev_year.replace("#", comment_marker)
143
+
144
+ # Check if the content already starts with the header or if the file should be skipped
145
+ if (
146
+ content.lstrip().startswith(updated_header)
147
+ or content.lstrip().startswith(updated_prev_header)
148
+ or should_skip(file_path, content)
149
+ ):
150
+ return
151
+
152
+ if not fix:
153
+ failed_headers.append(file_path)
154
+ else:
155
+ # Form the new content by combining the header, one blank line, and the actual content
156
+ new_content = updated_header.strip() + "\n\n" + actual_content
157
+ with open(file_path, "w", encoding="ISO-8859-1") as f:
158
+ f.write(new_content)
159
+ fixed_headers.append(file_path)
160
+
161
+ def process_files_in_directory(directory: str, fix: bool = False) -> int:
162
+ count = 0
163
+ if not os.path.isdir(directory):
164
+ return count
165
+ for root, _, files in os.walk(directory):
166
+ for file in files:
167
+ if file.endswith(VALID_EXTS):
168
+ file_path = os.path.join(root, file)
169
+ replace_or_add_header(file_path, fix)
170
+ count += 1
171
+ return count
172
+
173
+ checked = 0
174
+ for item in src_paths:
175
+ if os.path.isdir(item):
176
+ checked += process_files_in_directory(item, fix)
177
+ elif os.path.isfile(item) and item.endswith(VALID_EXTS):
178
+ replace_or_add_header(item, fix)
179
+ checked += 1
180
+ else:
181
+ if not os.path.isfile(item):
182
+ handle_error(
183
+ error_type="FileNotFoundError", message=f"Path '{item}' does not exist."
184
+ )
185
+
186
+ if checked == 0:
187
+ console.print(f"[bold]No {VALID_EXTS} files present. Nothing to do[/bold] 😴")
188
+ raise typer.Exit(0)
189
+
190
+ if not fix:
191
+ if failed_headers:
192
+ for file in failed_headers:
193
+ console.print(f"[bold]would fix {file}[/bold]")
194
+ num_failed = len(failed_headers)
195
+ num_passed = checked - num_failed
196
+ s1, s2 = ("", "s") if num_failed == 1 else ("s", "")
197
+ s_passed = "" if num_passed == 1 else "s"
198
+ console.print("[bold]\nOh no![/bold] 💥 💔 💥")
199
+ if num_passed > 0:
200
+ punc = ", "
201
+ passed_msg = f"[blue]{num_passed}[/blue] file{s_passed} would be left unchanged."
202
+ else:
203
+ punc = "."
204
+ passed_msg = ""
205
+
206
+ failed_msg = f"[bold][blue]{num_failed}[/blue] file{s1} need{s2} updating{punc}[/bold]"
207
+ console.print(f"{failed_msg}{passed_msg}")
208
+ raise typer.Exit(1)
209
+
210
+ s_checked = "" if checked == 1 else "s"
211
+ console.print("[bold]All done![/bold] ✨ 🚀 ✨")
212
+ console.print(f"[blue]{checked}[/blue] file{s_checked} would be left unchanged.")
213
+ raise typer.Exit(0)
214
+
215
+ for file in fixed_headers:
216
+ console.print(f"[bold]fixed {file}[/bold]")
217
+ num_fixed = len(fixed_headers)
218
+ num_ok = checked - num_fixed
219
+ s_fixed = "" if num_fixed == 1 else "s"
220
+ s_ok = "" if num_ok == 1 else "s"
221
+ console.print("\n[bold]All done![/bold] ✨ 🚀 ✨")
222
+ if num_ok > 0:
223
+ punc = ", "
224
+ unchanged_msg = f"[blue]{num_ok}[/blue] file{s_ok} left unchanged."
225
+ else:
226
+ punc = "."
227
+ unchanged_msg = ""
228
+
229
+ if num_fixed > 0:
230
+ fixed_msg = f"[bold][blue]{num_fixed}[/blue] file{s_fixed} fixed{punc}[/bold]"
231
+ else:
232
+ fixed_msg = ""
233
+
234
+ console.print(f"{fixed_msg}{unchanged_msg}")
235
+ raise typer.Exit(0)
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module for validating command arguments for qBraid admin commands.
6
+
7
+ """
8
+
9
+ import os
10
+
11
+ import typer
12
+
13
+ from qbraid_cli.admin.headers import HEADER_TYPES
14
+ from qbraid_cli.handlers import _format_list_items, validate_item
15
+
16
+
17
+ def validate_header_type(value: str) -> str:
18
+ """Validate header type."""
19
+ header_types = list(HEADER_TYPES.keys())
20
+ return validate_item(value, header_types, "Header type")
21
+
22
+
23
+ def validate_paths_exist(paths: list[str]) -> list[str]:
24
+ """Verifies that each path in the provided list exists."""
25
+ non_existent_paths = [path for path in paths if not os.path.exists(path)]
26
+ if non_existent_paths:
27
+ if len(non_existent_paths) == 1:
28
+ raise typer.BadParameter(f"Path '{non_existent_paths[0]}' does not exist")
29
+ raise typer.BadParameter(
30
+ f"The following paths do not exist: {_format_list_items(non_existent_paths)}"
31
+ )
32
+ return paths
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining the qbraid chat namespace
6
+
7
+ """
8
+
9
+ from .app import ChatFormat, list_models_callback, prompt_callback
10
+
11
+ __all__ = ["list_models_callback", "prompt_callback", "ChatFormat"]
qbraid_cli/chat/app.py ADDED
@@ -0,0 +1,76 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining commands in the 'qbraid chat' namespace.
6
+
7
+ """
8
+ from enum import Enum
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from qbraid_cli.handlers import handle_error, run_progress_task
14
+
15
+
16
+ class ChatFormat(str, Enum):
17
+ """Format of the response from the chat service."""
18
+
19
+ text = "text" # pylint: disable=invalid-name
20
+ code = "code" # pylint: disable=invalid-name
21
+
22
+
23
+ def list_models_callback():
24
+ """List available chat models."""
25
+ # pylint: disable-next=import-outside-toplevel
26
+ from qbraid_core.services.chat import ChatClient
27
+
28
+ client = ChatClient()
29
+
30
+ models = run_progress_task(
31
+ client.get_models,
32
+ description="Connecting to chat service...",
33
+ include_error_traceback=False,
34
+ )
35
+
36
+ console = Console()
37
+ table = Table(title="Available Chat Models\n", show_lines=True, title_justify="left")
38
+
39
+ table.add_column("Model", style="cyan", no_wrap=True)
40
+ table.add_column("Pricing [not bold](1k tokens ~750 words)", style="magenta")
41
+ table.add_column("Description", style="green")
42
+
43
+ for model in models:
44
+ table.add_row(
45
+ model["model"],
46
+ f"{model['pricing']['input']} credits / 1M input tokens\n"
47
+ f"{model['pricing']['output']} credits / 1M output tokens",
48
+ model["description"],
49
+ )
50
+
51
+ console.print(table)
52
+
53
+
54
+ def prompt_callback(prompt: str, model: str, response_format: ChatFormat, stream: bool):
55
+ """Send a chat prompt to the chat service."""
56
+ # pylint: disable-next=import-outside-toplevel
57
+ from qbraid_core.services.chat import ChatClient, ChatServiceRequestError
58
+
59
+ client = ChatClient()
60
+
61
+ if stream:
62
+ try:
63
+ for chunk in client.chat_stream(prompt, model, response_format):
64
+ print(chunk, end="")
65
+ except ChatServiceRequestError as err:
66
+ handle_error(message=str(err), include_traceback=False)
67
+ else:
68
+ content = run_progress_task(
69
+ client.chat,
70
+ prompt,
71
+ model,
72
+ response_format,
73
+ description="Connecting to chat service...",
74
+ include_error_traceback=False,
75
+ )
76
+ print(content)
@@ -1,6 +1,11 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
1
4
  """
2
5
  Module defining the qbraid configure namespace
3
6
 
4
7
  """
5
8
 
6
- from .app import app
9
+ from .app import configure_app
10
+
11
+ __all__ = ["configure_app"]
@@ -0,0 +1,111 @@
1
+ # Copyright (c) 2024, qBraid Development Team
2
+ # All rights reserved.
3
+
4
+ """
5
+ Module defining actions invoked by 'qbraid configure' command(s).
6
+
7
+ """
8
+
9
+ import configparser
10
+ import re
11
+ from copy import deepcopy
12
+ from typing import Optional
13
+
14
+ import typer
15
+ from qbraid_core.config import (
16
+ DEFAULT_CONFIG_SECTION,
17
+ DEFAULT_ENDPOINT_URL,
18
+ USER_CONFIG_PATH,
19
+ load_config,
20
+ save_config,
21
+ )
22
+ from rich.console import Console
23
+
24
+ from qbraid_cli.handlers import handle_filesystem_operation
25
+
26
+
27
+ def validate_input(key: str, value: str) -> str:
28
+ """Validate the user input based on the key.
29
+
30
+ Args:
31
+ key (str): The configuration key
32
+ value (str): The user input value
33
+
34
+ Returns:
35
+ str: The validated value
36
+
37
+ Raises:
38
+ typer.BadParameter: If the value is invalid
39
+ """
40
+ if key == "url":
41
+ if not re.match(r"^https?://\S+$", value):
42
+ raise typer.BadParameter("Invalid URL format.")
43
+ elif key == "email":
44
+ if not re.match(r"^\S+@\S+\.\S+$", value):
45
+ raise typer.BadParameter("Invalid email format.")
46
+ elif key == "api-key":
47
+ if not re.match(r"^[a-zA-Z0-9]{11}$", value):
48
+ raise typer.BadParameter("Invalid API key format.")
49
+ return value
50
+
51
+
52
+ def prompt_for_config(
53
+ config: configparser.ConfigParser,
54
+ section: str,
55
+ key: str,
56
+ default_values: Optional[dict[str, str]] = None,
57
+ ) -> str:
58
+ """Prompt the user for a configuration setting, showing the current value as default."""
59
+ default_values = default_values or {}
60
+ current_value = config.get(section, key, fallback=default_values.get(key, ""))
61
+ display_value = "None" if not current_value else current_value
62
+ hide_input = False
63
+ show_default = True
64
+
65
+ if section == DEFAULT_CONFIG_SECTION:
66
+ if key == "url":
67
+ return current_value
68
+
69
+ if key == "api-key":
70
+ if current_value:
71
+ display_value = "*" * len(current_value[:-4]) + current_value[-4:]
72
+ hide_input = True
73
+ show_default = False
74
+
75
+ new_value = typer.prompt(
76
+ f"Enter {key}", default=display_value, show_default=show_default, hide_input=hide_input
77
+ ).strip()
78
+
79
+ if new_value == display_value:
80
+ return current_value
81
+
82
+ return validate_input(key, new_value)
83
+
84
+
85
+ def default_action(section: str = DEFAULT_CONFIG_SECTION):
86
+ """Configure qBraid CLI options."""
87
+ config = load_config()
88
+ original_config = deepcopy(config)
89
+
90
+ if section not in config:
91
+ config[section] = {}
92
+
93
+ default_values = {"url": DEFAULT_ENDPOINT_URL}
94
+
95
+ config[section]["url"] = prompt_for_config(config, section, "url", default_values)
96
+ config[section]["api-key"] = prompt_for_config(config, section, "api-key", default_values)
97
+
98
+ for key in list(config[section]):
99
+ if not config[section][key]:
100
+ del config[section][key]
101
+
102
+ console = Console()
103
+ if config == original_config:
104
+ console.print("\n[bold grey70]Configuration saved, unchanged.")
105
+ else:
106
+
107
+ def _save_config():
108
+ save_config(config)
109
+
110
+ handle_filesystem_operation(_save_config, USER_CONFIG_PATH)
111
+ console.print("\n[bold green]Configuration updated successfully.")