diff --git a/main.py b/main.py index 2fceaf3..37e6274 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/modules/fulfillment_base/repositories/deal_product.py b/modules/fulfillment_base/repositories/deal_product.py index e993f35..8a237a8 100644 --- a/modules/fulfillment_base/repositories/deal_product.py +++ b/modules/fulfillment_base/repositories/deal_product.py @@ -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()) diff --git a/modules/fulfillment_base/repositories/deal_service.py b/modules/fulfillment_base/repositories/deal_service.py index e965cfa..601d30f 100644 --- a/modules/fulfillment_base/repositories/deal_service.py +++ b/modules/fulfillment_base/repositories/deal_service.py @@ -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()) diff --git a/modules/fulfillment_base/repositories/product.py b/modules/fulfillment_base/repositories/product.py index 21d1bdc..b564e7d 100644 --- a/modules/fulfillment_base/repositories/product.py +++ b/modules/fulfillment_base/repositories/product.py @@ -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] diff --git a/modules/fulfillment_base/repositories/product_service.py b/modules/fulfillment_base/repositories/product_service.py index a00554c..4a0cae0 100644 --- a/modules/fulfillment_base/repositories/product_service.py +++ b/modules/fulfillment_base/repositories/product_service.py @@ -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()) diff --git a/modules/fulfillment_base/repositories/service.py b/modules/fulfillment_base/repositories/service.py index c88284b..8cee055 100644 --- a/modules/fulfillment_base/repositories/service.py +++ b/modules/fulfillment_base/repositories/service.py @@ -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) diff --git a/modules/fulfillment_base/repositories/services_kit.py b/modules/fulfillment_base/repositories/services_kit.py index 893fbb8..d2deca3 100644 --- a/modules/fulfillment_base/repositories/services_kit.py +++ b/modules/fulfillment_base/repositories/services_kit.py @@ -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)) diff --git a/modules/fulfillment_base/services/deal_product.py b/modules/fulfillment_base/services/deal_product.py index 21e44cb..9c73bee 100644 --- a/modules/fulfillment_base/services/deal_product.py +++ b/modules/fulfillment_base/services/deal_product.py @@ -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( diff --git a/modules/fulfillment_base/services/deal_service.py b/modules/fulfillment_base/services/deal_service.py index b53a28e..5744bef 100644 --- a/modules/fulfillment_base/services/deal_service.py +++ b/modules/fulfillment_base/services/deal_service.py @@ -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="Услуга удалена из сделки") diff --git a/modules/fulfillment_base/services/product.py b/modules/fulfillment_base/services/product.py index 5944691..7c06690 100644 --- a/modules/fulfillment_base/services/product.py +++ b/modules/fulfillment_base/services/product.py @@ -15,7 +15,6 @@ class ProductService( ServiceDeleteMixin[Product], ): schema_class = ProductSchema - entity_not_found_msg = "Товар не найден" entity_deleted_msg = "Товар успешно удален" entity_updated_msg = "Товар успешно обновлен" entity_created_msg = "Товар успешно создан" diff --git a/modules/fulfillment_base/services/product_service.py b/modules/fulfillment_base/services/product_service.py index c7572c5..f50cb66 100644 --- a/modules/fulfillment_base/services/product_service.py +++ b/modules/fulfillment_base/services/product_service.py @@ -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) diff --git a/modules/fulfillment_base/services/service.py b/modules/fulfillment_base/services/service.py index e988a83..ea4c3c2 100644 --- a/modules/fulfillment_base/services/service.py +++ b/modules/fulfillment_base/services/service.py @@ -15,7 +15,6 @@ class ServiceModelService( ServiceDeleteMixin[Service], ): schema_class = ServiceSchema - entity_not_found_msg = "Услуга не найдена" entity_deleted_msg = "Услуга успешно удалена" entity_updated_msg = "Услуга успешно обновлена" entity_created_msg = "Услуга успешно создана" diff --git a/modules/fulfillment_base/services/services_kit.py b/modules/fulfillment_base/services/services_kit.py index 4b4f7c2..70405e0 100644 --- a/modules/fulfillment_base/services/services_kit.py +++ b/modules/fulfillment_base/services/services_kit.py @@ -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 = "Набор услуг успешно создан" diff --git a/repositories/board.py b/repositories/board.py index 71c07e1..9225fc3 100644 --- a/repositories/board.py +++ b/repositories/board.py @@ -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] diff --git a/repositories/deal.py b/repositories/deal.py index b4678e9..27b6885 100644 --- a/repositories/deal.py +++ b/repositories/deal.py @@ -15,6 +15,7 @@ class DealRepository( RepGetByIdMixin[Deal], ): entity_class = Deal + entity_not_found_msg = "Сделка не найдена" async def get_all( self, diff --git a/repositories/mixins.py b/repositories/mixins.py index a42bcb8..122002a 100644 --- a/repositories/mixins.py +++ b/repositories/mixins.py @@ -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]): diff --git a/repositories/project.py b/repositories/project.py index 87613ad..1d2a0e1 100644 --- a/repositories/project.py +++ b/repositories/project.py @@ -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) diff --git a/repositories/status.py b/repositories/status.py index 1199b5e..539f863 100644 --- a/repositories/status.py +++ b/repositories/status.py @@ -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] diff --git a/services/board.py b/services/board.py index 3f0a5a9..494fac7 100644 --- a/services/board.py +++ b/services/board.py @@ -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 = "Доска успешно создана" diff --git a/services/deal.py b/services/deal.py index 79e6287..b70f476 100644 --- a/services/deal.py +++ b/services/deal.py @@ -13,7 +13,6 @@ class DealService( ServiceDeleteMixin[Deal], ): schema_class = DealSchema - entity_not_found_msg = "Сделка не найдена" entity_deleted_msg = "Сделка успешно удалена" entity_updated_msg = "Сделка успешно обновлена" entity_created_msg = "Сделка успешно создана" diff --git a/services/mixins.py b/services/mixins.py index 832a6d8..f3249cc 100644 --- a/services/mixins.py +++ b/services/mixins.py @@ -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) diff --git a/services/project.py b/services/project.py index 5d95a52..4542238 100644 --- a/services/project.py +++ b/services/project.py @@ -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 = "Проект успешно создан" diff --git a/services/status.py b/services/status.py index db4c5b3..8b9a95b 100644 --- a/services/status.py +++ b/services/status.py @@ -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 = "Статус успешно создан" diff --git a/utils/exceptions.py b/utils/exceptions.py index f24fd1d..0607751 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -1,2 +1,3 @@ class ObjectNotFoundException(Exception): - pass + def __init__(self, name: str): + self.name = name