mobius-error-py 1.0.0__tar.gz
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_py-1.0.0/LICENSE +21 -0
- mobius_error_py-1.0.0/MANIFEST.in +6 -0
- mobius_error_py-1.0.0/PKG-INFO +575 -0
- mobius_error_py-1.0.0/README.md +540 -0
- mobius_error_py-1.0.0/examples/example_app.py +184 -0
- mobius_error_py-1.0.0/pyproject.toml +65 -0
- mobius_error_py-1.0.0/setup.cfg +4 -0
- mobius_error_py-1.0.0/src/mobius_error/__init__.py +60 -0
- mobius_error_py-1.0.0/src/mobius_error/config.py +13 -0
- mobius_error_py-1.0.0/src/mobius_error/errors/__init__.py +0 -0
- mobius_error_py-1.0.0/src/mobius_error/errors/common_errors.py +33 -0
- mobius_error_py-1.0.0/src/mobius_error/errors/error.py +17 -0
- mobius_error_py-1.0.0/src/mobius_error/errors/error_message.py +37 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/__init__.py +0 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/access_violation_exception.py +24 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/api_exception.py +28 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/application_exception.py +71 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/data_type_mismatch_exception.py +11 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/group_data_retrieval_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/invalid_name_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/invalid_tenant_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/kafka_consumption_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/kafka_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/object_mapping_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/rest_exception.py +11 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/rest_get_exception.py +11 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/rest_post_exception.py +11 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/token_exception.py +26 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/unsupported_operation_exception.py +8 -0
- mobius_error_py-1.0.0/src/mobius_error/exceptions/validation_exception.py +23 -0
- mobius_error_py-1.0.0/src/mobius_error/handlers/__init__.py +0 -0
- mobius_error_py-1.0.0/src/mobius_error/handlers/exception_handler.py +248 -0
- mobius_error_py-1.0.0/src/mobius_error/kafka/__init__.py +0 -0
- mobius_error_py-1.0.0/src/mobius_error/kafka/constants.py +5 -0
- mobius_error_py-1.0.0/src/mobius_error/kafka/producer.py +161 -0
- mobius_error_py-1.0.0/src/mobius_error/py.typed +0 -0
- mobius_error_py-1.0.0/src/mobius_error/responses/__init__.py +0 -0
- mobius_error_py-1.0.0/src/mobius_error/responses/api_error_response.py +117 -0
- mobius_error_py-1.0.0/src/mobius_error/responses/error_response.py +84 -0
- mobius_error_py-1.0.0/src/mobius_error_py.egg-info/PKG-INFO +575 -0
- mobius_error_py-1.0.0/src/mobius_error_py.egg-info/SOURCES.txt +44 -0
- mobius_error_py-1.0.0/src/mobius_error_py.egg-info/dependency_links.txt +1 -0
- mobius_error_py-1.0.0/src/mobius_error_py.egg-info/requires.txt +8 -0
- mobius_error_py-1.0.0/src/mobius_error_py.egg-info/top_level.txt +1 -0
- mobius_error_py-1.0.0/tests/__init__.py +0 -0
- mobius_error_py-1.0.0/tests/test_mobius_error.py +341 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AidTaas Mobius Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mobius-error-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Mobius Error Handling Library for FastAPI — structured error responses ported from Spring Boot
|
|
5
|
+
Author: AidTaas Mobius Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aidtaas/mobius-error-python
|
|
8
|
+
Project-URL: Documentation, https://github.com/aidtaas/mobius-error-python#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/aidtaas/mobius-error-python
|
|
10
|
+
Project-URL: Issues, https://github.com/aidtaas/mobius-error-python/issues
|
|
11
|
+
Keywords: fastapi,error-handling,exception-handler,microservices,mobius,rest-api
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: fastapi>=0.100.0
|
|
28
|
+
Requires-Dist: starlette>=0.27.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: uvicorn[standard]>=0.23.0; extra == "dev"
|
|
31
|
+
Requires-Dist: httpx>=0.24.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Mobius Error Services for FastAPI
|
|
37
|
+
|
|
38
|
+
A comprehensive error handling library for FastAPI, ported from the [Spring Boot `error-spring-lib`](https://github.com/aidtaas/error-spring-lib-dev). It provides a structured, consistent error response format across all your microservices — matching the exact JSON contract your existing Java services already produce.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Table of Contents
|
|
43
|
+
|
|
44
|
+
- [Installation](#installation)
|
|
45
|
+
- [Quick Start](#quick-start)
|
|
46
|
+
- [Response Format](#response-format)
|
|
47
|
+
- [Exception Hierarchy](#exception-hierarchy)
|
|
48
|
+
- [Built-in Error Codes](#built-in-error-codes)
|
|
49
|
+
- [Usage Examples](#usage-examples)
|
|
50
|
+
- [Basic: Raising Exceptions](#basic-raising-exceptions)
|
|
51
|
+
- [Custom Error Codes](#custom-error-codes)
|
|
52
|
+
- [Fluent Builder Pattern](#fluent-builder-pattern)
|
|
53
|
+
- [Wrapping Upstream Failures](#wrapping-upstream-failures)
|
|
54
|
+
- [Pydantic Validation (Automatic)](#pydantic-validation-automatic)
|
|
55
|
+
- [Exception Types Reference](#exception-types-reference)
|
|
56
|
+
- [Advanced Usage](#advanced-usage)
|
|
57
|
+
- [Setting the Application Name](#setting-the-application-name)
|
|
58
|
+
- [Custom Exception Classes](#custom-exception-classes)
|
|
59
|
+
- [Combining with FastAPI's HTTPException](#combining-with-fastapis-httpexception)
|
|
60
|
+
- [Java ↔ Python Mapping](#java--python-mapping)
|
|
61
|
+
- [Project Structure](#project-structure)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
Copy the `mobius_error/` package into your project, or install it as an editable package:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cd error_lib
|
|
71
|
+
pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Requirements:** Python 3.10+, FastAPI >= 0.100.0
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
Two lines to integrate into any FastAPI app:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fastapi import FastAPI
|
|
84
|
+
from mobius_error import register_exception_handlers
|
|
85
|
+
|
|
86
|
+
app = FastAPI(title="my-service")
|
|
87
|
+
register_exception_handlers(app, app_name="my-service")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
That's it. Every unhandled exception now returns a structured JSON response instead of a raw 500.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Response Format
|
|
95
|
+
|
|
96
|
+
All error responses follow this consistent JSON structure (empty fields are omitted):
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"timestamp": 1706000000000,
|
|
101
|
+
"origin": "my-service",
|
|
102
|
+
"httpStatusCode": 404,
|
|
103
|
+
"errorCode": 404001,
|
|
104
|
+
"errorMessage": "User not found",
|
|
105
|
+
"detailedErrorMessage": "No user with ID 42 exists in the database",
|
|
106
|
+
"subErrors": [
|
|
107
|
+
{
|
|
108
|
+
"message": "Checked primary and replica databases",
|
|
109
|
+
"timestamp": 1706000000
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"actionsRequired": [
|
|
113
|
+
"Verify the user ID and try again"
|
|
114
|
+
],
|
|
115
|
+
"cause": {
|
|
116
|
+
"message": "Record not found in table 'users'",
|
|
117
|
+
"origin": "my-service",
|
|
118
|
+
"timestamp": 1706000000
|
|
119
|
+
},
|
|
120
|
+
"docUrl": "https://mobiusdtaas.atlassian.net/wiki/spaces/EN/pages/2360868868/..."
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Field | Type | Description |
|
|
125
|
+
|------------------------|---------------|-------------------------------------------------------|
|
|
126
|
+
| `timestamp` | `int` | Unix timestamp in milliseconds |
|
|
127
|
+
| `origin` | `string` | Name of the service that produced the error |
|
|
128
|
+
| `httpStatusCode` | `int` | HTTP status code |
|
|
129
|
+
| `errorCode` | `int` | Application-specific numeric error code |
|
|
130
|
+
| `errorMessage` | `string` | Human-readable summary |
|
|
131
|
+
| `detailedErrorMessage` | `string` | More context about what went wrong |
|
|
132
|
+
| `subErrors` | `ErrorMessage[]` | List of nested/related error details |
|
|
133
|
+
| `actionsRequired` | `string[]` | Suggested actions the caller can take |
|
|
134
|
+
| `cause` | `ErrorMessage` | Root cause information (e.g., from a wrapped exception) |
|
|
135
|
+
| `docUrl` | `string` | Link to documentation for common issues |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Exception Hierarchy
|
|
140
|
+
|
|
141
|
+
The exception class hierarchy mirrors the Java library exactly:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Exception
|
|
145
|
+
└── ApplicationException (base — caught as 403)
|
|
146
|
+
└── ApiException (uses Error's HTTP status)
|
|
147
|
+
├── ValidationException (caught as 409)
|
|
148
|
+
│ ├── AccessViolationException (caught as 403)
|
|
149
|
+
│ └── InvalidNameException
|
|
150
|
+
├── TokenException (uses Error's HTTP status)
|
|
151
|
+
├── DataTypeMismatchException (caught as 400)
|
|
152
|
+
└── MethodArgumentsNotValidException
|
|
153
|
+
├── RestException
|
|
154
|
+
├── RestGetException
|
|
155
|
+
├── RestPostException
|
|
156
|
+
├── KafkaException
|
|
157
|
+
├── KafkaConsumptionException
|
|
158
|
+
├── ObjectMappingException
|
|
159
|
+
├── InvalidTenantException
|
|
160
|
+
├── UnsupportedOperationException
|
|
161
|
+
└── GroupDataRetrievalException
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Each exception handler matches the behavior of its Java `@ExceptionHandler` counterpart.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Built-in Error Codes
|
|
169
|
+
|
|
170
|
+
These are defined in `CommonErrors` and match the Java `CommonErrors` class:
|
|
171
|
+
|
|
172
|
+
| Constant | HTTP | Code | Message |
|
|
173
|
+
|------------------------------|------|--------|------------------------------------------------|
|
|
174
|
+
| `INVALID_TENANT_ID` | 403 | 403002 | You're not registered as a tenant yet |
|
|
175
|
+
| `TENANT_NOT_AUTHORIZED` | 401 | 401000 | You're not authorized to access the resource |
|
|
176
|
+
| `UNEXPECTED_ERROR` | 500 | 500000 | Encountered an unexpected error |
|
|
177
|
+
| `SERVICE_UNAVAILABLE` | 500 | 500001 | One or more service(s) are not up and running |
|
|
178
|
+
| `OBJECT_MAPPING_FAILURE` | 500 | 500002 | Failed to convert json to object/map |
|
|
179
|
+
| `GROUP_DATA_RETRIEVAL_FAILURE` | 500 | 500003 | Failed to get group data from data lake |
|
|
180
|
+
| `GET_API_FAILURE` | 500 | 500004 | Failed to GET data from the URL |
|
|
181
|
+
| `POST_API_FAILURE` | 500 | 500005 | Failed to make POST call to an api |
|
|
182
|
+
| `REST_API_FAILURE` | 500 | 500006 | Failed to make REST call to an api |
|
|
183
|
+
| `KAFKA_ERROR` | 400 | 400000 | An error occurred in kafka |
|
|
184
|
+
| `KAFKA_CONSUMPTION_ERROR` | 400 | 400001 | An error occurred while consuming from kafka |
|
|
185
|
+
| `INVALID_NAME` | 400 | 400002 | The name provided is invalid |
|
|
186
|
+
| `UNSUPPORTED_OPERATION` | 400 | 400003 | This operation is not yet supported |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Usage Examples
|
|
191
|
+
|
|
192
|
+
### Basic: Raising Exceptions
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from mobius_error import ApplicationException, ApiException, CommonErrors
|
|
196
|
+
|
|
197
|
+
# Simple — uses built-in error definition
|
|
198
|
+
@app.get("/items/{item_id}")
|
|
199
|
+
async def get_item(item_id: int):
|
|
200
|
+
item = db.find(item_id)
|
|
201
|
+
if not item:
|
|
202
|
+
raise ApiException(
|
|
203
|
+
error=CommonErrors.GET_API_FAILURE,
|
|
204
|
+
detailed_error_message=f"Item {item_id} not found in database",
|
|
205
|
+
)
|
|
206
|
+
return item
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Custom Error Codes
|
|
210
|
+
|
|
211
|
+
Define your own errors following the same pattern as `CommonErrors`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from mobius_error import Error, ApiException
|
|
215
|
+
|
|
216
|
+
class MyErrors:
|
|
217
|
+
USER_NOT_FOUND = Error(404, 404001, "User not found", "Check the user ID and try again")
|
|
218
|
+
DUPLICATE_EMAIL = Error(409, 409001, "Email already exists", "Use a different email address")
|
|
219
|
+
QUOTA_EXCEEDED = Error(429, 429001, "Rate limit exceeded", "Wait a moment and retry")
|
|
220
|
+
|
|
221
|
+
@app.get("/users/{user_id}")
|
|
222
|
+
async def get_user(user_id: int):
|
|
223
|
+
user = await find_user(user_id)
|
|
224
|
+
if not user:
|
|
225
|
+
raise ApiException(
|
|
226
|
+
error=MyErrors.USER_NOT_FOUND,
|
|
227
|
+
detailed_error_message=f"No user with ID {user_id}",
|
|
228
|
+
)
|
|
229
|
+
return user
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Fluent Builder Pattern
|
|
233
|
+
|
|
234
|
+
Chain `.add_sub_error()` and `.add_action_required()` for rich error detail:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from mobius_error import ApplicationException, CommonErrors
|
|
238
|
+
|
|
239
|
+
@app.post("/deploy")
|
|
240
|
+
async def deploy():
|
|
241
|
+
errors = run_preflight_checks()
|
|
242
|
+
if errors:
|
|
243
|
+
raise (
|
|
244
|
+
ApplicationException(
|
|
245
|
+
error=CommonErrors.UNEXPECTED_ERROR,
|
|
246
|
+
detailed_error_message="Deployment preflight checks failed",
|
|
247
|
+
)
|
|
248
|
+
.add_sub_error("Health check: database unreachable")
|
|
249
|
+
.add_sub_error("Health check: cache latency > 500ms")
|
|
250
|
+
.add_action_required("Verify database connectivity")
|
|
251
|
+
.add_action_required("Check Redis cluster status")
|
|
252
|
+
)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
This produces a response with all sub-errors and actions listed:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"errorCode": 500000,
|
|
260
|
+
"errorMessage": "Encountered an unexpected error",
|
|
261
|
+
"detailedErrorMessage": "Deployment preflight checks failed",
|
|
262
|
+
"subErrors": [
|
|
263
|
+
{ "message": "Health check: database unreachable" },
|
|
264
|
+
{ "message": "Health check: cache latency > 500ms" }
|
|
265
|
+
],
|
|
266
|
+
"actionsRequired": [
|
|
267
|
+
"Kindly try again after some time or contact the support team",
|
|
268
|
+
"Verify database connectivity",
|
|
269
|
+
"Check Redis cluster status"
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Wrapping Upstream Failures
|
|
275
|
+
|
|
276
|
+
Preserve the original cause when catching exceptions from external calls:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
import httpx
|
|
280
|
+
from mobius_error import RestGetException
|
|
281
|
+
|
|
282
|
+
@app.get("/proxy/weather")
|
|
283
|
+
async def proxy_weather():
|
|
284
|
+
try:
|
|
285
|
+
async with httpx.AsyncClient() as client:
|
|
286
|
+
resp = await client.get("https://api.weather.com/forecast")
|
|
287
|
+
resp.raise_for_status()
|
|
288
|
+
return resp.json()
|
|
289
|
+
except httpx.HTTPStatusError as exc:
|
|
290
|
+
raise RestGetException(
|
|
291
|
+
error_message="Failed to fetch weather forecast",
|
|
292
|
+
url="https://api.weather.com/forecast",
|
|
293
|
+
status_code=exc.response.status_code,
|
|
294
|
+
response_body=exc.response.text,
|
|
295
|
+
cause=exc,
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Pydantic Validation (Automatic)
|
|
300
|
+
|
|
301
|
+
FastAPI/Pydantic validation errors are caught automatically and formatted as sub-errors:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
from pydantic import BaseModel, Field
|
|
305
|
+
|
|
306
|
+
class CreateUserRequest(BaseModel):
|
|
307
|
+
name: str = Field(..., min_length=2, max_length=50)
|
|
308
|
+
email: str = Field(..., pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
|
|
309
|
+
age: int = Field(..., ge=0, le=150)
|
|
310
|
+
|
|
311
|
+
@app.post("/users")
|
|
312
|
+
async def create_user(body: CreateUserRequest):
|
|
313
|
+
# If validation fails, the library auto-returns a 422 with sub-errors:
|
|
314
|
+
# {
|
|
315
|
+
# "errorCode": 400000,
|
|
316
|
+
# "errorMessage": "Validation error",
|
|
317
|
+
# "subErrors": [
|
|
318
|
+
# { "message": "body → name: String should have at least 2 characters" },
|
|
319
|
+
# { "message": "body → email: String should match pattern ..." }
|
|
320
|
+
# ]
|
|
321
|
+
# }
|
|
322
|
+
return {"created": body.model_dump()}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Exception Types Reference
|
|
328
|
+
|
|
329
|
+
### `ApplicationException` → HTTP 403
|
|
330
|
+
|
|
331
|
+
The base exception. Use when something unexpected goes wrong.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
raise ApplicationException(
|
|
335
|
+
error=CommonErrors.UNEXPECTED_ERROR, # required
|
|
336
|
+
detailed_error_message="What happened", # optional
|
|
337
|
+
error_object={"debug": "info"}, # optional, included in response
|
|
338
|
+
cause=original_exception, # optional, populates 'cause' field
|
|
339
|
+
)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### `ApiException` → Uses Error's HTTP status
|
|
343
|
+
|
|
344
|
+
Most common for API-level errors. The HTTP status comes from the `Error` object.
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
raise ApiException(
|
|
348
|
+
error=MyErrors.USER_NOT_FOUND, # Error with http_status_code=404
|
|
349
|
+
detailed_error_message="User 42 not found",
|
|
350
|
+
)
|
|
351
|
+
# → Returns HTTP 404
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### `ValidationException` → HTTP 409
|
|
355
|
+
|
|
356
|
+
For data validation failures caught in business logic (not Pydantic).
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
raise ValidationException(
|
|
360
|
+
error=MyErrors.DUPLICATE_EMAIL,
|
|
361
|
+
detailed_error_message="user@example.com already registered",
|
|
362
|
+
)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### `AccessViolationException` → HTTP 403
|
|
366
|
+
|
|
367
|
+
For authorization/permission failures.
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
raise AccessViolationException(
|
|
371
|
+
error=CommonErrors.TENANT_NOT_AUTHORIZED,
|
|
372
|
+
detailed_error_message="Tenant xyz cannot access project abc",
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### `TokenException` → Uses Error's HTTP status
|
|
377
|
+
|
|
378
|
+
For authentication/token failures.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
raise TokenException(
|
|
382
|
+
error=CommonErrors.TENANT_NOT_AUTHORIZED,
|
|
383
|
+
detailed_error_message="JWT token expired",
|
|
384
|
+
)
|
|
385
|
+
# → Returns HTTP 401
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### `DataTypeMismatchException` → HTTP 400
|
|
389
|
+
|
|
390
|
+
For type conversion failures.
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
raise DataTypeMismatchException(
|
|
394
|
+
error=Error(400, 400004, "Invalid input type", "Provide valid input"),
|
|
395
|
+
detailed_error_message="Expected int for 'age', got string",
|
|
396
|
+
error_object={"field": "age", "received": "twenty-five"},
|
|
397
|
+
)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `InvalidNameException` → HTTP 409
|
|
401
|
+
|
|
402
|
+
Shortcut for invalid name validation.
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
raise InvalidNameException("Name contains invalid characters: @#$")
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### `InvalidTenantException` → HTTP 409
|
|
409
|
+
|
|
410
|
+
Shortcut for invalid tenant validation.
|
|
411
|
+
|
|
412
|
+
```python
|
|
413
|
+
raise InvalidTenantException("Tenant ID 'null' is not valid")
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### `ObjectMappingException` → HTTP 403
|
|
417
|
+
|
|
418
|
+
For serialization/deserialization failures.
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
raise ObjectMappingException("Failed to parse user payload", cause=json_error)
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### `RestGetException` / `RestPostException` / `RestException` → HTTP 403
|
|
425
|
+
|
|
426
|
+
For upstream HTTP call failures.
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
raise RestGetException(
|
|
430
|
+
error_message="Upstream service unavailable",
|
|
431
|
+
url="https://api.example.com/data",
|
|
432
|
+
status_code=502,
|
|
433
|
+
response_body='{"error": "bad gateway"}',
|
|
434
|
+
)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### `KafkaException` / `KafkaConsumptionException` → HTTP 403
|
|
438
|
+
|
|
439
|
+
For message queue failures.
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
raise KafkaException("Failed to publish event to topic 'orders'")
|
|
443
|
+
raise KafkaConsumptionException("Failed to deserialize message", cause=exc)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### `UnsupportedOperationException` → HTTP 403
|
|
447
|
+
|
|
448
|
+
For operations not yet implemented.
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
raise UnsupportedOperationException("PATCH is not supported on this resource")
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Advanced Usage
|
|
457
|
+
|
|
458
|
+
### Setting the Application Name
|
|
459
|
+
|
|
460
|
+
The `origin` field in every error response is set from the app name. You can configure it in three ways:
|
|
461
|
+
|
|
462
|
+
```python
|
|
463
|
+
# Option 1: Pass explicitly (recommended)
|
|
464
|
+
register_exception_handlers(app, app_name="my-service")
|
|
465
|
+
|
|
466
|
+
# Option 2: Falls back to app.title
|
|
467
|
+
app = FastAPI(title="my-service")
|
|
468
|
+
register_exception_handlers(app)
|
|
469
|
+
|
|
470
|
+
# Option 3: Set manually at any time
|
|
471
|
+
from mobius_error import App
|
|
472
|
+
App.set_app_name("my-service")
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Custom Exception Classes
|
|
476
|
+
|
|
477
|
+
Extend the hierarchy for your domain:
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
from mobius_error import ApiException, Error
|
|
481
|
+
|
|
482
|
+
class PaymentErrors:
|
|
483
|
+
INSUFFICIENT_FUNDS = Error(402, 402001, "Insufficient funds", "Add funds to your account")
|
|
484
|
+
CARD_DECLINED = Error(402, 402002, "Card declined", "Try a different payment method")
|
|
485
|
+
|
|
486
|
+
class PaymentException(ApiException):
|
|
487
|
+
def __init__(self, error: Error, detail: str, transaction_id: str | None = None):
|
|
488
|
+
super().__init__(error=error, detailed_error_message=detail)
|
|
489
|
+
if transaction_id:
|
|
490
|
+
self.add_sub_error(f"Transaction ID: {transaction_id}")
|
|
491
|
+
|
|
492
|
+
# Register handler for your custom exception
|
|
493
|
+
@app.exception_handler(PaymentException)
|
|
494
|
+
async def handle_payment(request, exc):
|
|
495
|
+
from mobius_error.responses.api_error_response import ApiErrorResponse
|
|
496
|
+
from fastapi.responses import JSONResponse
|
|
497
|
+
resp = ApiErrorResponse.from_application_exception(exc.http_status_code, exc)
|
|
498
|
+
return JSONResponse(status_code=exc.http_status_code, content=resp.to_dict())
|
|
499
|
+
|
|
500
|
+
# Usage
|
|
501
|
+
raise PaymentException(
|
|
502
|
+
PaymentErrors.CARD_DECLINED,
|
|
503
|
+
detail="Visa ending in 4242 was declined",
|
|
504
|
+
transaction_id="txn_abc123",
|
|
505
|
+
)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Combining with FastAPI's HTTPException
|
|
509
|
+
|
|
510
|
+
The library catches Starlette/FastAPI `HTTPException` too, wrapping them in the Mobius format. So `raise HTTPException(status_code=404, detail="Not found")` will return:
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"httpStatusCode": 404,
|
|
515
|
+
"errorCode": 500001,
|
|
516
|
+
"errorMessage": "Not found",
|
|
517
|
+
"origin": "my-service"
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## Java ↔ Python Mapping
|
|
524
|
+
|
|
525
|
+
| Java (Spring Boot) | Python (FastAPI) |
|
|
526
|
+
|-------------------------------------------|-----------------------------------------------|
|
|
527
|
+
| `@ControllerAdvice` | `register_exception_handlers(app)` |
|
|
528
|
+
| `@ExceptionHandler(ApiException.class)` | `@app.exception_handler(ApiException)` |
|
|
529
|
+
| `new Error(HttpStatus.NOT_FOUND, ...)` | `Error(404, 404001, ...)` |
|
|
530
|
+
| `throw new ApiException(error, msg)` | `raise ApiException(error=error, detailed_error_message=msg)` |
|
|
531
|
+
| `new ApiErrorResponse(status, msg)` | `ApiErrorResponse.from_status(status, msg)` |
|
|
532
|
+
| `ApplicationException.addSubError(msg)` | `.add_sub_error(msg)` |
|
|
533
|
+
| `App.setAppName(name)` | `App.set_app_name(name)` |
|
|
534
|
+
| `@Value("${spring.application.name}")` | `register_exception_handlers(app, app_name=...)` |
|
|
535
|
+
| `ResponseEntity<ApiErrorResponse>` | `JSONResponse(status_code=..., content=resp.to_dict())` |
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Project Structure
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
error_lib/
|
|
543
|
+
├── pyproject.toml # Package definition
|
|
544
|
+
├── example_app.py # Full working demo (run with uvicorn)
|
|
545
|
+
├── test_smoke.py # Smoke tests for all endpoints
|
|
546
|
+
└── mobius_error/
|
|
547
|
+
├── __init__.py # Public API — import everything from here
|
|
548
|
+
├── config.py # App.get_app_name() / App.set_app_name()
|
|
549
|
+
├── errors/
|
|
550
|
+
│ ├── error.py # Error dataclass (status + code + message)
|
|
551
|
+
│ ├── error_message.py # ErrorMessage model (nested cause chain)
|
|
552
|
+
│ └── common_errors.py # Built-in error constants (CommonErrors)
|
|
553
|
+
├── exceptions/
|
|
554
|
+
│ ├── application_exception.py # Base exception
|
|
555
|
+
│ ├── api_exception.py # API-level exception
|
|
556
|
+
│ ├── validation_exception.py # Validation errors
|
|
557
|
+
│ ├── access_violation_exception.py
|
|
558
|
+
│ ├── token_exception.py
|
|
559
|
+
│ ├── data_type_mismatch_exception.py
|
|
560
|
+
│ ├── rest_exception.py
|
|
561
|
+
│ ├── rest_get_exception.py
|
|
562
|
+
│ ├── rest_post_exception.py
|
|
563
|
+
│ ├── kafka_exception.py
|
|
564
|
+
│ ├── kafka_consumption_exception.py
|
|
565
|
+
│ ├── object_mapping_exception.py
|
|
566
|
+
│ ├── invalid_name_exception.py
|
|
567
|
+
│ ├── invalid_tenant_exception.py
|
|
568
|
+
│ ├── unsupported_operation_exception.py
|
|
569
|
+
│ └── group_data_retrieval_exception.py
|
|
570
|
+
├── handlers/
|
|
571
|
+
│ └── exception_handler.py # All FastAPI exception handlers + middleware
|
|
572
|
+
└── responses/
|
|
573
|
+
├── error_response.py # Base response model
|
|
574
|
+
└── api_error_response.py # Full API response with factory methods
|
|
575
|
+
```
|