Coverage for dj/errors.py: 100%

95 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-04-17 20:05 -0700

1""" 

2Errors and warnings. 

3""" 

4 

5from enum import Enum 

6from typing import Any, Dict, List, Literal, Optional, TypedDict 

7 

8from sqlmodel import SQLModel 

9 

10 

11class ErrorCode(int, Enum): 

12 """ 

13 Error codes. 

14 """ 

15 

16 # generic errors 

17 UNKNOWN_ERROR = 0 

18 NOT_IMPLEMENTED_ERROR = 1 

19 ALREADY_EXISTS = 2 

20 

21 # metric API 

22 INVALID_FILTER_PATTERN = 100 

23 INVALID_COLUMN_IN_FILTER = 101 

24 INVALID_VALUE_IN_FILTER = 102 

25 

26 # SQL API 

27 INVALID_ARGUMENTS_TO_FUNCTION = 200 

28 INVALID_SQL_QUERY = 201 

29 MISSING_COLUMNS = 202 

30 UNKNOWN_NODE = 203 

31 NODE_TYPE_ERROR = 204 

32 INVALID_DIMENSION_JOIN = 205 

33 INVALID_COLUMN = 206 

34 

35 # SQL Build Error 

36 COMPOUND_BUILD_EXCEPTION = 300 

37 MISSING_PARENT = 201 

38 

39 

40class DebugType(TypedDict, total=False): 

41 """ 

42 Type for debug information. 

43 """ 

44 

45 # link to where an issue can be filed 

46 issue: str 

47 

48 # link to documentation about the problem 

49 documentation: str 

50 

51 # any additional context 

52 context: Dict[str, Any] 

53 

54 

55class DJErrorType(TypedDict): 

56 """ 

57 Type for serialized errors. 

58 """ 

59 

60 code: int 

61 message: str 

62 debug: Optional[DebugType] 

63 

64 

65class DJError(SQLModel): 

66 """ 

67 An error. 

68 """ 

69 

70 code: ErrorCode 

71 message: str 

72 debug: Optional[Dict[str, Any]] 

73 context: str = "" 

74 

75 def __str__(self) -> str: 

76 """ 

77 Format the error nicely. 

78 """ 

79 context = f" from `{self.context}`" if self.context else "" 

80 return f"{self.message}{context} (error code: {self.code})" 

81 

82 

83class DJErrorException(Exception): 

84 """ 

85 Wrapper allows raising DJError 

86 """ 

87 

88 def __init__(self, dj_error: DJError): 

89 self.dj_error = dj_error 

90 

91 

92class DJWarningType(TypedDict): 

93 """ 

94 Type for serialized warnings. 

95 """ 

96 

97 code: Optional[int] 

98 message: str 

99 debug: Optional[DebugType] 

100 

101 

102class DJWarning(SQLModel): 

103 """ 

104 A warning. 

105 """ 

106 

107 code: Optional[ErrorCode] = None 

108 message: str 

109 debug: Optional[Dict[str, Any]] 

110 

111 

112DBAPIExceptions = Literal[ 

113 "Warning", 

114 "Error", 

115 "InterfaceError", 

116 "DatabaseError", 

117 "DataError", 

118 "OperationalError", 

119 "IntegrityError", 

120 "InternalError", 

121 "ProgrammingError", 

122 "NotSupportedError", 

123] 

124 

125 

126class DJExceptionType(TypedDict): 

127 """ 

128 Type for serialized exceptions. 

129 """ 

130 

131 message: Optional[str] 

132 errors: List[DJErrorType] 

133 warnings: List[DJWarningType] 

134 

135 

136class DJException(Exception): 

137 """ 

138 Base class for errors. 

139 """ 

140 

141 message: str 

142 errors: List[DJError] 

143 warnings: List[DJWarning] 

144 

145 # exception that should be raised when ``DJException`` is caught by the DB API cursor 

146 dbapi_exception: DBAPIExceptions = "Error" 

147 

148 # status code that should be returned when ``DJException`` is caught by the API layer 

149 http_status_code: int = 500 

150 

151 def __init__( # pylint: disable=too-many-arguments 

152 self, 

153 message: Optional[str] = None, 

154 errors: Optional[List[DJError]] = None, 

155 warnings: Optional[List[DJWarning]] = None, 

156 dbapi_exception: Optional[DBAPIExceptions] = None, 

157 http_status_code: Optional[int] = None, 

158 ): 

159 self.errors = errors or [] 

160 self.warnings = warnings or [] 

161 self.message = message or "\n".join(error.message for error in self.errors) 

162 

163 if dbapi_exception is not None: 

164 self.dbapi_exception = dbapi_exception 

165 if http_status_code is not None: 

166 self.http_status_code = http_status_code 

167 

168 super().__init__(self.message) 

169 

170 def to_dict(self) -> DJExceptionType: 

171 """ 

172 Convert to dict. 

173 """ 

174 return { 

175 "message": self.message, 

176 "errors": [error.dict() for error in self.errors], 

177 "warnings": [warning.dict() for warning in self.warnings], 

178 } 

179 

180 def __str__(self) -> str: 

181 """ 

182 Format the exception nicely. 

183 """ 

184 if not self.errors: 

185 return self.message 

186 

187 plural = "s" if len(self.errors) > 1 else "" 

188 combined_errors = "\n".join(f"- {error}" for error in self.errors) 

189 errors = f"The following error{plural} happened:\n{combined_errors}" 

190 

191 return f"{self.message}\n{errors}" 

192 

193 def __eq__(self, other) -> bool: 

194 return ( 

195 isinstance(other, DJException) 

196 and self.message == other.message 

197 and self.errors == other.errors 

198 and self.warnings == other.warnings 

199 and self.dbapi_exception == other.dbapi_exception 

200 and self.http_status_code == other.http_status_code 

201 ) 

202 

203 

204class DJInvalidInputException(DJException): 

205 """ 

206 Exception raised when the input provided by the user is invalid. 

207 """ 

208 

209 dbapi_exception: DBAPIExceptions = "ProgrammingError" 

210 http_status_code: int = 422 

211 

212 

213class DJNotImplementedException(DJException): 

214 """ 

215 Exception raised when some functionality hasn't been implemented in DJ yet. 

216 """ 

217 

218 dbapi_exception: DBAPIExceptions = "NotSupportedError" 

219 http_status_code: int = 500 

220 

221 

222class DJInternalErrorException(DJException): 

223 """ 

224 Exception raised when we do something wrong in the code. 

225 """ 

226 

227 dbapi_exception: DBAPIExceptions = "InternalError" 

228 http_status_code: int = 500 

229 

230 

231class DJAlreadyExistsException(DJException): 

232 """ 

233 Exception raised when trying to create an entity that already exists. 

234 """ 

235 

236 dbapi_exception: DBAPIExceptions = "DataError" 

237 http_status_code: int = 500 

238 

239 

240class DJDoesNotExistException(DJException): 

241 """ 

242 Exception raised when an entity doesn't exist. 

243 """ 

244 

245 dbapi_exception: DBAPIExceptions = "DataError" 

246 http_status_code: int = 404 

247 

248 

249class DJQueryServiceClientException(DJException): 

250 """ 

251 Exception raised when the query service returns an error 

252 """ 

253 

254 dbapi_exception: DBAPIExceptions = "InterfaceError" 

255 http_status_code: int = 500