diff --git a/README.md b/README.md index d74ed8a..9f02194 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,10 @@ A command-line interface to a CBRAIN service ============================================ -This repository contains a UNIX command-line interface (CLI) for [CBRAIN](https://github.com/aces/cbrain). +This repository contains a UNIX command-line interface (CLI) for [CBRAIN](https://github.com/aces/cbrain), a web-based neuroinformatics platform designed for collaborative brain imaging research. CBRAIN provides researchers with distributed computational resources, data management capabilities, and a framework for running neuroscience analysis pipelines across multiple high-performance computing environments. -The interface is implemented in Python using only standard libraries - no external dependencies required. +>The interface is implemented in Python using only standard libraries - no external dependencies required. -The main command is called "cbrain" and as is typical for such clients, works -with a set of subcommand and options (e.g. "cbrain file list -j") such as: -```bash -cbrain file list -cbrain project show -cbrain --json dataprovider list -``` ## CBRAIN Access Options @@ -26,7 +19,7 @@ There are two main ways to access CBRAIN: 2. **Custom/Development Setup** - Deploy CBRAIN on your lab cluster, cloud, or virtual machine - - Suitable for organizations wanting their own CBRAIN instance + - Suitable for organizations that require their own CBRAIN instance or which prefer to host CBRAIN themselves due to legal or corporate requirements - Local installation only needed for: - CLI software developers - Power users developing/debugging custom CLI scripts @@ -77,12 +70,85 @@ When prompted for "Enter CBRAIN server URL prefix", enter: 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) +## CLI Usage + +The main command is called "cbrain" and as is typical for such clients, works +with a set of subcommand and options. + +### Basic Usage + +To utilize the Cbrain cli, you can execute variations of the following command in your terminal: + +``` +cbrain -h # view the cli options +cbrain [options] [id_or_args] +``` +**Output Formats:** +- `--json` or `-j`: JSON format output +- `--jsonl` or `-jl`: JSON Lines format (one JSON object per line) + +## Available Commands +- `version` - Show CLI version +- `login` - Login to CBRAIN +- `logout` - Logout from CBRAIN +- `whoami` - Show current session +- `file` - File operations +- `dataprovider` - Data provider operations +- `project` - Project operations +- `tool` - Tool operations +- `tool-config` - Tool configuration operations +- `tag` - Tag operations +- `background` - Background activity operations +- `task` - Task operations +- `remote-resource` - Remote resource operations + +## Command Examples + +

+List, Total and Get GIF +

+ +>
Used cmds in the above GIF +> +> - `./cbrain project switch 2` +> - `./cbrain project show` +> - `./cbrain tool show 2` +> - `./cbrain dataprovider show 4` +> - `./cbrain file show 4` +> - `./cbrain background show 15` +> - `./cbrain remote-resource show 2` +> - `./cbrain tag show 17` +> - `./cbrain task show 1` +> +>
+ +

+ List, Total and Get GIF +

+ +>
Used cmds in the above GIF +> +> - `./cbrain file list` +> - `./cbrain project list` +> - `./cbrain background list` +> - `./cbrain dataprovider list` +> - `./cbrain remote-resource list` +> - `./cbrain tag list` +> - `./cbrain task list` +> - `./cbrain task list bourreau-id 3` +> +>
+ ## Development -This is part of a GSoC (Google Summer of Code) project sponsored by [INCF](https://www.incf.org/). +This is part of [**a GSoC (Google Summer of Code) 2025** project](https://summerofcode.withgoogle.com/programs/2025/projects/1An4Dp8N) sponsored by [INCF](https://www.incf.org/). The lead developer is [axif0](https://github.com/axif0), mentored by the developers of the CBRAIN project. +### Continuous Integration + +Continuous Integration (CI) tests and framework were initially configured by P. Rioux, providing automated validation of the codebase. This infrastructure follows best open source practices and ensures code quality through automated testing. + ## License See [LICENSE](LICENSE) file for details. diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt index 233ff2b..374289c 100644 --- a/capture_tests/expected_captures.txt +++ b/capture_tests/expected_captures.txt @@ -16,44 +16,47 @@ Stderr: ############################ Command: cbrain version Status: 0 -Stdout: TOFIX bytes +Stdout: 30 bytes Stderr: 0 bytes Stdout: -TOFIX +cbrain cli client version 1.0 Stderr: (No output) ############################ Command: cbrain --json version -Status: 1 -Stdout: TOFIX bytes +Status: 0 +Stdout: 30 bytes Stderr: 0 bytes Stdout: -TOFIX +cbrain cli client version 1.0 Stderr: (No output) ############################ Command: cbrain whoami Status: 1 -Stdout: 50 bytes +Stdout: 63 bytes Stderr: 0 bytes Stdout: -Not logged in. Use 'cbrain login' to login first. +Credential file is missing. Use 'cbrain login' to login first. Stderr: (No output) ############################ Command: cbrain --json whoami Status: 1 -Stdout: 50 bytes +Stdout: 66 bytes Stderr: 0 bytes Stdout: -TOFIX +{ + "error": "Credential file is missing", + "logged_in": false +} Stderr: (No output) @@ -75,18 +78,18 @@ Stdout: 30 bytes Stderr: 0 bytes Stdout: -cbrain cli client version 0.9 +cbrain cli client version 1.0 Stderr: (No output) ############################ Command: cbrain --json version Status: 0 -Stdout: TOFIX bytes +Stdout: 30 bytes Stderr: 0 bytes Stdout: -TOFIX +cbrain cli client version 1.0 Stderr: (No output) @@ -310,6 +313,15 @@ Name TestDP Type FlatDirLocalDataProvider Description Test DP +CONNECTION INFO +------------------------------ +Field Value +---------------- ----- +Remote User N/A +Remote Host N/A +Remote Directory N/A +Remote Port N/A + OWNERSHIP & STATUS ------------------------------ Field Value @@ -418,33 +430,48 @@ Stderr: ############################ Command: cbrain project show 10 Status: 0 -Stdout: TOFIX bytes +Stdout: 171 bytes Stderr: 0 bytes Stdout: -TOFIX +PROJECT DETAILS +------------------------------ +Field Value +--------- --------- +ID 10 +Name NormTest1 +Type WorkGroup +Site ID None +Invisible False Stderr: (No output) ############################ Command: cbrain --json project show 10 Status: 0 -Stdout: TOFIX bytes +Stdout: 125 bytes Stderr: 0 bytes Stdout: -TOFIX +{ + "id": 10, + "name": "NormTest1", + "description": null, + "type": "WorkGroup", + "site_id": null, + "invisible": false +} Stderr: (No output) ############################ Command: cbrain --jsonl project show 10 Status: 0 -Stdout: TOFIX bytes +Stdout: 100 bytes Stderr: 0 bytes Stdout: -TOFIX +{"id":10,"name":"NormTest1","description":null,"type":"WorkGroup","site_id":null,"invisible":false} Stderr: (No output) @@ -462,55 +489,55 @@ Stderr: ############################ Command: cbrain --json project switch 10 Status: 0 -Stdout: TOFIX bytes +Stdout: 37 bytes Stderr: 0 bytes Stdout: -TOFIX +Current project is "NormTest1" ID=10 Stderr: (No output) ############################ Command: cbrain --jsonl project switch 10 Status: 0 -Stdout: TOFIX bytes +Stdout: 37 bytes Stderr: 0 bytes Stdout: -TOFIX +Current project is "NormTest1" ID=10 Stderr: (No output) ############################ Command: cbrain project switch all # 'all' not yet implemented as of Aug 2025 Status: 0 -Stdout: TOFIX bytes +Stdout: 56 bytes Stderr: 0 bytes Stdout: -TOFIX +Project switch 'all' not yet implemented as of Aug 2025 Stderr: (No output) ############################ Command: cbrain --json project switch all Status: 0 -Stdout: TOFIX bytes +Stdout: 56 bytes Stderr: 0 bytes Stdout: -TOFIX +Project switch 'all' not yet implemented as of Aug 2025 Stderr: (No output) ############################ Command: cbrain --jsonl project switch all Status: 0 -Stdout: TOFIX bytes +Stdout: 56 bytes Stderr: 0 bytes Stdout: -TOFIX +Project switch 'all' not yet implemented as of Aug 2025 Stderr: (No output) @@ -528,17 +555,17 @@ Stderr: ############################ Command: cbrain tag list Status: 0 -Stdout: 219 bytes +Stdout: 179 bytes Stderr: 0 bytes Stdout: TAGS ------------------------------------------------------------- +---------------------------------------- ID Name User Group -- ------ ---- ----- 21 tag1 2 3 99 tagdel 2 3 ------------------------------------------------------------- +---------------------------------------- Total: 2 tag(s) Stderr: (No output) @@ -644,12 +671,12 @@ Stderr: ############################ Command: cbrain tag update 99 --name Renamed --user-id 2 --group-id 3 -Status: 0 -Stdout: TOFIX bytes +Status: 1 +Stdout: 37 bytes Stderr: 0 bytes Stdout: -TOFIX +Failed: Invalid response from server Stderr: (No output) @@ -674,11 +701,10 @@ Stderr: ############################ Command: cbrain tag delete 99 Status: 0 -Stdout: 30 bytes +Stdout: 29 bytes Stderr: 0 bytes Stdout: -TOFIX (why the blank line?) Tag 99 deleted successfully! Stderr: (No output) @@ -690,30 +716,29 @@ Stdout: 26 bytes Stderr: 0 bytes Stdout: -TAG CREATED SUCCESSFULLY! -TOFIX: why is the message all in uppercase? +Tag created successfully! Stderr: (No output) ############################ Command: cbrain --json tag create --name NewTag2 --user-id 2 --group-id 3 Status: 0 -Stdout: TOFIX bytes +Stdout: 26 bytes Stderr: 0 bytes Stdout: -TOFIX +Tag created successfully! Stderr: (No output) ############################ Command: cbrain --jsonl tag create --name NewTag3 --user-id 2 --group-id 3 Status: 0 -Stdout: TOFIX bytes +Stdout: 26 bytes Stderr: 0 bytes Stdout: -TOFIX +Tag created successfully! Stderr: (No output) @@ -926,12 +951,13 @@ Stderr: ############################ Command: cbrain task operation # should provide error message Status: 0 -Stdout: TOFIX bytes +Stdout: 41 bytes Stderr: 0 bytes Stdout: -"{\"message\":\"No operation selected\"}" -TOFIX : the output is a quoted string with JSON content? +{ + "message": "No operation selected" +} Stderr: (No output) @@ -1007,11 +1033,17 @@ Stderr: ############################ Command: cbrain tool-config show 19 # not visible to normal user norm Status: 0 -Stdout: TOFIX bytes +Stdout: 102 bytes Stderr: 0 bytes Stdout: -TOFIX : The output is in JSON +id: 19 +version_name: admin1 +tool_id: 17 +bourreau_id: 14 +group_id: 2 +ncpus: 99 +description: admin_only Stderr: (No output) @@ -1037,11 +1069,11 @@ Stderr: ############################ Command: cbrain --jsonl tool-config show 19 Status: 0 -Stdout: TOFIX bytes +Stdout: 115 bytes Stderr: 0 bytes Stdout: -TOFIX : the output is in JSON +{"id":19,"version_name":"admin1","description":"admin_only","tool_id":17,"bourreau_id":14,"group_id":2,"ncpus":99} Stderr: (No output) @@ -1106,33 +1138,36 @@ Stderr: ############################ Command: cbrain tool-config show 19 # visible to user admin Status: 1 -Stdout: 32 bytes +Stdout: 107 bytes Stderr: 0 bytes Stdout: -Connection failed: Unauthorized +Authentication error (401): Unauthorized +Error: Access denied. Please log in using authorized credentials. Stderr: (No output) ############################ Command: cbrain --json tool-config show 19 Status: 1 -Stdout: TOFIX bytes +Stdout: 107 bytes Stderr: 0 bytes Stdout: -TOFIX Connection failed: Unauthorized +Authentication error (401): Unauthorized +Error: Access denied. Please log in using authorized credentials. Stderr: (No output) ############################ Command: cbrain --jsonl tool-config show 19 Status: 1 -Stdout: TOFIX bytes +Stdout: 107 bytes Stderr: 0 bytes Stdout: -TOFIX Connection failed: Unauthorized +Authentication error (401): Unauthorized +Error: Access denied. Please log in using authorized credentials. Stderr: (No output) @@ -1560,15 +1595,12 @@ Stderr: ############################ Command: cbrain file delete 5 Status: 0 -Stdout: 96 bytes +Stdout: 71 bytes Stderr: 0 bytes Stdout: -TOFIX why in JSON ? -{ - "message": "Your files are being deleted in background.", - "background_activity_id": 265 -} +Your files are being deleted in background. +Background activity ID: 44 Stderr: (No output) @@ -1580,7 +1612,7 @@ Stderr: 0 bytes Stdout: Your files are being moved in the background. -Background activity ID: 47 +Background activity ID: 45 Stderr: (No output) diff --git a/cbrain_cli/cli_utils.py b/cbrain_cli/cli_utils.py index 48b17b5..cc8e7a6 100644 --- a/cbrain_cli/cli_utils.py +++ b/cbrain_cli/cli_utils.py @@ -84,7 +84,7 @@ def handle_connection_error(error): if error.code == 401: print(f"{status_description}: {error.reason}") - print("Try with Authorized Access") + print("Error: Access denied. Please log in using authorized credentials.") elif error.code == 404 or error.code == 422 or error.code == 500: # Try to extract specific error message from response try: diff --git a/cbrain_cli/data/projects.py b/cbrain_cli/data/projects.py index d639dc2..58ff27b 100644 --- a/cbrain_cli/data/projects.py +++ b/cbrain_cli/data/projects.py @@ -26,6 +26,18 @@ def switch_project(args): print("Error: Group ID is required") return None + # Handle the special case of "all" + if group_id == "all": + print("Project switch 'all' not yet implemented as of Aug 2025") + return None + + # Convert to integer for regular group IDs + try: + group_id = int(group_id) + except ValueError: + print(f"Error: Invalid group ID '{group_id}'. Must be a number or 'all'") + return None + # Step 1: Call the switch API switch_endpoint = f"{cbrain_url}/groups/switch?id={group_id}" headers = auth_headers(api_token) @@ -59,48 +71,70 @@ def switch_project(args): def show_project(args): """ - Get the current project/group from credentials. + Get the current project/group from credentials or show a specific project by ID. Parameters ---------- args : argparse.Namespace - Command line arguments + Command line arguments, may include project_id Returns ------- dict or None Dictionary containing project details if successful, None if no project set """ - with open(CREDENTIALS_FILE) as f: - credentials = json.load(f) - - current_group_id = credentials.get("current_group_id") - if not current_group_id: - return None - - # Get fresh group details from server - group_endpoint = f"{cbrain_url}/groups/{current_group_id}" - headers = auth_headers(api_token) - - request = urllib.request.Request(group_endpoint, data=None, headers=headers, method="GET") - - try: - with urllib.request.urlopen(request) as response: - data = response.read().decode("utf-8") - group_data = json.loads(data) - return group_data - - except urllib.error.HTTPError as e: - if e.code == 404: - print(f"Error: Current project (ID {current_group_id}) no longer exists") - # Clear the invalid group_id from credentials - 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) + # Check if a specific project ID was provided + project_id = getattr(args, "project_id", None) + + if project_id: + # Show specific project by ID + group_endpoint = f"{cbrain_url}/groups/{project_id}" + headers = auth_headers(api_token) + request = urllib.request.Request(group_endpoint, data=None, headers=headers, method="GET") + + try: + with urllib.request.urlopen(request) as response: + data = response.read().decode("utf-8") + group_data = json.loads(data) + return group_data + except urllib.error.HTTPError as e: + if e.code == 404: + print(f"Error: Project with ID {project_id} not found") + return None + else: + raise + else: + # Show current project from credentials + with open(CREDENTIALS_FILE) as f: + credentials = json.load(f) + + current_group_id = credentials.get("current_group_id") + if not current_group_id: return None - else: - raise + + # Get fresh group details from server + group_endpoint = f"{cbrain_url}/groups/{current_group_id}" + headers = auth_headers(api_token) + + request = urllib.request.Request(group_endpoint, data=None, headers=headers, method="GET") + + try: + with urllib.request.urlopen(request) as response: + data = response.read().decode("utf-8") + group_data = json.loads(data) + return group_data + + except urllib.error.HTTPError as e: + if e.code == 404: + print(f"Error: Current project (ID {current_group_id}) no longer exists") + # Clear the invalid group_id from credentials + 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) + return None + else: + raise def list_projects(args): diff --git a/cbrain_cli/data/tasks.py b/cbrain_cli/data/tasks.py index 63fa235..58e5453 100644 --- a/cbrain_cli/data/tasks.py +++ b/cbrain_cli/data/tasks.py @@ -98,4 +98,5 @@ def operation_task(args): with urllib.request.urlopen(request) as response: data = response.read().decode("utf-8") - json_printer(data) + parsed_data = json.loads(data) + json_printer(parsed_data) diff --git a/cbrain_cli/formatter/files_fmt.py b/cbrain_cli/formatter/files_fmt.py index 391e80a..8946b56 100644 --- a/cbrain_cli/formatter/files_fmt.py +++ b/cbrain_cli/formatter/files_fmt.py @@ -112,3 +112,33 @@ def print_move_copy_result(response_data, response_status, operation="move"): print(f"File {operation} initiated successfully") else: print(f"File {operation} failed with status: {response_status}") + + +def print_delete_result(response_data, args): + """ + Print the result of a file delete operation. + + Parameters + ---------- + response_data : dict + Response data from the server + args : argparse.Namespace + Command line arguments, including the --json flag + """ + if getattr(args, "json", False): + json_printer(response_data) + return + elif getattr(args, "jsonl", False): + jsonl_printer(response_data) + return + + # Show user-friendly message for normal output + message = response_data.get("message", "").strip() + if message: + print(message) + + background_activity_id = response_data.get("background_activity_id") + if background_activity_id: + print(f"Background activity ID: {background_activity_id}") + else: + print("File deletion initiated successfully") diff --git a/cbrain_cli/formatter/projects_fmt.py b/cbrain_cli/formatter/projects_fmt.py index 9ded41c..ef778b2 100644 --- a/cbrain_cli/formatter/projects_fmt.py +++ b/cbrain_cli/formatter/projects_fmt.py @@ -45,6 +45,49 @@ def print_current_project(project_data): print(f'Current project is "{group_name}" ID={group_id}') +def print_project_details(project_data, args): + """ + Print detailed information about a specific project. + + Parameters + ---------- + project_data : dict + Dictionary containing project details + args : argparse.Namespace + Command line arguments, including the --json flag + """ + if getattr(args, "json", False): + json_printer(project_data) + return + elif getattr(args, "jsonl", False): + jsonl_printer(project_data) + return + + print("PROJECT DETAILS") + print("-" * 30) + + # Basic project information + basic_info = [ + {"field": "ID", "value": str(project_data.get("id", "N/A"))}, + {"field": "Name", "value": str(project_data.get("name", "N/A"))}, + {"field": "Type", "value": str(project_data.get("type", "N/A"))}, + {"field": "Site ID", "value": str(project_data.get("site_id", "N/A"))}, + {"field": "Invisible", "value": str(project_data.get("invisible", "N/A"))}, + ] + + dynamic_table_print(basic_info, ["field", "value"], ["Field", "Value"]) + + # Display description if available + if project_data.get("description"): + print() + print("DESCRIPTION") + print("-" * 30) + description = project_data.get("description").strip() + # Handle multi-line descriptions + for line in description.split("\n"): + print(f"{line}") + + def print_no_project(): """ Print message when no current project is set. diff --git a/cbrain_cli/handlers.py b/cbrain_cli/handlers.py new file mode 100644 index 0000000..83c0ccf --- /dev/null +++ b/cbrain_cli/handlers.py @@ -0,0 +1,306 @@ +""" +Command handlers for the CBRAIN CLI. + +This module contains all the handler functions that process CLI commands +and format their output appropriately. +""" + +from cbrain_cli.cli_utils import json_printer +from cbrain_cli.data.background_activities import ( + list_background_activities, + show_background_activity, +) +from cbrain_cli.data.data_providers import ( + delete_unregistered_files, + is_alive, + list_data_providers, + show_data_provider, +) +from cbrain_cli.data.files import ( + copy_file, + delete_file, + list_files, + move_file, + show_file, + upload_file, +) +from cbrain_cli.data.projects import list_projects, show_project, switch_project +from cbrain_cli.data.remote_resources import list_remote_resources, show_remote_resource +from cbrain_cli.data.tags import create_tag, delete_tag, list_tags, show_tag, update_tag +from cbrain_cli.data.tasks import list_tasks, show_task +from cbrain_cli.data.tool_configs import ( + list_tool_configs, + show_tool_config, + tool_config_boutiques_descriptor, +) +from cbrain_cli.data.tools import list_tools +from cbrain_cli.formatter.background_activities_fmt import ( + print_activities_list, + print_activity_details, +) +from cbrain_cli.formatter.data_providers_fmt import print_provider_details, print_providers_list +from cbrain_cli.formatter.files_fmt import ( + print_delete_result, + print_file_details, + print_files_list, + print_move_copy_result, + print_upload_result, +) +from cbrain_cli.formatter.projects_fmt import ( + print_current_project, + print_no_project, + print_projects_list, +) +from cbrain_cli.formatter.remote_resources_fmt import print_resource_details, print_resources_list +from cbrain_cli.formatter.tags_fmt import ( + print_tag_details, + print_tag_operation_result, + print_tags_list, +) +from cbrain_cli.formatter.tasks_fmt import print_task_data, print_task_details +from cbrain_cli.formatter.tool_configs_fmt import ( + print_boutiques_descriptor, + print_tool_config_details, + print_tool_configs_list, +) +from cbrain_cli.formatter.tools_fmt import print_tool_details, print_tools_list + + +# File command handlers +def handle_file_list(args): + """ + Retrieve and display a paginated list of files from CBRAIN with optional filtering. + """ + result = list_files(args) + if result: + print_files_list(result, args) + + +def handle_file_show(args): + """ + Retrieve and display detailed information about a specific file by its ID. + """ + result = show_file(args) + if result: + print_file_details(result, args) + + +def handle_file_upload(args): + """Upload a local file to CBRAIN and display the upload result with file details.""" + result = upload_file(args) + if result: + print_upload_result(*result) + + +def handle_file_copy(args): + """Copy one or more files to a different data provider and display the operation results.""" + result = copy_file(args) + if result: + print_move_copy_result(*result, operation="copy") + + +def handle_file_move(args): + """Move one or more files to a different data provider and display the operation results.""" + result = move_file(args) + if result: + print_move_copy_result(*result, operation="move") + + +def handle_file_delete(args): + """Delete a specific file from CBRAIN and display the deletion status.""" + result = delete_file(args) + if result: + print_delete_result(result, args) + + +# Data provider command handlers +def handle_dataprovider_list(args): + """Retrieve and display a paginated list of available data providers in CBRAIN.""" + result = list_data_providers(args) + print_providers_list(result, args) + + +def handle_dataprovider_show(args): + """Retrieve and display detailed information about a specific data provider.""" + result = show_data_provider(args) + print_provider_details(result, args) + + +def handle_dataprovider_is_alive(args): + """Check and display the connectivity status of a specific data provider.""" + result = is_alive(args) + json_printer(result) + + +def handle_dataprovider_delete_unregistered(args): + """Remove unregistered files from a data provider and display the cleanup results.""" + result = delete_unregistered_files(args) + json_printer(result) + + +# Project command handlers +def handle_project_list(args): + """Retrieve and display a list of all available projects (groups) in CBRAIN.""" + result = list_projects(args) + print_projects_list(result, args) + + +def handle_project_switch(args): + """Switch the current working context to a different project and confirm the change.""" + result = switch_project(args) + if result: + print_current_project(result) + + +def handle_project_show(args): + """Display information about the currently active project or a specific project by ID.""" + result = show_project(args) + if result: + # Check if a specific project ID was requested + project_id = getattr(args, "project_id", None) + if project_id: + # Show detailed project information for specific project + from cbrain_cli.formatter.projects_fmt import print_project_details + + print_project_details(result, args) + else: + # Show current project information + print_current_project(result) + else: + # Only show "no project" message if no specific ID was requested + project_id = getattr(args, "project_id", None) + if not project_id: + print_no_project() + + +def handle_project_unswitch(args): + """Unswitch from current project context.""" + print("Project Unswitch 'all' not yet implemented as of Aug 2025") + + +# Tool command handlers +def handle_tool_show(args): + """Retrieve and display detailed information about a specific computational tool.""" + result = list_tools(args) + if result: + print_tool_details(result, args) + + +def handle_tool_list(args): + """Retrieve and display a paginated list of available computational tools in CBRAIN.""" + result = list_tools(args) + if result: + print_tools_list(result, args) + + +# Tool config command handlers +def handle_tool_config_list(args): + """Retrieve and display a paginated list of tool configurations available in CBRAIN.""" + result = list_tool_configs(args) + print_tool_configs_list(result, args) + + +def handle_tool_config_show(args): + """Retrieve and display detailed configuration settings for a specific tool.""" + result = show_tool_config(args) + if result: + print_tool_config_details(result, args) + + +def handle_tool_config_boutiques_descriptor(args): + """Retrieve and display the Boutiques descriptor JSON for a specific tool configuration.""" + result = tool_config_boutiques_descriptor(args) + if result: + print_boutiques_descriptor(result, args) + + +# Tag command handlers +def handle_tag_list(args): + """Retrieve and display a paginated list of tags available in CBRAIN.""" + result = list_tags(args) + print_tags_list(result, args) + + +def handle_tag_show(args): + """Retrieve and display detailed information about a specific tag by its ID.""" + result = show_tag(args) + if result: + print_tag_details(result, args) + + +def handle_tag_create(args): + """Create a new tag with specified name, user, and group, then display the creation result.""" + result = create_tag(args) + if result: + print_tag_operation_result( + "create", success=result[1], error_msg=result[2], response_status=result[3] + ) + + +def handle_tag_update(args): + """Update an existing tag's properties and display the modification result.""" + result = update_tag(args) + if result: + print_tag_operation_result( + "update", + tag_id=args.tag_id, + success=result[1], + error_msg=result[2], + response_status=result[3], + ) + + +def handle_tag_delete(args): + """Delete a specific tag from CBRAIN and display the deletion result.""" + result = delete_tag(args) + if result: + print_tag_operation_result( + "delete", + tag_id=args.tag_id, + success=result[0], + error_msg=result[1], + response_status=result[2], + ) + + +# Background activity command handlers +def handle_background_list(args): + """Retrieve and display a list of background activities currently running in CBRAIN.""" + result = list_background_activities(args) + if result: + print_activities_list(result, args) + + +def handle_background_show(args): + """Retrieve and display detailed information about a specific background activity.""" + result = show_background_activity(args) + if result: + print_activity_details(result, args) + + +# Task command handlers +def handle_task_list(args): + """Retrieve and display a paginated list of computational tasks with optional filtering.""" + result = list_tasks(args) + print_task_data(result, args) + + +def handle_task_show(args): + """Retrieve and display detailed information about a specific computational task.""" + result = show_task(args) + if result: + print_task_details(result, args) + + +# Remote resource command handlers +def handle_remote_resource_list(args): + """Retrieve and display a list of remote computational resources available in CBRAIN.""" + result = list_remote_resources(args) + print_resources_list(result, args) + + +def handle_remote_resource_show(args): + """Retrieve and display detailed information about a specific remote computational resource.""" + result = show_remote_resource(args) + if result: + print_resource_details(result, args) diff --git a/cbrain_cli/main.py b/cbrain_cli/main.py index 4cfc7e8..41e2d07 100644 --- a/cbrain_cli/main.py +++ b/cbrain_cli/main.py @@ -5,64 +5,40 @@ import argparse import sys -from cbrain_cli.cli_utils import handle_errors, is_authenticated, json_printer, version_info -from cbrain_cli.data.background_activities import ( - list_background_activities, - show_background_activity, +from cbrain_cli.cli_utils import handle_errors, is_authenticated, version_info +from cbrain_cli.data.tasks import operation_task +from cbrain_cli.handlers import ( + handle_background_list, + handle_background_show, + handle_dataprovider_delete_unregistered, + handle_dataprovider_is_alive, + handle_dataprovider_list, + handle_dataprovider_show, + handle_file_copy, + handle_file_delete, + handle_file_list, + handle_file_move, + handle_file_show, + handle_file_upload, + handle_project_list, + handle_project_show, + handle_project_switch, + handle_project_unswitch, + handle_remote_resource_list, + handle_remote_resource_show, + handle_tag_create, + handle_tag_delete, + handle_tag_list, + handle_tag_show, + handle_tag_update, + handle_task_list, + handle_task_show, + handle_tool_config_boutiques_descriptor, + handle_tool_config_list, + handle_tool_config_show, + handle_tool_list, + handle_tool_show, ) -from cbrain_cli.data.data_providers import ( - delete_unregistered_files, - is_alive, - list_data_providers, - show_data_provider, -) -from cbrain_cli.data.files import ( - copy_file, - delete_file, - list_files, - move_file, - show_file, - upload_file, -) -from cbrain_cli.data.projects import list_projects, show_project, switch_project -from cbrain_cli.data.remote_resources import list_remote_resources, show_remote_resource -from cbrain_cli.data.tags import create_tag, delete_tag, list_tags, show_tag, update_tag -from cbrain_cli.data.tasks import list_tasks, operation_task, show_task -from cbrain_cli.data.tool_configs import ( - list_tool_configs, - show_tool_config, - tool_config_boutiques_descriptor, -) -from cbrain_cli.data.tools import list_tools -from cbrain_cli.formatter.background_activities_fmt import ( - print_activities_list, - print_activity_details, -) -from cbrain_cli.formatter.data_providers_fmt import print_provider_details, print_providers_list -from cbrain_cli.formatter.files_fmt import ( - print_file_details, - print_files_list, - print_move_copy_result, - print_upload_result, -) -from cbrain_cli.formatter.projects_fmt import ( - print_current_project, - print_no_project, - print_projects_list, -) -from cbrain_cli.formatter.remote_resources_fmt import print_resource_details, print_resources_list -from cbrain_cli.formatter.tags_fmt import ( - print_tag_details, - print_tag_operation_result, - print_tags_list, -) -from cbrain_cli.formatter.tasks_fmt import print_task_data, print_task_details -from cbrain_cli.formatter.tool_configs_fmt import ( - print_boutiques_descriptor, - print_tool_config_details, - print_tool_configs_list, -) -from cbrain_cli.formatter.tools_fmt import print_tool_details, print_tools_list from cbrain_cli.sessions import create_session, logout_session from cbrain_cli.users import whoami_user @@ -121,24 +97,12 @@ def main(): file_list_parser.add_argument( "--per-page", type=int, default=25, help="Number of files per page (5-1000, default: 25)" ) - file_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_files_list(result, args) if result else None)( - list_files(args) - ) - ) - ) + file_list_parser.set_defaults(func=handle_errors(handle_file_list)) # file show file_show_parser = file_subparsers.add_parser("show", help="Show file details") file_show_parser.add_argument("file", type=int, help="File ID") - file_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_file_details(result, args) if result else None)( - show_file(args) - ) - ) - ) + file_show_parser.set_defaults(func=handle_errors(handle_file_show)) # file upload file_upload_parser = file_subparsers.add_parser("upload", help="Upload a file to CBRAIN") @@ -148,11 +112,7 @@ def main(): ) file_upload_parser.add_argument("--group-id", type=int, help="Group ID") - file_upload_parser.set_defaults( - func=handle_errors( - lambda args: print_upload_result(*result) if (result := upload_file(args)) else None - ) - ) + file_upload_parser.set_defaults(func=handle_errors(handle_file_upload)) # file copy file_copy_parser = file_subparsers.add_parser( @@ -168,13 +128,7 @@ def main(): file_copy_parser.add_argument( "--dp-id", type=int, required=True, help="Destination data provider ID" ) - file_copy_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_move_copy_result(*result, operation="copy") if result else None - )(copy_file(args)) - ) - ) + file_copy_parser.set_defaults(func=handle_errors(handle_file_copy)) # file move file_move_parser = file_subparsers.add_parser( @@ -190,24 +144,12 @@ def main(): file_move_parser.add_argument( "--dp-id", type=int, required=True, help="Destination data provider ID" ) - file_move_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_move_copy_result(*result, operation="move") if result else None - )(move_file(args)) - ) - ) + file_move_parser.set_defaults(func=handle_errors(handle_file_move)) # file delete file_delete_parser = file_subparsers.add_parser("delete", help="Delete a file") file_delete_parser.add_argument("file_id", type=int, help="ID of the file to delete") - file_delete_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: json_printer(result) if result else None)( - delete_file(args) - ) - ) - ) + file_delete_parser.set_defaults(func=handle_errors(handle_file_delete)) # Data provider commands dataprovider_parser = subparsers.add_parser("dataprovider", help="Data provider operations") @@ -219,13 +161,7 @@ def main(): dataprovider_list_parser = dataprovider_subparsers.add_parser( "list", help="List data providers" ) - dataprovider_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_providers_list(result, args))( - list_data_providers(args) - ) - ) - ) + dataprovider_list_parser.set_defaults(func=handle_errors(handle_dataprovider_list)) dataprovider_list_parser.add_argument( "--page", type=int, default=1, help="Page number (default: 1)" @@ -241,22 +177,14 @@ def main(): "show", help="Show data provider details" ) dataprovider_show_parser.add_argument("id", type=int, help="Data provider ID") - dataprovider_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_provider_details(result, args))( - show_data_provider(args) - ) - ) - ) + dataprovider_show_parser.set_defaults(func=handle_errors(handle_dataprovider_show)) # dataprovider is_alive dataprovider_is_alive_parser = dataprovider_subparsers.add_parser( "is-alive", help="Check if a data provider is alive" ) dataprovider_is_alive_parser.add_argument("id", type=int, help="Data provider ID") - dataprovider_is_alive_parser.set_defaults( - func=handle_errors(lambda args: (lambda result: json_printer(result))(is_alive(args))) - ) + dataprovider_is_alive_parser.set_defaults(func=handle_errors(handle_dataprovider_is_alive)) # dataprovider delete-unregistered-files dataprovider_delete_unregistered_files_parser = dataprovider_subparsers.add_parser( @@ -267,9 +195,7 @@ def main(): "id", type=int, help="Data provider ID" ) dataprovider_delete_unregistered_files_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: json_printer(result))(delete_unregistered_files(args)) - ) + func=handle_errors(handle_dataprovider_delete_unregistered) ) # Project commands @@ -278,32 +204,27 @@ def main(): # project list project_list_parser = project_subparsers.add_parser("list", help="List projects") - project_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_projects_list(result, args))(list_projects(args)) - ) - ) + project_list_parser.set_defaults(func=handle_errors(handle_project_list)) # project switch project_switch_parser = project_subparsers.add_parser("switch", help="Switch to a project") - project_switch_parser.add_argument("group_id", type=int, help="Project/Group ID") - project_switch_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_current_project(result) if result else None)( - switch_project(args) - ) - ) - ) + project_switch_parser.add_argument("group_id", help="Project/Group ID or 'all'") + project_switch_parser.set_defaults(func=handle_errors(handle_project_switch)) # project show - project_show_parser = project_subparsers.add_parser("show", help="Show current project") - project_show_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_current_project(result) if result else print_no_project() - )(show_project(args)) - ) + project_show_parser = project_subparsers.add_parser( + "show", help="Show current project or specific project by ID" + ) + project_show_parser.add_argument( + "project_id", type=int, nargs="?", help="Project ID to show (optional)" ) + project_show_parser.set_defaults(func=handle_errors(handle_project_show)) + + # project unswitch + project_unswitch_parser = project_subparsers.add_parser( + "unswitch", help="Unswitch from current project" + ) + project_unswitch_parser.set_defaults(func=handle_errors(handle_project_unswitch)) # Tool commands tool_parser = subparsers.add_parser("tool", help="Tool operations") @@ -312,13 +233,7 @@ def main(): # tool show tool_show_parser = tool_subparsers.add_parser("show", help="Show tool details") tool_show_parser.add_argument("id", type=int, help="Tool ID") - tool_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_tool_details(result, args) if result else None)( - list_tools(args) - ) - ) - ) + tool_show_parser.set_defaults(func=handle_errors(handle_tool_show)) # tool list (reusing show_tool without id) tool_list_parser = tool_subparsers.add_parser("list", help="List all tools") @@ -326,13 +241,7 @@ def main(): tool_list_parser.add_argument( "--per-page", type=int, default=25, help="Number of tools per page (5-1000, default: 25)" ) - tool_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_tools_list(result, args) if result else None)( - list_tools(args) - ) - ) - ) + tool_list_parser.set_defaults(func=handle_errors(handle_tool_list)) ## MARK: tool-config commands tool_configs_parser = subparsers.add_parser("tool-config", help="Tool configuration operations") @@ -344,13 +253,7 @@ def main(): tool_configs_list_parser = tool_configs_subparsers.add_parser( "list", help="List all tool configurations" ) - tool_configs_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_tool_configs_list(result, args))( - list_tool_configs(args) - ) - ) - ) + tool_configs_list_parser.set_defaults(func=handle_errors(handle_tool_config_list)) tool_configs_list_parser.add_argument( "--page", type=int, default=1, help="Page number (default: 1)" @@ -367,13 +270,7 @@ def main(): "show", help="Show tool configuration details" ) tool_configs_show_parser.add_argument("id", type=int, help="Tool configuration ID") - tool_configs_show_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_tool_config_details(result, args) if result else None - )(show_tool_config(args)) - ) - ) + tool_configs_show_parser.set_defaults(func=handle_errors(handle_tool_config_show)) # tool-config boutiques-descriptor tool_configs_boutiques_parser = tool_configs_subparsers.add_parser( @@ -381,11 +278,7 @@ def main(): ) tool_configs_boutiques_parser.add_argument("id", type=int, help="Tool configuration ID") tool_configs_boutiques_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_boutiques_descriptor(result, args) if result else None - )(tool_config_boutiques_descriptor(args)) - ) + func=handle_errors(handle_tool_config_boutiques_descriptor) ) # Tag commands @@ -394,11 +287,7 @@ def main(): # tag list tag_list_parser = tag_subparsers.add_parser("list", help="List tags") - tag_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_tags_list(result, args))(list_tags(args)) - ) - ) + tag_list_parser.set_defaults(func=handle_errors(handle_tag_list)) tag_list_parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)") tag_list_parser.add_argument( @@ -408,30 +297,14 @@ def main(): # tag show tag_show_parser = tag_subparsers.add_parser("show", help="Show tag details") tag_show_parser.add_argument("id", type=int, help="Tag ID") - tag_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_tag_details(result, args) if result else None)( - show_tag(args) - ) - ) - ) + tag_show_parser.set_defaults(func=handle_errors(handle_tag_show)) # tag create tag_create_parser = tag_subparsers.add_parser("create", help="Create a new tag") tag_create_parser.add_argument("--name", type=str, required=True, help="Tag name") tag_create_parser.add_argument("--user-id", type=int, required=True, help="User ID") tag_create_parser.add_argument("--group-id", type=int, required=True, help="Group ID") - tag_create_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_tag_operation_result( - "create", success=result[1], error_msg=result[2], response_status=result[3] - ) - if result - else None - )(create_tag(args)) - ) - ) + tag_create_parser.set_defaults(func=handle_errors(handle_tag_create)) # tag update tag_update_parser = tag_subparsers.add_parser("update", help="Update an existing tag") @@ -443,21 +316,7 @@ def main(): tag_update_parser.add_argument("--name", type=str, required=True, help="Tag name") tag_update_parser.add_argument("--user-id", type=int, required=True, help="User ID") tag_update_parser.add_argument("--group-id", type=int, required=True, help="Group ID") - tag_update_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_tag_operation_result( - "update", - tag_id=args.tag_id, - success=result[1], - error_msg=result[2], - response_status=result[3], - ) - if result - else None - )(update_tag(args)) - ) - ) + tag_update_parser.set_defaults(func=handle_errors(handle_tag_update)) # tag delete tag_delete_parser = tag_subparsers.add_parser("delete", help="Delete a tag") @@ -466,21 +325,7 @@ def main(): type=int, help="Tag ID to delete", ) - tag_delete_parser.set_defaults( - func=handle_errors( - lambda args: ( - lambda result: print_tag_operation_result( - "delete", - tag_id=args.tag_id, - success=result[0], - error_msg=result[1], - response_status=result[2], - ) - if result - else None - )(delete_tag(args)) - ) - ) + tag_delete_parser.set_defaults(func=handle_errors(handle_tag_delete)) # Background activity commands background_parser = subparsers.add_parser("background", help="Background activity operations") @@ -492,26 +337,14 @@ def main(): background_list_parser = background_subparsers.add_parser( "list", help="List background activities" ) - background_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_activities_list(result, args) if result else None)( - list_background_activities(args) - ) - ) - ) + background_list_parser.set_defaults(func=handle_errors(handle_background_list)) # background show background_show_parser = background_subparsers.add_parser( "show", help="Show background activity details" ) background_show_parser.add_argument("id", type=int, help="Background activity ID") - background_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_activity_details(result, args) if result else None)( - show_background_activity(args) - ) - ) - ) + background_show_parser.set_defaults(func=handle_errors(handle_background_show)) # Task commands task_parser = subparsers.add_parser("task", help="Task operations") @@ -532,22 +365,12 @@ def main(): nargs="?", help="Filter value (required if filter_type is specified)", ) - task_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_task_data(result, args))(list_tasks(args)) - ) - ) + task_list_parser.set_defaults(func=handle_errors(handle_task_list)) # task show task_show_parser = task_subparsers.add_parser("show", help="Show task details") task_show_parser.add_argument("task", type=int, help="Task ID") - task_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_task_details(result, args) if result else None)( - show_task(args) - ) - ) - ) + task_show_parser.set_defaults(func=handle_errors(handle_task_show)) # task operation task_operation_parser = task_subparsers.add_parser("operation", help="operation on a task") @@ -565,26 +388,14 @@ def main(): remote_resource_list_parser = remote_resource_subparsers.add_parser( "list", help="List remote resources" ) - remote_resource_list_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_resources_list(result, args))( - list_remote_resources(args) - ) - ) - ) + remote_resource_list_parser.set_defaults(func=handle_errors(handle_remote_resource_list)) # remote-resource show remote_resource_show_parser = remote_resource_subparsers.add_parser( "show", help="Show remote resource details" ) remote_resource_show_parser.add_argument("remote_resource", type=int, help="Remote resource ID") - remote_resource_show_parser.set_defaults( - func=handle_errors( - lambda args: (lambda result: print_resource_details(result, args) if result else None)( - show_remote_resource(args) - ) - ) - ) + remote_resource_show_parser.set_defaults(func=handle_errors(handle_remote_resource_show)) # MARK: Setup CLI args = parser.parse_args() @@ -593,9 +404,13 @@ def main(): parser.print_help() return - # Handle session commands (no authentication needed for login). + # Handle session commands (no authentication needed for login, version, and whoami). if args.command == "login": return handle_errors(create_session)(args) + elif args.command == "version": + return handle_errors(version_info)(args) + elif args.command == "whoami": + return handle_errors(whoami_user)(args) # All other commands require authentication. if not is_authenticated(): @@ -604,10 +419,6 @@ def main(): # Handle authenticated commands. if args.command == "logout": return handle_errors(logout_session)(args) - elif args.command == "whoami": - return handle_errors(whoami_user)(args) - elif args.command == "version": - return handle_errors(version_info)(args) elif args.command in [ "file", "dataprovider", diff --git a/cbrain_cli/users.py b/cbrain_cli/users.py index 6d46133..d8bcaaf 100644 --- a/cbrain_cli/users.py +++ b/cbrain_cli/users.py @@ -46,6 +46,15 @@ def whoami_user(args): Prints current user information. """ version = getattr(args, "version", False) + + # Check if we have credentials first + if user_id is None or cbrain_url is None or api_token is None: + if getattr(args, "json", False): + json_printer({"error": "Credential file is missing", "logged_in": False}) + else: + print("Credential file is missing. Use 'cbrain login' to login first.") + return 1 + user_data = user_details(user_id) # Check if user_data is valid before proceeding