Coverage for dj/errors.py: 100%
95 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-17 20:05 -0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-17 20:05 -0700
1"""
2Errors and warnings.
3"""
5from enum import Enum
6from typing import Any, Dict, List, Literal, Optional, TypedDict
8from sqlmodel import SQLModel
11class ErrorCode(int, Enum):
12 """
13 Error codes.
14 """
16 # generic errors
17 UNKNOWN_ERROR = 0
18 NOT_IMPLEMENTED_ERROR = 1
19 ALREADY_EXISTS = 2
21 # metric API
22 INVALID_FILTER_PATTERN = 100
23 INVALID_COLUMN_IN_FILTER = 101
24 INVALID_VALUE_IN_FILTER = 102
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
35 # SQL Build Error
36 COMPOUND_BUILD_EXCEPTION = 300
37 MISSING_PARENT = 201
40class DebugType(TypedDict, total=False):
41 """
42 Type for debug information.
43 """
45 # link to where an issue can be filed
46 issue: str
48 # link to documentation about the problem
49 documentation: str
51 # any additional context
52 context: Dict[str, Any]
55class DJErrorType(TypedDict):
56 """
57 Type for serialized errors.
58 """
60 code: int
61 message: str
62 debug: Optional[DebugType]
65class DJError(SQLModel):
66 """
67 An error.
68 """
70 code: ErrorCode
71 message: str
72 debug: Optional[Dict[str, Any]]
73 context: str = ""
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})"
83class DJErrorException(Exception):
84 """
85 Wrapper allows raising DJError
86 """
88 def __init__(self, dj_error: DJError):
89 self.dj_error = dj_error
92class DJWarningType(TypedDict):
93 """
94 Type for serialized warnings.
95 """
97 code: Optional[int]
98 message: str
99 debug: Optional[DebugType]
102class DJWarning(SQLModel):
103 """
104 A warning.
105 """
107 code: Optional[ErrorCode] = None
108 message: str
109 debug: Optional[Dict[str, Any]]
112DBAPIExceptions = Literal[
113 "Warning",
114 "Error",
115 "InterfaceError",
116 "DatabaseError",
117 "DataError",
118 "OperationalError",
119 "IntegrityError",
120 "InternalError",
121 "ProgrammingError",
122 "NotSupportedError",
123]
126class DJExceptionType(TypedDict):
127 """
128 Type for serialized exceptions.
129 """
131 message: Optional[str]
132 errors: List[DJErrorType]
133 warnings: List[DJWarningType]
136class DJException(Exception):
137 """
138 Base class for errors.
139 """
141 message: str
142 errors: List[DJError]
143 warnings: List[DJWarning]
145 # exception that should be raised when ``DJException`` is caught by the DB API cursor
146 dbapi_exception: DBAPIExceptions = "Error"
148 # status code that should be returned when ``DJException`` is caught by the API layer
149 http_status_code: int = 500
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)
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
168 super().__init__(self.message)
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 }
180 def __str__(self) -> str:
181 """
182 Format the exception nicely.
183 """
184 if not self.errors:
185 return self.message
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}"
191 return f"{self.message}\n{errors}"
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 )
204class DJInvalidInputException(DJException):
205 """
206 Exception raised when the input provided by the user is invalid.
207 """
209 dbapi_exception: DBAPIExceptions = "ProgrammingError"
210 http_status_code: int = 422
213class DJNotImplementedException(DJException):
214 """
215 Exception raised when some functionality hasn't been implemented in DJ yet.
216 """
218 dbapi_exception: DBAPIExceptions = "NotSupportedError"
219 http_status_code: int = 500
222class DJInternalErrorException(DJException):
223 """
224 Exception raised when we do something wrong in the code.
225 """
227 dbapi_exception: DBAPIExceptions = "InternalError"
228 http_status_code: int = 500
231class DJAlreadyExistsException(DJException):
232 """
233 Exception raised when trying to create an entity that already exists.
234 """
236 dbapi_exception: DBAPIExceptions = "DataError"
237 http_status_code: int = 500
240class DJDoesNotExistException(DJException):
241 """
242 Exception raised when an entity doesn't exist.
243 """
245 dbapi_exception: DBAPIExceptions = "DataError"
246 http_status_code: int = 404
249class DJQueryServiceClientException(DJException):
250 """
251 Exception raised when the query service returns an error
252 """
254 dbapi_exception: DBAPIExceptions = "InterfaceError"
255 http_status_code: int = 500