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
« 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#
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
21__all__ = ["ErrorContext"]
24class ErrorContext:
25 __slots__ = ("_data", "_error")
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:
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 )
45 if isinstance(message, ErrorMessage):
46 data = {}
47 reqd = message.required_fields()
48 if reqd is not None:
49 data = dict.fromkeys(reqd, "UNKNOWN")
51 message = message.formatted(data)
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 }
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:
73 from flipdare.error.app_error import AppError
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}"
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 )
90 return cls.server_error(
91 endpoint,
92 error_code=error_code,
93 message=message,
94 cause=str(error),
95 error=error,
96 )
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 )
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)
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)
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)
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)
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)
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)
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)
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)
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)
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
192 return code, title, actual_message
194 @property
195 def category(self) -> AppLogCategory:
196 return self._data["category"]
198 @property
199 def error_code(self) -> AppErrorProtocol:
200 return self._data["code"]
202 @property
203 def http_code(self) -> int:
204 return self._data["code"].http_code
206 @property
207 def endpoint(self) -> str:
208 return self._data["endpoint"]
210 @property
211 def title(self) -> str:
212 return self._data["title"]
214 @property
215 def message(self) -> str:
216 return self._data["message"]
218 @property
219 def cause(self) -> str | None:
220 cause = self._data.get("cause")
221 return cause if cause != NOT_APPLICABLE else None
223 @property
224 def error(self) -> Exception | None:
225 return self._error
227 def to_dict(self) -> ErrorSchema:
228 return self._data
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 )
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 )
247 @override
248 def __str__(self) -> str:
249 return self.__repr__()