Skip to content

Commit 35b9939

Browse files
committed
Refactor error handling for systems #74
1 parent 59f796c commit 35b9939

File tree

5 files changed

+89
-27
lines changed

5 files changed

+89
-27
lines changed

inventory_management_system_api/core/exceptions.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,42 @@
22
Module for custom exception classes.
33
"""
44

5+
from typing import Optional
56

6-
class DatabaseError(Exception):
7+
from fastapi import status
8+
9+
10+
class BaseAPIException(Exception):
11+
"""Base exception for API errors."""
12+
13+
# Status code to return if this exception is raised
14+
status_code: int
15+
16+
# Generic detail of the exception (That may be returned in a response)
17+
response_detail: str
18+
19+
detail: str
20+
21+
def __init__(self, detail: str, response_detail: Optional[str] = None, status_code: Optional[int] = None):
22+
"""
23+
Initialise the exception.
24+
25+
:param detail: Specific detail of the exception (just like Exception would take - this will only be logged
26+
and not returned in a response).
27+
:param response_detail: Generic detail of the exception that will be returned in a response.
28+
:param status_code: Status code that will be returned in a response.
29+
"""
30+
super().__init__(detail)
31+
32+
self.detail = detail
33+
34+
if response_detail is not None:
35+
self.response_detail = response_detail
36+
if status_code is not None:
37+
self.status_code = status_code
38+
39+
40+
class DatabaseError(BaseAPIException):
741
"""
842
Database related error.
943
"""
@@ -46,22 +80,29 @@ class MissingMandatoryProperty(Exception):
4680

4781

4882
class DuplicateRecordError(DatabaseError):
49-
"""
50-
The record being added to the database is a duplicate.
51-
"""
83+
"""The record being added to the database is a duplicate."""
84+
85+
status_code = status.HTTP_409_CONFLICT
86+
response_detail = "Duplicate record found"
5287

5388

5489
class InvalidObjectIdError(DatabaseError):
5590
"""
5691
The provided value is not a valid ObjectId.
5792
"""
5893

94+
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
95+
response_detail = "Invalid ID given"
96+
5997

6098
class MissingRecordError(DatabaseError):
6199
"""
62100
A specific database record was requested but could not be found.
63101
"""
64102

103+
status_code = status.HTTP_404_NOT_FOUND
104+
response_detail = "Requested record was not found"
105+
65106

66107
class ChildElementsExistError(DatabaseError):
67108
"""

inventory_management_system_api/main.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from fastapi.responses import JSONResponse
1212

1313
from inventory_management_system_api.core.config import config
14+
from inventory_management_system_api.core.exceptions import BaseAPIException
1415
from inventory_management_system_api.core.logger_setup import setup_logger
1516
from inventory_management_system_api.routers.v1 import (
1617
catalogue_category,
@@ -29,6 +30,22 @@
2930
logger.info("Logging now setup")
3031

3132

33+
@app.exception_handler(BaseAPIException)
34+
async def custom_base_api_exception_handler(_: Request, exc: BaseAPIException) -> JSONResponse:
35+
"""
36+
Custom exception handler for FastAPI to handle `BaseAPIException`'s.
37+
38+
This handler ensures that these exceptions return the appropriate response code and generalised detail
39+
while logging any specific detail.
40+
41+
:param _: Unused.
42+
:param exc: The exception object representing the `BaseAPIException`.
43+
:return: A JSON response with exception details.
44+
"""
45+
logger.exception(exc.detail)
46+
return JSONResponse(content={"detail": exc.response_detail}, status_code=exc.status_code)
47+
48+
3249
@app.exception_handler(Exception)
3350
async def custom_general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
3451
"""

inventory_management_system_api/repositories/system.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010

1111
from inventory_management_system_api.core.custom_object_id import CustomObjectId
1212
from inventory_management_system_api.core.database import DatabaseDep
13-
from inventory_management_system_api.core.exceptions import (
14-
DuplicateRecordError,
15-
InvalidActionError,
16-
MissingRecordError,
17-
)
13+
from inventory_management_system_api.core.exceptions import DuplicateRecordError, InvalidActionError, MissingRecordError
1814
from inventory_management_system_api.models.system import SystemIn, SystemOut
1915
from inventory_management_system_api.repositories import utils
2016
from inventory_management_system_api.schemas.breadcrumbs import BreadcrumbsGetSchema
@@ -53,10 +49,17 @@ def create(self, system: SystemIn, session: Optional[ClientSession] = None) -> S
5349
"""
5450
parent_id = str(system.parent_id) if system.parent_id else None
5551
if parent_id and not self.get(parent_id, session=session):
56-
raise MissingRecordError(f"No parent system found with ID: {parent_id}")
52+
raise MissingRecordError(
53+
f"No parent system found with ID: {parent_id}",
54+
"The specified parent system does not exist",
55+
status_code=422,
56+
)
5757

5858
if self._is_duplicate_system(parent_id, system.code, session=session):
59-
raise DuplicateRecordError("Duplicate system found within the parent system")
59+
raise DuplicateRecordError(
60+
"Duplicate system found within the parent system",
61+
"A system with the same name already exists within the parent system",
62+
)
6063

6164
logger.info("Inserting the new system into the database")
6265
result = self._systems_collection.insert_one(system.model_dump(), session=session)

inventory_management_system_api/routers/v1/system.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logging
77
from typing import Annotated, Optional
88

9-
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Query, status
9+
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status
1010

1111
from inventory_management_system_api.core.config import config
1212
from inventory_management_system_api.core.exceptions import (
@@ -40,17 +40,9 @@ def create_system(system: SystemPostSchema, system_service: SystemServiceDep) ->
4040
# pylint: disable=missing-function-docstring
4141
logger.info("Creating a new system")
4242
logger.debug("System data: %s", system)
43-
try:
44-
system = system_service.create(system)
45-
return SystemSchema(**system.model_dump())
46-
except (MissingRecordError, InvalidObjectIdError) as exc:
47-
message = "The specified parent system does not exist"
48-
logger.exception(message)
49-
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=message) from exc
50-
except DuplicateRecordError as exc:
51-
message = "A system with the same name already exists within the parent system"
52-
logger.exception(message)
53-
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=message) from exc
43+
44+
system = system_service.create(system)
45+
return SystemSchema(**system.model_dump())
5446

5547

5648
@router.get(path="", summary="Get systems", response_description="List of systems")

inventory_management_system_api/services/system.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
from fastapi import Depends
99

1010
from inventory_management_system_api.core.config import config
11-
from inventory_management_system_api.core.exceptions import MissingRecordError, ChildElementsExistError
11+
from inventory_management_system_api.core.custom_object_id import CustomObjectId
12+
from inventory_management_system_api.core.exceptions import (
13+
ChildElementsExistError,
14+
InvalidObjectIdError,
15+
MissingRecordError,
16+
)
1217
from inventory_management_system_api.core.object_storage_api_client import ObjectStorageAPIClient
1318
from inventory_management_system_api.models.system import SystemIn, SystemOut
1419
from inventory_management_system_api.repositories.system import SystemRepo
@@ -42,8 +47,8 @@ def create(self, system: SystemPostSchema) -> SystemOut:
4247
parent_id = system.parent_id
4348

4449
code = utils.generate_code(system.name, "system")
45-
return self._system_repository.create(
46-
SystemIn(
50+
try:
51+
system_in = SystemIn(
4752
parent_id=parent_id,
4853
description=system.description,
4954
name=system.name,
@@ -52,7 +57,11 @@ def create(self, system: SystemPostSchema) -> SystemOut:
5257
importance=system.importance,
5358
code=code,
5459
)
55-
)
60+
except InvalidObjectIdError as exc:
61+
# Provide more specific detail
62+
exc.response_detail = "The specified parent system does not exist"
63+
raise exc
64+
return self._system_repository.create(system_in)
5665

5766
def get(self, system_id: str) -> Optional[SystemOut]:
5867
"""

0 commit comments

Comments
 (0)