Skip to content

Commit 9c93d57

Browse files
committed
✨ Added middleware and pre-commit
1 parent e1a5421 commit 9c93d57

File tree

10 files changed

+198
-32
lines changed

10 files changed

+198
-32
lines changed

.flake8

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[flake8]
2+
max-line-length = 120
3+
select = C,E,F,W,B,B950
4+
extend-ignore = E203, E50, E712, W503
5+
exclude =
6+
# No need to traverse our git directory
7+
.git,
8+
# There's no value in checking cache directories
9+
__pycache__,
10+
__init__.py,
11+
.mypy_cache,
12+
.pytest_cache,
13+
# There's no value in checking IDE configs
14+
.idea,
15+
# There's no value in checking venv directories
16+
venv
17+
18+
per-file-ignores =
19+
# imported but unused
20+
__init__.py: F401

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
venv
33
__pycache__
44
dist
5+
fastapi_exceptionshandler.egg-info/

.pre-commit-config.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
files: ^fastapi_exceptionshandler/
2+
default_language_version:
3+
python: python3.11
4+
repos:
5+
- repo: https://github.com/psf/black
6+
rev: 23.9.1
7+
hooks:
8+
- id: black
9+
10+
- repo: https://github.com/pycqa/isort
11+
rev: 5.12.0
12+
hooks:
13+
- id: isort
14+
15+
- repo: https://github.com/pycqa/flake8
16+
rev: 6.1.0
17+
hooks:
18+
- id: flake8
19+
20+
- repo: https://github.com/pre-commit/mirrors-mypy
21+
rev: 'v1.5.1'
22+
hooks:
23+
- id: mypy
24+
additional_dependencies: [
25+
pydantic,
26+
]

fastapi_exceptionshandler/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from .api_exception import APIException, APIError
2-
from .api_exception_manager import APIExceptionManager
1+
from .api_exception import APIError, APIException
32
from .api_exception_handler import APIExceptionHandler
3+
from .api_exception_manager import APIExceptionManager
44

55
__all__ = [
66
"APIException",

fastapi_exceptionshandler/api_exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class APIException(ABC, Exception):
99
class ErrorCode(Enum):
1010
InternalError = "Internal Server Error"
1111

12-
def __init__(self, error_code: Enum = ErrorCode.InternalError, exc: Exception = None) -> None:
12+
def __init__(self, error_code: Enum = ErrorCode.InternalError, exc: Optional[Exception] = None) -> None:
1313
self._error_code = error_code
1414
self._exc = exc
1515

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
from .api_exception import APIException
2-
from starlette.responses import JSONResponse
3-
from typing import Any, Dict
1+
import logging
42
import os
3+
from typing import Any, Dict, Optional
4+
5+
from fastapi.exceptions import RequestValidationError
6+
from pydantic import ValidationError
7+
from starlette.requests import Request
8+
from starlette.responses import JSONResponse
9+
10+
from .api_exception import APIException
511

612

713
class APIExceptionHandler:
814
error_label = "errorCode"
915
message_label = "message"
1016

1117
@classmethod
12-
async def handled(cls, exc: APIException, body_extra: Dict = None, **kwargs: Any) -> JSONResponse:
13-
body_content = {cls.error_label: exc.get_error_code(), cls.message_label: str(exc)}
18+
def handled(
19+
cls, request: Request, exc: APIException, body_extra: Optional[Dict] = None, **kwargs: Any
20+
) -> JSONResponse:
21+
body_content = {
22+
cls.error_label: exc.get_error_code(),
23+
cls.message_label: str(exc),
24+
}
1425
if body_extra is not None:
1526
body_content = {**body_content, **body_extra}
1627

@@ -21,6 +32,36 @@ async def handled(cls, exc: APIException, body_extra: Dict = None, **kwargs: Any
2132
return JSONResponse(status_code=exc.status_code, content=body_content, **kwargs)
2233

2334
@classmethod
24-
async def unhandled(cls, exc: Exception, body_extra: Dict = None, **kwargs: Any) -> JSONResponse:
35+
def unhandled(
36+
cls, request: Request, exc: Exception, body_extra: Optional[Dict] = None, **kwargs: Any
37+
) -> JSONResponse:
2538
api_exc = APIException(exc=exc)
26-
return await cls.handled(api_exc, body_extra, **kwargs)
39+
return cls.handled(request, api_exc, body_extra, **kwargs)
40+
41+
@classmethod
42+
def handle_exception(
43+
cls,
44+
request: Request,
45+
exc: Exception,
46+
capture_unhandled: bool = True,
47+
capture_validation: bool = False,
48+
log_error: bool = True,
49+
logger_name: str = "app.exception_handler",
50+
) -> JSONResponse:
51+
logger = logging.getLogger(logger_name) if log_error else None
52+
53+
if issubclass(type(exc), APIException):
54+
if log_error:
55+
logger.error(str(exc))
56+
return APIExceptionHandler.handled(request, exc) # type: ignore
57+
58+
if log_error:
59+
logger.error(str(exc))
60+
61+
if issubclass(type(exc), (RequestValidationError, ValidationError)) and not capture_validation:
62+
raise
63+
64+
if not capture_unhandled:
65+
raise
66+
67+
return APIExceptionHandler.unhandled(request, exc)
Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
1-
from fastapi.exceptions import RequestValidationError
2-
from .api_exception import APIException
3-
from .api_exception_handler import APIExceptionHandler
1+
from typing import Callable, List, Optional, Type
2+
43
from fastapi_routesmanager import RouteManager
5-
from pydantic import ValidationError
64
from starlette.requests import Request
75
from starlette.responses import Response
8-
from typing import Callable, List, Type, Optional
96

7+
from . import APIExceptionHandler
108

11-
class APIExceptionManager(RouteManager):
129

13-
def __init__(self, capture_unhandled: bool = True, capture_validation=False):
10+
class APIExceptionManager(RouteManager):
11+
def __init__(
12+
self,
13+
capture_unhandled: bool = True,
14+
capture_validation: bool = False,
15+
logger_name: Optional[str] = None,
16+
log_error: bool = True,
17+
):
1418
self.capture_unhandled = capture_unhandled
1519
self.capture_validation = capture_validation
20+
self.logger_name = logger_name
21+
self.log_error = log_error
1622

1723
async def run(
18-
self,
19-
request: Request,
20-
call_next: Callable,
21-
remaining_managers: List[Type[RouteManager]],
24+
self,
25+
request: Request,
26+
call_next: Callable,
27+
remaining_managers: List[Type[RouteManager]],
2228
) -> Optional[Response]:
2329
try:
24-
response: Response = await call_next(request, remaining_managers)
25-
except APIException as exc:
26-
return await APIExceptionHandler.handled(exc)
30+
response = await call_next(request, remaining_managers)
2731
except Exception as exc:
28-
if type(exc) in [RequestValidationError, ValidationError] and not self.capture_validation:
29-
raise
30-
if not self.capture_unhandled:
31-
raise
32-
return await APIExceptionHandler.unhandled(exc)
32+
return APIExceptionHandler.handle_exception(
33+
request,
34+
exc,
35+
capture_unhandled=self.capture_unhandled,
36+
capture_validation=self.capture_validation,
37+
logger_name=self.logger_name,
38+
log_error=self.log_error,
39+
)
3340
return response
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import typing
2+
3+
from fastapi import Request, Response
4+
from starlette.middleware.base import BaseHTTPMiddleware, DispatchFunction, RequestResponseEndpoint
5+
from starlette.types import ASGIApp
6+
7+
from . import APIExceptionHandler
8+
9+
10+
class APIExceptionMiddleware(BaseHTTPMiddleware):
11+
"""
12+
Middleware that catches and transforms exceptions
13+
"""
14+
15+
def __init__(
16+
self,
17+
app: ASGIApp,
18+
dispatch: typing.Optional[DispatchFunction] = None,
19+
capture_unhandled: bool = True,
20+
logger_name: typing.Optional[str] = None,
21+
log_error: bool = True,
22+
):
23+
super().__init__(app, dispatch)
24+
self.capture_unhandled = capture_unhandled
25+
self.logger_name = logger_name
26+
self.log_error = log_error
27+
28+
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
29+
try:
30+
response = await call_next(request)
31+
except Exception as exc:
32+
return APIExceptionHandler.handle_exception(
33+
request,
34+
exc,
35+
capture_unhandled=self.capture_unhandled,
36+
log_error=self.log_error,
37+
logger_name=self.logger_name,
38+
)
39+
return response

pyproject.toml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = 'fastapi_exceptionshandler'
7-
version = "0.0.1"
7+
version = "0.0.2"
88
license = {text = "MIT License"}
99
authors = [
1010
{ name="SirPaulO", email="me@sirpauloliver.com" },
@@ -26,3 +26,35 @@ classifiers = [
2626
"Homepage" = "https://github.com/SirPaulO/fastapi_exceptionshandler"
2727
"Bug Tracker" = "https://github.com/SirPaulO/fastapi_exceptionshandler/issues"
2828
"Repository" = "https://github.com/SirPaulO/fastapi_exceptionshandler.git"
29+
30+
[tool.mypy]
31+
plugins = ["pydantic.mypy"]
32+
ignore_missing_imports = true
33+
disallow_untyped_defs = true
34+
warn_unused_ignores = true
35+
no_strict_optional = true
36+
no_implicit_optional = true
37+
implicit_reexport = true
38+
explicit_package_bases = true
39+
namespace_packages = true
40+
follow_imports = "silent"
41+
warn_redundant_casts = true
42+
check_untyped_defs = true
43+
no_implicit_reexport = true
44+
45+
[tool.pydantic-mypy]
46+
init_forbid_extra = true
47+
init_typed = true
48+
warn_required_dynamic_aliases = true
49+
warn_untyped_fields = true
50+
51+
[tool.isort]
52+
profile = "black"
53+
multi_line_output = 3
54+
include_trailing_comma = true
55+
force_grid_wrap = 0
56+
line_length = 120
57+
58+
[tool.black]
59+
line-length = 120
60+
target-version = ['py39']

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
starlette~=0.27.0
2-
fastapi_routesmanager~=0.0.3
32
pydantic>=1.10.7
4-
fastapi>=0.95.2
3+
fastapi>=0.95.2
4+
pre-commit~=3.4.0

0 commit comments

Comments
 (0)