Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from basalam.backbone_api.exceptions.client_error import ForbiddenException

# backbone-api
OpenAPI request and response models

Expand Down Expand Up @@ -56,5 +58,43 @@ app.include_router(router)
if __name__=="__main__":
uvicorn.run(app, host="localhost", port=8000)
```
### Using Exceptions
in app.py

```python
from fastapi import FastAPI
from basalam.backbone_api.exceptions.client_error.handlers import client_error_exception_handler
from basalam.backbone_api.exceptions.client_error import (
ClientErrorException,
ForbiddenException,
UnauthorizedException,
ConflictException,
NotFoundException,
UnprocessableEntityException
)

app = FastAPI()

exception_handlers = {
ClientErrorException: client_error_exception_handler,
ForbiddenException: client_error_exception_handler,
UnauthorizedException: client_error_exception_handler,
ConflictException: client_error_exception_handler,
NotFoundException: client_error_exception_handler,
UnprocessableEntityException: client_error_exception_handler,
}

...

```
If you raise any of these exceptions everywhere in you FastAPI project FastAPI will return a client error response
based on the excpetion.

### Example Usage

```python
def view_or_somthing_else():
raise ForbiddenException()
```
#### Credits
This project was inspired by the work of [Mr.MohammadAli Soltanipoor](https://github.com/soltanipoor) on OpenAPI.
Empty file.
22 changes: 22 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
This package provides custom exception handling for client errors.

It includes different types of client error exceptions, a structure for
handling error details, and a handler for mapping these exceptions to
HTTP responses.

Modules:
- base: Contains base classes for error handling.
- conflict: Defines conflict-related error exceptions.
- forbidden: Defines forbidden-related error exceptions.
- not_found: Defines not found-related error exceptions.
- unauthorized: Defines unauthorized error exceptions.
- unprocessable_entity: Defines unprocessable error exceptions.
"""

from basalam.backbone_api.exceptions.client_error.conflict import ConflictException
from basalam.backbone_api.exceptions.client_error.forbidden import ForbiddenException
from basalam.backbone_api.exceptions.client_error.not_found import NotFoundException
from basalam.backbone_api.exceptions.client_error.unauthorized import UnauthorizedException
from basalam.backbone_api.exceptions.client_error.unprocessable_entity import UnprocessableEntityException
from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail, Error
73 changes: 73 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from http import HTTPStatus
from typing import List, Dict, Optional

from fastapi import HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from starlette.requests import Request
from basalam.backbone_api.responses.client_error.base import Base400Response

"""
This module defines the base error handling for client errors, including
the structure of error details, and a custom HTTP exception class for
client errors.

Classes:
- ErrorDetail: Structure for error details.
- Error: Structure for the full error response.
- ClientErrorException: Custom exception for client errors.
- ClientErrorExceptionMapper: Maps client errors to JSON responses.
"""


class ErrorDetail(BaseModel):
"""
Represents detailed information about a specific error.

Attributes:
code: A numerical code associated with the error.
message: A description of the error.
fields: Optional fields associated with the error.
data: Optional additional data related to the error.
"""

code: Optional[int] = 0
message: Optional[str] = None
fields: Optional[List[str]] = None
data: Optional[List[Dict]] = None


class Error(BaseModel):
http_status: int
message: str
errors: List[ErrorDetail]


class ClientErrorException(HTTPException):
"""
Custom exception for client errors.

This class is used to raise HTTP exceptions with detailed error
information in the response.

Attributes:
http_status: HTTP status code of the error (e.g., 400, 404).
errors: A list of `ErrorDetail` objects providing specifics
about the error.

"""

def __init__(self, http_status: int, errors: List[ErrorDetail] | None = None) -> None:
if errors is None:
errors = [ErrorDetail()]
self.http_status = http_status
self.errors = errors
super().__init__(status_code=http_status, detail=self.message)

@property
def message(self) -> str:
return HTTPStatus(self.http_status).phrase

@property
def response_data(self) -> List[ErrorDetail]:
return self.errors
14 changes: 14 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/conflict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Optional, List, Dict

from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail


class ConflictException(ClientErrorException):
def __init__(self, data: Optional[List[Dict]], message: str = None):
errors = [
ErrorDetail(
data=data,
message=message,
)
]
super().__init__(http_status=409, errors=errors)
12 changes: 12 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/forbidden.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional

from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail


class ForbiddenException(ClientErrorException):
def __init__(self, message: str = None) -> None:
if message:
errors = ErrorDetail(message=message)
else:
errors = None
super().__init__(http_status=403, errors=errors)
56 changes: 56 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Type

from starlette.requests import Request

from basalam.backbone_api.exceptions.client_error.conflict import ConflictException
from basalam.backbone_api.exceptions.client_error.forbidden import ForbiddenException
from basalam.backbone_api.exceptions.client_error.not_found import NotFoundException
from basalam.backbone_api.exceptions.client_error.unauthorized import UnauthorizedException
from basalam.backbone_api.exceptions.client_error.unprocessable_entity import UnprocessableEntityException
from basalam.backbone_api.exceptions.client_error.base import ClientErrorException
from basalam.backbone_api.responses.client_error.base import Base400Response, Error as ClientError
from basalam.backbone_api.responses.client_error.conflict import ConflictResponse, \
ExtendedError as ConflictExtendedError
from basalam.backbone_api.responses.client_error.not_found import NotFoundResponse
from basalam.backbone_api.responses.client_error.unprocessable_content import UnprocessableContentResponse, \
ExtendedError as UnprocessableContentExtendedError
from basalam.backbone_api.responses.client_error.unauthorized import UnauthorizedResponse
from basalam.backbone_api.responses.client_error.forbidden import ForbiddenResponse


# Helper function to map exception to response
def map_exception_to_response(exception: ClientErrorException, response_class: Type[Base400Response],
error_class: Type[ClientError], is_conflict: bool = False):
"""
Maps the exception data to a response.
"""
response_data = exception.response_data
errors = [error_class(message=data.message, code=data.code, fields=data.fields) for data in response_data]
if is_conflict:
return response_class(errors=errors, data=response_data[0].data).as_json_response()
return response_class(errors=errors).as_json_response()


async def client_error_exception_handler(request: Request, exception: ClientErrorException):
"""
Handles client error exceptions and maps them to a JSON response.

Parameters:
request: The incoming HTTP request.
exception: The raised `ClientErrorException`.

Returns:
JSONResponse: A structured JSON response with error details.
"""
if isinstance(exception, UnprocessableEntityException):
return map_exception_to_response(exception, UnprocessableContentResponse, UnprocessableContentExtendedError)
elif isinstance(exception, ConflictException):
return map_exception_to_response(exception, ConflictResponse, ConflictExtendedError, is_conflict=True)
elif isinstance(exception, NotFoundException):
return map_exception_to_response(exception, NotFoundResponse, ClientError)
elif isinstance(exception, ForbiddenException):
return map_exception_to_response(exception, ForbiddenResponse, ClientError)
elif isinstance(exception, UnauthorizedException):
return map_exception_to_response(exception, UnauthorizedResponse, ClientError)
else:
return map_exception_to_response(exception, Base400Response, ClientError)
12 changes: 12 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/not_found.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional

from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail


class NotFoundException(ClientErrorException):
def __init__(self, message: str = None) -> None:
if message:
errors = ErrorDetail(message=message)
else:
errors = None
super().__init__(http_status=404, errors=errors)
12 changes: 12 additions & 0 deletions src/basalam/backbone_api/exceptions/client_error/unauthorized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Optional

from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail


class UnauthorizedException(ClientErrorException):
def __init__(self, message: str = None) -> None:
if message:
errors = ErrorDetail(message=message)
else:
errors = None
super().__init__(http_status=401, errors=errors)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import List, Optional, Tuple

from basalam.backbone_api.exceptions.client_error.base import ClientErrorException, ErrorDetail


class UnprocessableEntityException(ClientErrorException):
def __init__(self, message: str, fields: Optional[List[str]] = None) -> None:
errors = [
ErrorDetail(
message=message,
fields=fields,
)
]
super().__init__(http_status=404, errors=errors)
8 changes: 6 additions & 2 deletions src/basalam/backbone_api/responses/client_error/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pydantic import BaseModel
from starlette.responses import JSONResponse
from starlette.status import HTTP_400_BAD_REQUEST

from basalam.backbone_api.responses.response_model_abstract import ResponseModelAbstract

Expand All @@ -12,9 +13,12 @@ class Error(BaseModel):


class Base400Response(ResponseModelAbstract):
http_status: int
http_status: int = 400
message: str
errors: List[Error] | None

async def as_json_response(self) -> JSONResponse:
pass
return JSONResponse(
content=self.model_dump(),
status_code=HTTP_400_BAD_REQUEST
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Generic
from typing import List, GenericList

from starlette.responses import JSONResponse
from starlette.status import HTTP_409_CONFLICT
Expand Down