Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions cbrain_cli/cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import functools
import importlib.metadata
import json
import re
import socket
import urllib.error
import urllib.parse
import urllib.request

# import importlib.metadata
from cbrain_cli.config import DEFAULT_HEADERS, auth_headers, load_credentials
from cbrain_cli.config import DEFAULT_HEADERS, DEFAULT_TIMEOUT, auth_headers, load_credentials

credentials = load_credentials() or {}
cbrain_url = credentials.get("cbrain_url")
Expand Down Expand Up @@ -136,11 +137,11 @@ def handle_connection_error(error):
error_data = json.loads(error_response)
if isinstance(error_data, dict):
# Look for common error message fields
error_msg = (
error_msg = str(
error_data.get("message")
or error_data.get("error")
or error_data.get("notice")
or str(error_data)
or error_data
)
# Check if this looks like a password change redirect
if "change_password" in error_msg:
Expand Down Expand Up @@ -186,7 +187,12 @@ def handle_connection_error(error):
else:
print(f"{status_description}: {error.reason}")
elif isinstance(error, urllib.error.URLError):
if "Connection refused" in str(error):
if isinstance(error.reason, socket.timeout):
print(
f"Error: Request timed out after {DEFAULT_TIMEOUT}s. "
"Check your connection or set CBRAIN_TIMEOUT env var."
)
elif "Connection refused" in str(error):
print(f"Error: Cannot connect to CBRAIN server at {cbrain_url}")
print("Please check if the CBRAIN server is running and accessible.")
else:
Expand Down Expand Up @@ -215,6 +221,10 @@ def wrapper(*args, **kwargs):
except urllib.error.URLError as e:
handle_connection_error(e)
return 1
except socket.timeout as e:
# Read-stage timeouts are raised bare, not wrapped in URLError.
handle_connection_error(urllib.error.URLError(e))
return 1
except json.JSONDecodeError:
print("Failed: Invalid response from server")
return 1
Expand Down Expand Up @@ -245,14 +255,15 @@ def version_info(args):
int
Exit code (0 for success, 1 for failure)
"""
print("cbrain cli client version 1.0")
# try:
# cbrain_cli_version = importlib.metadata.version('cbrain-cli')
# print(f"cbrain cli client version {cbrain_cli_version}")
# return 0
# except importlib.metadata.PackageNotFoundError:
# print("Warning: Could not determine version. Package may not be installed properly.")
# return 1
try:
cbrain_cli_version = importlib.metadata.version("cbrain-cli")
if output_json(args, {"version": cbrain_cli_version}):
return 0
print(f"cbrain cli client version {cbrain_cli_version}")
return 0
except importlib.metadata.PackageNotFoundError:
print("Warning: Could not determine version. Package may not be installed properly.")
return 1


def api_get(url, token, params=None):
Expand All @@ -262,7 +273,7 @@ def api_get(url, token, params=None):
if params:
url = f"{url}?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers=auth_headers(token), method="GET")
with urllib.request.urlopen(req) as r:
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT) as r:
return json.loads(r.read().decode())


Expand All @@ -273,7 +284,7 @@ def api_post_form(url, form_data, headers=None):
headers = headers or DEFAULT_HEADERS
body = urllib.parse.urlencode(form_data).encode()
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req) as r:
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT) as r:
return json.loads(r.read().decode())


Expand All @@ -287,7 +298,7 @@ def api_send(url, token, method="POST", payload=None):
headers["Content-Type"] = "application/json"
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, headers=headers, method=method)
with urllib.request.urlopen(req) as r:
with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT) as r:
raw = r.read().decode()
return (json.loads(raw) if raw.strip() else {}), r.status

Expand Down
6 changes: 6 additions & 0 deletions cbrain_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
CREDENTIALS_FILE = SESSION_FILE_DIR / SESSION_FILE_NAME
DEFAULT_CREDENTIALS_MODE = 0o600

# HTTP request timeout in seconds; override with CBRAIN_TIMEOUT env var.
try:
DEFAULT_TIMEOUT = max(1, int(os.environ.get("CBRAIN_TIMEOUT", "30")))
except ValueError:
DEFAULT_TIMEOUT = 30

# HTTP headers.
DEFAULT_HEADERS = {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down
6 changes: 3 additions & 3 deletions cbrain_cli/data/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
cbrain_url,
pagination,
)
from cbrain_cli.config import auth_headers
from cbrain_cli.config import DEFAULT_TIMEOUT, auth_headers


def show_file(args):
Expand Down Expand Up @@ -50,7 +50,7 @@ def upload_file(args):
(response_data, response_status, file_name, file_size) or None if error
"""
# Check if file exists.
if not os.path.exists(args.file_path):
if not os.path.isfile(args.file_path):
raise CliValidationError(f"File not found: {args.file_path}", field="file_path")

if args.group_id is None:
Expand Down Expand Up @@ -92,7 +92,7 @@ def upload_file(args):
request = urllib.request.Request(
f"{cbrain_url}/userfiles", data=body, headers=headers, method="POST"
)
with urllib.request.urlopen(request) as response:
with urllib.request.urlopen(request, timeout=DEFAULT_TIMEOUT) as response:
response_data = json.loads(response.read().decode("utf-8"))
return response_data, response.status, file_name, file_size, args.data_provider

Expand Down
4 changes: 3 additions & 1 deletion cbrain_cli/data/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def switch_project(args):
f"Invalid group ID '{group_id}'. Must be a number or 'all'", field="group_id"
) from None

api_send(f"{cbrain_url}/groups/switch?id={group_id}", api_token)
_, switch_status = api_send(f"{cbrain_url}/groups/switch?id={group_id}", api_token)
if switch_status not in (200, 201, 204):
raise CliApiError(f"Failed to switch project (HTTP {switch_status})")
group_data = api_get(f"{cbrain_url}/groups/{group_id}", api_token)

credentials = load_credentials()
Expand Down
4 changes: 2 additions & 2 deletions cbrain_cli/data/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ def show_tool(args):
api_token,
{"page": str(page), "per_page": str(per_page)},
)
if not tools_page:
if not tools_page or not isinstance(tools_page, list):
break
for tool in tools_page:
if tool.get("id") == tool_id:
if str(tool.get("id")) == str(tool_id):
return tool
if len(tools_page) < per_page:
break
Expand Down
6 changes: 1 addition & 5 deletions cbrain_cli/formatter/background_activities_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ def print_activities_list(activities_data, args):
"remote_resource_id": a.get("remote_resource_id", ""),
"status": a.get("status", ""),
"created_at": (
a.get("created_at", "").split("T")[0]
+ " "
+ a.get("created_at", "").split("T")[1].split(".")[0]
if a.get("created_at")
else ""
a["created_at"].replace("T", " ").split(".")[0] if a.get("created_at") else ""
),
"items": ",".join(map(str, a.get("items", []))) if a.get("items") else "",
"num_successes": a.get("num_successes", 0),
Expand Down
16 changes: 14 additions & 2 deletions cbrain_cli/formatter/files_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def print_files_list(files_data, args):
dynamic_table_print(files_data, ["id", "type", "name"], ["ID", "Type", "File Name"])


def print_upload_result(response_data, response_status, file_name, file_size, data_provider_id):
def print_upload_result(
response_data, response_status, file_name, file_size, data_provider_id, args=None
):
"""
Print the result of a file upload operation.

Expand All @@ -74,7 +76,12 @@ def print_upload_result(response_data, response_status, file_name, file_size, da
Size of the uploaded file in bytes
data_provider_id : int
ID of the data provider
args : argparse.Namespace, optional
Command line arguments, including the --json flag
"""
if args is not None and output_json(args, response_data):
return

print(f"Uploading {file_name} ({file_size} bytes) to data provider {data_provider_id}...")

if response_status == 200 or response_status == 201:
Expand All @@ -83,7 +90,7 @@ def print_upload_result(response_data, response_status, file_name, file_size, da
print(f"Server response: {response_data['notice']}")


def print_move_copy_result(response_data, response_status, operation="move"):
def print_move_copy_result(response_data, response_status, operation="move", args=None):
"""
Print the result of a file move or copy operation.

Expand All @@ -95,7 +102,12 @@ def print_move_copy_result(response_data, response_status, operation="move"):
HTTP status code
operation : str
Operation type ("move" or "copy")
args : argparse.Namespace, optional
Command line arguments, including the --json flag
"""
if args is not None and output_json(args, response_data):
return

if response_status == 200 or response_status == 201:
# Show the message from the API response
message = response_data.get("message", "").strip()
Expand Down
26 changes: 25 additions & 1 deletion cbrain_cli/formatter/tags_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ def print_tag_details(tag_data, args):


def print_tag_operation_result(
operation, tag_id=None, success=True, error_msg=None, response_status=None
operation,
tag_id=None,
success=True,
error_msg=None,
response_status=None,
args=None,
response_data=None,
):
"""
Print result of a tag operation (create, update, delete).
Expand All @@ -75,7 +81,25 @@ def print_tag_operation_result(
Error message if operation failed
response_status : int, optional
HTTP response status code if operation failed
args : argparse.Namespace, optional
Command line arguments, including the --json flag
response_data : dict, optional
Raw API response data for structured output
"""
if args is not None:
structured = (
response_data
if response_data
else {
"operation": operation,
"success": success,
"tag_id": tag_id,
"error": error_msg,
}
)
if output_json(args, structured):
return

if success:
if operation == "create":
print("Tag created successfully!")
Expand Down
4 changes: 2 additions & 2 deletions cbrain_cli/formatter/tasks_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def print_task_data(tasks_data, args):
formatted_tasks = [
{
"id": task.get("id", ""),
"type": task.get("type", "").replace("BoutiquesTask::", ""),
"type": (task.get("type") or "").replace("BoutiquesTask::", ""),
"status": task.get("status", ""),
"bourreau_id": task.get("bourreau_id", ""),
"user_id": task.get("user_id", ""),
Expand Down Expand Up @@ -108,7 +108,7 @@ def print_task_details(task_data, args):
print()
print("DESCRIPTION")
print("-" * 30)
for line in task_data.get("description").strip().split("\n"):
for line in str(task_data.get("description", "")).strip().split("\n"):
print(line)

# Display params if they exist.
Expand Down
2 changes: 2 additions & 0 deletions cbrain_cli/formatter/tool_configs_fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ def print_boutiques_descriptor(boutiques_descriptor, args):
print("No Boutiques descriptor found.")
return

if output_json(args, boutiques_descriptor):
return
json_printer(boutiques_descriptor)
24 changes: 19 additions & 5 deletions cbrain_cli/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
and format their output appropriately.
"""

from cbrain_cli.cli_utils import json_printer
from cbrain_cli.cli_utils import json_printer, output_json
from cbrain_cli.data import (
background_activities,
data_providers,
Expand Down Expand Up @@ -56,7 +56,7 @@ def handle_file_upload(args):
result = files.upload_file(args)
if result is None:
return 1
files_fmt.print_upload_result(*result)
files_fmt.print_upload_result(*result, args=args)
if result[1] not in (200, 201):
return 1

Expand All @@ -66,7 +66,7 @@ def handle_file_copy(args):
result = files.copy_file(args)
if result is None:
return 1
files_fmt.print_move_copy_result(*result, operation="copy")
files_fmt.print_move_copy_result(*result, operation="copy", args=args)
if result[1] not in (200, 201):
return 1

Expand All @@ -76,7 +76,7 @@ def handle_file_move(args):
result = files.move_file(args)
if result is None:
return 1
files_fmt.print_move_copy_result(*result, operation="move")
files_fmt.print_move_copy_result(*result, operation="move", args=args)
if result[1] not in (200, 201):
return 1

Expand Down Expand Up @@ -111,6 +111,8 @@ def handle_dataprovider_is_alive(args):
result = data_providers.is_alive(args)
if result is None:
return 1
if output_json(args, result):
return
json_printer(result)


Expand All @@ -119,6 +121,8 @@ def handle_dataprovider_delete_unregistered(args):
result = data_providers.delete_unregistered_files(args)
if result is None:
return 1
if output_json(args, result):
return
json_printer(result)


Expand Down Expand Up @@ -161,6 +165,8 @@ def handle_project_show(args):
def handle_project_unswitch(args):
"""Unswitch from current project context."""
result = projects.unswitch_project(args)
if result is None:
return 1
projects_fmt.print_unswitch_result(result, args)


Expand Down Expand Up @@ -229,7 +235,12 @@ def handle_tag_create(args):
if result is None:
return 1
tags_fmt.print_tag_operation_result(
"create", success=result[1], error_msg=result[2], response_status=result[3]
"create",
success=result[1],
error_msg=result[2],
response_status=result[3],
args=args,
response_data=result[0],
)
if not result[1]:
return 1
Expand All @@ -246,6 +257,8 @@ def handle_tag_update(args):
success=result[1],
error_msg=result[2],
response_status=result[3],
args=args,
response_data=result[0],
)
if not result[1]:
return 1
Expand All @@ -262,6 +275,7 @@ def handle_tag_delete(args):
success=result[0],
error_msg=result[1],
response_status=result[2],
args=args,
)
if not result[0]:
return 1
Expand Down
Loading
Loading