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.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from starlette.responses import JSONResponse
import modules import modules
import routers import routers
from utils.auto_include_routers import auto_include_routers from utils.auto_include_routers import auto_include_routers
from utils.exceptions import ObjectNotFoundException
origins = ["http://localhost:3000"] origins = ["http://localhost:3000"]
@ -28,6 +30,12 @@ app.add_middleware(
minimum_size=1_000, 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, routers)
auto_include_routers(app, modules, True) 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.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealProductRepository( class DealProductRepository(
@ -34,7 +35,9 @@ class DealProductRepository(
.order_by(DealProduct.product_id) .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 = ( stmt = (
select(DealProduct) select(DealProduct)
.options( .options(
@ -45,8 +48,10 @@ class DealProductRepository(
) )
.where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id) .where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id)
) )
result = await self.session.execute(stmt) result = (await self.session.execute(stmt)).scalar_one_or_none()
return result.scalar_one_or_none() if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с товаром не найдена")
return result
async def create(self, data: CreateDealProductSchema): async def create(self, data: CreateDealProductSchema):
deal_product = DealProduct(**data.model_dump()) 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.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealServiceRepository( class DealServiceRepository(
@ -29,14 +30,18 @@ class DealServiceRepository(
.order_by(DealService.service_id) .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 = ( stmt = (
select(DealService) select(DealService)
.options(joinedload(DealService.service)) .options(joinedload(DealService.service))
.where(DealService.deal_id == deal_id, DealService.service_id == service_id) .where(DealService.deal_id == deal_id, DealService.service_id == service_id)
) )
result = await self.session.execute(stmt) result = (await self.session.execute(stmt)).scalar_one_or_none()
return result.scalar_one_or_none() if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с услугой не найдена")
return result
async def create(self, data: CreateDealServiceSchema): async def create(self, data: CreateDealServiceSchema):
deal_service = DealService(**data.model_dump()) deal_service = DealService(**data.model_dump())

View File

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

View File

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

View File

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

View File

@ -53,14 +53,8 @@ class DealProductService(ServiceGetAllMixin[DealProduct, DealProductSchema]):
) -> DealProductAddKitResponse: ) -> DealProductAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session) services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id) 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( deal_product = await self.repository.get_by_id(request.deal_id, request.product_id)
request.deal_id, request.product_id
)
if not deal_product:
raise HTTPException(status_code=404, detail=self.entity_not_found_msg)
product_service_repo = ProductServiceRepository(self.repository.session) product_service_repo = ProductServiceRepository(self.repository.session)
await product_service_repo.delete_product_services( await product_service_repo.delete_product_services(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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