Metadata-Version: 2.4
Name: mobius-error-py
Version: 1.0.2
Summary: Mobius Error Handling Library for FastAPI — structured error responses ported from Spring Boot
Author: AidTaas Mobius Team
License-Expression: MIT
Project-URL: Homepage, https://github.com/aidtaas/mobius-error-python
Project-URL: Documentation, https://github.com/aidtaas/mobius-error-python#readme
Project-URL: Repository, https://github.com/aidtaas/mobius-error-python
Project-URL: Issues, https://github.com/aidtaas/mobius-error-python/issues
Keywords: fastapi,error-handling,exception-handler,microservices,mobius,rest-api
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.100.0
Requires-Dist: starlette>=0.27.0
Provides-Extra: dev
Requires-Dist: uvicorn[standard]>=0.23.0; extra == "dev"
Requires-Dist: httpx>=0.24.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
Dynamic: license-file

# Mobius Error Services for FastAPI

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.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Response Format](#response-format)
- [Exception Hierarchy](#exception-hierarchy)
- [Built-in Error Codes](#built-in-error-codes)
- [Usage Examples](#usage-examples)
  - [Basic: Raising Exceptions](#basic-raising-exceptions)
  - [Custom Error Codes](#custom-error-codes)
  - [Fluent Builder Pattern](#fluent-builder-pattern)
  - [Wrapping Upstream Failures](#wrapping-upstream-failures)
  - [Pydantic Validation (Automatic)](#pydantic-validation-automatic)
- [Exception Types Reference](#exception-types-reference)
- [Advanced Usage](#advanced-usage)
  - [Setting the Application Name](#setting-the-application-name)
  - [Custom Exception Classes](#custom-exception-classes)
  - [Combining with FastAPI's HTTPException](#combining-with-fastapis-httpexception)
- [Java ↔ Python Mapping](#java--python-mapping)
- [Project Structure](#project-structure)

---

## Installation

Copy the `mobius_error/` package into your project, or install it as an editable package:

```bash
cd error_lib
pip install -e .
```

**Requirements:** Python 3.10+, FastAPI >= 0.100.0

---

## Quick Start

Two lines to integrate into any FastAPI app:

```python
from fastapi import FastAPI
from mobius_error import register_exception_handlers

app = FastAPI(title="my-service")
register_exception_handlers(app, app_name="my-service")
```

That's it. Every unhandled exception now returns a structured JSON response instead of a raw 500.

---

## Response Format

All error responses follow this consistent JSON structure (empty fields are omitted):

```json
{
  "timestamp": 1706000000000,
  "origin": "my-service",
  "httpStatusCode": 404,
  "errorCode": 404001,
  "errorMessage": "User not found",
  "detailedErrorMessage": "No user with ID 42 exists in the database",
  "subErrors": [
    {
      "message": "Checked primary and replica databases",
      "timestamp": 1706000000
    }
  ],
  "actionsRequired": [
    "Verify the user ID and try again"
  ],
  "cause": {
    "message": "Record not found in table 'users'",
    "origin": "my-service",
    "timestamp": 1706000000
  },
  "docUrl": "https://mobiusdtaas.atlassian.net/wiki/spaces/EN/pages/2360868868/..."
}
```

| Field                  | Type          | Description                                           |
|------------------------|---------------|-------------------------------------------------------|
| `timestamp`            | `int`         | Unix timestamp in milliseconds                        |
| `origin`               | `string`      | Name of the service that produced the error            |
| `httpStatusCode`       | `int`         | HTTP status code                                       |
| `errorCode`            | `int`         | Application-specific numeric error code                |
| `errorMessage`         | `string`      | Human-readable summary                                 |
| `detailedErrorMessage` | `string`      | More context about what went wrong                     |
| `subErrors`            | `ErrorMessage[]` | List of nested/related error details                |
| `actionsRequired`      | `string[]`    | Suggested actions the caller can take                  |
| `cause`                | `ErrorMessage` | Root cause information (e.g., from a wrapped exception) |
| `docUrl`               | `string`      | Link to documentation for common issues                |

---

## Exception Hierarchy

The exception class hierarchy mirrors the Java library exactly:

```
Exception
└── ApplicationException          (base — caught as 403)
    └── ApiException              (uses Error's HTTP status)
        ├── ValidationException   (caught as 409)
        │   ├── AccessViolationException  (caught as 403)
        │   └── InvalidNameException
        ├── TokenException        (uses Error's HTTP status)
        ├── DataTypeMismatchException     (caught as 400)
        └── MethodArgumentsNotValidException
    ├── RestException
    ├── RestGetException
    ├── RestPostException
    ├── KafkaException
    ├── KafkaConsumptionException
    ├── ObjectMappingException
    ├── InvalidTenantException
    ├── UnsupportedOperationException
    └── GroupDataRetrievalException
```

Each exception handler matches the behavior of its Java `@ExceptionHandler` counterpart.

---

## Built-in Error Codes

These are defined in `CommonErrors` and match the Java `CommonErrors` class:

| Constant                     | HTTP | Code   | Message                                        |
|------------------------------|------|--------|------------------------------------------------|
| `INVALID_TENANT_ID`          | 403  | 403002 | You're not registered as a tenant yet          |
| `TENANT_NOT_AUTHORIZED`      | 401  | 401000 | You're not authorized to access the resource   |
| `UNEXPECTED_ERROR`           | 500  | 500000 | Encountered an unexpected error                |
| `SERVICE_UNAVAILABLE`        | 500  | 500001 | One or more service(s) are not up and running  |
| `OBJECT_MAPPING_FAILURE`     | 500  | 500002 | Failed to convert json to object/map           |
| `GROUP_DATA_RETRIEVAL_FAILURE` | 500 | 500003 | Failed to get group data from data lake        |
| `GET_API_FAILURE`            | 500  | 500004 | Failed to GET data from the URL                |
| `POST_API_FAILURE`           | 500  | 500005 | Failed to make POST call to an api             |
| `REST_API_FAILURE`           | 500  | 500006 | Failed to make REST call to an api             |
| `KAFKA_ERROR`                | 400  | 400000 | An error occurred in kafka                     |
| `KAFKA_CONSUMPTION_ERROR`    | 400  | 400001 | An error occurred while consuming from kafka   |
| `INVALID_NAME`               | 400  | 400002 | The name provided is invalid                   |
| `UNSUPPORTED_OPERATION`      | 400  | 400003 | This operation is not yet supported            |

---

## Usage Examples

### Basic: Raising Exceptions

```python
from mobius_error import ApplicationException, ApiException, CommonErrors

# Simple — uses built-in error definition
@app.get("/items/{item_id}")
async def get_item(item_id: int):
    item = db.find(item_id)
    if not item:
        raise ApiException(
            error=CommonErrors.GET_API_FAILURE,
            detailed_error_message=f"Item {item_id} not found in database",
        )
    return item
```

### Custom Error Codes

Define your own errors following the same pattern as `CommonErrors`:

```python
from mobius_error import Error, ApiException

class MyErrors:
    USER_NOT_FOUND = Error(404, 404001, "User not found", "Check the user ID and try again")
    DUPLICATE_EMAIL = Error(409, 409001, "Email already exists", "Use a different email address")
    QUOTA_EXCEEDED = Error(429, 429001, "Rate limit exceeded", "Wait a moment and retry")

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await find_user(user_id)
    if not user:
        raise ApiException(
            error=MyErrors.USER_NOT_FOUND,
            detailed_error_message=f"No user with ID {user_id}",
        )
    return user
```

### Fluent Builder Pattern

Chain `.add_sub_error()` and `.add_action_required()` for rich error detail:

```python
from mobius_error import ApplicationException, CommonErrors

@app.post("/deploy")
async def deploy():
    errors = run_preflight_checks()
    if errors:
        raise (
            ApplicationException(
                error=CommonErrors.UNEXPECTED_ERROR,
                detailed_error_message="Deployment preflight checks failed",
            )
            .add_sub_error("Health check: database unreachable")
            .add_sub_error("Health check: cache latency > 500ms")
            .add_action_required("Verify database connectivity")
            .add_action_required("Check Redis cluster status")
        )
```

This produces a response with all sub-errors and actions listed:

```json
{
  "errorCode": 500000,
  "errorMessage": "Encountered an unexpected error",
  "detailedErrorMessage": "Deployment preflight checks failed",
  "subErrors": [
    { "message": "Health check: database unreachable" },
    { "message": "Health check: cache latency > 500ms" }
  ],
  "actionsRequired": [
    "Kindly try again after some time or contact the support team",
    "Verify database connectivity",
    "Check Redis cluster status"
  ]
}
```

### Wrapping Upstream Failures

Preserve the original cause when catching exceptions from external calls:

```python
import httpx
from mobius_error import RestGetException

@app.get("/proxy/weather")
async def proxy_weather():
    try:
        async with httpx.AsyncClient() as client:
            resp = await client.get("https://api.weather.com/forecast")
            resp.raise_for_status()
            return resp.json()
    except httpx.HTTPStatusError as exc:
        raise RestGetException(
            error_message="Failed to fetch weather forecast",
            url="https://api.weather.com/forecast",
            status_code=exc.response.status_code,
            response_body=exc.response.text,
            cause=exc,
        )
```

### Pydantic Validation (Automatic)

FastAPI/Pydantic validation errors are caught automatically and formatted as sub-errors:

```python
from pydantic import BaseModel, Field

class CreateUserRequest(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    email: str = Field(..., pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
    age: int = Field(..., ge=0, le=150)

@app.post("/users")
async def create_user(body: CreateUserRequest):
    # If validation fails, the library auto-returns a 422 with sub-errors:
    # {
    #   "errorCode": 400000,
    #   "errorMessage": "Validation error",
    #   "subErrors": [
    #     { "message": "body → name: String should have at least 2 characters" },
    #     { "message": "body → email: String should match pattern ..." }
    #   ]
    # }
    return {"created": body.model_dump()}
```

---

## Exception Types Reference

### `ApplicationException` → HTTP 403

The base exception. Use when something unexpected goes wrong.

```python
raise ApplicationException(
    error=CommonErrors.UNEXPECTED_ERROR,          # required
    detailed_error_message="What happened",       # optional
    error_object={"debug": "info"},               # optional, included in response
    cause=original_exception,                     # optional, populates 'cause' field
)
```

### `ApiException` → Uses Error's HTTP status

Most common for API-level errors. The HTTP status comes from the `Error` object.

```python
raise ApiException(
    error=MyErrors.USER_NOT_FOUND,                # Error with http_status_code=404
    detailed_error_message="User 42 not found",
)
# → Returns HTTP 404
```

### `ValidationException` → HTTP 409

For data validation failures caught in business logic (not Pydantic).

```python
raise ValidationException(
    error=MyErrors.DUPLICATE_EMAIL,
    detailed_error_message="user@example.com already registered",
)
```

### `AccessViolationException` → HTTP 403

For authorization/permission failures.

```python
raise AccessViolationException(
    error=CommonErrors.TENANT_NOT_AUTHORIZED,
    detailed_error_message="Tenant xyz cannot access project abc",
)
```

### `TokenException` → Uses Error's HTTP status

For authentication/token failures.

```python
raise TokenException(
    error=CommonErrors.TENANT_NOT_AUTHORIZED,
    detailed_error_message="JWT token expired",
)
# → Returns HTTP 401
```

### `DataTypeMismatchException` → HTTP 400

For type conversion failures.

```python
raise DataTypeMismatchException(
    error=Error(400, 400004, "Invalid input type", "Provide valid input"),
    detailed_error_message="Expected int for 'age', got string",
    error_object={"field": "age", "received": "twenty-five"},
)
```

### `InvalidNameException` → HTTP 409

Shortcut for invalid name validation.

```python
raise InvalidNameException("Name contains invalid characters: @#$")
```

### `InvalidTenantException` → HTTP 409

Shortcut for invalid tenant validation.

```python
raise InvalidTenantException("Tenant ID 'null' is not valid")
```

### `ObjectMappingException` → HTTP 403

For serialization/deserialization failures.

```python
raise ObjectMappingException("Failed to parse user payload", cause=json_error)
```

### `RestGetException` / `RestPostException` / `RestException` → HTTP 403

For upstream HTTP call failures.

```python
raise RestGetException(
    error_message="Upstream service unavailable",
    url="https://api.example.com/data",
    status_code=502,
    response_body='{"error": "bad gateway"}',
)
```

### `KafkaException` / `KafkaConsumptionException` → HTTP 403

For message queue failures.

```python
raise KafkaException("Failed to publish event to topic 'orders'")
raise KafkaConsumptionException("Failed to deserialize message", cause=exc)
```

### `UnsupportedOperationException` → HTTP 403

For operations not yet implemented.

```python
raise UnsupportedOperationException("PATCH is not supported on this resource")
```

---

## Advanced Usage

### Setting the Application Name

The `origin` field in every error response is set from the app name. You can configure it in three ways:

```python
# Option 1: Pass explicitly (recommended)
register_exception_handlers(app, app_name="my-service")

# Option 2: Falls back to app.title
app = FastAPI(title="my-service")
register_exception_handlers(app)

# Option 3: Set manually at any time
from mobius_error import App
App.set_app_name("my-service")
```

### Custom Exception Classes

Extend the hierarchy for your domain:

```python
from mobius_error import ApiException, Error

class PaymentErrors:
    INSUFFICIENT_FUNDS = Error(402, 402001, "Insufficient funds", "Add funds to your account")
    CARD_DECLINED = Error(402, 402002, "Card declined", "Try a different payment method")

class PaymentException(ApiException):
    def __init__(self, error: Error, detail: str, transaction_id: str | None = None):
        super().__init__(error=error, detailed_error_message=detail)
        if transaction_id:
            self.add_sub_error(f"Transaction ID: {transaction_id}")

# Register handler for your custom exception
@app.exception_handler(PaymentException)
async def handle_payment(request, exc):
    from mobius_error.responses.api_error_response import ApiErrorResponse
    from fastapi.responses import JSONResponse
    resp = ApiErrorResponse.from_application_exception(exc.http_status_code, exc)
    return JSONResponse(status_code=exc.http_status_code, content=resp.to_dict())

# Usage
raise PaymentException(
    PaymentErrors.CARD_DECLINED,
    detail="Visa ending in 4242 was declined",
    transaction_id="txn_abc123",
)
```

### Combining with FastAPI's HTTPException

The library catches Starlette/FastAPI `HTTPException` too, wrapping them in the Mobius format. So `raise HTTPException(status_code=404, detail="Not found")` will return:

```json
{
  "httpStatusCode": 404,
  "errorCode": 500001,
  "errorMessage": "Not found",
  "origin": "my-service"
}
```

---

## Java ↔ Python Mapping

| Java (Spring Boot)                        | Python (FastAPI)                              |
|-------------------------------------------|-----------------------------------------------|
| `@ControllerAdvice`                       | `register_exception_handlers(app)`            |
| `@ExceptionHandler(ApiException.class)`   | `@app.exception_handler(ApiException)`        |
| `new Error(HttpStatus.NOT_FOUND, ...)`    | `Error(404, 404001, ...)`                     |
| `throw new ApiException(error, msg)`      | `raise ApiException(error=error, detailed_error_message=msg)` |
| `new ApiErrorResponse(status, msg)`       | `ApiErrorResponse.from_status(status, msg)`   |
| `ApplicationException.addSubError(msg)`   | `.add_sub_error(msg)`                         |
| `App.setAppName(name)`                    | `App.set_app_name(name)`                      |
| `@Value("${spring.application.name}")`    | `register_exception_handlers(app, app_name=...)` |
| `ResponseEntity<ApiErrorResponse>`        | `JSONResponse(status_code=..., content=resp.to_dict())` |

---

## Project Structure

```
error_lib/
├── pyproject.toml                  # Package definition
├── example_app.py                  # Full working demo (run with uvicorn)
├── test_smoke.py                   # Smoke tests for all endpoints
└── mobius_error/
    ├── __init__.py                 # Public API — import everything from here
    ├── config.py                   # App.get_app_name() / App.set_app_name()
    ├── errors/
    │   ├── error.py                # Error dataclass (status + code + message)
    │   ├── error_message.py        # ErrorMessage model (nested cause chain)
    │   └── common_errors.py        # Built-in error constants (CommonErrors)
    ├── exceptions/
    │   ├── application_exception.py    # Base exception
    │   ├── api_exception.py            # API-level exception
    │   ├── validation_exception.py     # Validation errors
    │   ├── access_violation_exception.py
    │   ├── token_exception.py
    │   ├── data_type_mismatch_exception.py
    │   ├── rest_exception.py
    │   ├── rest_get_exception.py
    │   ├── rest_post_exception.py
    │   ├── kafka_exception.py
    │   ├── kafka_consumption_exception.py
    │   ├── object_mapping_exception.py
    │   ├── invalid_name_exception.py
    │   ├── invalid_tenant_exception.py
    │   ├── unsupported_operation_exception.py
    │   └── group_data_retrieval_exception.py
    ├── handlers/
    │   └── exception_handler.py    # All FastAPI exception handlers + middleware
    └── responses/
        ├── error_response.py       # Base response model
        └── api_error_response.py   # Full API response with factory methods
```
