Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b125b1b
Refactor error handling for systems create #74
joelvdavies May 23, 2025
f5ee823
Refactor error handling for systems get and list #74
joelvdavies May 23, 2025
c28bb84
Allow CustomObjectId to customise the errors and refactor handling fo…
joelvdavies May 23, 2025
1073aa7
Create and use custom type for parent ids in system to avoid needing …
joelvdavies May 23, 2025
e3dbdd2
Some further tidy up and use for items when system id invalid/not fou…
joelvdavies May 27, 2025
42399c9
Refactor some catalogue category error handling #74
joelvdavies May 27, 2025
f555fcd
Refactor some catalogue category property error handling #74
joelvdavies May 27, 2025
b69e4f6
Refactor units and usage status error handling #74
joelvdavies May 27, 2025
b3f74d4
Refactor breadcrumbs error handling and property value errors #74
joelvdavies May 28, 2025
11c4c21
Refactor manufacturers error handling #74
joelvdavies May 28, 2025
9a47126
Refactor some of catalogue items error handling #74
joelvdavies May 28, 2025
2cefb38
Refactor some of items error handling #74
joelvdavies May 28, 2025
eb11f22
Move system existence check from item repo to service #74
joelvdavies May 28, 2025
b7e7b1d
Refactor remaining error handling found in routers #74
joelvdavies May 28, 2025
a6e69a0
Remove custom field type #74
joelvdavies May 28, 2025
f776aa3
Clean up duplicate error messages and update repo get types #74
joelvdavies May 29, 2025
edd2b50
Simplify missing record errors #74
joelvdavies May 29, 2025
6574659
Clean up repos and exception doc strings #74
joelvdavies May 29, 2025
f6471c8
Improve test coverage #74
joelvdavies May 29, 2025
7e4e79f
Remove unnecessary added line #74
joelvdavies Jun 2, 2025
94c6e23
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jun 2, 2025
bb41ed1
Address some comments #74
joelvdavies Jun 4, 2025
078e182
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jun 11, 2025
b4b0a1a
Minor change from object-storage-api #74
joelvdavies Jun 18, 2025
6fe5ac0
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jun 24, 2025
2c26921
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jun 27, 2025
d683d8f
Merge branch 'develop' into refactor-error-handling-#74
VKTB Jul 7, 2025
acc92b7
Refactor after discussing changes #74
joelvdavies Jul 10, 2025
a5bee7f
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jul 11, 2025
2b32c8b
Merge branch 'develop' into refactor-error-handling-#74
joelvdavies Jul 11, 2025
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
189 changes: 126 additions & 63 deletions inventory_management_system_api/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,114 @@
Module for custom exception classes.
"""

from typing import Optional

class DatabaseError(Exception):
"""
Database related error.
"""
from fastapi import status


class ObjectStorageAPIError(Exception):
"""
Object Storage API related error.
"""
class BaseAPIException(Exception):
"""Base exception for API errors."""

# Status code to return if this exception is raised
status_code: int

class LeafCatalogueCategoryError(Exception):
"""
Catalogue category is attempted to be added to a leaf parent catalogue category.
"""
# Generic detail of the exception (That will be returned in a response if left uncaught)
response_detail: str

# Specific detail of the exception (That will be logged if left uncaught)
detail: str

class NonLeafCatalogueCategoryError(Exception):
"""
Catalogue item is attempted to be added to a non-leaf catalogue category.
"""
def __init__(self, detail: str, response_detail: Optional[str] = None, status_code: Optional[int] = None):
"""
Initialise the exception.

:param detail: Specific detail of the exception (just like Exception would take - this will only be logged
and not returned in a response).
:param response_detail: Generic detail of the exception that will be returned in a response if left uncaught.
:param status_code: Status code that will be returned in a response.
"""
super().__init__(detail)

class DuplicateCatalogueCategoryPropertyNameError(Exception):
"""
Catalogue category is attempted to be created with duplicate property names.
"""
self.detail = detail

if response_detail is not None:
self.response_detail = response_detail
# If there is no response detail defined just use the detail
elif not hasattr(self, "response_detail"):
self.response_detail = detail
if status_code is not None:
self.status_code = status_code

class InvalidPropertyTypeError(Exception):
"""
The type of the provided value does not match the expected type of the property.
"""

class DatabaseError(BaseAPIException):
"""Database related error."""

class MissingMandatoryProperty(Exception):
"""
A mandatory property is missing when a catalogue item or item is attempted to be created.
"""

class ObjectStorageAPIError(BaseAPIException):
"""Object Storage API related error."""


class LeafCatalogueCategoryError(BaseAPIException):
"""Catalogue category is attempted to be added to a leaf parent catalogue category."""

status_code = status.HTTP_409_CONFLICT
response_detail = "Adding a catalogue category to a leaf parent catalogue category is not allowed"


class NonLeafCatalogueCategoryError(BaseAPIException):
"""Catalogue item is attempted to be added to a non-leaf catalogue category."""

status_code = status.HTTP_409_CONFLICT


class DuplicateCatalogueCategoryPropertyNameError(BaseAPIException):
"""Catalogue category is attempted to be created with duplicate property names."""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY


class InvalidPropertyTypeError(BaseAPIException):
"""The type of the provided value does not match the expected type of the property."""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY


class MissingMandatoryProperty(BaseAPIException):
"""A mandatory property is missing when a catalogue item or item is attempted to be created."""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY


class DuplicateRecordError(DatabaseError):
"""
The record being added to the database is a duplicate.
"""
"""The record being added to the database is a duplicate."""

status_code = status.HTTP_409_CONFLICT
response_detail = "Duplicate record found"


class InvalidObjectIdError(DatabaseError):
"""
The provided value is not a valid ObjectId.
"""
"""The provided value is not a valid ObjectId."""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
response_detail = "Invalid ID given"


class MissingRecordError(DatabaseError):
"""
A specific database record was requested but could not be found.
"""
"""A specific database record was requested but could not be found."""

status_code = status.HTTP_404_NOT_FOUND

def __init__(self, entity_id: str, entity_type: str):
"""
Initialise the exception.

:param entity_id: ID of the record that was found to be missing.
:param entity_type: Name of the entity type e.g. catalogue categories/systems (Used for logging).
"""
super().__init__(
detail=f"No {entity_type} found with ID: {entity_id}",
response_detail=f"{entity_type.capitalize()} not found",
)


class ChildElementsExistError(DatabaseError):
Expand All @@ -69,56 +118,70 @@ class ChildElementsExistError(DatabaseError):
elements.
"""

status_code = status.HTTP_409_CONFLICT


class ReplacementForObsoleteCatalogueItemError(DatabaseError):
"""
Exception raised when attempting to delete a catalogue item that is the replacement for an obsolete catalogue item.
"""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
response_detail = "Catalogue item is the replacement for an obsolete catalogue item and cannot be deleted"


class PartOfCatalogueItemError(DatabaseError):
"""
Exception raised when attempting to delete a manufacturer that is a part of a catalogue item
"""
"""Exception raised when attempting to delete a manufacturer that is a part of a catalogue item"""

status_code = status.HTTP_409_CONFLICT

class PartOfCatalogueCategoryError(DatabaseError):
"""
Exception raised when attempting to delete a unit that is a part of a catalogue category
"""

class PartOfCatalogueCategoryError(BaseAPIException):
"""Exception raised when attempting to delete a unit that is a part of a catalogue category"""

status_code = status.HTTP_409_CONFLICT


class PartOfItemError(DatabaseError):
"""
Exception raised when attempting to delete a usage status that is a part of an item
"""
"""Exception raised when attempting to delete a usage status that is a part of an item"""

status_code = status.HTTP_409_CONFLICT


class DatabaseIntegrityError(DatabaseError):
"""
Exception raised when something is found in the database that shouldn't have been
"""
"""Exception raised when something is found in the database that shouldn't have been"""

status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
response_detail = "Database integrity error"

class InvalidActionError(DatabaseError):
"""
Exception raised when trying to update an item's catalogue item ID
"""

class InvalidActionError(BaseAPIException):
"""Exception raised when trying to update an item's catalogue item ID"""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY


class WriteConflictError(DatabaseError):
"""
Exception raised when a transaction has a write conflict.
"""
"""Exception raised when a transaction has a write conflict."""

status_code = status.HTTP_409_CONFLICT


class ObjectStorageAPIAuthError(ObjectStorageAPIError):
"""
Exception raised for auth failures or expired tokens while communicating with the Object Storage API.
"""
"""Exception raised for auth failures or expired tokens while communicating with the Object Storage API."""

status_code = status.HTTP_403_FORBIDDEN
response_detail = "Unable to delete attachments and/or images"


class ObjectStorageAPIServerError(ObjectStorageAPIError):
"""
Exception raised when server errors occur while communicating with the Object Storage API.
"""
"""Exception raised when server errors occur while communicating with the Object Storage API."""

status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
response_detail = "Unable to delete attachments and/or images"


class PropertyValueError(BaseAPIException, ValueError):
"""Exception raised when there is an error caused by a property value"""

status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
17 changes: 17 additions & 0 deletions inventory_management_system_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi.responses import JSONResponse

from inventory_management_system_api.core.config import config
from inventory_management_system_api.core.exceptions import BaseAPIException
from inventory_management_system_api.core.logger_setup import setup_logger
from inventory_management_system_api.routers.v1 import (
catalogue_category,
Expand All @@ -30,6 +31,22 @@
logger.info("Logging now setup")


@app.exception_handler(BaseAPIException)
async def custom_base_api_exception_handler(_: Request, exc: BaseAPIException) -> JSONResponse:
"""
Custom exception handler for FastAPI to handle `BaseAPIException`'s.

This handler ensures that these exceptions return the appropriate response code and generalised detail
while logging any specific detail.

:param _: Unused.
:param exc: The exception object representing the `BaseAPIException`.
:return: A JSON response with exception details.
"""
logger.exception(exc.detail)
return JSONResponse(content={"detail": exc.response_detail}, status_code=exc.status_code)


@app.exception_handler(Exception)
async def custom_general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
"""
Expand Down
Loading
Loading