From 8d1dd54634314b075eedc87676e7a1e69fe2b068 Mon Sep 17 00:00:00 2001
From: axif
Date: Sat, 6 Sep 2025 03:41:39 +0600
Subject: [PATCH 01/14] Update README and enhance CLI error messages; add
command handlers for improved functionality
---
README.md | 12 +-
cbrain_cli/cli_utils.py | 2 +-
cbrain_cli/handlers.py | 288 ++++++++++++++++++++++++++++++++++
cbrain_cli/main.py | 333 ++++++++--------------------------------
cbrain_cli/users.py | 9 ++
5 files changed, 373 insertions(+), 271 deletions(-)
create mode 100644 cbrain_cli/handlers.py
diff --git a/README.md b/README.md
index d74ed8a..45d5858 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,11 @@
# CBRAIN CLI
+## **Google Summer of Code 2025 Project**
+
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.
@@ -26,7 +28,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
@@ -79,10 +81,14 @@ This CLI interfaces with the CBRAIN REST API. For complete API documentation and
## 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 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/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/handlers.py b/cbrain_cli/handlers.py
new file mode 100644
index 0000000..d216fd0
--- /dev/null
+++ b/cbrain_cli/handlers.py
@@ -0,0 +1,288 @@
+"""
+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_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:
+ json_printer(result)
+
+
+# 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 indicate if none is set."""
+ result = show_project(args)
+ if result:
+ print_current_project(result)
+ else:
+ print_no_project()
+
+
+# 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..c94c2dc 100644
--- a/cbrain_cli/main.py
+++ b/cbrain_cli/main.py
@@ -5,64 +5,39 @@
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_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 +96,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 +111,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 +127,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 +143,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 +160,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 +176,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 +194,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 +203,16 @@ 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.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.set_defaults(func=handle_errors(handle_project_show))
# Tool commands
tool_parser = subparsers.add_parser("tool", help="Tool operations")
@@ -312,13 +221,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 +229,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 +241,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 +258,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 +266,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 +275,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 +285,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 +304,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 +313,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 +325,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 +353,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 +376,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 +392,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 +407,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
From 26bef81c2753a763968d6e7422210615a6fc616d Mon Sep 17 00:00:00 2001
From: axif
Date: Sat, 6 Sep 2025 03:50:02 +0600
Subject: [PATCH 02/14] Update test captures: Fix TOFIX placeholders and
improve command outputs
- Update version output to 1.0
- Fix JSON command status codes and outputs
- Improve credential and auth error messages
- Fix argument parsing errors for invalid commands
- Clean up table formatting and output consistency
- Remove debug artifacts and TOFIX comments
---
capture_tests/expected_captures.txt | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 233ff2b..4645744 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -16,11 +16,11 @@ 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)
From 7b8a33457841d612721b486f60f18cb4de05a8b5 Mon Sep 17 00:00:00 2001
From: axif
Date: Sat, 6 Sep 2025 04:12:17 +0600
Subject: [PATCH 03/14] Test fix
---
capture_tests/expected_captures.txt | 42 ++++++++++++++---------------
cbrain_cli/data/tasks.py | 3 ++-
2 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 4645744..48c36a1 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -27,11 +27,11 @@ Stderr:
############################
Command: cbrain --json version
Status: 1
-Stdout: TOFIX bytes
+Stdout: 30 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+cbrain cli client version 1.0
Stderr:
(No output)
@@ -82,11 +82,11 @@ Stderr:
############################
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)
@@ -462,22 +462,22 @@ 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)
@@ -528,17 +528,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)
@@ -645,7 +645,7 @@ Stderr:
############################
Command: cbrain tag update 99 --name Renamed --user-id 2 --group-id 3
Status: 0
-Stdout: TOFIX bytes
+Stdout: 37 bytes
Stderr: 0 bytes
Stdout:
@@ -678,7 +678,6 @@ Stdout: 30 bytes
Stderr: 0 bytes
Stdout:
-TOFIX (why the blank line?)
Tag 99 deleted successfully!
Stderr:
(No output)
@@ -690,30 +689,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,11 +924,11 @@ Stderr:
############################
Command: cbrain task operation # should provide error message
Status: 0
-Stdout: TOFIX bytes
+Stdout: 42 bytes
Stderr: 0 bytes
Stdout:
-"{\"message\":\"No operation selected\"}"
+{"message":"No operation selected"}
TOFIX : the output is a quoted string with JSON content?
Stderr:
(No output)
@@ -1007,7 +1005,7 @@ 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:
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)
From e7e321d70db974d0491295762ef94b1097b8aff2 Mon Sep 17 00:00:00 2001
From: axif
Date: Sat, 6 Sep 2025 04:30:58 +0600
Subject: [PATCH 04/14] Enhance file deletion output formatting and update
expected capture results
---
capture_tests/expected_captures.txt | 36 ++++++++++++++---------------
cbrain_cli/formatter/files_fmt.py | 30 ++++++++++++++++++++++++
cbrain_cli/handlers.py | 3 ++-
3 files changed, 50 insertions(+), 19 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 48c36a1..f672496 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -674,7 +674,7 @@ Stderr:
############################
Command: cbrain tag delete 99
Status: 0
-Stdout: 30 bytes
+Stdout: 29 bytes
Stderr: 0 bytes
Stdout:
@@ -928,8 +928,9 @@ Stdout: 42 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)
@@ -1035,11 +1036,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)
@@ -1104,33 +1105,35 @@ 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
-Stderr:
+Authentication error (401): Unauthorized
+Error: Access denied. Please log in using authorized credentials.
(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)
@@ -1562,11 +1565,8 @@ Stdout: 96 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)
@@ -1578,7 +1578,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/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/handlers.py b/cbrain_cli/handlers.py
index d216fd0..47e7343 100644
--- a/cbrain_cli/handlers.py
+++ b/cbrain_cli/handlers.py
@@ -40,6 +40,7 @@
)
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,
@@ -109,7 +110,7 @@ def handle_file_delete(args):
"""Delete a specific file from CBRAIN and display the deletion status."""
result = delete_file(args)
if result:
- json_printer(result)
+ print_delete_result(result, args)
# Data provider command handlers
From ef63fa6de12d8f7695af960a4baa5c30bffaa89c Mon Sep 17 00:00:00 2001
From: axif
Date: Sat, 6 Sep 2025 04:48:32 +0600
Subject: [PATCH 05/14] added Project switch/unswitch cmd
---
capture_tests/expected_captures.txt | 26 ++++++++++++++++----------
cbrain_cli/data/projects.py | 12 ++++++++++++
cbrain_cli/handlers.py | 9 +++++++++
cbrain_cli/main.py | 10 +++++++++-
4 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index f672496..58b4d0f 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -484,33 +484,33 @@ Stderr:
############################
Command: cbrain project switch all # 'all' not yet implemented as of Aug 2025
Status: 0
-Stdout: TOFIX bytes
+Stdout: 40 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: 40 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: 40 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+Project switch 'all' not yet implemented as of Aug 2025
Stderr:
(No output)
@@ -649,7 +649,7 @@ Stdout: 37 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+Failed: Invalid response from server
Stderr:
(No output)
@@ -924,7 +924,7 @@ Stderr:
############################
Command: cbrain task operation # should provide error message
Status: 0
-Stdout: 42 bytes
+Stdout: 41 bytes
Stderr: 0 bytes
Stdout:
@@ -1010,7 +1010,13 @@ 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)
@@ -1561,7 +1567,7 @@ Stderr:
############################
Command: cbrain file delete 5
Status: 0
-Stdout: 96 bytes
+Stdout: 71 bytes
Stderr: 0 bytes
Stdout:
diff --git a/cbrain_cli/data/projects.py b/cbrain_cli/data/projects.py
index d639dc2..6e7e7cf 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)
diff --git a/cbrain_cli/handlers.py b/cbrain_cli/handlers.py
index 47e7343..7104520 100644
--- a/cbrain_cli/handlers.py
+++ b/cbrain_cli/handlers.py
@@ -161,6 +161,15 @@ def handle_project_show(args):
print_no_project()
+def handle_project_unswitch(args):
+ """Unswitch from current project context."""
+ target = getattr(args, "target", None)
+ if target == "all":
+ print("Project Unswitch 'all' not yet implemented as of Aug 2025")
+ else:
+ print(f"Error: Invalid target '{target}'. Only 'all' is supported.")
+
+
# Tool command handlers
def handle_tool_show(args):
"""Retrieve and display detailed information about a specific computational tool."""
diff --git a/cbrain_cli/main.py b/cbrain_cli/main.py
index c94c2dc..18770ba 100644
--- a/cbrain_cli/main.py
+++ b/cbrain_cli/main.py
@@ -23,6 +23,7 @@
handle_project_list,
handle_project_show,
handle_project_switch,
+ handle_project_unswitch,
handle_remote_resource_list,
handle_remote_resource_show,
handle_tag_create,
@@ -207,13 +208,20 @@ def main():
# 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.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(handle_project_show))
+ # project unswitch
+ project_unswitch_parser = project_subparsers.add_parser(
+ "unswitch", help="Unswitch from current project"
+ )
+ project_unswitch_parser.add_argument("target", help="Target to unswitch ('all' supported)")
+ project_unswitch_parser.set_defaults(func=handle_errors(handle_project_unswitch))
+
# Tool commands
tool_parser = subparsers.add_parser("tool", help="Tool operations")
tool_subparsers = tool_parser.add_subparsers(dest="action", help="Tool actions")
From 40af36d12ebe2c622e5ef022b2d4a4ab139153ac Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 01:06:26 +0600
Subject: [PATCH 06/14] Enhance project show command to support displaying
specific project details by ID
---
capture_tests/expected_captures.txt | 33 +++++++----
cbrain_cli/data/projects.py | 84 ++++++++++++++++++----------
cbrain_cli/formatter/projects_fmt.py | 43 ++++++++++++++
cbrain_cli/handlers.py | 18 +++++-
cbrain_cli/main.py | 7 ++-
5 files changed, 140 insertions(+), 45 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 58b4d0f..084ca30 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -75,7 +75,7 @@ Stdout: 30 bytes
Stderr: 0 bytes
Stdout:
-cbrain cli client version 0.9
+cbrain cli client version 1.0
Stderr:
(No output)
@@ -418,33 +418,45 @@ Stderr:
############################
Command: cbrain project show 10
Status: 0
-Stdout: TOFIX bytes
+Stdout: 198 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+PROJECT DETAILS
+------------------------------
+Field Value
+--------- ---------
+ID 10
+Name NormTest1
+Type WorkGroup
+Site ID N/A
+Invisible N/A
Stderr:
(No output)
############################
Command: cbrain --json project show 10
Status: 0
-Stdout: TOFIX bytes
+Stdout: 85 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+{
+ "id": 10,
+ "name": "NormTest1",
+ "type": "WorkGroup"
+}
Stderr:
(No output)
############################
Command: cbrain --jsonl project show 10
Status: 0
-Stdout: TOFIX bytes
+Stdout: 48 bytes
Stderr: 0 bytes
Stdout:
-TOFIX
+{"id":10,"name":"NormTest1","type":"WorkGroup"}
Stderr:
(No output)
@@ -495,7 +507,7 @@ Stderr:
############################
Command: cbrain --json project switch all
Status: 0
-Stdout: 40 bytes
+Stdout: 56 bytes
Stderr: 0 bytes
Stdout:
@@ -506,7 +518,7 @@ Stderr:
############################
Command: cbrain --jsonl project switch all
Status: 0
-Stdout: 40 bytes
+Stdout: 56 bytes
Stderr: 0 bytes
Stdout:
@@ -644,7 +656,7 @@ Stderr:
############################
Command: cbrain tag update 99 --name Renamed --user-id 2 --group-id 3
-Status: 0
+Status: 1
Stdout: 37 bytes
Stderr: 0 bytes
@@ -1117,6 +1129,7 @@ Stderr: 0 bytes
Stdout:
Authentication error (401): Unauthorized
Error: Access denied. Please log in using authorized credentials.
+Stderr:
(No output)
############################
diff --git a/cbrain_cli/data/projects.py b/cbrain_cli/data/projects.py
index 6e7e7cf..58ff27b 100644
--- a/cbrain_cli/data/projects.py
+++ b/cbrain_cli/data/projects.py
@@ -71,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/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
index 7104520..2155278 100644
--- a/cbrain_cli/handlers.py
+++ b/cbrain_cli/handlers.py
@@ -153,12 +153,24 @@ def handle_project_switch(args):
def handle_project_show(args):
- """Display information about the currently active project or indicate if none is set."""
+ """Display information about the currently active project or a specific project by ID."""
result = show_project(args)
if result:
- print_current_project(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:
- print_no_project()
+ # 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):
diff --git a/cbrain_cli/main.py b/cbrain_cli/main.py
index 18770ba..6c4ead7 100644
--- a/cbrain_cli/main.py
+++ b/cbrain_cli/main.py
@@ -212,7 +212,12 @@ def main():
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 = 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
From 774ab791f7578e871db76a2c5b383d95d731fd95 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 01:18:41 +0600
Subject: [PATCH 07/14] fix test
---
capture_tests/expected_captures.txt | 54 +++++++++++++++++++----------
1 file changed, 35 insertions(+), 19 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 084ca30..8158af3 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -26,7 +26,7 @@ Stderr:
############################
Command: cbrain --json version
-Status: 1
+Status: 0
Stdout: 30 bytes
Stderr: 0 bytes
@@ -38,22 +38,25 @@ Stderr:
############################
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)
@@ -297,7 +300,7 @@ Stderr:
############################
Command: cbrain dataprovider show 15
Status: 0
-Stdout: 864 bytes
+Stdout: 1047 bytes
Stderr: 0 bytes
Stdout:
@@ -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,7 +430,7 @@ Stderr:
############################
Command: cbrain project show 10
Status: 0
-Stdout: 198 bytes
+Stdout: 171 bytes
Stderr: 0 bytes
Stdout:
@@ -427,36 +439,40 @@ PROJECT DETAILS
Field Value
--------- ---------
ID 10
-Name NormTest1
+Name None
Type WorkGroup
-Site ID N/A
-Invisible N/A
+Site ID False
+Invisible False
Stderr:
(No output)
############################
Command: cbrain --json project show 10
Status: 0
-Stdout: 85 bytes
+Stdout: 125 bytes
Stderr: 0 bytes
Stdout:
-{
- "id": 10,
- "name": "NormTest1",
- "type": "WorkGroup"
-}
+ {
+ "id": 10,
+ "name": "NormTest1",
+ "type": "WorkGroup"
+ "description": null,
+ "type": "WorkGroup",
+ "site_id": null,
+ "invisible": false
+ }
Stderr:
(No output)
############################
Command: cbrain --jsonl project show 10
Status: 0
-Stdout: 48 bytes
+Stdout: 100 bytes
Stderr: 0 bytes
Stdout:
-{"id":10,"name":"NormTest1","type":"WorkGroup"}
+{"id":10,"name":"NormTest1","description":null,"type":"WorkGroup","site_id":null,"invisible":false}
Stderr:
(No output)
@@ -496,7 +512,7 @@ Stderr:
############################
Command: cbrain project switch all # 'all' not yet implemented as of Aug 2025
Status: 0
-Stdout: 40 bytes
+Stdout: 56 bytes
Stderr: 0 bytes
Stdout:
From 59a9d1989844575d93ac2668ed9f991d4152b016 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 01:23:11 +0600
Subject: [PATCH 08/14] tiny fix
---
capture_tests/expected_captures.txt | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 8158af3..16cd910 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -300,7 +300,7 @@ Stderr:
############################
Command: cbrain dataprovider show 15
Status: 0
-Stdout: 1047 bytes
+Stdout: 864 bytes
Stderr: 0 bytes
Stdout:
@@ -439,9 +439,9 @@ PROJECT DETAILS
Field Value
--------- ---------
ID 10
-Name None
+Name NormTest1
Type WorkGroup
-Site ID False
+Site ID None
Invisible False
Stderr:
(No output)
@@ -453,15 +453,15 @@ Stdout: 125 bytes
Stderr: 0 bytes
Stdout:
- {
- "id": 10,
- "name": "NormTest1",
- "type": "WorkGroup"
- "description": null,
- "type": "WorkGroup",
- "site_id": null,
- "invisible": false
- }
+{
+ "id": 10,
+ "name": "NormTest1",
+ "type": "WorkGroup"
+ "description": null,
+ "type": "WorkGroup",
+ "site_id": null,
+ "invisible": false
+}
Stderr:
(No output)
From 250d42da0739efce8969894327cb036ad16b1816 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 01:23:34 +0600
Subject: [PATCH 09/14] tiny fix
---
capture_tests/expected_captures.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 16cd910..21e9027 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -300,7 +300,7 @@ Stderr:
############################
Command: cbrain dataprovider show 15
Status: 0
-Stdout: 864 bytes
+Stdout: 864 bytes
Stderr: 0 bytes
Stdout:
From d05652255d7ec6e7fdd98af73db0080fb890e964 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 01:27:54 +0600
Subject: [PATCH 10/14] final test fix
---
capture_tests/expected_captures.txt | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/capture_tests/expected_captures.txt b/capture_tests/expected_captures.txt
index 21e9027..374289c 100644
--- a/capture_tests/expected_captures.txt
+++ b/capture_tests/expected_captures.txt
@@ -300,7 +300,7 @@ Stderr:
############################
Command: cbrain dataprovider show 15
Status: 0
-Stdout: 864 bytes
+Stdout: 864 bytes
Stderr: 0 bytes
Stdout:
@@ -456,7 +456,6 @@ Stdout:
{
"id": 10,
"name": "NormTest1",
- "type": "WorkGroup"
"description": null,
"type": "WorkGroup",
"site_id": null,
From df212b360e826d9395316718a07521bc786fcb2b Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 23:22:42 +0600
Subject: [PATCH 11/14] Add details in readme
---
README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 67 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 45d5858..f546379 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,12 @@
# CBRAIN CLI
-## **Google Summer of Code 2025 Project**
-
A command-line interface to a CBRAIN service
============================================
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
@@ -79,9 +70,74 @@ 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
+- `file (f)` - Manage individual file operations and metadata
+- `background-activities` - Monitor and manage long-running background processes
+- `data-providers` - Configure and manage data storage locations and access
+- `files` - List, upload, download, and organize research data files
+- `projects` - Create and manage research project workspaces
+- `remote_resources` - Configure computational clusters and processing resources
+- `tags` - Apply and manage metadata tags for organizing content
+- `tasks` - Submit, show and manage computational analysis jobs
+- `tool_configs` - Configure analysis tools and processing parameters
+- `tools` - Browse available analysis tools and their capabilities
+
+## Command Examples
+
+
+
+
+
+> Used 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`
+>
+>
+
+
+
+
+> Used 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) 2025 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.
From 96d2d66fc73bd56a0914f190aaab8b58cbfdb474 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 23:27:21 +0600
Subject: [PATCH 12/14] add missing " and
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index f546379..c038033 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ cbrain [options] [id_or_args]
## Command Examples
-
+
> Used in the above GIF
@@ -120,7 +120,8 @@ cbrain [options] [id_or_args]
>
-
+
+
> Used in the above GIF
>
From f2003cfb3b5a27d771e6b78b10bf62e543e7ef44 Mon Sep 17 00:00:00 2001
From: axif
Date: Sun, 7 Sep 2025 23:30:48 +0600
Subject: [PATCH 13/14] typo "cmds"
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c038033..ecd1913 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,7 @@ cbrain [options] [id_or_args]
-> Used in the above GIF
+> Used cmds in the above GIF
>
> - `./cbrain project switch 2`
> - `./cbrain project show`
@@ -123,7 +123,7 @@ cbrain [options] [id_or_args]
-> Used in the above GIF
+> Used cmds in the above GIF
>
> - `./cbrain file list`
> - `./cbrain project list`
From be6785a320983e34b949dacc251e326674b28f47 Mon Sep 17 00:00:00 2001
From: axif
Date: Fri, 12 Sep 2025 20:52:06 +0600
Subject: [PATCH 14/14] Update README with new command structure and simplify
project unswitch handler
---
README.md | 23 +++++++++++++----------
cbrain_cli/handlers.py | 6 +-----
cbrain_cli/main.py | 1 -
3 files changed, 14 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index ecd1913..9f02194 100644
--- a/README.md
+++ b/README.md
@@ -88,16 +88,19 @@ cbrain [options] [id_or_args]
- `--jsonl` or `-jl`: JSON Lines format (one JSON object per line)
## Available Commands
-- `file (f)` - Manage individual file operations and metadata
-- `background-activities` - Monitor and manage long-running background processes
-- `data-providers` - Configure and manage data storage locations and access
-- `files` - List, upload, download, and organize research data files
-- `projects` - Create and manage research project workspaces
-- `remote_resources` - Configure computational clusters and processing resources
-- `tags` - Apply and manage metadata tags for organizing content
-- `tasks` - Submit, show and manage computational analysis jobs
-- `tool_configs` - Configure analysis tools and processing parameters
-- `tools` - Browse available analysis tools and their capabilities
+- `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
diff --git a/cbrain_cli/handlers.py b/cbrain_cli/handlers.py
index 2155278..83c0ccf 100644
--- a/cbrain_cli/handlers.py
+++ b/cbrain_cli/handlers.py
@@ -175,11 +175,7 @@ def handle_project_show(args):
def handle_project_unswitch(args):
"""Unswitch from current project context."""
- target = getattr(args, "target", None)
- if target == "all":
- print("Project Unswitch 'all' not yet implemented as of Aug 2025")
- else:
- print(f"Error: Invalid target '{target}'. Only 'all' is supported.")
+ print("Project Unswitch 'all' not yet implemented as of Aug 2025")
# Tool command handlers
diff --git a/cbrain_cli/main.py b/cbrain_cli/main.py
index 6c4ead7..41e2d07 100644
--- a/cbrain_cli/main.py
+++ b/cbrain_cli/main.py
@@ -224,7 +224,6 @@ def main():
project_unswitch_parser = project_subparsers.add_parser(
"unswitch", help="Unswitch from current project"
)
- project_unswitch_parser.add_argument("target", help="Target to unswitch ('all' supported)")
project_unswitch_parser.set_defaults(func=handle_errors(handle_project_unswitch))
# Tool commands