Coverage for functions \ flipdare \ error \ error_context.py: 84%

109 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-05-08 12:22 +1000

1#!/usr/bin/env python 

2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved. 

3# 

4# This file is part of Flipdare's proprietary software and contains 

5# confidential and copyrighted material. Unauthorised copying, 

6# modification, distribution, or use of this file is strictly 

7# prohibited without prior written permission from Flipdare Pty Ltd. 

8# 

9# This software includes third-party components licensed under MIT, 

10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details. 

11# 

12 

13 

14from typing import Any, Self, override 

15from flipdare.app_globals import is_text_present, truncate_error 

16from flipdare.constants import NOT_APPLICABLE 

17from flipdare.error.app_error_protocol import AppErrorProtocol 

18from flipdare.generated import AppLogCategory, AppErrorCode, ErrorSchema 

19from flipdare.message.error_message import ErrorMessage 

20 

21__all__ = ["ErrorContext"] 

22 

23 

24class ErrorContext: 

25 __slots__ = ("_data", "_error") 

26 

27 def __init__( 

28 self, 

29 endpoint: str, 

30 *, 

31 error_code: AppErrorProtocol = AppErrorCode.SERVER, 

32 message: str | ErrorMessage = ErrorMessage.INTERNAL_ERROR, 

33 title: str | None = None, 

34 cause: str | None = None, 

35 error: Exception | None = None, 

36 ) -> None: 

37 

38 cause = truncate_error(cause) if is_text_present(cause) else NOT_APPLICABLE 

39 endpoint = ( 

40 truncate_error(endpoint, max_length=200) 

41 if is_text_present(endpoint) 

42 else NOT_APPLICABLE 

43 ) 

44 

45 if isinstance(message, ErrorMessage): 

46 data = {} 

47 reqd = message.required_fields() 

48 if reqd is not None: 

49 data = dict.fromkeys(reqd, "UNKNOWN") 

50 

51 message = message.formatted(data) 

52 

53 title = title if is_text_present(title) else error_code.category.label 

54 self._error = error 

55 self._data: ErrorSchema = { 

56 "endpoint": endpoint, 

57 "code": error_code, 

58 "category": error_code.category, 

59 "title": title, 

60 "message": message, 

61 "cause": cause, 

62 } 

63 

64 @classmethod 

65 def from_exception( 

66 cls, 

67 endpoint: str, 

68 message: ErrorMessage | str, 

69 error: Exception, 

70 error_code: AppErrorProtocol = AppErrorCode.SERVER, 

71 ) -> Self: 

72 

73 from flipdare.error.app_error import AppError 

74 

75 if isinstance(error, AppError): 

76 msg = error.message 

77 error_msg = str(message) 

78 if error_msg not in msg: 

79 msg = f"{msg}\n{error_msg}" 

80 

81 return cls( 

82 endpoint=endpoint, 

83 error_code=error.error_code, 

84 message=msg, 

85 title=error.title, 

86 cause=str(error.cause) if error.cause else None, 

87 error=error, 

88 ) 

89 

90 return cls.server_error( 

91 endpoint, 

92 error_code=error_code, 

93 message=message, 

94 cause=str(error), 

95 error=error, 

96 ) 

97 

98 @classmethod 

99 def db_error(cls, endpoint: str, message: ErrorMessage | str, error: Exception) -> Self: 

100 return cls.from_exception( 

101 endpoint=endpoint, 

102 message=message, 

103 error=error, 

104 error_code=AppErrorCode.DATABASE_EX, 

105 ) 

106 

107 @classmethod 

108 def code_path(cls, endpoint: str, **kwargs: Any) -> Self: 

109 code, title, message = cls._get_overrides( 

110 AppErrorCode.UNEXPECTED_CODE_PATH, 

111 ErrorMessage.CODE_PATH_ERROR, 

112 kwargs, 

113 ) 

114 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

115 

116 @classmethod 

117 def not_found(cls, endpoint: str, **kwargs: Any) -> Self: 

118 code, title, message = cls._get_overrides( 

119 AppErrorCode.NOT_FOUND, 

120 ErrorMessage.INTERNAL_ERROR, 

121 kwargs, 

122 ) 

123 return cls(endpoint, title=title, error_code=code, message=message, **kwargs) 

124 

125 @classmethod 

126 def invalid_data(cls, endpoint: str, **kwargs: Any) -> Self: 

127 code, title, message = cls._get_overrides( 

128 AppErrorCode.INVALID_DATA, 

129 ErrorMessage.INTERNAL_ERROR, 

130 kwargs, 

131 ) 

132 return cls(endpoint, title=title, error_code=code, message=message, **kwargs) 

133 

134 @classmethod 

135 def server_error(cls, endpoint: str, **kwargs: Any) -> Self: 

136 code, title, message = cls._get_overrides( 

137 AppErrorCode.SERVER, 

138 ErrorMessage.INTERNAL_ERROR, 

139 kwargs, 

140 ) 

141 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

142 

143 @classmethod 

144 def unauthorized(cls, endpoint: str, **kwargs: Any) -> Self: 

145 code, title, message = cls._get_overrides( 

146 AppErrorCode.PERMISSION_DENIED, 

147 ErrorMessage.FIREBASE_AUTH_ERROR, 

148 kwargs, 

149 ) 

150 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

151 

152 @classmethod 

153 def forbidden(cls, endpoint: str, **kwargs: Any) -> Self: 

154 code, title, message = cls._get_overrides( 

155 AppErrorCode.FORBIDDEN, 

156 ErrorMessage.FIREBASE_AUTH_ERROR, 

157 kwargs, 

158 ) 

159 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

160 

161 @classmethod 

162 def malformed(cls, endpoint: str, **kwargs: Any) -> Self: 

163 code, title, message = cls._get_overrides( 

164 AppErrorCode.MALFORMED_REQUEST, 

165 ErrorMessage.INTERNAL_ERROR, 

166 kwargs, 

167 ) 

168 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

169 

170 @classmethod 

171 def search_error(cls, endpoint: str, **kwargs: Any) -> Self: 

172 code, title, message = cls._get_overrides( 

173 AppErrorCode.SEARCH, 

174 ErrorMessage.INTERNAL_ERROR, 

175 kwargs, 

176 ) 

177 return cls(endpoint, error_code=code, title=title, message=message, **kwargs) 

178 

179 @staticmethod 

180 def _get_overrides( 

181 def_code: AppErrorProtocol, 

182 def_message: ErrorMessage | str, 

183 kwargs: dict[str, Any], 

184 ) -> tuple[AppErrorProtocol, str, str]: 

185 code = kwargs.pop("error_code", def_code) 

186 override_title = kwargs.pop("title", None) 

187 override_message = kwargs.pop("message", None) 

188 

189 title = override_title if override_title is not None else code.display_title 

190 actual_message = override_message if override_message is not None else def_message 

191 

192 return code, title, actual_message 

193 

194 @property 

195 def category(self) -> AppLogCategory: 

196 return self._data["category"] 

197 

198 @property 

199 def error_code(self) -> AppErrorProtocol: 

200 return self._data["code"] 

201 

202 @property 

203 def http_code(self) -> int: 

204 return self._data["code"].http_code 

205 

206 @property 

207 def endpoint(self) -> str: 

208 return self._data["endpoint"] 

209 

210 @property 

211 def title(self) -> str: 

212 return self._data["title"] 

213 

214 @property 

215 def message(self) -> str: 

216 return self._data["message"] 

217 

218 @property 

219 def cause(self) -> str | None: 

220 cause = self._data.get("cause") 

221 return cause if cause != NOT_APPLICABLE else None 

222 

223 @property 

224 def error(self) -> Exception | None: 

225 return self._error 

226 

227 def to_dict(self) -> ErrorSchema: 

228 return self._data 

229 

230 def copy_with(self, **kwargs: Any) -> Self: 

231 return self.__class__( 

232 endpoint=kwargs.get("endpoint", self.endpoint), 

233 error_code=kwargs.get("error_code", self.error_code), 

234 title=kwargs.get("title", self.title), 

235 message=kwargs.get("message", self.message), 

236 cause=kwargs.get("cause", self.cause), 

237 error=kwargs.get("error", self.error), 

238 ) 

239 

240 @override 

241 def __repr__(self) -> str: 

242 return ( 

243 f"{self.__class__.__name__}(endpoint={self.endpoint}, " 

244 f"error_code={self.error_code}, title={self.title}, message={self.message}, cause={self.cause})" 

245 ) 

246 

247 @override 

248 def __str__(self) -> str: 

249 return self.__repr__()