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.
- mobius_error/__init__.py +60 -0
- mobius_error/config.py +13 -0
- mobius_error/errors/__init__.py +0 -0
- mobius_error/errors/common_errors.py +33 -0
- mobius_error/errors/error.py +17 -0
- mobius_error/errors/error_message.py +37 -0
- mobius_error/exceptions/__init__.py +0 -0
- mobius_error/exceptions/access_violation_exception.py +24 -0
- mobius_error/exceptions/api_exception.py +28 -0
- mobius_error/exceptions/application_exception.py +71 -0
- mobius_error/exceptions/data_type_mismatch_exception.py +11 -0
- mobius_error/exceptions/group_data_retrieval_exception.py +8 -0
- mobius_error/exceptions/invalid_name_exception.py +8 -0
- mobius_error/exceptions/invalid_tenant_exception.py +8 -0
- mobius_error/exceptions/kafka_consumption_exception.py +8 -0
- mobius_error/exceptions/kafka_exception.py +8 -0
- mobius_error/exceptions/object_mapping_exception.py +8 -0
- mobius_error/exceptions/rest_exception.py +11 -0
- mobius_error/exceptions/rest_get_exception.py +11 -0
- mobius_error/exceptions/rest_post_exception.py +11 -0
- mobius_error/exceptions/token_exception.py +26 -0
- mobius_error/exceptions/unsupported_operation_exception.py +8 -0
- mobius_error/exceptions/validation_exception.py +23 -0
- mobius_error/handlers/__init__.py +0 -0
- mobius_error/handlers/exception_handler.py +248 -0
- mobius_error/kafka/__init__.py +0 -0
- mobius_error/kafka/constants.py +5 -0
- mobius_error/kafka/producer.py +161 -0
- mobius_error/py.typed +0 -0
- mobius_error/responses/__init__.py +0 -0
- mobius_error/responses/api_error_response.py +117 -0
- mobius_error/responses/error_response.py +84 -0
- mobius_error_py-1.0.0.dist-info/METADATA +575 -0
- mobius_error_py-1.0.0.dist-info/RECORD +37 -0
- mobius_error_py-1.0.0.dist-info/WHEEL +5 -0
- mobius_error_py-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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,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
|