Coverage for harbor_cli/exceptions.py: 57%
76 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from typing import Any
4from typing import Dict
5from typing import Mapping
6from typing import NoReturn
7from typing import Protocol
8from typing import runtime_checkable
9from typing import Type
11from harborapi.exceptions import BadRequest
12from harborapi.exceptions import Conflict
13from harborapi.exceptions import Forbidden
14from harborapi.exceptions import InternalServerError
15from harborapi.exceptions import MethodNotAllowed
16from harborapi.exceptions import NotFound
17from harborapi.exceptions import PreconditionFailed
18from harborapi.exceptions import StatusError
19from harborapi.exceptions import Unauthorized
20from harborapi.exceptions import UnsupportedMediaType
21from pydantic import ValidationError
24class HarborCLIError(Exception):
25 """Base class for all exceptions."""
28class ConfigError(HarborCLIError):
29 """Error loading the configuration file."""
32class ConfigFileNotFoundError(ConfigError, FileNotFoundError):
33 """Configuration file not found."""
36class DirectoryCreateError(HarborCLIError, OSError):
37 """Error creating a required program directory."""
40class CredentialsError(HarborCLIError):
41 """Error loading credentials."""
44class OverwriteError(HarborCLIError, FileExistsError):
45 """Error overwriting an existing file."""
48class ArtifactNameFormatError(HarborCLIError):
49 def __init__(self, s: str) -> None:
50 super().__init__(
51 self,
52 f"Artifact string {s} is not in the correct format. "
53 "Expected format: [domain/]<project>/<repo>{@sha256:<digest>,:<tag>}",
54 )
57MESSAGE_BADREQUEST = "400 Bad request: {method} {url}. Check your input. If you think this is a bug, please report it."
58MESSAGE_UNAUTHORIZED = "401 Unauthorized: {method} {url}. Check your credentials."
59MESSAGE_FORBIDDEN = "403 Forbidden: {method} {url}. Make sure you have permissions to access the resource."
60MESSAGE_NOTFOUND = "404 Not Found: {method} {url}. Resource not found."
61MESSAGE_METHODNOTALLOWED = "405 Method Not Allowed: {method} {url}. This is either a bug, or a problem with your server or credentials."
62MESSAGE_CONFLICT = "409 Conflict: {method} {url}. Resource already exists."
63MESSAGE_PRECONDITIONFAILED = "412 Precondition Failed: {method} {url} Check your input. If you think this is a bug, please report it."
64MESSAGE_UNSUPPORTEDMEDIATYPE = "415 Unsupported Media Type: {method} {url}. Check your input. If you think this is a bug, please report it."
65MESSAGE_INTERNALSERVERERROR = "500 Internal Server Error: {method} {url}. Check your input. If you think this is a bug, please report it."
67MESSAGE_MAPPING = {
68 BadRequest: MESSAGE_BADREQUEST,
69 Unauthorized: MESSAGE_UNAUTHORIZED,
70 Forbidden: MESSAGE_FORBIDDEN,
71 NotFound: MESSAGE_NOTFOUND,
72 MethodNotAllowed: MESSAGE_METHODNOTALLOWED,
73 Conflict: MESSAGE_CONFLICT,
74 PreconditionFailed: MESSAGE_PRECONDITIONFAILED,
75 UnsupportedMediaType: MESSAGE_UNSUPPORTEDMEDIATYPE,
76 InternalServerError: MESSAGE_INTERNALSERVERERROR,
77}
80class Default(Dict[str, Any]):
81 """Dict subclass used for str.format_map() to provide default.
82 Missing keys are replaced with the key surrounded by curly braces."""
84 def __missing__(self, key: str) -> str:
85 return "{" + key + "}"
88def handle_status_error(e: StatusError) -> NoReturn:
89 """Handles an HTTP status error from the Harbor API and exits with
90 the appropriate message.
91 """
92 from .output.console import exit_err # avoid circular import
93 from .logs import logger
95 # It's not _guaranteed_ that the StatusError has a __cause__, but
96 # in practice it should always have one. It is up to harborapi to
97 # ensure that this is the case, but it's currently not guaranteed.
98 # In the cases where it's not, we just exit with the default message.
99 if not e.__cause__:
100 exit_err(str(e))
102 url = e.__cause__.request.url
103 method = e.__cause__.request.method
104 httpx_message = e.__cause__.args[0]
106 # Log all errors from the API
107 for error in e.errors:
108 logger.error(f"{error.code}: {error.message}")
110 # Exception has custom message if its message is different from the
111 # underlying HTTPX exception's message
112 msg = e.args[0]
113 has_default_message = httpx_message == msg
115 # Use custom message from our mapping if the exception has default HTTPX msg
116 # and we have a custom message for the exception type
117 # The default HTTPX messages are not very helpful.
118 if has_default_message:
119 template = MESSAGE_MAPPING.get(type(e), None)
120 if template:
121 msg = template.format_map(Default(url=url, method=method))
122 exit_err(msg)
125class Exiter(Protocol):
126 def __call__(
127 self, msg: str, code: int = ..., prefix: str = ..., **extra: Any
128 ) -> NoReturn:
129 ...
132@runtime_checkable
133class HandleFunc(Protocol):
134 def __call__(self, e: Any, exiter: Exiter) -> NoReturn:
135 ...
138def handle_validationerror(e: ValidationError, exiter: Exiter) -> NoReturn:
139 """Handles a pydantic ValidationError and exits with the appropriate message."""
140 from .output.console import err_console
142 err_console.print("Failed to validate data from API.")
143 exiter(str(e), errors=e.errors())
146EXC_HANDLERS: Mapping[Type[Exception], HandleFunc] = {
147 ValidationError: handle_validationerror,
148}
151def handle_exception(e: Exception) -> NoReturn:
152 """Handles an exception and exits with the appropriate message."""
153 from .output.console import exit_err # avoid circular import
155 handler = EXC_HANDLERS.get(type(e), None)
156 if not handler:
157 exit_err(str(e))
158 handler(e, exit_err)