-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: standardized ErrorResponse model
- Loading branch information
Showing
33 changed files
with
1,448 additions
and
1,432 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,66 @@ | ||
from typing import Optional | ||
|
||
from fastapi import HTTPException | ||
from starlette import status | ||
from pydantic import BaseModel | ||
from starlette import status as request_status | ||
|
||
|
||
# Pydantic models can not inherit from "Exception", but we use it for OpenAPI spec | ||
class ErrorResponse(BaseModel): | ||
status: int = 500 | ||
type: str = "ApplicationException" | ||
message: str = "The requested operation failed" | ||
debug: str = "An unknown and unhandled exception occurred in the API" | ||
extra: dict | None = None | ||
|
||
|
||
class ApplicationException(Exception): | ||
def __init__(self, message: Optional[str] = "Something went wrong"): | ||
super() | ||
status: int = 500 | ||
type: str = "ApplicationException" | ||
message: str = "The requested operation failed" | ||
debug: str = "An unknown and unhandled exception occurred in the API" | ||
extra: dict | None = None | ||
|
||
def __init__(self, message: str = "The requested operation failed", | ||
debug: str = "An unknown and unhandled exception occurred in the API", extra: dict = None, | ||
status: int = 500, ): | ||
self.status = status | ||
self.type = self.__class__.__name__ | ||
self.message = message | ||
self.debug = debug | ||
self.extra = extra | ||
|
||
def dict(self): | ||
return {"status": self.status, "type": self.type, "message": self.message, "debug": self.debug, | ||
"extra": self.extra, } | ||
|
||
|
||
def __str__(self): | ||
return self.message | ||
class MissingPrivilegeException(ApplicationException): | ||
def __init__(self, message: str = "You do not have the required permissions", | ||
debug: str = "Action denied because of insufficient permissions", extra: dict = None, ): | ||
super().__init__(message, debug, extra, request_status.HTTP_403_FORBIDDEN) | ||
self.type = self.__class__.__name__ | ||
|
||
|
||
class EntityNotFoundException(ApplicationException): | ||
def __init__(self, message: str = None): | ||
super().__init__(message) | ||
class NotFoundException(ApplicationException): | ||
def __init__(self, message: str = "The requested resource could not be found", | ||
debug: str = "The requested resource could not be found", extra: dict = None, ): | ||
super().__init__(message, debug, extra, request_status.HTTP_404_NOT_FOUND) | ||
self.type = self.__class__.__name__ | ||
|
||
|
||
class BadRequest(ApplicationException): | ||
def __init__(self, message: str = None): | ||
super().__init__(message) | ||
class BadRequestException(ApplicationException): | ||
def __init__(self, message: str = "Invalid data for the operation", | ||
debug: str = "Unable to complete the requested operation with the given input values.", | ||
extra: dict = None, ): | ||
super().__init__(message, debug, extra, request_status.HTTP_400_BAD_REQUEST) | ||
self.type = self.__class__.__name__ | ||
|
||
|
||
class MissingPrivilegeException(Exception): | ||
def __init__(self, message=None): | ||
self.message = message if message else "Missing privileges to perform operation on the resource" | ||
class ValidationException(ApplicationException): | ||
def __init__(self, message: str = "The received data is invalid", | ||
debug: str = "Values are invalid for requested operation.", extra: dict = None, ): | ||
super().__init__(message, debug, extra, request_status.HTTP_422_UNPROCESSABLE_ENTITY) | ||
self.type = self.__class__.__name__ | ||
|
||
|
||
credentials_exception = HTTPException( | ||
status_code=status.HTTP_401_UNAUTHORIZED, | ||
detail="Token validation failed", | ||
headers={"WWW-Authenticate": "Bearer"}, | ||
) | ||
credentials_exception = HTTPException(status_code=request_status.HTTP_401_UNAUTHORIZED, | ||
detail="Token validation failed", headers={"WWW-Authenticate": "Bearer"}, ) |
34 changes: 17 additions & 17 deletions
34
api/src/common/utils/logger.py → api/src/common/logger.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,17 @@ | ||
""" | ||
Setup API and Uvicorn logger. | ||
""" | ||
|
||
import logging | ||
|
||
from config import config | ||
|
||
uvicorn_logger = logging.getLogger("uvicorn") | ||
|
||
logger = logging.getLogger("API") | ||
logger.setLevel(config.LOGGER_LEVEL.upper()) | ||
formatter = logging.Formatter("%(levelname)s:%(asctime)s %(message)s") | ||
channel = logging.StreamHandler() | ||
channel.setFormatter(formatter) | ||
channel.setLevel(config.LOGGER_LEVEL.upper()) | ||
logger.addHandler(channel) | ||
""" | ||
Setup API and Uvicorn logger. | ||
""" | ||
|
||
import logging | ||
|
||
from config import config | ||
|
||
uvicorn_logger = logging.getLogger("uvicorn") | ||
|
||
logger = logging.getLogger("API") | ||
logger.setLevel(config.LOGGER_LEVEL.upper()) | ||
formatter = logging.Formatter("%(levelname)s:%(asctime)s %(message)s") | ||
channel = logging.StreamHandler() | ||
channel.setFormatter(formatter) | ||
channel.setLevel(config.LOGGER_LEVEL.upper()) | ||
logger.addHandler(channel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,87 @@ | ||
import functools | ||
import traceback | ||
from inspect import iscoroutinefunction | ||
from typing import Callable, Type, TypeVar | ||
|
||
from pydantic import ValidationError | ||
from requests import HTTPError | ||
from starlette import status | ||
from starlette.responses import PlainTextResponse, Response | ||
from starlette.responses import JSONResponse, Response | ||
|
||
from common.exceptions import BadRequest, EntityNotFoundException | ||
from common.utils.logger import logger | ||
from common.exceptions import ( | ||
ApplicationException, | ||
BadRequestException, | ||
ErrorResponse, | ||
MissingPrivilegeException, | ||
NotFoundException, | ||
ValidationException, | ||
) | ||
from common.logger import logger | ||
|
||
responses = { | ||
400: {"model": ErrorResponse, "content": {"application/json": {"example": BadRequestException().dict()}}}, | ||
401: { | ||
"model": ErrorResponse, | ||
"content": { | ||
"application/json": { | ||
"example": ErrorResponse( | ||
status=401, type="UnauthorizedException", message="Token validation failed" | ||
).dict() | ||
} | ||
}, | ||
}, | ||
403: {"model": ErrorResponse, "content": {"application/json": {"example": MissingPrivilegeException().dict()}}}, | ||
404: {"model": ErrorResponse, "content": {"application/json": {"example": NotFoundException().dict()}}}, | ||
422: {"model": ErrorResponse, "content": {"application/json": {"example": ValidationException().dict()}}}, | ||
500: {"model": ErrorResponse, "content": {"application/json": {"example": ApplicationException().dict()}}}, | ||
} | ||
|
||
TResponse = TypeVar("TResponse", bound=Response) | ||
|
||
""" | ||
Function made to be used as a decorator for a route. | ||
It will execute the function, and return a successful response of the passed response class. | ||
If the execution fails, it will return a JSONResponse with a standardized error model. | ||
""" | ||
|
||
|
||
def create_response(response_class: Type[TResponse]) -> Callable[..., Callable[..., TResponse | PlainTextResponse]]: | ||
def func_wrapper(func) -> Callable[..., TResponse | PlainTextResponse]: | ||
def create_response(response_class: Type[TResponse]) -> Callable[..., Callable[..., TResponse | JSONResponse]]: | ||
def func_wrapper(func) -> Callable[..., TResponse | JSONResponse]: | ||
@functools.wraps(func) | ||
def wrapper_decorator(*args, **kwargs) -> TResponse | PlainTextResponse: | ||
async def wrapper_decorator(*args, **kwargs) -> TResponse | JSONResponse: | ||
try: | ||
result = func(*args, **kwargs) | ||
# Await function if needed | ||
if not iscoroutinefunction(func): | ||
result = func(*args, **kwargs) | ||
else: | ||
result = await func(*args, **kwargs) | ||
return response_class(result, status_code=200) | ||
except HTTPError as e: | ||
logger.error(e) | ||
return PlainTextResponse(str(e), status_code=e.response.status_code) | ||
except ValidationError as e: | ||
except HTTPError as http_error: | ||
error_response = ErrorResponse( | ||
type="ExternalFetchException", | ||
status=http_error.response.status, | ||
message="Failed to fetch an external resource", | ||
debug=http_error.response, | ||
) | ||
logger.error(error_response) | ||
return JSONResponse(error_response.dict(), status_code=error_response.status) | ||
except ValidationException as e: | ||
logger.error(e) | ||
return PlainTextResponse(str(e), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) | ||
except EntityNotFoundException as e: | ||
return JSONResponse(e.dict(), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) | ||
except NotFoundException as e: | ||
logger.error(e) | ||
return PlainTextResponse(str(e), status_code=status.HTTP_404_NOT_FOUND) | ||
except BadRequest as e: | ||
return JSONResponse(e.dict(), status_code=status.HTTP_404_NOT_FOUND) | ||
except BadRequestException as e: | ||
logger.error(e) | ||
return PlainTextResponse(str(e), status_code=status.HTTP_400_BAD_REQUEST) | ||
logger.error(e.dict()) | ||
return JSONResponse(e.dict(), status_code=status.HTTP_400_BAD_REQUEST) | ||
except MissingPrivilegeException as e: | ||
logger.warning(e) | ||
return JSONResponse(e.dict(), status_code=status.HTTP_403_FORBIDDEN) | ||
except Exception as e: | ||
traceback.print_exc() | ||
logger.error(f"Unexpected unhandled exception: {e}") | ||
return PlainTextResponse( | ||
"Unexpected unhandled exception", | ||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||
) | ||
return JSONResponse(ErrorResponse().dict(), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) | ||
|
||
return wrapper_decorator | ||
return wrapper_decorator # type: ignore | ||
|
||
return func_wrapper |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.