Skip to content
Merged
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
16 changes: 8 additions & 8 deletions capture_tests/cbrain_cli_commands
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ cbrain --json project unswitch
cbrain --jsonl project switch 10
cbrain --jsonl project unswitch

cbrain project switch all # 'all' not yet implemented as of Aug 2025
cbrain project switch all
cbrain --json project switch all
cbrain --jsonl project switch all
cbrain project switch 3 # need to reset for rest of test to work
cbrain project switch 3 # reset project filter for task list below

# Tags
cbrain tag list
Expand All @@ -103,10 +103,7 @@ cbrain --json tag create --name NewTag2 --user-id 2 --group-id 3
cbrain --jsonl tag create --name NewTag3 --user-id 2 --group-id 3

# Tasks
# NOTE: we needed to reset the current project to 3 above,
# until 'group switch all' is implemented; this means only
# tasks in group 3 are shown below, but that's OK, that's
# what's in the test DB anyway
# Reset above to project 3 so task list matches seeded DB view.
cbrain task list
cbrain --json task list
cbrain --jsonl task list
Expand All @@ -115,8 +112,11 @@ cbrain task show 2
cbrain --json task show 2
cbrain --jsonl task show 2

# Not yet implemented
cbrain task operation # should provide error message
# Task operations (hold on Completed task → skipped; deterministic, no BAC)
cbrain task operation hold --task-id 2
cbrain --json task operation hold --task-id 2
cbrain --jsonl task operation hold --task-id 2
cbrain task operation hold # missing --task-id / --batch-id

# ToolConfigs, as admin user
./switch_session admin
Expand Down
92 changes: 72 additions & 20 deletions capture_tests/expected_captures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -558,40 +558,43 @@ Stderr:
(No output)

############################
Command: cbrain project switch all # 'all' not yet implemented as of Aug 2025
Status: 1
Stdout: 74 bytes
Command: cbrain project switch all
Status: 0
Stdout: 32 bytes
Stderr: 0 bytes

Stdout:
Error: Project switch 'all' not yet implemented as of Aug 2025 (group_id)
Current project is "all" ID=all
Stderr:
(No output)

############################
Command: cbrain --json project switch all
Status: 1
Stdout: 74 bytes
Status: 0
Stdout: 35 bytes
Stderr: 0 bytes

Stdout:
Error: Project switch 'all' not yet implemented as of Aug 2025 (group_id)
{
"id": "all",
"name": "all"
}
Stderr:
(No output)

############################
Command: cbrain --jsonl project switch all
Status: 1
Stdout: 74 bytes
Status: 0
Stdout: 26 bytes
Stderr: 0 bytes

Stdout:
Error: Project switch 'all' not yet implemented as of Aug 2025 (group_id)
{"id":"all","name":"all"}
Stderr:
(No output)

############################
Command: cbrain project switch 3 # need to reset for rest of test to work
Command: cbrain project switch 3 # reset project filter for task list below
Status: 0
Stdout: 31 bytes
Stderr: 0 bytes
Expand Down Expand Up @@ -998,18 +1001,67 @@ Stderr:
(No output)

############################
Command: cbrain task operation # should provide error message
Command: cbrain task operation hold --task-id 2
Status: 0
Stdout: 41 bytes
Stdout: 182 bytes
Stderr: 0 bytes

Stdout:
TASK OPERATION RESULT
------------------------------
Operation: hold
Succeeded: 0
Skipped: 1
Failed: 0

SKIPPED
------------------------------
Tasks have incompatible states: 2
Stderr:
(No output)

############################
Command: cbrain --json task operation hold --task-id 2
Status: 0
Stdout: 142 bytes
Stderr: 0 bytes

Stdout:
{
"message": "No operation selected"
"skipped_list": {
"Tasks have incompatible states": [
2
]
},
"success_list": [],
"failed_list": {},
"bac_ids": []
}
Stderr:
(No output)

############################
Command: cbrain --jsonl task operation hold --task-id 2
Status: 0
Stdout: 104 bytes
Stderr: 0 bytes

Stdout:
{"skipped_list":{"Tasks have incompatible states":[2]},"success_list":[],"failed_list":{},"bac_ids":[]}
Stderr:
(No output)

############################
Command: cbrain task operation hold # missing --task-id / --batch-id
Status: 1
Stdout: 68 bytes
Stderr: 0 bytes

Stdout:
Error: At least one --task-id or --batch-id is required (--task-id)
Stderr:
(No output)

############################
Command: ./switch_session admin
Status: 0
Expand Down Expand Up @@ -1187,36 +1239,36 @@ Stderr:
############################
Command: cbrain tool-config show 19 # visible to user admin
Status: 1
Stdout: 107 bytes
Stdout: 117 bytes
Stderr: 0 bytes

Stdout:
Authentication error (401): Unauthorized
Error: Access denied. Please log in using authorized credentials.
Error: Session expired or invalid. Run 'cbrain logout' then 'cbrain login'.
Stderr:
(No output)

############################
Command: cbrain --json tool-config show 19
Status: 1
Stdout: 107 bytes
Stdout: 117 bytes
Stderr: 0 bytes

Stdout:
Authentication error (401): Unauthorized
Error: Access denied. Please log in using authorized credentials.
Error: Session expired or invalid. Run 'cbrain logout' then 'cbrain login'.
Stderr:
(No output)

############################
Command: cbrain --jsonl tool-config show 19
Status: 1
Stdout: 107 bytes
Stdout: 117 bytes
Stderr: 0 bytes

Stdout:
Authentication error (401): Unauthorized
Error: Access denied. Please log in using authorized credentials.
Error: Session expired or invalid. Run 'cbrain logout' then 'cbrain login'.
Stderr:
(No output)

Expand Down
32 changes: 29 additions & 3 deletions cbrain_cli/cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
from cbrain_cli import config as cbrain_config
from cbrain_cli.config import DEFAULT_HEADERS, DEFAULT_TIMEOUT, auth_headers

_debug = False


def set_debug(flag: bool) -> None:
"""Enable or disable debug output."""
global _debug
_debug = bool(flag)


def debug_log(message: str) -> None:
"""Print a debug line to stderr when debug mode is active."""
if _debug:
print(f"[DEBUG] {message}", file=sys.stderr)


class CbrainClient:
"""
Expand Down Expand Up @@ -46,11 +60,23 @@ def _request(self, method, path, *, headers=None, body=None, params=None):
if params:
target = f"{target}?{urllib.parse.urlencode(params)}"
hdrs = headers or auth_headers(self.token)
# strip host from full URL for debug display; preserve all query params
parsed = urllib.parse.urlsplit(target)
display_path = parsed.path
if display_path.startswith(self.base_url):
display_path = display_path[len(self.base_url) :]
query = urllib.parse.urlencode(params) if params else parsed.query
if query:
display_path = f"{display_path}?{query}"
debug_log(f"{method} {display_path}")
req = urllib.request.Request(target, data=body, headers=hdrs, method=method)
try:
with urllib.request.urlopen(req, timeout=self.timeout) as r:
return r.read(), r.status
status = r.status
debug_log(f"→ HTTP {status}")
return r.read(), status
except urllib.error.HTTPError as e:
debug_log(f"→ HTTP {e.code} ({e.reason})")
raise CliApiError(e.reason or f"HTTP {e.code}", status=e.code) from e

def get(self, path, params=None):
Expand Down Expand Up @@ -95,7 +121,7 @@ def post_multipart(self, path, body, content_type):

PAGINATABLE_ACTIONS = {
("file", "list"),
("dataprovider", "list"),
("data-provider", "list"),
("tool", "list"),
("tool-config", "list"),
("tag", "list"),
Expand Down Expand Up @@ -204,7 +230,7 @@ def handle_connection_error(error):

if error.code == 401:
print(f"{status_description}: {error.reason}")
print("Error: Access denied. Please log in using authorized credentials.")
print("Error: Session expired or invalid. Run 'cbrain logout' then 'cbrain login'.")
elif error.code in (400, 404, 422, 500):
# Try to extract specific error message from response
try:
Expand Down
4 changes: 3 additions & 1 deletion cbrain_cli/data/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ def _change_provider(args, operation):
if not file_ids:
raise CliValidationError("File ID(s) are required", field="--file-id")
if not dest_provider_id:
raise CliValidationError("Destination data provider ID is required", field="--dp-id")
raise CliValidationError(
"Destination data provider ID is required", field="--data-provider-id"
)
payload = {
"file_ids": file_ids,
"data_provider_id_for_mv_cp": dest_provider_id,
Expand Down
33 changes: 21 additions & 12 deletions cbrain_cli/data/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,25 @@ def switch_project(args):
if not group_id:
raise CliValidationError("Group ID is required", field="group_id")

if group_id == "all":
raise CliValidationError(
"Project switch 'all' not yet implemented as of Aug 2025", field="group_id"
)

try:
group_id = int(group_id)
except ValueError:
raise CliValidationError(
f"Invalid group ID '{group_id}'. Must be a number or 'all'", field="group_id"
) from None
# Server accepts numeric id or "all" (no single project filter).
if group_id != "all":
try:
group_id = int(group_id)
except ValueError:
raise CliValidationError(
f"Invalid group ID '{group_id}'. Must be a number or 'all'", field="group_id"
) from None

client = CbrainClient.from_credentials()
_, switch_status = client.send("POST", f"/groups/switch?id={group_id}")
if switch_status not in (200, 201, 204):
raise CliApiError(f"Failed to switch project (HTTP {switch_status})")
group_data = client.get(f"/groups/{group_id}")

# "all" is session state only — no /groups/all resource.
if group_id == "all":
group_data = {"id": "all", "name": "all"}
else:
group_data = client.get(f"/groups/{group_id}")

credentials = load_credentials()
if credentials is not None:
Expand Down Expand Up @@ -124,6 +126,13 @@ def show_project(args):
if not current_group_id:
return None

# Session "all" has no Group row; mirror switch_project synthetic result.
if current_group_id == "all":
return {
"id": "all",
"name": credentials.get("current_group_name") or "all",
}

try:
return CbrainClient.from_credentials().get(f"/groups/{current_group_id}")
except CliApiError as e:
Expand Down
Loading