From b33edad17bc0b03d0fda6d54ee8206f42ec74d3f Mon Sep 17 00:00:00 2001 From: riroan Date: Sun, 25 Aug 2024 03:26:23 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=EB=B2=84=EC=8A=A4?= =?UTF-8?q?=20=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- command/__init__.py | 2 ++ command/cart.py | 20 ++++++++++++++ command/like.py | 15 ++++++++++ command/product.py | 11 ++++++++ depends.py => depends/__init__.py | 8 ++++++ depends/cart.py | 22 +++++++++++++++ depends/like.py | 16 +++++++++++ depends/product.py | 11 ++++++++ messagebus.py | 41 +++++++++++++++++++++++++++ presentation/cart.py | 46 +++++++++++++++---------------- presentation/like.py | 28 ++++++++----------- presentation/product.py | 18 ++++++------ service/cart.py | 43 ++++++++++++++--------------- service/like.py | 28 +++++++++---------- service/product.py | 13 +++++---- tests/e2e/test_cart.py | 16 +++++------ 16 files changed, 237 insertions(+), 101 deletions(-) create mode 100644 command/__init__.py create mode 100644 command/cart.py create mode 100644 command/like.py create mode 100644 command/product.py rename depends.py => depends/__init__.py (75%) create mode 100644 depends/cart.py create mode 100644 depends/like.py create mode 100644 depends/product.py create mode 100644 messagebus.py diff --git a/command/__init__.py b/command/__init__.py new file mode 100644 index 0000000..9eaa1e2 --- /dev/null +++ b/command/__init__.py @@ -0,0 +1,2 @@ +class Command: + pass diff --git a/command/cart.py b/command/cart.py new file mode 100644 index 0000000..bf6bf07 --- /dev/null +++ b/command/cart.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass + +from command import Command + + +@dataclass(frozen=True) +class AddCartCommand(Command): + user_id: int + product_id: int + + +@dataclass(frozen=True) +class UpdateCartCommand(Command): + cart_id: int + count: int + + +@dataclass(frozen=True) +class DeleteCartCommand(Command): + cart_id: int diff --git a/command/like.py b/command/like.py new file mode 100644 index 0000000..fc857e6 --- /dev/null +++ b/command/like.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from command import Command + + +@dataclass(frozen=True) +class LikeCommand(Command): + user_id: int + product_id: int + + +@dataclass(frozen=True) +class DislikeCommand(Command): + user_id: int + product_id: int diff --git a/command/product.py b/command/product.py new file mode 100644 index 0000000..d0aec55 --- /dev/null +++ b/command/product.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from command import Command + + +@dataclass(frozen=True) +class AddProductCommand(Command): + name: str + image_path: str + price: int + summary: str | None diff --git a/depends.py b/depends/__init__.py similarity index 75% rename from depends.py rename to depends/__init__.py index c755728..0a52fe1 100644 --- a/depends.py +++ b/depends/__init__.py @@ -1,8 +1,10 @@ from typing import Annotated from fastapi import Depends, Request +from sqlalchemy.orm.session import Session from db import Database +from messagebus import MessageBus from settings import Settings @@ -26,3 +28,9 @@ async def get_session( except Exception as e: session.rollback() raise e + + +async def get_messagebus( + session: Annotated[Session, Depends(get_session)] +): + return MessageBus(session) diff --git a/depends/cart.py b/depends/cart.py new file mode 100644 index 0000000..50e6484 --- /dev/null +++ b/depends/cart.py @@ -0,0 +1,22 @@ +from command.cart import AddCartCommand, DeleteCartCommand, UpdateCartCommand +from dto.cart import CartDto, CartUpdateDto + + +async def add_cart_command(cart: CartDto): + return AddCartCommand( + user_id=cart.user_id, + product_id=cart.product_id + ) + + +async def update_cart_command(cart: CartUpdateDto, cart_id: int): + return UpdateCartCommand( + cart_id=cart_id, + count=cart.count + ) + + +async def delete_cart_command(cart_id: int): + return DeleteCartCommand( + cart_id=cart_id + ) diff --git a/depends/like.py b/depends/like.py new file mode 100644 index 0000000..7abb5ee --- /dev/null +++ b/depends/like.py @@ -0,0 +1,16 @@ +from command.like import DislikeCommand, LikeCommand +from dto.like import LikeDto + + +async def like_command(like_data: LikeDto): + return LikeCommand( + user_id=like_data.user_id, + product_id=like_data.product_id + ) + + +async def dislike_command(like_data: LikeDto): + return DislikeCommand( + user_id=like_data.user_id, + product_id=like_data.product_id + ) diff --git a/depends/product.py b/depends/product.py new file mode 100644 index 0000000..0653823 --- /dev/null +++ b/depends/product.py @@ -0,0 +1,11 @@ +from command.product import AddProductCommand +from dto.product import ProductDto + + +async def add_product_command(product: ProductDto): + return AddProductCommand( + name=product.name, + image_path=product.image_path, + price=product.price, + summary=product.summary + ) diff --git a/messagebus.py b/messagebus.py new file mode 100644 index 0000000..f8ec1a3 --- /dev/null +++ b/messagebus.py @@ -0,0 +1,41 @@ +from collections import deque + +from sqlalchemy.orm.session import Session + +from command import Command +from command.cart import AddCartCommand, DeleteCartCommand, UpdateCartCommand +from command.like import DislikeCommand, LikeCommand +from command.product import AddProductCommand +from service.cart import add_cart, delete_cart, update_cart +from service.like import dislike, like +from service.product import add_product + +Message = Command + + +class MessageBus: + def __init__(self, session: Session): + self.session = session + self.message_queue = None + self.command_handlers = COMMAND_HANDLERS + + async def handle(self, message: Message): + self.message_queue = deque([message]) + while self.message_queue: + target = self.message_queue.popleft() + ret = await self.handle_command(target) + return ret + + async def handle_command(self, command: Command): + handler = self.command_handlers[type(command)] + return await handler(command, self.session) + + +COMMAND_HANDLERS = { + AddCartCommand: add_cart, + UpdateCartCommand: update_cart, + DeleteCartCommand: delete_cart, + LikeCommand: like, + DislikeCommand: dislike, + AddProductCommand: add_product, +} diff --git a/presentation/cart.py b/presentation/cart.py index d48338d..eee0a18 100644 --- a/presentation/cart.py +++ b/presentation/cart.py @@ -4,19 +4,23 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.orm.session import Session -from depends import get_session -from dto.cart import CartDto, CartResponseDto, CartResponseModel, CartUpdateDto +from command.cart import AddCartCommand, DeleteCartCommand, UpdateCartCommand +from depends import get_messagebus, get_session +from depends.cart import (add_cart_command, delete_cart_command, + update_cart_command) +from dto.cart import CartResponseDto, CartResponseModel +from messagebus import MessageBus from service import cart as cart_service api = APIRouter() @api.get("/{user_id}", status_code=status.HTTP_200_OK) -async def list_shoppingcart( +async def list_cart( user_id: int, session: Annotated[Session, Depends(get_session)] ) -> CartResponseModel: - cart_products = await cart_service.list_shoppingcart( + cart_products = await cart_service.list_cart( user_id, session ) @@ -30,30 +34,24 @@ async def list_shoppingcart( @api.post("", status_code=status.HTTP_201_CREATED) -async def add_shoppingcart( - cart: CartDto, - session: Annotated[Session, Depends(get_session)] -) -> CartDto: - await cart_service.add_shoppingcart(cart, session) - return cart +async def add_cart( + command: Annotated[AddCartCommand, Depends(add_cart_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] +): + return await messagebus.handle(command) @api.put("/{cart_id}", status_code=status.HTTP_202_ACCEPTED) -async def update_shoppingcart( - cart_id: int, - cart: CartUpdateDto, - session: Annotated[Session, Depends(get_session)] -) -> CartUpdateDto: - await cart_service.update_shoppingcart(cart_id, cart, session) - - return cart +async def update_cart( + command: Annotated[UpdateCartCommand, Depends(update_cart_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] +): + return await messagebus.handle(command) @api.delete("/{cart_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_shoppingcart( - cart_id: int, - session: Annotated[Session, Depends(get_session)] +async def delete_cart( + command: Annotated[DeleteCartCommand, Depends(delete_cart_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] ) -> None: - await cart_service.delete_shoppingcart( - cart_id, session - ) + await messagebus.handle(command) diff --git a/presentation/like.py b/presentation/like.py index a3674d1..b8c1208 100644 --- a/presentation/like.py +++ b/presentation/like.py @@ -1,30 +1,26 @@ from typing import Annotated from fastapi import APIRouter, Depends, status -from sqlalchemy.orm.session import Session -from depends import get_session -from dto.like import LikeDto -from service import like as like_service +from command.like import DislikeCommand, LikeCommand +from depends import get_messagebus +from depends.like import dislike_command, like_command +from messagebus import MessageBus api = APIRouter() @api.post("/like", status_code=status.HTTP_201_CREATED) async def like( - like_data: LikeDto, - session: Annotated[Session, Depends(get_session)] -) -> LikeDto: - await like_service.like(like_data, session) - - return like_data + command: Annotated[LikeCommand, Depends(like_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] +): + return await messagebus.handle(command) @api.post("/dislike", status_code=status.HTTP_201_CREATED) async def dislike( - like_data: LikeDto, - session: Annotated[Session, Depends(get_session)] -) -> LikeDto: - await like_service.dislike(like_data, session) - - return like_data + command: Annotated[DislikeCommand, Depends(dislike_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] +): + return await messagebus.handle(command) diff --git a/presentation/product.py b/presentation/product.py index 5dc29eb..dc48d69 100644 --- a/presentation/product.py +++ b/presentation/product.py @@ -3,8 +3,11 @@ from fastapi import APIRouter, Depends, status from sqlalchemy.orm.session import Session -from depends import get_session -from dto.product import ProductDto, ProductResponseDto, ProductResponseModel +from command.product import AddProductCommand +from depends import get_messagebus, get_session +from depends.product import add_product_command +from dto.product import ProductResponseDto, ProductResponseModel +from messagebus import MessageBus from service import product as product_service api = APIRouter() @@ -37,10 +40,7 @@ async def list_product( @api.post("", status_code=status.HTTP_201_CREATED) async def add_product( - product: ProductDto, - session: Annotated[Session, Depends(get_session)] -) -> ProductDto: - await product_service.add_product( - product, session - ) - return product + command: Annotated[AddProductCommand, Depends(add_product_command)], + messagebus: Annotated[MessageBus, Depends(get_messagebus)] +): + return await messagebus.handle(command) diff --git a/service/cart.py b/service/cart.py index 822538c..fdaf0de 100644 --- a/service/cart.py +++ b/service/cart.py @@ -1,12 +1,12 @@ from sqlalchemy.orm.session import Session -from dto.cart import CartDto, CartUpdateDto +from command.cart import AddCartCommand, DeleteCartCommand, UpdateCartCommand from exception import BadRequestException, NotFoundException from orm.cart import Cart from repository.cart import CartRepository -async def list_shoppingcart( +async def list_cart( user_id: int, session: Session ): @@ -16,55 +16,52 @@ async def list_shoppingcart( return cart_products -async def add_shoppingcart( - cart: CartDto, +async def add_cart( + command: AddCartCommand, session: Session ): repository = CartRepository(session) obj = repository.get_by_user_id_and_product_id( - user_id=cart.user_id, - product_id=cart.product_id + user_id=command.user_id, + product_id=command.product_id ) if obj is None: obj = Cart( - user_id=cart.user_id, - product_id=cart.product_id, + user_id=command.user_id, + product_id=command.product_id, count=1 ) repository.add(obj) else: - cart_update = CartUpdateDto( - user_id=obj.user_id, - product_id=obj.product_id, + update_command = UpdateCartCommand( + cart_id=obj.id, count=obj.count + 1 ) - await update_shoppingcart( - obj.id, - cart_update, + await update_cart( + update_command, session ) -async def update_shoppingcart( - cart_id: int, - cart: CartUpdateDto, +async def update_cart( + command: UpdateCartCommand, session: Session ): - if cart.count <= 0: + if command.count <= 0: raise BadRequestException repository = CartRepository(session) - obj = repository.get_by_id(cart_id) + obj = repository.get_by_id(command.cart_id) if obj is None: raise NotFoundException - obj.count = cart.count + obj.count = command.count -async def delete_shoppingcart( - cart_id: int, +async def delete_cart( + command: DeleteCartCommand, session: Session, ): repository = CartRepository(session) - obj = repository.get_by_id(cart_id) + obj = repository.get_by_id(command.cart_id) if obj is None: raise NotFoundException repository.delete(obj) diff --git a/service/like.py b/service/like.py index 07a7cbe..cc3195a 100644 --- a/service/like.py +++ b/service/like.py @@ -1,26 +1,24 @@ - - from sqlalchemy.orm.session import Session +from command.like import DislikeCommand, LikeCommand from const import LikeStatus -from dto.like import LikeDto from orm.like import Like from repository.like import LikeRepository async def like( - like_data: LikeDto, + command: LikeCommand, session: Session -) -> LikeDto: +): repository = LikeRepository(session) obj = repository.get_by_user_id_and_product_id( - user_id=like_data.user_id, - product_id=like_data.product_id + user_id=command.user_id, + product_id=command.product_id ) if obj is None: obj = Like( - user_id=like_data.user_id, - product_id=like_data.product_id, + user_id=command.user_id, + product_id=command.product_id, is_like=LikeStatus.LIKE ) repository.add(obj) @@ -29,18 +27,18 @@ async def like( async def dislike( - like_data: LikeDto, + command: DislikeCommand, session: Session -) -> LikeDto: +): repository = LikeRepository(session) obj = repository.get_by_user_id_and_product_id( - user_id=like_data.user_id, - product_id=like_data.product_id + user_id=command.user_id, + product_id=command.product_id ) if obj is None: obj = Like( - user_id=like_data.user_id, - product_id=like_data.product_id, + user_id=command.user_id, + product_id=command.product_id, is_like=LikeStatus.DISLIKE ) repository.add(obj) diff --git a/service/product.py b/service/product.py index b8a630a..dcefac4 100644 --- a/service/product.py +++ b/service/product.py @@ -1,7 +1,8 @@ from sqlalchemy.orm.session import Session +from command.product import AddProductCommand from const import LikeStatus -from dto.product import ProductDto, ProductResponseDto +from dto.product import ProductResponseDto from orm.product import Product from repository.like import LikeRepository from repository.product import ProductRepository @@ -45,14 +46,14 @@ async def list_product( async def add_product( - product: ProductDto, + command: AddProductCommand, session: Session ): repository = ProductRepository(session) obj = Product( - name=product.name, - image_path=product.image_path, - price=product.price, - summary=product.summary + name=command.name, + image_path=command.image_path, + price=command.price, + summary=command.summary ) repository.add(obj) diff --git a/tests/e2e/test_cart.py b/tests/e2e/test_cart.py index 13b641e..e101f0e 100644 --- a/tests/e2e/test_cart.py +++ b/tests/e2e/test_cart.py @@ -4,7 +4,7 @@ @pytest.mark.asyncio -async def test_list_shoppingcart_returns_200( +async def test_list_cart_returns_200( test_fastapi_app, bootstrap, shopping_cart ): async with AsyncClient( @@ -21,7 +21,7 @@ async def test_list_shoppingcart_returns_200( @pytest.mark.asyncio -async def test_add_shoppingcart_returns_201( +async def test_add_cart_returns_201( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -42,7 +42,7 @@ async def test_add_shoppingcart_returns_201( @pytest.mark.asyncio -async def test_add_shoppingcart_returns_201_when_increment_count( +async def test_add_cart_returns_201_when_increment_count( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -63,7 +63,7 @@ async def test_add_shoppingcart_returns_201_when_increment_count( @pytest.mark.asyncio -async def test_update_shoppingcart_returns_202( +async def test_update_cart_returns_202( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -83,7 +83,7 @@ async def test_update_shoppingcart_returns_202( @pytest.mark.asyncio -async def test_update_shoppingcart_returns_400_when_invalid_count( +async def test_update_cart_returns_400_when_invalid_count( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -103,7 +103,7 @@ async def test_update_shoppingcart_returns_400_when_invalid_count( @pytest.mark.asyncio -async def test_update_shoppingcart_returns_404( +async def test_update_cart_returns_404( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -123,7 +123,7 @@ async def test_update_shoppingcart_returns_404( @pytest.mark.asyncio -async def test_delete_shoppingcart_returns_204( +async def test_delete_cart_returns_204( test_fastapi_app, bootstrap ): async with AsyncClient( @@ -137,7 +137,7 @@ async def test_delete_shoppingcart_returns_204( @pytest.mark.asyncio -async def test_delete_shoppingcart_returns_404( +async def test_delete_cart_returns_404( test_fastapi_app, bootstrap ): async with AsyncClient(