From c1c09aeb85a2cad2c924094ae17c2211cc9e2529 Mon Sep 17 00:00:00 2001 From: rafsanneloy Date: Wed, 17 Jun 2026 23:39:44 +0600 Subject: [PATCH 1/5] Implement `Phase 1` items from `plan.md` Signed-off-by: rafsanneloy --- cbrain_cli/cli_utils.py | 33 +++++++------- cbrain_cli/config.py | 22 +++++++++- cbrain_cli/data/projects.py | 64 ++++++++++++++++++++-------- cbrain_cli/data/tools.py | 54 ++++++++++++----------- cbrain_cli/formatter/files_fmt.py | 4 ++ cbrain_cli/formatter/projects_fmt.py | 44 ++++++++++++++++++- cbrain_cli/handlers.py | 13 +++--- cbrain_cli/main.py | 26 ++++++++--- cbrain_cli/sessions.py | 30 +++++++++---- 9 files changed, 206 insertions(+), 84 deletions(-) diff --git a/cbrain_cli/cli_utils.py b/cbrain_cli/cli_utils.py index c800db4..15191a7 100644 --- a/cbrain_cli/cli_utils.py +++ b/cbrain_cli/cli_utils.py @@ -6,23 +6,22 @@ import urllib.request # import importlib.metadata -from cbrain_cli.config import CREDENTIALS_FILE, DEFAULT_HEADERS, auth_headers - -try: - # MARK: Credentials. - with open(CREDENTIALS_FILE) as f: - credentials = json.load(f) - - # Get credentials. - cbrain_url = credentials.get("cbrain_url") - api_token = credentials.get("api_token") - user_id = credentials.get("user_id") - cbrain_timestamp = credentials.get("timestamp") -except FileNotFoundError: - cbrain_url = None - api_token = None - user_id = None - cbrain_timestamp = None +from cbrain_cli.config import DEFAULT_HEADERS, auth_headers, load_credentials + +credentials = load_credentials() or {} +cbrain_url = credentials.get("cbrain_url") +api_token = credentials.get("api_token") +user_id = credentials.get("user_id") +cbrain_timestamp = credentials.get("timestamp") + +PAGINATABLE_ACTIONS = { + ("file", "list"), + ("dataprovider", "list"), + ("tool", "list"), + ("tool-config", "list"), + ("tag", "list"), + ("task", "list"), +} class CliValidationError(Exception): diff --git a/cbrain_cli/config.py b/cbrain_cli/config.py index cc74b5b..73573e9 100644 --- a/cbrain_cli/config.py +++ b/cbrain_cli/config.py @@ -2,6 +2,7 @@ CBRAIN CLI Configuration """ +import json from pathlib import Path # Default settings. @@ -10,7 +11,6 @@ # Session file configuration. SESSION_FILE_DIR = Path.home() / ".config" / "cbrain" SESSION_FILE_NAME = "credentials.json" -SESSION_FILE_DIR.mkdir(parents=True, exist_ok=True) CREDENTIALS_FILE = SESSION_FILE_DIR / SESSION_FILE_NAME # HTTP headers. @@ -35,3 +35,23 @@ def auth_headers(api_token): Headers dictionary with authorization """ return {"Accept": "application/json", "Authorization": f"Bearer {api_token}"} + + +def load_credentials(): + """ + Load credentials from the session file. + """ + try: + with open(CREDENTIALS_FILE) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return None + + +def save_credentials(credentials): + """ + Save credentials to the session file. + """ + CREDENTIALS_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(CREDENTIALS_FILE, "w") as f: + json.dump(credentials, f, indent=2) diff --git a/cbrain_cli/data/projects.py b/cbrain_cli/data/projects.py index 5c6d0aa..6298dcc 100644 --- a/cbrain_cli/data/projects.py +++ b/cbrain_cli/data/projects.py @@ -1,4 +1,3 @@ -import json import urllib.error from cbrain_cli.cli_utils import ( @@ -9,7 +8,7 @@ api_token, cbrain_url, ) -from cbrain_cli.config import CREDENTIALS_FILE +from cbrain_cli.config import load_credentials, save_credentials def switch_project(args): @@ -46,19 +45,52 @@ def switch_project(args): api_send(f"{cbrain_url}/groups/switch?id={group_id}", api_token) group_data = api_get(f"{cbrain_url}/groups/{group_id}", api_token) - if CREDENTIALS_FILE.exists(): - with open(CREDENTIALS_FILE) as f: - credentials = json.load(f) - + credentials = load_credentials() + if credentials is not None: credentials["current_group_id"] = group_id credentials["current_group_name"] = group_data.get("name", "Unknown") - - with open(CREDENTIALS_FILE, "w") as f: - json.dump(credentials, f, indent=2) + save_credentials(credentials) return group_data +def unswitch_project(args): + """ + Clear the current project/group on the server and in local credentials. + + Parameters + ---------- + args : argparse.Namespace + Command line arguments + + Returns + ------- + dict + Previous and current group identifiers (all None when no project was set) + """ + credentials = load_credentials() + previous_group_id = None + previous_group_name = None + + if credentials is not None: + previous_group_id = credentials.get("current_group_id") + previous_group_name = credentials.get("current_group_name") + + api_send(f"{cbrain_url}/groups/switch", api_token) + + if credentials is not None: + credentials.pop("current_group_id", None) + credentials.pop("current_group_name", None) + save_credentials(credentials) + + return { + "previous_group_id": previous_group_id, + "previous_group_name": previous_group_name, + "current_group_id": None, + "current_group_name": None, + } + + def show_project(args): """ Get the current project/group from credentials or show a specific project by ID. @@ -77,7 +109,7 @@ def show_project(args): project_id = getattr(args, "project_id", None) if project_id: - # Show specific project by ID + # Show specific project by ID try: return api_get(f"{cbrain_url}/groups/{project_id}", api_token) except urllib.error.HTTPError as e: @@ -85,8 +117,9 @@ def show_project(args): raise CliApiError(f"Project with ID {project_id} not found") raise - with open(CREDENTIALS_FILE) as f: - credentials = json.load(f) + credentials = load_credentials() + if credentials is None: + return None current_group_id = credentials.get("current_group_id") if not current_group_id: @@ -98,11 +131,8 @@ def show_project(args): if e.code == 404: credentials.pop("current_group_id", None) credentials.pop("current_group_name", None) - with open(CREDENTIALS_FILE, "w") as f: - json.dump(credentials, f, indent=2) - raise CliApiError( - f"Current project (ID {current_group_id}) no longer exists" - ) + save_credentials(credentials) + raise CliApiError(f"Current project (ID {current_group_id}) no longer exists") raise diff --git a/cbrain_cli/data/tools.py b/cbrain_cli/data/tools.py index 7a24053..bd86b93 100644 --- a/cbrain_cli/data/tools.py +++ b/cbrain_cli/data/tools.py @@ -1,6 +1,5 @@ from cbrain_cli.cli_utils import ( CliApiError, - CliResponseError, CliValidationError, api_get, api_token, @@ -11,35 +10,38 @@ def list_tools(args): """ - Get tool details or list of all tools. + Get paginated list of tools from CBRAIN. + """ + params = pagination(args, {}) + return api_get(f"{cbrain_url}/tools", api_token, params) + - Parameters - ---------- - args : argparse.Namespace - Command line arguments, including the tool argument with tool_id if specified +def show_tool(args): + """ + Get detailed information about a specific tool from CBRAIN. - Returns - ------- - dict or list or None - - dict: when tool_id is provided and found - - list: when no tool_id is provided - - None: when error occurs or tool not found + Searches paginated ``GET /tools`` results because ``GET /tools/{id}`` + returns 204 No Content on this API. """ - # Get the tool ID from the id argument if provided. tool_id = getattr(args, "id", None) - if tool_id is None and getattr(args, "action", None) == "show": + if not tool_id: raise CliValidationError("Tool ID is required", field="id") - params = pagination(args, {}) - tools_data = api_get(f"{cbrain_url}/tools", api_token, params) - - if not isinstance(tools_data, list): - raise CliResponseError("Unexpected response format from server") - if tool_id: - # Filter for a specific tool - tool = next((t for t in tools_data if t.get("id") == tool_id), None) - if not tool: - raise CliApiError(f"Tool with ID {tool_id} not found") - return tool + per_page = 20 + page = 1 + while True: + tools_page = api_get( + f"{cbrain_url}/tools", + api_token, + {"page": str(page), "per_page": str(per_page)}, + ) + if not tools_page: + break + for tool in tools_page: + if tool.get("id") == tool_id: + return tool + if len(tools_page) < per_page: + break + page += 1 - return tools_data + raise CliApiError(f"Tool with ID {tool_id} not found") diff --git a/cbrain_cli/formatter/files_fmt.py b/cbrain_cli/formatter/files_fmt.py index 9b99934..e2ee24d 100644 --- a/cbrain_cli/formatter/files_fmt.py +++ b/cbrain_cli/formatter/files_fmt.py @@ -50,6 +50,10 @@ def print_files_list(files_data, args): if output_json(args, files_data): return + if not files_data: + print("No files found.") + return + # Use the reusable dynamic table formatter dynamic_table_print(files_data, ["id", "type", "name"], ["ID", "Type", "File Name"]) diff --git a/cbrain_cli/formatter/projects_fmt.py b/cbrain_cli/formatter/projects_fmt.py index b292201..b80cf5b 100644 --- a/cbrain_cli/formatter/projects_fmt.py +++ b/cbrain_cli/formatter/projects_fmt.py @@ -22,10 +22,14 @@ def print_projects_list(projects_data, args): if output_json(args, formatted_data): return + if not formatted_data: + print("No projects found.") + return + dynamic_table_print(formatted_data, ["id", "type", "name"], ["ID", "Type", "Project Name"]) -def print_current_project(project_data): +def print_current_project(project_data, args=None): """ Print current project details. @@ -33,7 +37,12 @@ def print_current_project(project_data): ---------- project_data : dict Dictionary containing project name and ID + args : argparse.Namespace, optional + Command line arguments, including the --json flag """ + if args is not None and output_json(args, project_data): + return + group_name = project_data.get("name", "Unknown") group_id = project_data.get("id") print(f'Current project is "{group_name}" ID={group_id}') @@ -73,8 +82,39 @@ def print_project_details(project_data, args): print(line) -def print_no_project(): +def print_no_project(args=None): """ Print message when no current project is set. + + Parameters + ---------- + args : argparse.Namespace, optional + Command line arguments, including the --json flag """ + result = {"current_group_id": None, "current_group_name": None} + if args is not None and output_json(args, result): + return + print("No current project set. Use 'cbrain project switch ' to set a project.") + + +def print_unswitch_result(result, args): + """ + Print the result of clearing the current project context. + + Parameters + ---------- + result : dict + Unswitch result with previous and current group identifiers + args : argparse.Namespace + Command line arguments, including the --json flag + """ + if output_json(args, result): + return + + previous_group_id = result.get("previous_group_id") + if previous_group_id: + previous_group_name = result.get("previous_group_name", "Unknown") + print(f'Cleared current project "{previous_group_name}" ID={previous_group_id}') + else: + print("No current project set. Use 'cbrain project switch ' to set a project.") diff --git a/cbrain_cli/handlers.py b/cbrain_cli/handlers.py index b40a166..578e413 100644 --- a/cbrain_cli/handlers.py +++ b/cbrain_cli/handlers.py @@ -136,13 +136,13 @@ def handle_project_switch(args): result = projects.switch_project(args) if result is None: return 1 - projects_fmt.print_current_project(result) + projects_fmt.print_current_project(result, args) def handle_project_show(args): """Display information about the currently active project or a specific project by ID.""" result = projects.show_project(args) - if result: + if result is not None: # Check if a specific project ID was requested project_id = getattr(args, "project_id", None) if project_id: @@ -150,23 +150,24 @@ def handle_project_show(args): projects_fmt.print_project_details(result, args) else: # Show current project information - projects_fmt.print_current_project(result) + projects_fmt.print_current_project(result, args) else: # Only show "no project" message if no specific ID was requested project_id = getattr(args, "project_id", None) if not project_id: - projects_fmt.print_no_project() + projects_fmt.print_no_project(args) def handle_project_unswitch(args): """Unswitch from current project context.""" - print("Project Unswitch 'all' not yet implemented as of Aug 2025") + result = projects.unswitch_project(args) + projects_fmt.print_unswitch_result(result, args) # Tool command handlers def handle_tool_show(args): """Retrieve and display detailed information about a specific computational tool.""" - result = tools.list_tools(args) + result = tools.show_tool(args) if result is None: return 1 tools_fmt.print_tool_details(result, args) diff --git a/cbrain_cli/main.py b/cbrain_cli/main.py index 628963a..e7fdc44 100644 --- a/cbrain_cli/main.py +++ b/cbrain_cli/main.py @@ -5,7 +5,14 @@ import argparse import sys -from cbrain_cli.cli_utils import handle_errors, is_authenticated, version_info +from cbrain_cli.cli_utils import ( + PAGINATABLE_ACTIONS, + CliValidationError, + handle_errors, + is_authenticated, + pagination, + version_info, +) from cbrain_cli.data.tasks import operation_task from cbrain_cli.handlers import ( handle_background_list, @@ -235,7 +242,7 @@ def main(): tool_show_parser.add_argument("id", type=int, help="Tool ID") tool_show_parser.set_defaults(func=handle_errors(handle_tool_show)) - # tool list (reusing show_tool without id) + # tool list tool_list_parser = tool_subparsers.add_parser("list", help="List all tools") tool_list_parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)") tool_list_parser.add_argument( @@ -404,9 +411,18 @@ def main(): parser.print_help() return - # Handle session commands (no authentication needed for login, version, and whoami). + if (args.command, getattr(args, "action", None)) in PAGINATABLE_ACTIONS: + try: + pagination(args, {}) + except CliValidationError as e: + print(f"Error: {e}") + return 1 + + # Handle session commands (no authentication needed for login, logout, version, and whoami). if args.command == "login": return handle_errors(create_session)(args) + elif args.command == "logout": + return handle_errors(logout_session)(args) elif args.command == "version": return handle_errors(version_info)(args) elif args.command == "whoami": @@ -417,9 +433,7 @@ def main(): return 1 # Handle authenticated commands. - if args.command == "logout": - return handle_errors(logout_session)(args) - elif args.command in [ + if args.command in [ "file", "dataprovider", "project", diff --git a/cbrain_cli/sessions.py b/cbrain_cli/sessions.py index d73dd52..3beb31f 100644 --- a/cbrain_cli/sessions.py +++ b/cbrain_cli/sessions.py @@ -1,6 +1,5 @@ import datetime import getpass -import json import urllib.error from cbrain_cli.cli_utils import ( @@ -10,7 +9,7 @@ api_token, cbrain_url, ) -from cbrain_cli.config import CREDENTIALS_FILE, DEFAULT_BASE_URL +from cbrain_cli.config import CREDENTIALS_FILE, DEFAULT_BASE_URL, load_credentials, save_credentials # MARK: Create Session. @@ -41,7 +40,9 @@ def create_session(args): if not password: raise CliValidationError("Password is required", field="password") - response_data = api_post_form(f"{cbrain_url}/session", {"login": username, "password": password}) + response_data = api_post_form( + f"{cbrain_url}/session", {"login": username, "password": password} + ) cbrain_api_token = response_data.get("cbrain_api_token") cbrain_user_id = response_data.get("user_id") @@ -57,8 +58,7 @@ def create_session(args): "timestamp": datetime.datetime.now().isoformat(), } - with open(CREDENTIALS_FILE, "w") as f: - json.dump(credentials, f, indent=2) + save_credentials(credentials) print(f"Connection successful, API token saved in {CREDENTIALS_FILE}") return 0 @@ -75,9 +75,21 @@ def logout_session(args): A command is run via inputs from the user. """ + if not CREDENTIALS_FILE.exists(): + print("Not logged in. Use 'cbrain login' to login first.") + return 0 + + credentials = load_credentials() + if credentials is None: + print("Invalid credentials file. Removing local session.") + CREDENTIALS_FILE.unlink(missing_ok=True) + print(f"Local session removed from {CREDENTIALS_FILE}") + return 0 + if not cbrain_url or not api_token: print("Invalid credentials file. Removing local session.") - CREDENTIALS_FILE.unlink() + CREDENTIALS_FILE.unlink(missing_ok=True) + print(f"Local session removed from {CREDENTIALS_FILE}") return 0 try: @@ -94,7 +106,7 @@ def logout_session(args): except urllib.error.URLError as e: print(f"Network error during logout: {e}") - # Always remove local credentials file. - CREDENTIALS_FILE.unlink() - print(f"Local session removed from {CREDENTIALS_FILE}") + if CREDENTIALS_FILE.exists(): + CREDENTIALS_FILE.unlink() + print(f"Local session removed from {CREDENTIALS_FILE}") return 0 From a0734c39f00d184cf27e018dde91bd7eb0c83cca Mon Sep 17 00:00:00 2001 From: rafsanneloy Date: Thu, 18 Jun 2026 00:24:12 +0600 Subject: [PATCH 2/5] Update expected captures for switch command outputs Signed-off-by: rafsanneloy --- capture_tests/expected_captures.txt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt index 8c8e8a0..962259c 100644 --- a/capture_tests/expected_captures.txt +++ b/capture_tests/expected_captures.txt @@ -489,22 +489,29 @@ Stderr: ############################ Command: cbrain --json project switch 10 Status: 0 -Stdout: 37 bytes +Stdout: 125 bytes Stderr: 0 bytes Stdout: -Current project is "NormTest1" ID=10 +{ + "id": 10, + "name": "NormTest1", + "description": null, + "type": "WorkGroup", + "site_id": null, + "invisible": false +} Stderr: (No output) ############################ Command: cbrain --jsonl project switch 10 Status: 0 -Stdout: 37 bytes +Stdout: 100 bytes Stderr: 0 bytes Stdout: -Current project is "NormTest1" ID=10 +{"id":10,"name":"NormTest1","description":null,"type":"WorkGroup","site_id":null,"invisible":false} Stderr: (No output) From d34a67027bced6ee4aabbae520077bb37a195184 Mon Sep 17 00:00:00 2001 From: rafsanneloy Date: Fri, 19 Jun 2026 00:55:45 +0600 Subject: [PATCH 3/5] Updated pre-commit workflow, dependencies and enhance project unswitch command Signed-off-by: rafsanneloy --- .github/workflows/ruff.yaml | 21 ++++++++++++ .pre-commit-config.yaml | 6 ++-- README.md | 4 +-- capture_tests/cbrain_cli_commands | 5 ++- capture_tests/expected_captures.txt | 38 ++++++++++++++++++++++ cbrain_cli/data/data_providers.py | 4 +-- cbrain_cli/data/files.py | 4 +-- cbrain_cli/data/projects.py | 9 ++--- cbrain_cli/data/tags.py | 4 +-- cbrain_cli/data/tool_configs.py | 4 +-- cbrain_cli/formatter/data_providers_fmt.py | 2 +- pyproject.toml | 3 ++ 12 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ruff.yaml diff --git a/.github/workflows/ruff.yaml b/.github/workflows/ruff.yaml new file mode 100644 index 0000000..c3e616a --- /dev/null +++ b/.github/workflows/ruff.yaml @@ -0,0 +1,21 @@ +name: pre-commit + +on: [push, pull_request] + +jobs: + pre-commit: + name: Pre-commit checks + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run pre-commit + run: | + pip install pre-commit + pre-commit run --config .pre-commit-config.yaml --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b7e4931..51c8cd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: ^capture_tests/expected_captures\.txt$ @@ -9,13 +9,13 @@ repos: - id: check-yaml - repo: https://github.com/tcort/markdown-link-check - rev: v3.13.6 + rev: v3.14.2 hooks: - id: markdown-link-check args: [-q] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.5 + rev: v0.15.18 hooks: - id: ruff args: [--fix] diff --git a/README.md b/README.md index 8f7d51c..fb562d8 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ When prompted for "Enter CBRAIN server URL prefix", enter: ## API Reference This CLI interfaces with the CBRAIN REST API. For complete API documentation and specifications, refer to: -- [CBRAIN API Documentation (Swagger)](https://app.swaggerhub.com/apis/prioux/CBRAIN/7.0.0) +- [CBRAIN API Documentation (Swagger)](https://portal.cbrain.mcgill.ca/swagger) ## CLI Usage @@ -197,7 +197,7 @@ The hooks currently: - run `ruff --fix`; - run `ruff format`. -The generated capture fixture `capture_tests/expected_captures.txt` is excluded from whitespace and Ruff hooks because exact captured output is intentional there. +The generated capture fixture `capture_tests/expected_captures.txt` is excluded from whitespace hooks and from Ruff (via `pyproject.toml`) because exact captured output is intentional there. To run the hooks manually: diff --git a/capture_tests/cbrain_cli_commands b/capture_tests/cbrain_cli_commands index 853fcf0..a90e99d 100755 --- a/capture_tests/cbrain_cli_commands +++ b/capture_tests/cbrain_cli_commands @@ -70,6 +70,10 @@ cbrain project show 10 cbrain --json project show 10 cbrain --jsonl project show 10 +cbrain project unswitch +cbrain --json project unswitch +cbrain --jsonl project unswitch + cbrain project switch 10 cbrain --json project switch 10 cbrain --jsonl project switch 10 @@ -167,4 +171,3 @@ cbrain file move --file-id 2 --dp-id 15 # Logging out cbrain logout - diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt index 962259c..119c940 100644 --- a/capture_tests/expected_captures.txt +++ b/capture_tests/expected_captures.txt @@ -475,6 +475,44 @@ Stdout: Stderr: (No output) +############################ +Command: cbrain project unswitch +Status: 0 +Stdout: 75 bytes +Stderr: 0 bytes + +Stdout: +No current project set. Use 'cbrain project switch ' to set a project. +Stderr: +(No output) + +############################ +Command: cbrain --json project unswitch +Status: 0 +Stdout: 121 bytes +Stderr: 0 bytes + +Stdout: +{ + "previous_group_id": null, + "previous_group_name": null, + "current_group_id": null, + "current_group_name": null +} +Stderr: +(No output) + +############################ +Command: cbrain --jsonl project unswitch +Status: 0 +Stdout: 104 bytes +Stderr: 0 bytes + +Stdout: +{"previous_group_id":null,"previous_group_name":null,"current_group_id":null,"current_group_name":null} +Stderr: +(No output) + ############################ Command: cbrain project switch 10 Status: 0 diff --git a/cbrain_cli/data/data_providers.py b/cbrain_cli/data/data_providers.py index 80e4ffd..4aa6436 100644 --- a/cbrain_cli/data/data_providers.py +++ b/cbrain_cli/data/data_providers.py @@ -78,7 +78,5 @@ def delete_unregistered_files(args): data_provider_id = getattr(args, "id", None) if not data_provider_id: raise CliValidationError("Data provider ID is required", field="id") - data, _ = api_send( - f"{cbrain_url}/data_providers/{data_provider_id}/delete", api_token - ) + data, _ = api_send(f"{cbrain_url}/data_providers/{data_provider_id}/delete", api_token) return data diff --git a/cbrain_cli/data/files.py b/cbrain_cli/data/files.py index 3b05fd9..b9fc20a 100644 --- a/cbrain_cli/data/files.py +++ b/cbrain_cli/data/files.py @@ -105,9 +105,7 @@ def _change_provider(args, operation): if not file_ids: raise CliValidationError("File ID(s) are required", field="--file-id") if not dest_provider_id: - raise CliValidationError( - "Destination data provider ID is required", field="--dp-id" - ) + raise CliValidationError("Destination data provider ID is required", field="--dp-id") payload = { "file_ids": file_ids, "data_provider_id_for_mv_cp": dest_provider_id, diff --git a/cbrain_cli/data/projects.py b/cbrain_cli/data/projects.py index 6298dcc..c399988 100644 --- a/cbrain_cli/data/projects.py +++ b/cbrain_cli/data/projects.py @@ -40,7 +40,7 @@ def switch_project(args): except ValueError: raise CliValidationError( f"Invalid group ID '{group_id}'. Must be a number or 'all'", field="group_id" - ) + ) from None api_send(f"{cbrain_url}/groups/switch?id={group_id}", api_token) group_data = api_get(f"{cbrain_url}/groups/{group_id}", api_token) @@ -76,7 +76,8 @@ def unswitch_project(args): previous_group_id = credentials.get("current_group_id") previous_group_name = credentials.get("current_group_name") - api_send(f"{cbrain_url}/groups/switch", api_token) + if previous_group_id: + api_send(f"{cbrain_url}/groups/switch", api_token) if credentials is not None: credentials.pop("current_group_id", None) @@ -114,7 +115,7 @@ def show_project(args): return api_get(f"{cbrain_url}/groups/{project_id}", api_token) except urllib.error.HTTPError as e: if e.code == 404: - raise CliApiError(f"Project with ID {project_id} not found") + raise CliApiError(f"Project with ID {project_id} not found") from None raise credentials = load_credentials() @@ -132,7 +133,7 @@ def show_project(args): credentials.pop("current_group_id", None) credentials.pop("current_group_name", None) save_credentials(credentials) - raise CliApiError(f"Current project (ID {current_group_id}) no longer exists") + raise CliApiError(f"Current project (ID {current_group_id}) no longer exists") from None raise diff --git a/cbrain_cli/data/tags.py b/cbrain_cli/data/tags.py index 3655c64..04b62c7 100644 --- a/cbrain_cli/data/tags.py +++ b/cbrain_cli/data/tags.py @@ -100,9 +100,7 @@ def update_tag(args): if not tag_id: raise CliValidationError("Tag ID is required", field="tag_id") payload = _tag_payload(args) - data, status = api_send( - f"{cbrain_url}/tags/{tag_id}", api_token, method="PUT", payload=payload - ) + data, status = api_send(f"{cbrain_url}/tags/{tag_id}", api_token, method="PUT", payload=payload) success = status in (200, 201, 204) return data, success, None, status diff --git a/cbrain_cli/data/tool_configs.py b/cbrain_cli/data/tool_configs.py index ff6504f..1e07758 100644 --- a/cbrain_cli/data/tool_configs.py +++ b/cbrain_cli/data/tool_configs.py @@ -48,6 +48,4 @@ def tool_config_boutiques_descriptor(args): config_id = getattr(args, "id", None) if not config_id: raise CliValidationError("Tool configuration ID is required", field="id") - return api_get( - f"{cbrain_url}/tool_configs/{config_id}/boutiques_descriptor", api_token - ) + return api_get(f"{cbrain_url}/tool_configs/{config_id}/boutiques_descriptor", api_token) diff --git a/cbrain_cli/formatter/data_providers_fmt.py b/cbrain_cli/formatter/data_providers_fmt.py index 740fd32..c1e7bcc 100644 --- a/cbrain_cli/formatter/data_providers_fmt.py +++ b/cbrain_cli/formatter/data_providers_fmt.py @@ -1,4 +1,4 @@ -from cbrain_cli.cli_utils import dynamic_table_print, display_key_value_table, output_json +from cbrain_cli.cli_utils import display_key_value_table, dynamic_table_print, output_json def print_provider_details(provider_data, args): diff --git a/pyproject.toml b/pyproject.toml index 2efd68a..9559f2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,9 @@ [tool.ruff] line-length = 100 target-version = "py38" +exclude = [ + "capture_tests/expected_captures.txt", +] [tool.ruff.lint] select = [ From 0978a8bb55ee030f509e191ab4720cb93b54f5ed Mon Sep 17 00:00:00 2001 From: rafsanneloy Date: Fri, 19 Jun 2026 01:23:17 +0600 Subject: [PATCH 4/5] Update project switch and unswitch commands in capture tests Signed-off-by: rafsanneloy --- capture_tests/cbrain_cli_commands | 7 ++-- capture_tests/expected_captures.txt | 60 ++++++++++++++--------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/capture_tests/cbrain_cli_commands b/capture_tests/cbrain_cli_commands index a90e99d..f709cbc 100755 --- a/capture_tests/cbrain_cli_commands +++ b/capture_tests/cbrain_cli_commands @@ -70,13 +70,14 @@ cbrain project show 10 cbrain --json project show 10 cbrain --jsonl project show 10 +cbrain project switch 10 cbrain project unswitch -cbrain --json project unswitch -cbrain --jsonl project unswitch -cbrain project switch 10 cbrain --json project switch 10 +cbrain --json project unswitch + cbrain --jsonl project switch 10 +cbrain --jsonl project unswitch cbrain project switch all # 'all' not yet implemented as of Aug 2025 cbrain --json project switch all diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt index 119c940..42f03d2 100644 --- a/capture_tests/expected_captures.txt +++ b/capture_tests/expected_captures.txt @@ -476,80 +476,80 @@ Stderr: (No output) ############################ -Command: cbrain project unswitch +Command: cbrain project switch 10 Status: 0 -Stdout: 75 bytes +Stdout: 37 bytes Stderr: 0 bytes Stdout: -No current project set. Use 'cbrain project switch ' to set a project. +Current project is "NormTest1" ID=10 Stderr: (No output) ############################ -Command: cbrain --json project unswitch +Command: cbrain project unswitch Status: 0 -Stdout: 121 bytes +Stdout: 75 bytes Stderr: 0 bytes Stdout: -{ - "previous_group_id": null, - "previous_group_name": null, - "current_group_id": null, - "current_group_name": null -} +No current project set. Use 'cbrain project switch ' to set a project. Stderr: (No output) ############################ -Command: cbrain --jsonl project unswitch +Command: cbrain --json project switch 10 Status: 0 -Stdout: 104 bytes +Stdout: 125 bytes Stderr: 0 bytes Stdout: -{"previous_group_id":null,"previous_group_name":null,"current_group_id":null,"current_group_name":null} +{ + "id": 10, + "name": "NormTest1", + "description": null, + "type": "WorkGroup", + "site_id": null, + "invisible": false +} Stderr: (No output) ############################ -Command: cbrain project switch 10 +Command: cbrain --json project unswitch Status: 0 -Stdout: 37 bytes +Stdout: 121 bytes Stderr: 0 bytes Stdout: -Current project is "NormTest1" ID=10 +{ + "previous_group_id": null, + "previous_group_name": null, + "current_group_id": null, + "current_group_name": null +} Stderr: (No output) ############################ -Command: cbrain --json project switch 10 +Command: cbrain --jsonl project switch 10 Status: 0 -Stdout: 125 bytes +Stdout: 100 bytes Stderr: 0 bytes Stdout: -{ - "id": 10, - "name": "NormTest1", - "description": null, - "type": "WorkGroup", - "site_id": null, - "invisible": false -} +{"id":10,"name":"NormTest1","description":null,"type":"WorkGroup","site_id":null,"invisible":false} Stderr: (No output) ############################ -Command: cbrain --jsonl project switch 10 +Command: cbrain --jsonl project unswitch Status: 0 -Stdout: 100 bytes +Stdout: 104 bytes Stderr: 0 bytes Stdout: -{"id":10,"name":"NormTest1","description":null,"type":"WorkGroup","site_id":null,"invisible":false} +{"previous_group_id":null,"previous_group_name":null,"current_group_id":null,"current_group_name":null} Stderr: (No output) From c7ae1ca708ad6e05fe9cb61212e42edbd56fba68 Mon Sep 17 00:00:00 2001 From: rafsanneloy Date: Fri, 19 Jun 2026 01:33:09 +0600 Subject: [PATCH 5/5] Update expected captures for project unswitch command outputs Signed-off-by: rafsanneloy --- capture_tests/expected_captures.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt index 42f03d2..3cb06d6 100644 --- a/capture_tests/expected_captures.txt +++ b/capture_tests/expected_captures.txt @@ -489,11 +489,11 @@ Stderr: ############################ Command: cbrain project unswitch Status: 0 -Stdout: 75 bytes +Stdout: 42 bytes Stderr: 0 bytes Stdout: -No current project set. Use 'cbrain project switch ' to set a project. +Cleared current project "NormTest1" ID=10 Stderr: (No output) @@ -518,13 +518,13 @@ Stderr: ############################ Command: cbrain --json project unswitch Status: 0 -Stdout: 121 bytes +Stdout: 126 bytes Stderr: 0 bytes Stdout: { - "previous_group_id": null, - "previous_group_name": null, + "previous_group_id": 10, + "previous_group_name": "NormTest1", "current_group_id": null, "current_group_name": null } @@ -545,11 +545,11 @@ Stderr: ############################ Command: cbrain --jsonl project unswitch Status: 0 -Stdout: 104 bytes +Stdout: 109 bytes Stderr: 0 bytes Stdout: -{"previous_group_id":null,"previous_group_name":null,"current_group_id":null,"current_group_name":null} +{"previous_group_id":10,"previous_group_name":"NormTest1","current_group_id":null,"current_group_name":null} Stderr: (No output)