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

1from __future__ import annotations 

2 

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 

10 

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 

22 

23 

24class HarborCLIError(Exception): 

25 """Base class for all exceptions.""" 

26 

27 

28class ConfigError(HarborCLIError): 

29 """Error loading the configuration file.""" 

30 

31 

32class ConfigFileNotFoundError(ConfigError, FileNotFoundError): 

33 """Configuration file not found.""" 

34 

35 

36class DirectoryCreateError(HarborCLIError, OSError): 

37 """Error creating a required program directory.""" 

38 

39 

40class CredentialsError(HarborCLIError): 

41 """Error loading credentials.""" 

42 

43 

44class OverwriteError(HarborCLIError, FileExistsError): 

45 """Error overwriting an existing file.""" 

46 

47 

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 ) 

55 

56 

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." 

66 

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} 

78 

79 

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.""" 

83 

84 def __missing__(self, key: str) -> str: 

85 return "{" + key + "}" 

86 

87 

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 

94 

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)) 

101 

102 url = e.__cause__.request.url 

103 method = e.__cause__.request.method 

104 httpx_message = e.__cause__.args[0] 

105 

106 # Log all errors from the API 

107 for error in e.errors: 

108 logger.error(f"{error.code}: {error.message}") 

109 

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 

114 

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) 

123 

124 

125class Exiter(Protocol): 

126 def __call__( 

127 self, msg: str, code: int = ..., prefix: str = ..., **extra: Any 

128 ) -> NoReturn: 

129 ... 

130 

131 

132@runtime_checkable 

133class HandleFunc(Protocol): 

134 def __call__(self, e: Any, exiter: Exiter) -> NoReturn: 

135 ... 

136 

137 

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 

141 

142 err_console.print("Failed to validate data from API.") 

143 exiter(str(e), errors=e.errors()) 

144 

145 

146EXC_HANDLERS: Mapping[Type[Exception], HandleFunc] = { 

147 ValidationError: handle_validationerror, 

148} 

149 

150 

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 

154 

155 handler = EXC_HANDLERS.get(type(e), None) 

156 if not handler: 

157 exit_err(str(e)) 

158 handler(e, exit_err)