mobius-error-py 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. mobius_error/__init__.py +60 -0
  2. mobius_error/config.py +13 -0
  3. mobius_error/errors/__init__.py +0 -0
  4. mobius_error/errors/common_errors.py +33 -0
  5. mobius_error/errors/error.py +17 -0
  6. mobius_error/errors/error_message.py +37 -0
  7. mobius_error/exceptions/__init__.py +0 -0
  8. mobius_error/exceptions/access_violation_exception.py +24 -0
  9. mobius_error/exceptions/api_exception.py +28 -0
  10. mobius_error/exceptions/application_exception.py +71 -0
  11. mobius_error/exceptions/data_type_mismatch_exception.py +11 -0
  12. mobius_error/exceptions/group_data_retrieval_exception.py +8 -0
  13. mobius_error/exceptions/invalid_name_exception.py +8 -0
  14. mobius_error/exceptions/invalid_tenant_exception.py +8 -0
  15. mobius_error/exceptions/kafka_consumption_exception.py +8 -0
  16. mobius_error/exceptions/kafka_exception.py +8 -0
  17. mobius_error/exceptions/object_mapping_exception.py +8 -0
  18. mobius_error/exceptions/rest_exception.py +11 -0
  19. mobius_error/exceptions/rest_get_exception.py +11 -0
  20. mobius_error/exceptions/rest_post_exception.py +11 -0
  21. mobius_error/exceptions/token_exception.py +26 -0
  22. mobius_error/exceptions/unsupported_operation_exception.py +8 -0
  23. mobius_error/exceptions/validation_exception.py +23 -0
  24. mobius_error/handlers/__init__.py +0 -0
  25. mobius_error/handlers/exception_handler.py +248 -0
  26. mobius_error/kafka/__init__.py +0 -0
  27. mobius_error/kafka/constants.py +5 -0
  28. mobius_error/kafka/producer.py +161 -0
  29. mobius_error/py.typed +0 -0
  30. mobius_error/responses/__init__.py +0 -0
  31. mobius_error/responses/api_error_response.py +117 -0
  32. mobius_error/responses/error_response.py +84 -0
  33. mobius_error_py-1.0.0.dist-info/METADATA +575 -0
  34. mobius_error_py-1.0.0.dist-info/RECORD +37 -0
  35. mobius_error_py-1.0.0.dist-info/WHEEL +5 -0
  36. mobius_error_py-1.0.0.dist-info/licenses/LICENSE +21 -0
  37. mobius_error_py-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,248 @@
1
+ """
2
+ Mirrors com.aidtaas.mobius.error.services.handler.RestExceptionHandler
3
+
4
+ Call ``register_exception_handlers(app)`` once at startup to install all
5
+ handlers on a FastAPI application instance.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Optional
12
+
13
+ from fastapi import FastAPI, Request
14
+ from fastapi.exceptions import RequestValidationError
15
+ from fastapi.responses import JSONResponse
16
+ from starlette.exceptions import HTTPException as StarletteHTTPException
17
+ from starlette.middleware.base import BaseHTTPMiddleware
18
+
19
+ from mobius_error.config import App
20
+ from mobius_error.errors.error_message import ErrorMessage
21
+ from mobius_error.exceptions.access_violation_exception import AccessViolationException
22
+ from mobius_error.exceptions.api_exception import ApiException
23
+ from mobius_error.exceptions.application_exception import ApplicationException
24
+ from mobius_error.exceptions.data_type_mismatch_exception import DataTypeMismatchException
25
+ from mobius_error.exceptions.token_exception import TokenException
26
+ from mobius_error.exceptions.validation_exception import ValidationException
27
+ from mobius_error.kafka.producer import (
28
+ KafkaErrorProducer,
29
+ LogEventSender,
30
+ PyKafkaProducerClientSender,
31
+ get_error_producer,
32
+ set_error_producer,
33
+ )
34
+ from mobius_error.responses.api_error_response import ApiErrorResponse
35
+
36
+ logger = logging.getLogger("mobius_error.handler")
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Internal helpers
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def _emit(exc: Exception, resp: ApiErrorResponse) -> None:
44
+ """Fire-and-forget error event to Kafka if a producer is configured."""
45
+ producer = get_error_producer()
46
+ if producer is None:
47
+ return
48
+ producer.send_error_event(exc, resp)
49
+
50
+
51
+ def _resolve_sender(
52
+ kafka_sender: Optional[LogEventSender],
53
+ kafka_producer: Optional[Any],
54
+ kafka_config: Optional[Any],
55
+ ) -> Optional[LogEventSender]:
56
+ if kafka_sender is not None:
57
+ return kafka_sender
58
+ if kafka_producer is not None:
59
+ return PyKafkaProducerClientSender(kafka_producer)
60
+ try:
61
+ from kafka_producer_client.action_logger import ( # type: ignore[import]
62
+ configure_kafka_producer,
63
+ get_kafka_producer,
64
+ )
65
+ except ImportError:
66
+ return None
67
+ try:
68
+ if kafka_config is not None:
69
+ configure_kafka_producer(kafka_config)
70
+ return PyKafkaProducerClientSender(get_kafka_producer())
71
+ except Exception: # noqa: BLE001
72
+ logger.warning("mobius-error: could not initialize py-kafka-producer-client.", exc_info=True)
73
+ return None
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # Module-level exception handlers
78
+ # ---------------------------------------------------------------------------
79
+
80
+ def _handle_request_validation(_request: Request, exc: RequestValidationError) -> JSONResponse:
81
+ logger.error("Validation error :: %s", exc.errors())
82
+ resp = ApiErrorResponse.from_status_message_code(422, "Validation error", 400000)
83
+ for err in exc.errors():
84
+ field = " → ".join(str(loc) for loc in err.get("loc", []))
85
+ resp.add_sub_error(ErrorMessage(message=f"{field}: {err.get('msg', '')}"))
86
+ _emit(exc, resp)
87
+ return JSONResponse(status_code=422, content=resp.to_dict())
88
+
89
+
90
+ def _handle_http_exception(_request: Request, exc: StarletteHTTPException) -> JSONResponse:
91
+ status = exc.status_code
92
+ detail = exc.detail or "HTTP error"
93
+ logger.error("HTTP %s :: %s", status, detail)
94
+ resp = ApiErrorResponse.from_status(status, str(detail))
95
+ _emit(exc, resp)
96
+ return JSONResponse(status_code=status, content=resp.to_dict())
97
+
98
+
99
+ def _handle_token_exception(_request: Request, exc: TokenException) -> JSONResponse:
100
+ status = exc.http_status_code
101
+ logger.error("TokenException :: %s", exc)
102
+ error = exc.error
103
+ cause = error.error_message_description or exc.nested_error_message
104
+ resp = ApiErrorResponse.from_status_message_sub(
105
+ http_status_code=status,
106
+ message=error.error_message,
107
+ sub_error=cause if isinstance(cause, ErrorMessage) else None,
108
+ action_required=error.action_required,
109
+ error_code=error.error_code,
110
+ )
111
+ _emit(exc, resp)
112
+ return JSONResponse(status_code=status, content=resp.to_dict())
113
+
114
+
115
+ def _handle_access_violation(_request: Request, exc: AccessViolationException) -> JSONResponse:
116
+ logger.error("AccessViolationException :: %s", exc)
117
+ resp = ApiErrorResponse.from_application_exception(403, exc)
118
+ _emit(exc, resp)
119
+ return JSONResponse(status_code=403, content=resp.to_dict())
120
+
121
+
122
+ def _handle_data_type_mismatch(_request: Request, exc: DataTypeMismatchException) -> JSONResponse:
123
+ logger.error("DataTypeMismatchException :: %s", exc)
124
+ resp = ApiErrorResponse.from_application_exception(400, exc)
125
+ _emit(exc, resp)
126
+ return JSONResponse(status_code=400, content=resp.to_dict())
127
+
128
+
129
+ def _handle_validation_exception(_request: Request, exc: ValidationException) -> JSONResponse:
130
+ logger.error("ValidationException :: %s", exc)
131
+ resp = ApiErrorResponse.from_application_exception(409, exc)
132
+ _emit(exc, resp)
133
+ return JSONResponse(status_code=409, content=resp.to_dict())
134
+
135
+
136
+ def _handle_api_exception(_request: Request, exc: ApiException) -> JSONResponse:
137
+ status = exc.http_status_code
138
+ logger.error("ApiException :: %s", exc)
139
+ if exc.error_object is not None:
140
+ resp = ApiErrorResponse.from_application_exception(status, exc)
141
+ _emit(exc, resp)
142
+ return JSONResponse(status_code=status, content=resp.to_dict())
143
+ error = exc.error
144
+ cause = error.error_message_description or exc.nested_error_message
145
+ resp = ApiErrorResponse.from_status_message_sub(
146
+ http_status_code=status,
147
+ message=error.error_message,
148
+ sub_error=cause if isinstance(cause, ErrorMessage) else None,
149
+ action_required=error.action_required,
150
+ error_code=error.error_code,
151
+ )
152
+ _emit(exc, resp)
153
+ return JSONResponse(status_code=status, content=resp.to_dict())
154
+
155
+
156
+ def _handle_application_exception(_request: Request, exc: ApplicationException) -> JSONResponse:
157
+ logger.error("ApplicationException :: %s", exc)
158
+ resp = ApiErrorResponse.from_application_exception(403, exc)
159
+ _emit(exc, resp)
160
+ return JSONResponse(status_code=403, content=resp.to_dict())
161
+
162
+
163
+ def _handle_generic(_request: Request, exc: Exception) -> JSONResponse:
164
+ logger.error("Unexpected error :: %s", exc, exc_info=True)
165
+ resp = ApiErrorResponse.from_exception(500, exc)
166
+ _emit(exc, resp)
167
+ return JSONResponse(status_code=500, content=resp.to_dict())
168
+
169
+
170
+ class _CatchAllMiddleware(BaseHTTPMiddleware):
171
+ async def dispatch(self, request: Request, call_next): # type: ignore[override]
172
+ try:
173
+ return await call_next(request)
174
+ except Exception as exc:
175
+ logger.exception("Unexpected error (middleware) :: %s", exc)
176
+ resp = ApiErrorResponse.from_exception(500, exc)
177
+ _emit(exc, resp)
178
+ return JSONResponse(status_code=500, content=resp.to_dict())
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Public registration function
183
+ # ---------------------------------------------------------------------------
184
+
185
+ def register_exception_handlers(
186
+ app: FastAPI,
187
+ app_name: Optional[str] = None,
188
+ kafka_sender: Optional[LogEventSender] = None,
189
+ kafka_producer: Optional[Any] = None,
190
+ kafka_config: Optional[Any] = None,
191
+ topic: Optional[str] = None,
192
+ schema_id: Optional[str] = None,
193
+ tenant_id: Optional[str] = None,
194
+ ) -> Optional[KafkaErrorProducer]:
195
+ """
196
+ Register all Mobius-style exception handlers on a FastAPI app.
197
+
198
+ Parameters
199
+ ----------
200
+ app : FastAPI
201
+ The application instance.
202
+ app_name : str, optional
203
+ Sets ``App.app_name`` (equivalent to ``spring.application.name``).
204
+ If not provided, uses ``app.title``.
205
+ kafka_sender : LogEventSender, optional
206
+ Any object with ``send(value, *, topic)``. Highest priority.
207
+ kafka_producer : KafkaProducerClient, optional
208
+ A ``py-kafka-producer-client`` instance, wrapped automatically.
209
+ kafka_config : KafkaProducerConfig, optional
210
+ Configures the ``py-kafka-producer-client`` singleton.
211
+ topic : str, optional
212
+ Override the default Kafka topic (``platform-exception-topic``).
213
+ schema_id : str, optional
214
+ Override the default schema ID.
215
+ tenant_id : str, optional
216
+ Override the default tenant ID.
217
+
218
+ Returns
219
+ -------
220
+ KafkaErrorProducer or None
221
+ The registered producer, if any was configured.
222
+ """
223
+ name = app_name or app.title
224
+ App.set_app_name(name)
225
+
226
+ producer: Optional[KafkaErrorProducer] = None
227
+ sender = _resolve_sender(kafka_sender, kafka_producer, kafka_config)
228
+ if sender is not None:
229
+ producer = KafkaErrorProducer(sender, name, topic=topic, schema_id=schema_id, tenant_id=tenant_id)
230
+ set_error_producer(producer)
231
+ else:
232
+ logger.warning(
233
+ "mobius-error: no Kafka producer resolved; error events will not be published. "
234
+ "Pass kafka_config/kafka_producer/kafka_sender."
235
+ )
236
+
237
+ app.add_exception_handler(RequestValidationError, _handle_request_validation)
238
+ app.add_exception_handler(StarletteHTTPException, _handle_http_exception)
239
+ app.add_exception_handler(TokenException, _handle_token_exception)
240
+ app.add_exception_handler(AccessViolationException, _handle_access_violation)
241
+ app.add_exception_handler(DataTypeMismatchException, _handle_data_type_mismatch)
242
+ app.add_exception_handler(ValidationException, _handle_validation_exception)
243
+ app.add_exception_handler(ApiException, _handle_api_exception)
244
+ app.add_exception_handler(ApplicationException, _handle_application_exception)
245
+ app.add_exception_handler(Exception, _handle_generic)
246
+ app.add_middleware(_CatchAllMiddleware)
247
+
248
+ return producer
File without changes
@@ -0,0 +1,5 @@
1
+ """Kafka pipeline constants for the error schema — mirrors mobius-logging-py's constants.py."""
2
+
3
+ TOPIC = "platform-exception-topic"
4
+ SCHEMA_ID = "6a2bfc671730d70feb345ab5"
5
+ TENANT_ID = "2cf76e5f-26ad-4f2c-bccc-f4bc1e7bfb64"
@@ -0,0 +1,161 @@
1
+ """Kafka error-event producer. Mirrors KafkaLogProducer from mobius-logging-py.
2
+
3
+ Sends a DataIngestionOperation envelope to the platform-exception-topic whenever
4
+ an exception handler fires. Sending is best-effort and non-blocking (background
5
+ thread pool). Failures are logged, never raised into the request path.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import time
11
+ import uuid
12
+ from concurrent.futures import ThreadPoolExecutor
13
+ from dataclasses import dataclass
14
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, runtime_checkable
15
+
16
+ from mobius_error.kafka import constants
17
+
18
+ if TYPE_CHECKING:
19
+ from mobius_error.responses.api_error_response import ApiErrorResponse
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ _executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="mobius-error-producer")
24
+
25
+
26
+ @runtime_checkable
27
+ class LogEventSender(Protocol):
28
+ """Anything able to publish a dict event to a topic.
29
+
30
+ Matches py-kafka-producer-client shape: ``value`` is a dict,
31
+ ``topic`` is keyword-only.
32
+ """
33
+
34
+ def send(self, value: Dict[str, Any], *, topic: str) -> Any: ... # pragma: no cover
35
+
36
+
37
+ class PyKafkaProducerClientSender:
38
+ """Adapter for the internal py-kafka-producer-client (>=0.1.7).
39
+
40
+ Signature::
41
+
42
+ producer.send(value: dict, *, topic: str | None = None, ...) -> None
43
+ """
44
+
45
+ def __init__(self, producer: Any) -> None:
46
+ self._producer = producer
47
+
48
+ def send(self, value: Dict[str, Any], *, topic: str) -> Any:
49
+ return self._producer.send(value, topic=topic)
50
+
51
+
52
+ @dataclass
53
+ class DataIngestionOperation:
54
+ """Envelope payload — same shape as the logging lib."""
55
+
56
+ actionType: str
57
+ object: Dict[str, Any]
58
+ id: str
59
+ schemaId: str
60
+ tenantId: str
61
+
62
+ def to_dict(self) -> Dict[str, Any]:
63
+ return {
64
+ "actionType": self.actionType,
65
+ "object": self.object,
66
+ "id": self.id,
67
+ "schemaId": self.schemaId,
68
+ "tenantId": self.tenantId,
69
+ }
70
+
71
+
72
+ def _cause_string(exc: Exception, resp: "ApiErrorResponse") -> Optional[str]:
73
+ """Return cause as a plain string, matching the sample payload shape."""
74
+ if exc.__cause__ is not None:
75
+ return str(exc.__cause__)
76
+ if resp.cause is not None:
77
+ return resp.cause.message or resp.cause.root_message
78
+ return None
79
+
80
+
81
+ def _build_error_object(
82
+ exc: Exception,
83
+ resp: "ApiErrorResponse",
84
+ error_id: str,
85
+ timestamp_ms: int,
86
+ errorcategory: str,
87
+ ) -> Dict[str, Any]:
88
+ """Map ApiErrorResponse fields to the Error-Response-01 schema attribute names."""
89
+ return {
90
+ "errorid": error_id, # string (PK)
91
+ "timestamp": timestamp_ms, # long (ms)
92
+ "errorcategory": errorcategory, # string
93
+ "errormessage": resp.error_message or None, # string
94
+ "origin": resp.origin or None, # string
95
+ "httpstatuscode": resp.http_status_code, # integer (matches sample)
96
+ "errorcode": resp.error_code or None, # integer
97
+ "detailederrormessage": resp.detailed_error_message or None, # longtext
98
+ "cause": _cause_string(exc, resp), # string (matches sample)
99
+ "suberrors": [se.to_dict() for se in resp.sub_errors], # json_array (always present)
100
+ "actionsrequired": resp.actions_required or [], # string_array
101
+ "errorobject": resp.error_object, # json (null if absent)
102
+ "docurl": resp.doc_url or None, # longtext
103
+ "hash": None, # string (null — reserved)
104
+ }
105
+
106
+
107
+ class KafkaErrorProducer:
108
+ def __init__(
109
+ self,
110
+ sender: LogEventSender,
111
+ service_name: str,
112
+ *,
113
+ topic: Optional[str] = None,
114
+ schema_id: Optional[str] = None,
115
+ tenant_id: Optional[str] = None,
116
+ ) -> None:
117
+ self._sender = sender
118
+ self._service_name = service_name
119
+ self._topic = topic or constants.TOPIC
120
+ self._schema_id = schema_id or constants.SCHEMA_ID
121
+ self._tenant_id = tenant_id or constants.TENANT_ID
122
+
123
+ def send_error_event(
124
+ self,
125
+ exc: Exception,
126
+ resp: "ApiErrorResponse",
127
+ errorcategory: str = "PLATFORM",
128
+ ) -> None:
129
+ """Build the error object dict and ship it to the schema topic."""
130
+ timestamp_ms = int(time.time() * 1000)
131
+ error_id = f"{uuid.uuid4()}_{timestamp_ms}"
132
+ obj = _build_error_object(exc, resp, error_id, timestamp_ms, errorcategory)
133
+
134
+ payload = DataIngestionOperation(
135
+ actionType="CREATE",
136
+ object=obj,
137
+ id=error_id,
138
+ schemaId=self._schema_id,
139
+ tenantId=self._tenant_id,
140
+ )
141
+ _executor.submit(self._send, payload.to_dict())
142
+
143
+ def _send(self, value: Dict[str, Any]) -> None:
144
+ try:
145
+ self._sender.send(value, topic=self._topic)
146
+ except Exception: # noqa: BLE001
147
+ log.warning("Failed to publish error event to schema topic", exc_info=True)
148
+
149
+
150
+ # --- module-level default producer (set at app startup) ---
151
+
152
+ _default_producer: Optional[KafkaErrorProducer] = None
153
+
154
+
155
+ def set_error_producer(producer: Optional[KafkaErrorProducer]) -> None:
156
+ global _default_producer
157
+ _default_producer = producer
158
+
159
+
160
+ def get_error_producer() -> Optional[KafkaErrorProducer]:
161
+ return _default_producer
mobius_error/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,117 @@
1
+ """Mirrors com.aidtaas.mobius.error.services.response.ApiErrorResponse"""
2
+
3
+ from __future__ import annotations
4
+ from typing import Any, Optional
5
+
6
+ from mobius_error.config import App
7
+ from mobius_error.errors.error import Error
8
+ from mobius_error.errors.error_message import ErrorMessage
9
+ from mobius_error.exceptions.application_exception import ApplicationException
10
+ from mobius_error.responses.error_response import ErrorResponse
11
+
12
+
13
+ class ApiErrorResponse(ErrorResponse):
14
+
15
+ def __init__(
16
+ self,
17
+ http_status_code: int = 500,
18
+ error: Optional[Error] = None,
19
+ error_message: Optional[str] = None,
20
+ detailed_error_message: Optional[str] = None,
21
+ cause: Optional[ErrorMessage] = None,
22
+ sub_errors: Optional[list[ErrorMessage]] = None,
23
+ actions_required: Optional[list[str]] = None,
24
+ error_object: Any = None,
25
+ error_code: Optional[int] = None,
26
+ ) -> None:
27
+ super().__init__(
28
+ error=error,
29
+ error_message=error_message,
30
+ detailed_error_message=detailed_error_message,
31
+ cause=cause,
32
+ sub_errors=sub_errors,
33
+ actions_required=actions_required,
34
+ error_object=error_object,
35
+ origin=App.get_app_name(),
36
+ )
37
+ self.http_status_code: int = http_status_code
38
+
39
+ # Override error_message when explicitly supplied
40
+ if error_message and not error:
41
+ self.error_message = error_message
42
+ elif not error_message and not error:
43
+ self.error_message = "Encountered an unexpected error"
44
+
45
+ # Override error_code when explicitly supplied
46
+ if error_code is not None:
47
+ self.error_code = error_code
48
+
49
+ # ---- convenience factory methods matching Java overloads ----
50
+
51
+ @classmethod
52
+ def from_status(cls, http_status_code: int, message: str = "Encountered an unexpected error") -> "ApiErrorResponse":
53
+ return cls(http_status_code=http_status_code, error_message=message)
54
+
55
+ @classmethod
56
+ def from_status_and_error(cls, http_status_code: int, error: Error) -> "ApiErrorResponse":
57
+ return cls(http_status_code=http_status_code, error=error)
58
+
59
+ @classmethod
60
+ def from_status_message_code(cls, http_status_code: int, message: str, error_code: int) -> "ApiErrorResponse":
61
+ return cls(http_status_code=http_status_code, error_message=message, error_code=error_code)
62
+
63
+ @classmethod
64
+ def from_status_message_sub(
65
+ cls,
66
+ http_status_code: int,
67
+ message: str,
68
+ sub_error: Optional[ErrorMessage] = None,
69
+ action_required: Optional[str] = None,
70
+ error_code: Optional[int] = None,
71
+ ) -> "ApiErrorResponse":
72
+ resp = cls(
73
+ http_status_code=http_status_code,
74
+ error_message=message,
75
+ error_code=error_code,
76
+ )
77
+ if sub_error:
78
+ resp.add_sub_error(sub_error)
79
+ if action_required:
80
+ resp.add_action_required(action_required)
81
+ return resp
82
+
83
+ @classmethod
84
+ def from_application_exception(cls, http_status_code: int, exc: ApplicationException) -> "ApiErrorResponse":
85
+ resp = cls(
86
+ http_status_code=http_status_code,
87
+ error=exc.error,
88
+ error_message=exc.error.error_message,
89
+ error_object=exc.error_object,
90
+ )
91
+ resp.origin = exc.origin
92
+ resp.error_code = exc.error.error_code
93
+
94
+ if exc.detailed_error_message:
95
+ resp.detailed_error_message = exc.detailed_error_message
96
+
97
+ # nested cause
98
+ if exc.__cause__:
99
+ resp.cause = ErrorMessage(message=str(exc.__cause__))
100
+ elif exc.nested_error_message:
101
+ resp.cause = exc.nested_error_message
102
+
103
+ resp.add_sub_errors(exc.sub_errors)
104
+ resp.add_actions_required(exc.actions_required)
105
+ return resp
106
+
107
+ @classmethod
108
+ def from_exception(cls, http_status_code: int, exc: Exception) -> "ApiErrorResponse":
109
+ resp = cls(http_status_code=http_status_code, error_message=str(exc))
110
+ if exc.__cause__:
111
+ resp.cause = ErrorMessage(message=str(exc.__cause__))
112
+ return resp
113
+
114
+ def to_dict(self) -> dict[str, Any]:
115
+ d = super().to_dict()
116
+ d["httpStatusCode"] = self.http_status_code
117
+ return d
@@ -0,0 +1,84 @@
1
+ """Mirrors com.aidtaas.mobius.error.services.response.ErrorResponse"""
2
+
3
+ from __future__ import annotations
4
+ import time
5
+ from typing import Any, Optional
6
+
7
+ from mobius_error.errors.error import Error
8
+ from mobius_error.errors.error_message import ErrorMessage
9
+
10
+
11
+ DOC_URL = "https://mobiusdtaas.atlassian.net/wiki/spaces/EN/pages/2360868868/Mobius+Documentation+for+Common+Issues"
12
+
13
+
14
+ class ErrorResponse:
15
+
16
+ def __init__(
17
+ self,
18
+ error: Optional[Error] = None,
19
+ error_message: Optional[str] = None,
20
+ detailed_error_message: Optional[str] = None,
21
+ cause: Optional[ErrorMessage] = None,
22
+ sub_errors: Optional[list[ErrorMessage]] = None,
23
+ actions_required: Optional[list[str]] = None,
24
+ error_object: Any = None,
25
+ origin: Optional[str] = None,
26
+ ) -> None:
27
+ self.timestamp: int = int(time.time() * 1000)
28
+ self.origin: Optional[str] = origin
29
+ self.error_code: int = error.error_code if error else 500001
30
+ self.error_message: str = (
31
+ error.error_message if error else (error_message or "")
32
+ )
33
+ self.detailed_error_message: Optional[str] = detailed_error_message
34
+ self.sub_errors: list[ErrorMessage] = list(sub_errors) if sub_errors else []
35
+ self.actions_required: list[str] = list(actions_required) if actions_required else []
36
+ self.cause: Optional[ErrorMessage] = cause
37
+ self.error_object: Any = error_object
38
+ self.doc_url: str = DOC_URL
39
+
40
+ # populate action_required from Error if provided
41
+ if error and error.action_required:
42
+ if error.action_required not in self.actions_required:
43
+ self.actions_required.append(error.action_required)
44
+
45
+ # ---- helpers ----
46
+
47
+ def add_sub_error(self, sub_error: ErrorMessage) -> None:
48
+ if sub_error is not None:
49
+ self.sub_errors.append(sub_error)
50
+
51
+ def add_sub_errors(self, sub_errors: list[ErrorMessage]) -> None:
52
+ if sub_errors:
53
+ self.sub_errors.extend(sub_errors)
54
+
55
+ def add_action_required(self, action: str) -> None:
56
+ if action:
57
+ self.actions_required.append(action)
58
+
59
+ def add_actions_required(self, actions: list[str]) -> None:
60
+ if actions:
61
+ self.actions_required.extend(actions)
62
+
63
+ # ---- serialisation ----
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ """Return a JSON-serialisable dict, omitting empty fields (NON_EMPTY)."""
67
+ d: dict[str, Any] = {"timestamp": self.timestamp}
68
+ if self.origin:
69
+ d["origin"] = self.origin
70
+ d["errorCode"] = self.error_code
71
+ if self.error_message:
72
+ d["errorMessage"] = self.error_message
73
+ if self.detailed_error_message:
74
+ d["detailedErrorMessage"] = self.detailed_error_message
75
+ if self.sub_errors:
76
+ d["subErrors"] = [se.to_dict() for se in self.sub_errors]
77
+ if self.actions_required:
78
+ d["actionsRequired"] = self.actions_required
79
+ if self.cause is not None:
80
+ d["cause"] = self.cause.to_dict()
81
+ if self.error_object is not None:
82
+ d["errorObject"] = self.error_object
83
+ d["docUrl"] = self.doc_url
84
+ return d