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
7 changes: 4 additions & 3 deletions contree_cli/cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from dataclasses import dataclass
from multiprocessing.pool import ThreadPool

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import WhoAmIResponse

from contree_cli import FORMATTER, ArgumentsProtocol, SetupResult
Expand Down Expand Up @@ -263,12 +263,13 @@ def cmd_auth(args: AuthArgs) -> int | None:
try:
with client:
whoami = client.whoami()
except ContreeAPIError as exc:
except (APIStatusError, ValueError) as exc:
# ValueError covers parse_whoami's own non-2xx status check.
# Logs the API error message, not the token itself.
# nosemgrep: python-logger-credential-disclosure
logger.error("Token verification failed: %s. Profile not changed.", exc)
return 1
except (KeyError, TypeError, ValueError) as exc:
except (KeyError, TypeError) as exc:
# Strict contree-client models raise TypeError (missing required
# field) or KeyError (parse_fields lookup) on incomplete payloads.
logger.error("Could not parse /v1/whoami response: %s", exc)
Expand Down
4 changes: 2 additions & 2 deletions contree_cli/cli/cd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import posixpath
from dataclasses import dataclass

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError

from contree_cli import CLIENT, SESSION_STORE, ArgumentsProtocol, SetupResult

Expand Down Expand Up @@ -64,7 +64,7 @@ def cmd_cd(args: CdArgs) -> int | None:
client = CLIENT.get()
uuid = client.resolve_image(session.current_image)
client.inspect_image_list(uuid, new_cwd)
except ContreeAPIError as exc:
except APIStatusError as exc:
if exc.status == 404:
logger.error("cd: %s: no such directory", new_cwd)
return 1
Expand Down
4 changes: 2 additions & 2 deletions contree_cli/cli/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from dataclasses import dataclass, field
from datetime import datetime

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import (
TERMINAL_STATUSES,
ImageImportRegistry,
Expand Down Expand Up @@ -393,7 +393,7 @@ def cmd_import(args: ImportArgs) -> int | None:
try:
client.cancel_operation(op_uuid)
logger.info("Cancelled operation %s", op_uuid)
except (ContreeAPIError, KeyboardInterrupt, OSError):
except (APIStatusError, KeyboardInterrupt, OSError):
pass
raise

Expand Down
2 changes: 1 addition & 1 deletion contree_cli/cli/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def cmd_ls(args: LsArgs) -> None:
)
)
if response.status != 200:
raise error_for_response(response)
raise error_for_response(response.status, response.headers, response.body)
sys.stdout.write(response.body.decode())
return

Expand Down
10 changes: 5 additions & 5 deletions contree_cli/cli/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from datetime import datetime
from typing import Any

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import ACTIVE_STATUSES, TERMINAL_STATUSES

from contree_cli import CLIENT, FORMATTER, SESSION_STORE, ArgumentsProtocol, SetupResult
Expand Down Expand Up @@ -460,7 +460,7 @@ def cmd_show_multi(args: ShowMultiArgs) -> int | None:
for uuid in args.uuids:
try:
result = cmd_show(ShowArgs(uuid=uuid, raw=args.raw))
except ContreeAPIError as exc:
except APIStatusError as exc:
logger.error("Failed to fetch %s: %s", uuid, exc)
exit_code = max(exit_code, 1)
continue
Expand Down Expand Up @@ -490,7 +490,7 @@ def cmd_cancel(args: CancelArgs) -> int | None:
try:
client.cancel_operation(uuid)
logger.info("Cancelled operation %s", uuid)
except ContreeAPIError as exc:
except APIStatusError as exc:
logger.error("Failed to cancel %s: %s", uuid, exc)
failed += 1
return 1 if failed else None
Expand All @@ -506,7 +506,7 @@ def cmd_events(args: EventsArgs) -> int | None:
try:
for ev in client.iter_operation_events(uuid, follow=False):
formatter(uuid=uuid, **ev.to_dict())
except ContreeAPIError as exc:
except APIStatusError as exc:
logger.error("Failed to fetch events for %s: %s", uuid, exc)
exit_code = 1
formatter.flush()
Expand Down Expand Up @@ -592,7 +592,7 @@ def cmd_wait(args: WaitArgs) -> int | None:
for uuid in sorted(pending):
try:
op = client.get_operation_status(uuid).to_dict()
except ContreeAPIError as exc:
except APIStatusError as exc:
logger.error("Failed to fetch %s: %s", uuid, exc)
continue
formatter(**{**op, "timed_out": True})
Expand Down
16 changes: 5 additions & 11 deletions contree_cli/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
from multiprocessing.pool import ThreadPool
from typing import Any

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError, ContreeError
from contree_client.models import (
TERMINAL_STATUSES,
ClosableStreamRepr,
Expand Down Expand Up @@ -94,14 +94,8 @@

logger = logging.getLogger(__name__)

# Backend transport exceptions (httpx/urllib3/aiohttp/...) don't inherit
# from OSError, so ContreeAPIError/OSError alone miss a lost connection.
SIGNAL_ERRORS: tuple[type[BaseException], ...] = (
ContreeAPIError,
OSError,
*CliClient.retryable_errors,
*CliClient.nonretryable_errors,
)
# ContreeError covers APIConnectionError from any transport backend.
SIGNAL_ERRORS: tuple[type[BaseException], ...] = (ContreeError, OSError)

EPILOG = """\
examples:
Expand Down Expand Up @@ -688,7 +682,7 @@ def stream_events_until_close(
# payload the caller would otherwise build from `completion`.
try:
op = client.get_operation_status(op_uuid).to_dict()
except ContreeAPIError as exc:
except APIStatusError as exc:
logger.debug("terminal op fetch failed: %s", exc)
return summary
if op.get("status") in TERMINAL_STATUSES:
Expand Down Expand Up @@ -1027,7 +1021,7 @@ def norm(item: object) -> dict[str, object]:
# so callers see the SIGPIPE convention (128 + 13).
if forwarder is not None:
forwarder.abandon()
with contextlib.suppress(ContreeAPIError, OSError):
with contextlib.suppress(APIStatusError, OSError):
client.cancel_operation(op_uuid)
with contextlib.suppress(OSError):
devnull = os.open(os.devnull, os.O_WRONLY)
Expand Down
4 changes: 3 additions & 1 deletion contree_cli/cli/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,9 @@ def cmd_wait(args: WaitArgs) -> int | None:
RequestSpec(method="GET", path="/operations", idempotent=True)
)
if response.status != 200:
raise error_for_response(response)
raise error_for_response(
response.status, response.headers, response.body
)
operations = json.loads(response.body)
api_op_ids: list[str] = []
for op in operations:
Expand Down
6 changes: 3 additions & 3 deletions contree_cli/docker/kw_from.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dataclasses import dataclass
from typing import ClassVar

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import ImageImportRegistry

from contree_cli.cli.images import normalize_registry_url
Expand Down Expand Up @@ -110,7 +110,7 @@ def resolve_or_import(ctx: BuildContext, ref: str) -> str:
"""Resolve ``ref`` to a UUID, importing from a registry on miss."""
try:
return ctx.client.resolve_image(ref)
except ContreeAPIError as exc:
except APIStatusError as exc:
if exc.status != 404:
raise

Expand All @@ -127,7 +127,7 @@ def resolve_or_import(ctx: BuildContext, ref: str) -> str:
try:
return wait_import(ctx, op_uuid, tag)
except KeyboardInterrupt:
with contextlib.suppress(ContreeAPIError, OSError):
with contextlib.suppress(APIStatusError, OSError):
ctx.client.cancel_operation(op_uuid)
raise

Expand Down
6 changes: 3 additions & 3 deletions contree_cli/shell/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from dataclasses import dataclass
from functools import cached_property

from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError

from contree_cli import FORMATTER, IN_SHELL, PROFILE, SESSION_STORE, ArgumentsProtocol
from contree_cli.output import FORMATTERS, OutputFormatter
Expand Down Expand Up @@ -519,7 +519,7 @@ def dispatch_contree(self, tokens: list[str]) -> None:
before = self.session_snapshot()
try:
handler(loader.from_args(ns))
except ContreeAPIError as exc:
except APIStatusError as exc:
print(f"API error: {exc}", file=sys.stderr)
except KeyboardInterrupt:
print()
Expand Down Expand Up @@ -558,7 +558,7 @@ def dispatch_run(self, line: str, *, timeout: int | None = None) -> None:
before = self.session_snapshot()
try:
cmd_run(args)
except ContreeAPIError as exc:
except APIStatusError as exc:
print(f"API error: {exc}", file=sys.stderr)
except KeyboardInterrupt:
print()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"contree-client>=0.2.1",
"contree-client~=0.4.0",
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def request(self, spec: RequestSpec) -> ResponseData:
self.raw_requests.append(spec)
if self.raw_responses:
return self.raw_responses.popleft()
raise testing.unmocked(spec)
return self.mocked_response(spec)


class ContreeTestIAMClient(ContreeTestClient):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def mock_whoami_response(tc: ContreeTestClient, status: int, body: bytes) -> Non

Runs ``operations.parse_whoami`` over a synthetic response so the
tests keep exercising the client's own status/JSON/model error
behavior (401 -> UnauthorizedError, non-dict -> ContreeAPIError,
behavior (401 -> AuthenticationError, non-dict -> APIStatusError,
missing model field -> TypeError, invalid JSON -> ValueError).
"""
response = ResponseData(status=status, headers={}, body=body)
Expand Down Expand Up @@ -277,7 +277,7 @@ def test_non_dict_whoami_payload_rejected(self, config_dir, caplog):
with caplog.at_level("ERROR"), mock_whoami(body=b"[]"):
rc = cmd_auth(args)
assert rc == 1
assert "Token verification failed" in caplog.text
assert "Could not parse /v1/whoami response" in caplog.text
assert Config().resolve().token is None

def test_success_logs_saved(self, config_dir, caplog):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_file_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest
from conftest import ContreeTestClient
from contree_client.exceptions import ContreeAPIError, NotFoundError
from contree_client.exceptions import APIStatusError, NotFoundError
from contree_client.models import File, FileResponse, FilesListResponse

from contree_cli import CLIENT, FORMATTER, SESSION_STORE
Expand Down Expand Up @@ -227,9 +227,9 @@ def test_non_404_error_propagates(
args = FileEditArgs(path="/etc/config.ini")
contree_client.mock(
"inspect_image_download_stream",
error=ContreeAPIError(403, "forbidden"),
error=APIStatusError(403, "forbidden"),
)
with pytest.raises(ContreeAPIError) as exc_info:
with pytest.raises(APIStatusError) as exc_info:
_run_file_edit(contree_client, args, store=session_store)
assert exc_info.value.status == 403

Expand Down
8 changes: 4 additions & 4 deletions tests/test_kill.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextvars import copy_context

from conftest import ContreeTestClient
from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import OperationSummary

from contree_cli import CLIENT
Expand Down Expand Up @@ -31,7 +31,7 @@ def test_logs_cancellation(self, contree_client, caplog):
assert "Cancelled operation op-456" in caplog.text

def test_not_found_logs_and_sets_exit(self, contree_client, caplog):
contree_client.mock("cancel_operation", error=ContreeAPIError(404, "nope"))
contree_client.mock("cancel_operation", error=APIStatusError(404, "nope"))
CLIENT.set(contree_client)
ctx = copy_context()
with caplog.at_level("ERROR"):
Expand All @@ -41,7 +41,7 @@ def test_not_found_logs_and_sets_exit(self, contree_client, caplog):

def test_conflict_logs_and_sets_exit(self, contree_client, caplog):
contree_client.mock(
"cancel_operation", error=ContreeAPIError(409, "already done")
"cancel_operation", error=APIStatusError(409, "already done")
)
CLIENT.set(contree_client)
ctx = copy_context()
Expand Down Expand Up @@ -86,7 +86,7 @@ def _run_kill_all(pages, *, cancel_failures=None):
if op["uuid"] in cancel_failures:
tc.mock(
"cancel_operation",
error=ContreeAPIError(409, "conflict"),
error=APIStatusError(409, "conflict"),
)
else:
tc.mock("cancel_operation", None)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
from conftest import ContreeTestClient
from contree_client.exceptions import ContreeAPIError
from contree_client.exceptions import APIStatusError
from contree_client.models import OperationEvent, OperationResponse, OperationSummary

from contree_cli import CLIENT, FORMATTER, SESSION_STORE
Expand Down Expand Up @@ -222,7 +222,7 @@ def test_show_continues_on_api_error(
):
# First UUID -> 404, then a successful one
contree_client.mock(
"get_operation_status", error=ContreeAPIError(404, "not found")
"get_operation_status", error=APIStatusError(404, "not found")
)
mock_op(contree_client, make_op("op-b"))

Expand Down Expand Up @@ -378,7 +378,7 @@ def test_cancel_continues_on_error(self, contree_client, caplog):
rc = run_cancel(
contree_client,
uuids=["op-a", "op-b"],
cancel_outcomes=[ContreeAPIError(409, "conflict"), None],
cancel_outcomes=[APIStatusError(409, "conflict"), None],
)
assert rc == 1
assert "Failed to cancel op-a" in caplog.text
Expand Down
Loading
Loading