Skip to content

Commit d80b417

Browse files
committed
global exception handling
1 parent 64c8070 commit d80b417

File tree

6 files changed

+100
-9
lines changed

6 files changed

+100
-9
lines changed

main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from config import Settings
1212
from src.interface_adapters import api_router
13+
from src.interface_adapters.exceptions import AppException
14+
from src.interface_adapters.middleware.error_handler import app_exception_handler
1315

1416
# https://brandur.org/logfmt
1517
# https://github.com/Delgan/loguru
@@ -44,6 +46,9 @@ async def isConfigured():
4446
openapi_url="/v1/openapi.json",
4547
)
4648

49+
# Register global exception handler
50+
app.add_exception_handler(AppException, app_exception_handler)
51+
4752
app.add_middleware(
4853
CORSMiddleware,
4954
allow_origins=["*"], # Set this to lock down if applicable to your use case

rapidapi/api_tests.paw

0 Bytes
Binary file not shown.

src/app/usecases/analyze_issue.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from loguru import logger
55

66
from src.app.ports.repositories.issues import IssueRepository
7-
from src.app.repository import (
8-
UnitOfWork,
9-
)
7+
from src.app.repository import UnitOfWork
108
from src.domain.issue import Issue
9+
from src.interface_adapters.exceptions import NotFoundException
1110

1211

1312
class AnalyzeIssueProtocol(Protocol):
@@ -34,4 +33,9 @@ def __init__(
3433
async def analyze(self) -> Issue:
3534
logger.info(f"analyzing issue: {self.issue_number}")
3635
issue = await self.repo.get_by_id(self.issue_number)
36+
if issue.issue_number == 0:
37+
raise NotFoundException(
38+
message="Issue not found",
39+
detail=f"Issue with number {self.issue_number} does not exist"
40+
)
3741
return issue

src/interface_adapters/endpoints/issues.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,33 @@
44

55
from src.app.usecases.analyze_issue import AnalyzeIssue
66
from config import Settings
7-
87
from src.domain.issue import Issue
8+
from src.interface_adapters.exceptions import UnsupportedOperationException
99
from src.resource_adapters.persistence.in_memory.issues import InMemoryIssueRepository
1010
from src.resource_adapters.persistence.in_memory.unit_of_work import UnitOfWork
11-
from typing import Optional
1211

1312
router = APIRouter()
1413

1514

1615
# https://fastapi.tiangolo.com/tutorial/dependencies/
17-
async def configure_unit_of_work() -> Optional[UnitOfWork]:
16+
async def configure_unit_of_work() -> UnitOfWork:
1817
if Settings.get_settings().execution_mode == "in-memory":
1918
return UnitOfWork()
2019
else:
21-
return None
20+
raise UnsupportedOperationException(
21+
message="Unsupported unit of work configuration",
22+
detail="Only in-memory unit of work is currently supported"
23+
)
2224

2325

24-
async def configure_repository() -> Optional[InMemoryIssueRepository]:
26+
async def configure_repository() -> InMemoryIssueRepository:
2527
if Settings.get_settings().execution_mode == "in-memory":
2628
return InMemoryIssueRepository()
2729
else:
28-
return None
30+
raise UnsupportedOperationException(
31+
message="Unsupported repository configuration",
32+
detail="Only in-memory repository is currently supported"
33+
)
2934

3035

3136
@router.post("/issues/{issue_number}/analyze", response_model=Issue)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import Optional
2+
3+
class AppException(Exception):
4+
"""Base exception class for application exceptions"""
5+
def __init__(
6+
self,
7+
message: str,
8+
status_code: int = 500,
9+
detail: Optional[str] = None
10+
) -> None:
11+
self.message = message
12+
self.status_code = status_code
13+
self.detail = detail
14+
super().__init__(self.message)
15+
16+
17+
class UnsupportedOperationException(AppException):
18+
"""Exception raised when an operation is not supported"""
19+
def __init__(
20+
self,
21+
message: str = "Operation not supported",
22+
detail: Optional[str] = None
23+
) -> None:
24+
super().__init__(
25+
message=message,
26+
status_code=501,
27+
detail=detail
28+
)
29+
30+
31+
class ConfigurationException(AppException):
32+
"""Exception raised for configuration errors"""
33+
def __init__(
34+
self,
35+
message: str = "Invalid configuration",
36+
detail: Optional[str] = None
37+
) -> None:
38+
super().__init__(
39+
message=message,
40+
status_code=500,
41+
detail=detail
42+
)
43+
44+
45+
class NotFoundException(AppException):
46+
"""Exception raised when a requested resource is not found"""
47+
def __init__(
48+
self,
49+
message: str = "Resource not found",
50+
detail: Optional[str] = None
51+
) -> None:
52+
super().__init__(
53+
message=message,
54+
status_code=404,
55+
detail=detail
56+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import Request
2+
from fastapi.responses import JSONResponse
3+
from loguru import logger
4+
5+
from src.interface_adapters.exceptions import AppException
6+
7+
8+
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
9+
"""Global exception handler for AppException and its subclasses"""
10+
logger.error(f"Request to {request.url} failed: {exc.message}")
11+
if exc.detail:
12+
logger.error(f"Detail: {exc.detail}")
13+
14+
return JSONResponse(
15+
status_code=exc.status_code,
16+
content={
17+
"message": exc.message,
18+
"detail": exc.detail,
19+
"path": str(request.url)
20+
}
21+
)

0 commit comments

Comments
 (0)