refactor: entity not found exceptions handler

This commit is contained in:
2025-09-16 16:56:10 +04:00
parent 276626d6f7
commit 98d3026e0d
24 changed files with 56 additions and 49 deletions

10
main.py
View File

@ -1,12 +1,14 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import ORJSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.responses import JSONResponse
import modules
import routers
from utils.auto_include_routers import auto_include_routers
from utils.exceptions import ObjectNotFoundException
origins = ["http://localhost:3000"]
@ -28,6 +30,12 @@ app.add_middleware(
minimum_size=1_000,
)
@app.exception_handler(ObjectNotFoundException)
async def unicorn_exception_handler(request: Request, exc: ObjectNotFoundException):
return JSONResponse(status_code=404, content={"detail": exc.name})
auto_include_routers(app, routers)
auto_include_routers(app, modules, True)

View File

@ -12,6 +12,7 @@ from modules.fulfillment_base.schemas.deal_product import (
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealProductRepository(
@ -34,7 +35,9 @@ class DealProductRepository(
.order_by(DealProduct.product_id)
)
async def get_by_id(self, deal_id: int, product_id: int) -> Optional[DealProduct]:
async def get_by_id(
self, deal_id: int, product_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[DealProduct]:
stmt = (
select(DealProduct)
.options(
@ -45,8 +48,10 @@ class DealProductRepository(
)
.where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с товаром не найдена")
return result
async def create(self, data: CreateDealProductSchema):
deal_product = DealProduct(**data.model_dump())

View File

@ -10,6 +10,7 @@ from modules.fulfillment_base.schemas.deal_service import (
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealServiceRepository(
@ -29,14 +30,18 @@ class DealServiceRepository(
.order_by(DealService.service_id)
)
async def get_by_id(self, deal_id: int, service_id: int) -> Optional[DealService]:
async def get_by_id(
self, deal_id: int, service_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[DealService]:
stmt = (
select(DealService)
.options(joinedload(DealService.service))
.where(DealService.deal_id == deal_id, DealService.service_id == service_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с услугой не найдена")
return result
async def create(self, data: CreateDealServiceSchema):
deal_service = DealService(**data.model_dump())

View File

@ -16,6 +16,7 @@ class ProductRepository(
RepGetByIdMixin[Product],
):
entity_class = Product
entity_not_found_msg = "Товар не найден"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
search_input = args[0]

View File

@ -8,6 +8,7 @@ from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.schemas.product_service import *
from repositories.base import BaseRepository
from repositories.mixins import RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class ProductServiceRepository(
@ -15,10 +16,15 @@ class ProductServiceRepository(
RepUpdateMixin[DealProductService, UpdateProductServiceSchema],
):
entity_class = DealProductService
entity_not_found_msg = "Связь услуги с товаром не найдена"
session: AsyncSession
async def get_by_id(
self, deal_id: int, product_id: int, service_id: int
self,
deal_id: int,
product_id: int,
service_id: int,
raise_if_not_found: Optional[bool] = True,
) -> Optional[DealProductService]:
stmt = (
select(DealProductService)
@ -31,8 +37,10 @@ class ProductServiceRepository(
DealProductService.service_id == service_id,
)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь услуги с товаром не найдена")
return result
async def create(self, data: CreateProductServiceSchema):
deal_product_service = DealProductService(**data.model_dump())

View File

@ -15,6 +15,7 @@ class ServiceRepository(
RepGetByIdMixin[Service],
):
entity_class = Service
entity_not_found_msg = "Услуга не найдена"
async def update(self, service: Service, data: UpdateServiceSchema) -> Service:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -12,6 +12,7 @@ class ServicesKitRepository(
RepCrudMixin[ServicesKit, CreateServicesKitSchema, UpdateServicesKitSchema],
):
entity_class = ServicesKit
entity_not_found_msg = "Набор услуг не найден"
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(ServicesKit.services))

View File

@ -53,14 +53,8 @@ class DealProductService(ServiceGetAllMixin[DealProduct, DealProductSchema]):
) -> DealProductAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id)
if not services_kit:
raise HTTPException(status_code=404, detail="Набор услуг не найден")
deal_product = await self.repository.get_by_id(
request.deal_id, request.product_id
)
if not deal_product:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
deal_product = await self.repository.get_by_id(request.deal_id, request.product_id)
product_service_repo = ProductServiceRepository(self.repository.session)
await product_service_repo.delete_product_services(

View File

@ -1,4 +1,3 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealService
@ -9,7 +8,6 @@ from services.mixins import ServiceGetAllMixin
class DealServiceService(ServiceGetAllMixin[DealService, DealServiceSchema]):
schema_class = DealServiceSchema
entity_not_found_msg = "Связь услуги со сделкой не найдена"
def __init__(self, session: AsyncSession):
self.repository = DealServiceRepository(session)
@ -30,16 +28,10 @@ class DealServiceService(ServiceGetAllMixin[DealService, DealServiceSchema]):
self, deal_id: int, service_id: int, data: UpdateDealServiceRequest
) -> UpdateDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, data.entity)
return UpdateDealServiceResponse(message="Услуга сделки обновлена")
async def delete(self, deal_id: int, service_id: int) -> DeleteDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.delete(entity)
return DeleteDealServiceResponse(message="Услуга удалена из сделки")

View File

@ -15,7 +15,6 @@ class ProductService(
ServiceDeleteMixin[Product],
):
schema_class = ProductSchema
entity_not_found_msg = "Товар не найден"
entity_deleted_msg = "Товар успешно удален"
entity_updated_msg = "Товар успешно обновлен"
entity_created_msg = "Товар успешно создан"

View File

@ -8,7 +8,6 @@ from modules.fulfillment_base.schemas.product_service import *
class ProductServiceService:
schema_class = ProductServiceSchema
entity_not_found_msg = "Связь услуги с товаром не найдена"
def __init__(self, session: AsyncSession):
self.repository = ProductServiceRepository(session)

View File

@ -15,7 +15,6 @@ class ServiceModelService(
ServiceDeleteMixin[Service],
):
schema_class = ServiceSchema
entity_not_found_msg = "Услуга не найдена"
entity_deleted_msg = "Услуга успешно удалена"
entity_updated_msg = "Услуга успешно обновлена"
entity_created_msg = "Услуга успешно создана"

View File

@ -12,7 +12,6 @@ class ServicesKitService(
ServiceCrudMixin[ServicesKit, ServicesKitSchema, CreateServicesKitRequest, UpdateServicesKitRequest]
):
schema_class = ServicesKitSchema
entity_not_found_msg = "Набор услуг не найден"
entity_deleted_msg = "Набор услуг успешно удален"
entity_updated_msg = "Набор услуг успешно обновлен"
entity_created_msg = "Набор услуг успешно создан"

View File

@ -7,6 +7,7 @@ from schemas.board import UpdateBoardSchema, CreateBoardSchema
class BoardRepository(RepCrudMixin[Board, CreateBoardSchema, UpdateBoardSchema]):
entity_class = Board
entity_not_found_msg = "Доска не найдена"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
project_id = args[0]

View File

@ -15,6 +15,7 @@ class DealRepository(
RepGetByIdMixin[Deal],
):
entity_class = Deal
entity_not_found_msg = "Сделка не найдена"
async def get_all(
self,

View File

@ -5,6 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from repositories.base import BaseRepository
from schemas.base import BaseSchema
from utils.exceptions import ObjectNotFoundException
EntityType = TypeVar("EntityType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseSchema)
@ -70,18 +71,24 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
class RepGetByIdMixin(Generic[EntityType], RepBaseMixin[EntityType]):
entity_class: Type[EntityType]
entity_not_found_msg = "Entity not found"
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt
async def get_by_id(self, item_id: int) -> Optional[EntityType]:
async def get_by_id(
self, item_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[EntityType]:
stmt = select(self.entity_class).where(self.entity_class.id == item_id)
if hasattr(self, "is_deleted"):
stmt = stmt.where(self.entity_class.is_deleted.is_(False))
stmt = self._process_get_by_id_stmt(stmt)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException(self.entity_not_found_msg)
return result
class RepGetAllMixin(Generic[EntityType], RepBaseMixin[EntityType]):

View File

@ -10,6 +10,7 @@ class ProjectRepository(
RepCrudMixin[Project, CreateProjectSchema, UpdateProjectSchema]
):
entity_class = Project
entity_not_found_msg = "Проект не найден"
def _process_get_all_stmt(self, stmt: Select) -> Select:
return stmt.order_by(Project.id)

View File

@ -7,6 +7,7 @@ from schemas.status import UpdateStatusSchema, CreateStatusSchema
class StatusRepository(RepCrudMixin[Status, CreateStatusSchema, UpdateStatusSchema]):
entity_class = Status
entity_not_found_msg = "Статус не найден"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
board_id = args[0]

View File

@ -8,7 +8,6 @@ class BoardService(
ServiceCrudMixin[Board, BoardSchema, CreateBoardSchema, UpdateBoardSchema]
):
schema_class = BoardSchema
entity_not_found_msg = "Доска не найдена"
entity_deleted_msg = "Доска успешно удалена"
entity_updated_msg = "Доска успешно обновлена"
entity_created_msg = "Доска успешно создана"

View File

@ -13,7 +13,6 @@ class DealService(
ServiceDeleteMixin[Deal],
):
schema_class = DealSchema
entity_not_found_msg = "Сделка не найдена"
entity_deleted_msg = "Сделка успешно удалена"
entity_updated_msg = "Сделка успешно обновлена"
entity_created_msg = "Сделка успешно создана"

View File

@ -1,7 +1,3 @@
from typing import Generic
from fastapi import HTTPException
from repositories.mixins import *
from schemas.base_crud import *
@ -50,15 +46,12 @@ class ServiceUpdateMixin(
Generic[EntityType, UpdateRequestType],
ServiceBaseMixin[RepUpdateMixin | RepGetByIdMixin],
):
entity_not_found_msg = "Entity not found"
entity_updated_msg = "Entity updated"
async def update(
self, entity_id: int, request: UpdateRequestType
) -> BaseUpdateResponse:
entity = await self.repository.get_by_id(entity_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
await self.repository.update(entity, request.entity)
return BaseUpdateResponse(message=self.entity_updated_msg)
@ -67,7 +60,6 @@ class ServiceDeleteMixin(
Generic[EntityType],
ServiceBaseMixin[RepDeleteMixin | RepGetByIdMixin],
):
entity_not_found_msg = "Entity not found"
entity_deleted_msg = "Entity deleted"
async def is_soft_delete(self, entity: EntityType) -> bool:
@ -75,11 +67,7 @@ class ServiceDeleteMixin(
async def delete(self, entity_id: int) -> BaseDeleteResponse:
entity = await self.repository.get_by_id(entity_id)
if not entity:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
is_soft = await self.is_soft_delete(entity)
await self.repository.delete(entity, is_soft)
return BaseDeleteResponse(message=self.entity_deleted_msg)

View File

@ -8,7 +8,6 @@ class ProjectService(
ServiceCrudMixin[Project, ProjectSchema, CreateProjectSchema, UpdateProjectSchema]
):
schema_class = ProjectSchema
entity_not_found_msg = "Проект не найден"
entity_deleted_msg = "Проект успешно удален"
entity_updated_msg = "Проект успешно обновлен"
entity_created_msg = "Проект успешно создан"

View File

@ -8,7 +8,6 @@ class StatusService(
ServiceCrudMixin[Status, StatusSchema, CreateStatusRequest, UpdateStatusRequest]
):
schema_class = StatusSchema
entity_not_found_msg = "Статус не найден"
entity_deleted_msg = "Статус успешно удален"
entity_updated_msg = "Статус успешно обновлен"
entity_created_msg = "Статус успешно создан"

View File

@ -1,2 +1,3 @@
class ObjectNotFoundException(Exception):
pass
def __init__(self, name: str):
self.name = name