├── ch06 ├── __init__.py ├── 6_3_usecase │ ├── __init__.py │ ├── fastapi.py │ ├── unit_test.py │ └── calculate_average_usecase.py ├── 6_4_gateway │ ├── __init__.py │ ├── README.md │ ├── dependency_injection.py │ ├── fastapi.py │ ├── presenter.py │ ├── controllers.py │ ├── repository.py │ └── usecase.py ├── 6_6_entity │ ├── __init__.py │ ├── infrastructure.py │ ├── fastapi.py │ ├── dependency_injection.py │ ├── presenter.py │ ├── repository.py │ ├── README.md │ ├── controllers.py │ ├── entity.py │ └── usecase.py ├── 6_1_framework │ ├── __init__.py │ ├── click │ │ ├── __init__.py │ │ └── main.py │ └── fastapi │ │ ├── __init__.py │ │ └── main.py ├── 6_2_controller │ ├── __init__.py │ ├── type1 │ │ ├── __init__.py │ │ ├── controller │ │ │ ├── __init__.py │ │ │ └── calculater_score_controller.py │ │ └── presentation │ │ │ ├── __init__.py │ │ │ ├── cli.py │ │ │ └── fastapi.py │ └── type2 │ │ ├── __init__.py │ │ └── presentation │ │ ├── __init__.py │ │ └── fastapi.py ├── 6_5_external │ ├── __init__.py │ ├── README.md │ ├── infrastructure.py │ ├── fastapi.py │ ├── presenter.py │ ├── repository.py │ ├── controllers.py │ ├── test_mocks.py │ ├── dependency_injection.py │ └── usecase.py └── 6_7_presenter │ ├── __init__.py │ ├── __pycache__ │ ├── views.cpython-313.pyc │ ├── entity.cpython-313.pyc │ ├── fastapi.cpython-313.pyc │ ├── usecase.cpython-313.pyc │ ├── __init__.cpython-313.pyc │ ├── presenter.cpython-313.pyc │ ├── repository.cpython-313.pyc │ ├── viewmodels.cpython-313.pyc │ ├── controllers.cpython-313.pyc │ ├── infrastructure.cpython-313.pyc │ └── dependency_injection.cpython-313.pyc │ ├── viewmodels.py │ ├── infrastructure.py │ ├── README.md │ ├── repository.py │ ├── views.py │ ├── fastapi.py │ ├── controllers.py │ ├── presenter.py │ ├── dependency_injection.py │ ├── entity.py │ └── usecase.py ├── ch07 ├── domain │ ├── __init__.py │ ├── value_objects │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── money.cpython-313.pyc │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── order_status.cpython-313.pyc │ │ ├── order_status.py │ │ └── money.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ ├── entities │ │ ├── __pycache__ │ │ │ ├── order.cpython-313.pyc │ │ │ ├── __init__.cpython-313.pyc │ │ │ ├── coffee.cpython-313.pyc │ │ │ └── order_item.cpython-313.pyc │ │ ├── __init__.py │ │ ├── order_item.py │ │ ├── coffee.py │ │ └── order.py │ └── exceptions │ │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ │ └── __init__.py ├── tests │ ├── __init__.py │ ├── api │ │ └── __init__.py │ ├── unit │ │ ├── __init__.py │ │ ├── controller │ │ │ ├── __init__.py │ │ │ └── __pycache__ │ │ │ │ ├── __init__.cpython-313.pyc │ │ │ │ └── test_create_order_controller.cpython-313.pyc │ │ ├── entity │ │ │ ├── __init__.py │ │ │ ├── __pycache__ │ │ │ │ └── test_order.cpython-313.pyc │ │ │ └── test_order.py │ │ ├── usecase │ │ │ ├── __init__.py │ │ │ └── __pycache__ │ │ │ │ └── test_create_order_usecase.cpython-313.pyc │ │ └── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ ├── scripts │ │ ├── run_gateway_test.sh │ │ └── run_unit_test.sh │ └── __pycache__ │ │ └── __init__.cpython-313.pyc ├── adapter │ ├── __init__.py │ ├── presenter │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── create_order_presenter.cpython-313.pyc │ │ └── create_order_presenter.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ └── repository │ │ └── __pycache__ │ │ └── __init__.cpython-313.pyc ├── application │ ├── __init__.py │ ├── ports │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ │ ├── inbound │ │ │ ├── __pycache__ │ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ │ ├── outbound │ │ │ ├── __pycache__ │ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ │ └── repository │ │ │ ├── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ ├── usecases │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── create_order_usecase.cpython-313.pyc │ │ └── create_order_usecase.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ └── dtos │ │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ │ └── __init__.py ├── infrastructure │ ├── web │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── fastapi.cpython-313.pyc │ │ │ └── __init__.cpython-313.pyc │ │ └── fastapi.py │ ├── persistence │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── tables.cpython-313.pyc │ │ │ └── __init__.cpython-313.pyc │ │ └── tables.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ └── __init__.py ├── package.json ├── requirements.txt ├── Pipfile └── domain_class_diagram.puml ├── ch09 ├── domain │ ├── __init__.py │ ├── value_objects │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── money.cpython-313.pyc │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── order_status.cpython-313.pyc │ │ ├── order_status.py │ │ └── money.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ ├── entities │ │ ├── __pycache__ │ │ │ ├── order.cpython-313.pyc │ │ │ ├── __init__.cpython-313.pyc │ │ │ ├── coffee.cpython-313.pyc │ │ │ └── order_item.cpython-313.pyc │ │ ├── __init__.py │ │ ├── order_item.py │ │ └── coffee.py │ ├── exceptions │ │ ├── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ │ └── __init__.py │ ├── shared │ │ └── aggregate_root.py │ └── events │ │ └── order_events.py ├── tests │ ├── __init__.py │ ├── api │ │ └── __init__.py │ ├── unit │ │ ├── __init__.py │ │ ├── controller │ │ │ ├── __init__.py │ │ │ └── __pycache__ │ │ │ │ ├── __init__.cpython-313.pyc │ │ │ │ └── test_create_order_controller.cpython-313.pyc │ │ ├── entity │ │ │ ├── __init__.py │ │ │ ├── __pycache__ │ │ │ │ └── test_order.cpython-313.pyc │ │ │ └── test_order.py │ │ ├── usecase │ │ │ ├── __init__.py │ │ │ └── __pycache__ │ │ │ │ └── test_create_order_usecase.cpython-313.pyc │ │ └── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ ├── scripts │ │ ├── run_gateway_test.sh │ │ └── run_unit_test.sh │ └── __pycache__ │ │ └── __init__.cpython-313.pyc ├── adapter │ ├── __init__.py │ ├── presenter │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── create_order_presenter.cpython-313.pyc │ │ └── create_order_presenter.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ ├── repository │ │ └── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ ├── events │ │ ├── __init__.py │ │ └── sns_publisher.py │ └── gateway │ │ └── __init__.py ├── application │ ├── __init__.py │ ├── ports │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ │ ├── inbound │ │ │ ├── __pycache__ │ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ │ ├── outbound │ │ │ ├── __pycache__ │ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ │ └── repository │ │ │ ├── __pycache__ │ │ │ └── __init__.cpython-313.pyc │ │ │ └── __init__.py │ ├── usecases │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-313.pyc │ │ │ └── create_order_usecase.cpython-313.pyc │ │ └── process_payment_usecase.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ └── dtos │ │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ │ └── __init__.py ├── infrastructure │ ├── web │ │ ├── __init__.py │ │ └── __pycache__ │ │ │ ├── fastapi.cpython-313.pyc │ │ │ └── __init__.cpython-313.pyc │ ├── persistence │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── tables.cpython-313.pyc │ │ │ └── __init__.cpython-313.pyc │ │ └── tables.py │ ├── __pycache__ │ │ └── __init__.cpython-313.pyc │ ├── __init__.py │ └── handlers │ │ └── sns_payment_handler.py └── requirements.txt └── ch10 ├── chain-of-thought-prompt-with-guardrail ├── src │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ └── database.py │ ├── domain │ │ ├── __init__.py │ │ ├── entities │ │ │ ├── __init__.py │ │ │ ├── customer.py │ │ │ ├── menu.py │ │ │ └── order.py │ │ ├── repositories │ │ │ ├── customer_repository.py │ │ │ ├── menu_repository.py │ │ │ └── order_repository.py │ │ ├── dtos │ │ │ ├── menu_dtos.py │ │ │ ├── customer_dtos.py │ │ │ └── order_dtos.py │ │ └── usecases │ │ │ ├── menu_usecases.py │ │ │ └── customer_usecases.py │ ├── infrastructure │ │ ├── __init__.py │ │ ├── repositories │ │ │ ├── __init__.py │ │ │ ├── mysql_customer_repository.py │ │ │ ├── dynamodb_customer_repository.py │ │ │ ├── mysql_order_repository.py │ │ │ ├── dynamodb_order_repository.py │ │ │ └── dynamodb_menu_repository.py │ │ └── database │ │ │ └── models │ │ │ ├── customer.py │ │ │ ├── menu.py │ │ │ ├── order.py │ │ │ └── base.py │ ├── main.py │ └── presentation │ │ ├── schemas │ │ └── api_response.py │ │ └── controllers │ │ ├── menu_controller.py │ │ └── customer_controller.py ├── requirements-dev.txt ├── pytest.ini ├── requirements.txt ├── alembic.ini ├── setup.py ├── pyproject.toml ├── alembic │ └── env.py └── tests │ └── unit │ ├── test_menu_usecase.py │ └── test_customer_usecase.py ├── chain-of-thought-prompt ├── requirements.txt ├── src │ ├── main.py │ ├── infrastructure │ │ ├── presenters │ │ │ └── json_presenter.py │ │ └── repositories │ │ │ └── dynamodb_models.py │ ├── domain │ │ ├── entities │ │ │ ├── menu.py │ │ │ └── order.py │ │ ├── usecases │ │ │ ├── menu_usecases.py │ │ │ └── order_usecases.py │ │ └── interfaces │ │ │ ├── repositories.py │ │ │ └── boundaries.py │ ├── application │ │ └── controllers │ │ │ └── menu_controller.py │ └── config │ │ └── dependencies.py ├── README.md └── serverless.yml ├── few-shot-prompt-with-constraints ├── requirements.txt ├── src │ ├── domain │ │ ├── entities │ │ │ ├── menu.py │ │ │ └── order.py │ │ ├── usecases │ │ │ ├── menu_usecases.py │ │ │ └── order_usecases.py │ │ └── interfaces │ │ │ ├── repositories.py │ │ │ └── boundaries.py │ ├── infrastructure │ │ ├── presenters │ │ │ └── json_presenter.py │ │ └── repositories │ │ │ └── dynamodb_models.py │ ├── application │ │ └── controllers │ │ │ └── menu_controller.py │ ├── config │ │ └── dependencies.py │ └── main.py ├── tests │ └── unit │ │ └── test_menu_usecase.py ├── README.md └── serverless.yml ├── simple-prompt ├── requirements.txt ├── src │ ├── interfaces │ │ └── schemas.py │ ├── domain │ │ ├── entities.py │ │ └── repositories.py │ ├── infrastructure │ │ └── models.py │ └── application │ │ └── usecases.py ├── serverless.yml └── tests │ └── unit │ └── test_usecases.py └── few-shot-prompt ├── requirements.txt ├── src ├── application │ └── exceptions.py ├── interfaces │ └── schemas.py ├── infrastructure │ └── models.py └── domain │ ├── repositories.py │ └── entities.py ├── README.md └── serverless.yml /ch06/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_3_usecase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_6_entity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/adapter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/application/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/adapter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/application/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_1_framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_5_external/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_1_framework/click/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_1_framework/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/adapter/presenter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/application/ports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/application/usecases/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/infrastructure/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/scripts/run_gateway_test.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/unit/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/unit/entity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/tests/unit/usecase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/adapter/presenter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/application/ports/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/application/usecases/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/infrastructure/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/scripts/run_gateway_test.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/unit/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/unit/entity/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/tests/unit/usecase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/infrastructure/persistence/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch09/infrastructure/persistence/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/controller/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/presentation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type2/presentation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ch07/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "serverless-python-requirements": "^6.1.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.68.1 2 | mangum==0.12.3 3 | pynamodb==5.2.1 4 | pydantic==1.8.2 5 | uvicorn==0.15.0 -------------------------------------------------------------------------------- /ch07/domain/value_objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .money import Money 2 | from .order_status import OrderStatus 3 | 4 | __all__ = ["Money", "OrderStatus"] 5 | -------------------------------------------------------------------------------- /ch09/domain/value_objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .money import Money 2 | from .order_status import OrderStatus 3 | 4 | __all__ = ["Money", "OrderStatus"] 5 | -------------------------------------------------------------------------------- /ch07/tests/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/adapter/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/adapter/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/adapter/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/adapter/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/views.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/views.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/tests/unit/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/unit/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/unit/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/unit/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/entity.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/entity.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/fastapi.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/fastapi.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/usecase.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/usecase.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/entities/__pycache__/order.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/entities/__pycache__/order.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/entities/__pycache__/order.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/entities/__pycache__/order.cpython-313.pyc -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | mangum==0.17.0 3 | pynamodb==5.5.0 4 | pydantic==2.5.2 5 | uvicorn==0.24.0 6 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/presenter.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/presenter.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/repository.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/repository.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/viewmodels.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/viewmodels.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .coffee import Coffee 2 | from .order import Order 3 | from .order_item import OrderItem 4 | 5 | __all__ = ["Coffee", "Order", "OrderItem"] 6 | -------------------------------------------------------------------------------- /ch07/domain/entities/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/entities/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/entities/__pycache__/coffee.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/entities/__pycache__/coffee.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/infrastructure/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/infrastructure/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .coffee import Coffee 2 | from .order import Order 3 | from .order_item import OrderItem 4 | 5 | __all__ = ["Coffee", "Order", "OrderItem"] 6 | -------------------------------------------------------------------------------- /ch09/domain/entities/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/entities/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/entities/__pycache__/coffee.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/entities/__pycache__/coffee.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/infrastructure/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/infrastructure/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/controllers.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/controllers.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/adapter/presenter/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/adapter/presenter/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/dtos/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/dtos/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/ports/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/ports/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/entities/__pycache__/order_item.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/entities/__pycache__/order_item.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/exceptions/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/exceptions/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/value_objects/__pycache__/money.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/value_objects/__pycache__/money.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/infrastructure/web/__pycache__/fastapi.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/infrastructure/web/__pycache__/fastapi.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/adapter/presenter/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/adapter/presenter/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/dtos/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/dtos/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/ports/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/ports/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/entities/__pycache__/order_item.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/entities/__pycache__/order_item.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/exceptions/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/exceptions/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/value_objects/__pycache__/money.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/value_objects/__pycache__/money.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/infrastructure/web/__pycache__/fastapi.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/infrastructure/web/__pycache__/fastapi.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/infrastructure.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/infrastructure.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/adapter/repository/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/adapter/repository/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/usecases/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/usecases/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/value_objects/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/value_objects/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/infrastructure/web/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/infrastructure/web/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/tests/unit/entity/__pycache__/test_order.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/unit/entity/__pycache__/test_order.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/adapter/repository/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/adapter/repository/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/usecases/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/usecases/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/value_objects/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/value_objects/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/infrastructure/web/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/infrastructure/web/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/unit/entity/__pycache__/test_order.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/unit/entity/__pycache__/test_order.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/tests/unit/controller/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/unit/controller/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/unit/controller/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/unit/controller/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/__pycache__/dependency_injection.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch06/6_7_presenter/__pycache__/dependency_injection.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/ports/inbound/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/ports/inbound/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/domain/value_objects/__pycache__/order_status.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/domain/value_objects/__pycache__/order_status.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/infrastructure/persistence/__pycache__/tables.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/infrastructure/persistence/__pycache__/tables.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/ports/inbound/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/ports/inbound/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/domain/value_objects/__pycache__/order_status.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/domain/value_objects/__pycache__/order_status.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/infrastructure/persistence/__pycache__/tables.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/infrastructure/persistence/__pycache__/tables.cpython-313.pyc -------------------------------------------------------------------------------- /ch10/simple-prompt/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | mangum==0.17.0 3 | pynamodb==5.5.0 4 | pydantic==2.5.2 5 | uvicorn==0.24.0 6 | pytest==7.4.3 7 | pytest-asyncio==0.21.1 8 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /ch07/application/ports/outbound/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/ports/outbound/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/infrastructure/persistence/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/infrastructure/persistence/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/ports/outbound/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/ports/outbound/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/infrastructure/persistence/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/infrastructure/persistence/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/ports/repository/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/ports/repository/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.40.5 2 | botocore==1.40.5 3 | fastapi==0.88.0 4 | mangum==0.19.0 5 | pydantic==1.10.12 6 | pynamodb==6.1.0 7 | requests==2.32.4 8 | uvicorn==0.35.0 9 | httpx==0.27.2 10 | -------------------------------------------------------------------------------- /ch09/application/ports/repository/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/ports/repository/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.40.5 2 | botocore==1.40.5 3 | fastapi==0.116.1 4 | mangum==0.19.0 5 | pydantic==2.11.7 6 | pynamodb==6.1.0 7 | requests==2.32.4 8 | uvicorn==0.35.0 9 | httpx==0.27.2 10 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Code quality tools 2 | pre-commit>=3.6.0 3 | black>=24.1.0 4 | isort>=5.13.2 5 | ruff>=0.2.0 6 | mypy>=1.8.0 7 | bandit>=1.7.7 8 | types-all>=1.0.0 -------------------------------------------------------------------------------- /ch07/adapter/presenter/__pycache__/create_order_presenter.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/adapter/presenter/__pycache__/create_order_presenter.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/application/usecases/__pycache__/create_order_usecase.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/application/usecases/__pycache__/create_order_usecase.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/adapter/presenter/__pycache__/create_order_presenter.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/adapter/presenter/__pycache__/create_order_presenter.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/application/usecases/__pycache__/create_order_usecase.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/application/usecases/__pycache__/create_order_usecase.cpython-313.pyc -------------------------------------------------------------------------------- /ch07/tests/unit/usecase/__pycache__/test_create_order_usecase.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/unit/usecase/__pycache__/test_create_order_usecase.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/unit/usecase/__pycache__/test_create_order_usecase.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/unit/usecase/__pycache__/test_create_order_usecase.cpython-313.pyc -------------------------------------------------------------------------------- /ch10/few-shot-prompt/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.104.1 2 | mangum==0.17.0 3 | pynamodb==5.5.0 4 | pydantic==2.5.2 5 | uvicorn==0.24.0 6 | python-dotenv==1.0.0 7 | pytest==7.4.3 8 | pytest-mock==3.12.0 9 | pytest-asyncio==0.21.1 -------------------------------------------------------------------------------- /ch07/domain/value_objects/order_status.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrderStatus(Enum): 5 | PENDING = "pending" 6 | PREPARING = "preparing" 7 | COMPLETED = "completed" 8 | CANCELLED = "cancelled" 9 | -------------------------------------------------------------------------------- /ch09/domain/value_objects/order_status.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class OrderStatus(Enum): 5 | PENDING = "pending" 6 | PREPARING = "preparing" 7 | COMPLETED = "completed" 8 | CANCELLED = "cancelled" 9 | -------------------------------------------------------------------------------- /ch07/tests/unit/controller/__pycache__/test_create_order_controller.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch07/tests/unit/controller/__pycache__/test_create_order_controller.cpython-313.pyc -------------------------------------------------------------------------------- /ch09/tests/unit/controller/__pycache__/test_create_order_controller.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wikibook/clean-architecture-guide/main/ch09/tests/unit/controller/__pycache__/test_create_order_controller.cpython-313.pyc -------------------------------------------------------------------------------- /ch06/6_7_presenter/viewmodels.py: -------------------------------------------------------------------------------- 1 | # ViewModels Layer 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class ScoreViewModel: 7 | student_id: str 8 | average: str 9 | status: str 10 | grade: str 11 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | python_files = test_*.py 4 | python_classes = Test* 5 | python_functions = test_* 6 | addopts = -v --strict-markers 7 | pythonpath = . 8 | markers = 9 | unit: Unit tests 10 | integration: Integration tests -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/requirements.txt: -------------------------------------------------------------------------------- 1 | # FastAPI and related 2 | fastapi>=0.109.0 3 | pydantic>=2.6.0 4 | mangum>=0.17.0 5 | uvicorn>=0.27.0 6 | 7 | # Database 8 | SQLAlchemy>=2.0.0 9 | alembic>=1.13.0 10 | mysqlclient>=2.2.0 11 | python-dotenv>=1.0.0 12 | 13 | # Testing 14 | pytest>=8.0.0 15 | pytest-mock>=3.12.0 16 | httpx>=0.26.0 -------------------------------------------------------------------------------- /ch07/application/ports/inbound/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from application.dtos import InputDto 4 | 5 | 6 | class InputBoundary(metaclass=ABCMeta): 7 | @abstractmethod 8 | def execute(self, input_dto: InputDto) -> None: 9 | pass 10 | 11 | 12 | class CreateOrderInputBoundary(InputBoundary): 13 | pass 14 | -------------------------------------------------------------------------------- /ch09/application/ports/inbound/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from application.dtos import InputDto 4 | 5 | 6 | class InputBoundary(metaclass=ABCMeta): 7 | @abstractmethod 8 | def execute(self, input_dto: InputDto) -> None: 9 | pass 10 | 11 | 12 | class CreateOrderInputBoundary(InputBoundary): 13 | pass 14 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/repositories/customer_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from ..entities.customer import Customer 5 | 6 | 7 | class CustomerRepository(ABC): 8 | @abstractmethod 9 | def get_by_id(self, customer_id: str) -> Optional[Customer]: 10 | pass 11 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/dtos/menu_dtos.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class MenuResponseDTO: 7 | menu_id: str 8 | name: str 9 | price: int 10 | category: str 11 | 12 | 13 | @dataclass 14 | class MenuListResponseDTO: 15 | menus: List[MenuResponseDTO] 16 | -------------------------------------------------------------------------------- /ch07/application/ports/outbound/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from application.dtos import OutputDto 4 | 5 | 6 | class OutputBoundary(metaclass=ABCMeta): 7 | @abstractmethod 8 | def set_result(self, output_dto: OutputDto) -> None: 9 | pass 10 | 11 | @abstractmethod 12 | def present(self) -> dict: 13 | pass 14 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/entities/customer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | 6 | @dataclass 7 | class Customer: 8 | customer_id: str 9 | name: str 10 | email: str 11 | phone: str 12 | created_at: datetime 13 | updated_at: Optional[datetime] = None 14 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/dtos/customer_dtos.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | 6 | @dataclass 7 | class CustomerResponseDTO: 8 | customer_id: str 9 | name: str 10 | email: str 11 | phone: str 12 | created_at: datetime 13 | updated_at: Optional[datetime] = None 14 | -------------------------------------------------------------------------------- /ch06/6_5_external/README.md: -------------------------------------------------------------------------------- 1 | ## 실행 방법 2 | 3 | ### 1. FastAPI 서버 실행 4 | ```bash 5 | cd /Users/alphahacker/clean-architecture-with-python-test-3 6 | python -m uvicorn ch06.6_5_external.fastapi:app --reload --port 8000 7 | ``` 8 | 9 | ### 2. API 테스트 10 | ```bash 11 | curl -X POST "http://localhost:8000/calculate" \ 12 | -H "Content-Type: application/json" \ 13 | -d '{"student_id": "student001", "calculation_type": "average"}' 14 | ``` -------------------------------------------------------------------------------- /ch07/domain/entities/order_item.py: -------------------------------------------------------------------------------- 1 | from ..value_objects.money import Money 2 | 3 | 4 | class OrderItem: 5 | def __init__(self, id: str, order_id: str, coffee_id: str, quantity: int, unit_price: Money): 6 | self.id = id 7 | self.order_id = order_id 8 | self.coffee_id = coffee_id 9 | self.quantity = quantity 10 | self.unit_price = unit_price 11 | 12 | def calculate_subtotal(self) -> Money: 13 | return self.unit_price * self.quantity 14 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/repositories/menu_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | 4 | from ..entities.menu import Menu 5 | 6 | 7 | class MenuRepository(ABC): 8 | @abstractmethod 9 | def get_all(self) -> List[Menu]: 10 | pass 11 | 12 | @abstractmethod 13 | def get_by_id(self, menu_id: str) -> Optional[Menu]: 14 | pass 15 | 16 | @abstractmethod 17 | def update(self, menu: Menu) -> None: 18 | pass 19 | -------------------------------------------------------------------------------- /ch10/simple-prompt/src/interfaces/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional 3 | from datetime import datetime 4 | 5 | class MenuResponse(BaseModel): 6 | id: str 7 | name: str 8 | price: int 9 | category: str 10 | 11 | class CreateOrderRequest(BaseModel): 12 | customer_id: str 13 | menu_id: str 14 | quantity: int 15 | 16 | class OrderResponse(BaseModel): 17 | id: str 18 | status: str 19 | created_at: datetime 20 | updated_at: Optional[datetime] = None -------------------------------------------------------------------------------- /ch09/domain/entities/order_item.py: -------------------------------------------------------------------------------- 1 | from ..value_objects.money import Money 2 | 3 | 4 | class OrderItem: 5 | def __init__(self, id: str, order_id: str, coffee_id: str, quantity: int, unit_price: Money): 6 | self.id = id 7 | self.order_id = order_id 8 | self.coffee_id = coffee_id 9 | self.quantity = quantity 10 | self.unit_price = unit_price 11 | 12 | def calculate_subtotal(self) -> Money: 13 | """주문 항목의 단가와 수량을 곱해 소계를 계산한다""" 14 | return self.unit_price * self.quantity 15 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/README.md: -------------------------------------------------------------------------------- 1 | ### FastAPI 실행 2 | cd /Users/alphahacker/clean-architecture-with-python-test-3 && python -m uvicorn ch06.6_4_gateway.fastapi:app --reload --host 0.0.0.0 --port 8000 3 | 4 | 5 | ### 호출 테스트 6 | curl -X POST "http://localhost:8000/calculate" \ 7 | -H "Content-Type: application/json" \ 8 | -d '{ 9 | "student_id": "student123", 10 | "calculation_type": "average" 11 | }' 12 | 13 | 14 | ### 응답 예시 15 | { 16 | "student_id": "student001", 17 | "result": 85.5, 18 | "status": "success" 19 | } -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from mangum import Mangum 3 | from .config.dependencies import Dependencies 4 | 5 | # FastAPI 애플리케이션 생성 6 | app = FastAPI( 7 | title="Coffee Shop API", 8 | description="Clean Architecture를 적용한 커피 주문 API", 9 | version="1.0.0" 10 | ) 11 | 12 | # 의존성 설정 13 | menu_controller, order_controller = Dependencies.configure() 14 | 15 | # 라우터 등록 16 | menu_controller.register_routes(app) 17 | order_controller.register_routes(app) 18 | 19 | # AWS Lambda 핸들러 20 | handler = Mangum(app) -------------------------------------------------------------------------------- /ch10/few-shot-prompt/src/application/exceptions.py: -------------------------------------------------------------------------------- 1 | class ApplicationError(Exception): 2 | """애플리케이션 기본 예외 클래스""" 3 | pass 4 | 5 | 6 | class MenuNotFoundError(ApplicationError): 7 | """메뉴를 찾을 수 없을 때 발생하는 예외""" 8 | pass 9 | 10 | 11 | class InsufficientStockError(ApplicationError): 12 | """재고가 부족할 때 발생하는 예외""" 13 | pass 14 | 15 | 16 | class OrderNotFoundError(ApplicationError): 17 | """주문을 찾을 수 없을 때 발생하는 예외""" 18 | pass 19 | 20 | 21 | class DatabaseError(ApplicationError): 22 | """데이터베이스 오류 발생 시 발생하는 예외""" 23 | pass -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/database/models/customer.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from .base import Base 5 | 6 | 7 | class CustomerModel(Base): 8 | """고객 테이블 모델""" 9 | 10 | customer_id: Mapped[str] = mapped_column(String(36), unique=True, nullable=False) 11 | name: Mapped[str] = mapped_column(String(100), nullable=False) 12 | email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) 13 | phone: Mapped[str] = mapped_column(String(20), nullable=False) 14 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/entities/menu.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | 5 | @dataclass 6 | class Menu: 7 | menu_id: str 8 | name: str 9 | price: int 10 | category: str 11 | stock: int 12 | 13 | def has_sufficient_stock(self, quantity: int) -> bool: 14 | return self.stock >= quantity 15 | 16 | def decrease_stock(self, quantity: int) -> None: 17 | if not self.has_sufficient_stock(quantity): 18 | raise ValueError("Insufficient stock") 19 | self.stock -= quantity -------------------------------------------------------------------------------- /ch06/6_3_usecase/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from .calculate_average_usecase import CalculateAverageUseCase, ScoreInputDTO 4 | 5 | app = FastAPI() 6 | 7 | 8 | class ScoreRequest(BaseModel): 9 | student_id: str 10 | calculation_type: str 11 | 12 | 13 | @app.post("/calculate") 14 | async def calculate_score(request: ScoreRequest): 15 | input_dto = ScoreInputDTO(student_id=request.student_id) 16 | 17 | usecase = CalculateAverageUseCase() 18 | result = usecase.execute(input_dto) # 유스케이스 호출 19 | 20 | # 응답 변환 21 | return result.present() 22 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/dependency_injection.py: -------------------------------------------------------------------------------- 1 | from .controllers import CalculateScoreController 2 | from .usecase import CalculateAverageUseCase 3 | from .presenter import ConsolePresenter 4 | from .repository import DdbScoreRepository 5 | 6 | 7 | def initialize_application() -> CalculateScoreController: 8 | # Repository와 Presenter 초기화 9 | repository = DdbScoreRepository() 10 | presenter = ConsolePresenter() 11 | 12 | # 유스케이스 초기화 13 | usecase = CalculateAverageUseCase(repository, presenter) 14 | 15 | # 컨트롤러 초기화 16 | controller = CalculateScoreController(usecase) 17 | return controller 18 | -------------------------------------------------------------------------------- /ch06/6_1_framework/click/main.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.command() 5 | @click.option("--student-id", required=True, type=str, help="Student ID") 6 | @click.option("--calculation-type", required=True, type=str, help="Calculation type (average/total)") 7 | def calculate_score(student_id: str, calculation_type: str): 8 | # 컨트롤러로 요청 전달 (6.2 섹션에서 구현) 9 | print(f"Received request: student_id={student_id}, calculation_type={calculation_type}") 10 | 11 | 12 | if __name__ == "__main__": 13 | calculate_score() 14 | 15 | 16 | # 실행 17 | # 경로는 ch06/cli 들어와서 18 | # python main.py --student-id test --calculation-type average 19 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/dtos/order_dtos.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Dict, Optional 4 | 5 | 6 | @dataclass 7 | class CreateOrderRequestDTO: 8 | customer_id: str 9 | menu_id: str 10 | quantity: int 11 | options: Dict[str, str] 12 | 13 | 14 | @dataclass 15 | class OrderResponseDTO: 16 | order_id: str 17 | status: str 18 | created_at: datetime 19 | updated_at: Optional[datetime] = None 20 | 21 | 22 | @dataclass 23 | class DeleteOrderResponseDTO: 24 | order_id: str 25 | success: bool 26 | message: str 27 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/database/models/menu.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from .base import Base 5 | 6 | 7 | class MenuModel(Base): 8 | """메뉴 테이블 모델""" 9 | 10 | menu_id: Mapped[str] = mapped_column(String(36), unique=True, nullable=False) 11 | name: Mapped[str] = mapped_column(String(100), nullable=False) 12 | price: Mapped[int] = mapped_column(Integer, nullable=False) 13 | category: Mapped[str] = mapped_column(String(50), nullable=False) 14 | stock: Mapped[int] = mapped_column(Integer, nullable=False, default=0) 15 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/infrastructure/presenters/json_presenter.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from ...domain.interfaces.boundaries import OutputBoundary, ResponseDTO 3 | 4 | 5 | class JSONPresenter(OutputBoundary): 6 | def present_success(self, data: Any, message: str) -> ResponseDTO: 7 | return ResponseDTO( 8 | status="success", 9 | data=data, 10 | message=message 11 | ) 12 | 13 | def present_error(self, message: str) -> ResponseDTO: 14 | return ResponseDTO( 15 | status="error", 16 | data=None, 17 | message=message 18 | ) -------------------------------------------------------------------------------- /ch10/simple-prompt/src/domain/entities.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from enum import Enum 4 | from typing import Optional 5 | 6 | class OrderStatus(Enum): 7 | PENDING = "PENDING" 8 | PREPARING = "PREPARING" 9 | COMPLETED = "COMPLETED" 10 | CANCELLED = "CANCELLED" 11 | 12 | @dataclass 13 | class Menu: 14 | id: str 15 | name: str 16 | price: int 17 | category: str 18 | stock: int 19 | 20 | @dataclass 21 | class Order: 22 | id: str 23 | customer_id: str 24 | menu_id: str 25 | quantity: int 26 | status: OrderStatus 27 | created_at: datetime 28 | updated_at: Optional[datetime] = None -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/infrastructure/presenters/json_presenter.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from ...domain.interfaces.boundaries import OutputBoundary, ResponseDTO 3 | 4 | class JSONPresenter(OutputBoundary): 5 | def present_success(self, data: Any, message: str) -> ResponseDTO: 6 | """성공 응답 생성""" 7 | return ResponseDTO( 8 | status="success", 9 | data=data, 10 | message=message 11 | ) 12 | 13 | def present_error(self, message: str) -> ResponseDTO: 14 | """에러 응답 생성""" 15 | return ResponseDTO( 16 | status="error", 17 | data=None, 18 | message=message 19 | ) -------------------------------------------------------------------------------- /ch06/6_6_entity/infrastructure.py: -------------------------------------------------------------------------------- 1 | # Framework & Drivers Layer (Infrastructure) 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from typing import Dict, Any 5 | 6 | 7 | class DynamoDBClient: 8 | def __init__(self, region: str = "ap-northeast-2"): 9 | self.client = boto3.resource("dynamodb", region_name=region) 10 | 11 | def get_item(self, table_name: str, key: Dict[str, Any]) -> Dict[str, Any]: 12 | try: 13 | table = self.client.Table(table_name) 14 | response = table.get_item(Key=key) 15 | return response 16 | except ClientError as e: 17 | raise ValueError(f"Failed to retrieve data: {str(e)}") 18 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from .dependency_injection import initialize_application 4 | from .controllers import ScoreRequestDTO 5 | 6 | app = FastAPI() 7 | 8 | # 애플리케이션 초기화 9 | controller = initialize_application() 10 | 11 | 12 | class ScoreRequest(BaseModel): 13 | student_id: str 14 | calculation_type: str 15 | 16 | 17 | @app.post("/calculate") 18 | async def calculate_score(request: ScoreRequest): 19 | # 요청을 DTO로 변환 20 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 21 | 22 | # 컨트롤러를 통해 유스케이스 실행 및 응답 23 | return controller.execute(request_dto) 24 | -------------------------------------------------------------------------------- /ch06/6_5_external/infrastructure.py: -------------------------------------------------------------------------------- 1 | # Framework & Drivers Layer (Infrastructure) 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from typing import Dict, Any 5 | 6 | 7 | class DynamoDBClient: 8 | def __init__(self, region: str = "ap-northeast-2"): 9 | self.client = boto3.resource("dynamodb", region_name=region) 10 | 11 | def get_item(self, table_name: str, key: Dict[str, Any]) -> Dict[str, Any]: 12 | try: 13 | table = self.client.Table(table_name) 14 | response = table.get_item(Key=key) 15 | return response 16 | except ClientError as e: 17 | raise ValueError(f"Failed to retrieve data: {str(e)}") 18 | -------------------------------------------------------------------------------- /ch06/6_6_entity/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from .dependency_injection import initialize_application 4 | from .controllers import ScoreRequestDTO 5 | 6 | app = FastAPI() 7 | 8 | # 애플리케이션 초기화 9 | controller = initialize_application() 10 | 11 | 12 | class ScoreRequest(BaseModel): 13 | student_id: str 14 | calculation_type: str 15 | 16 | 17 | @app.post("/calculate") 18 | async def calculate_score(request: ScoreRequest): 19 | # 요청을 DTO로 변환 20 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 21 | 22 | # 컨트롤러를 통해 유스케이스 실행 및 응답 23 | return controller.execute(request_dto) 24 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/infrastructure.py: -------------------------------------------------------------------------------- 1 | # Framework & Drivers Layer (Infrastructure) 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from typing import Dict, Any 5 | 6 | 7 | class DynamoDBClient: 8 | def __init__(self, region: str = "ap-northeast-2"): 9 | self.client = boto3.resource("dynamodb", region_name=region) 10 | 11 | def get_item(self, table_name: str, key: Dict[str, Any]) -> Dict[str, Any]: 12 | try: 13 | table = self.client.Table(table_name) 14 | response = table.get_item(Key=key) 15 | return response 16 | except ClientError as e: 17 | raise ValueError(f"Failed to retrieve data: {str(e)}") 18 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/config/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Generator 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import Session, sessionmaker 6 | 7 | # 환경 변수에서 데이터베이스 설정 읽기 8 | DATABASE_URL = os.getenv( 9 | "DATABASE_URL", "mysql://root:password@localhost:3306/coffee_order" # 기본값 10 | ) 11 | 12 | engine = create_engine(DATABASE_URL) 13 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 14 | 15 | 16 | def get_db() -> Generator[Session, None, None]: 17 | """데이터베이스 세션을 생성하고 관리하는 의존성 주입 함수""" 18 | db = SessionLocal() 19 | try: 20 | yield db 21 | finally: 22 | db.close() 23 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/repositories/order_repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from ..entities.order import Order 5 | 6 | 7 | class OrderRepository(ABC): 8 | @abstractmethod 9 | def save(self, order: Order) -> None: 10 | pass 11 | 12 | @abstractmethod 13 | def get_by_id(self, order_id: str) -> Optional[Order]: 14 | pass 15 | 16 | @abstractmethod 17 | def delete(self, order_id: str) -> bool: 18 | """주문을 삭제합니다. 19 | 20 | Args: 21 | order_id: 삭제할 주문의 ID 22 | 23 | Returns: 24 | bool: 삭제 성공 여부 25 | """ 26 | pass 27 | -------------------------------------------------------------------------------- /ch06/6_3_usecase/unit_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | from calculate_average_usecase import ( 3 | ScoreRepository, 4 | OutputBoundary, 5 | CalculateAverageUseCase, 6 | ScoreInputDTO, 7 | ScoreOutputDTO, 8 | ) 9 | 10 | # 테스트용 모의 객체 11 | mock_repository = Mock(spec=ScoreRepository) 12 | mock_repository.get_scores.return_value = [90.0, 85.0, 95.0] 13 | mock_presenter = Mock(spec=OutputBoundary) 14 | 15 | # 유스케이스 테스트 16 | usecase = CalculateAverageUseCase(mock_repository, mock_presenter) 17 | usecase.execute(ScoreInputDTO(student_id="123")) 18 | 19 | # 결과 검증 20 | mock_presenter.set_result.assert_called_with(ScoreOutputDTO(student_id="123", average=90.00, status="success")) 21 | -------------------------------------------------------------------------------- /ch06/6_5_external/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from .dependency_injection import initialize_application 4 | from .controllers import ScoreRequestDTO 5 | 6 | app = FastAPI() 7 | 8 | # 애플리케이션 초기화 9 | controller = initialize_application() 10 | 11 | 12 | class ScoreRequest(BaseModel): 13 | student_id: str 14 | calculation_type: str 15 | 16 | 17 | @app.post("/calculate") 18 | async def calculate_score(request: ScoreRequest): 19 | # 요청을 DTO로 변환 20 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 21 | 22 | # 컨트롤러를 통해 유스케이스 실행 및 응답 23 | return controller.execute(request_dto) 24 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/README.md: -------------------------------------------------------------------------------- 1 | ## 실행 방법 2 | 3 | ### 1. FastAPI 서버 실행 4 | ```bash 5 | cd /Users/alphahacker/clean-architecture-with-python-test-3 6 | python -m uvicorn ch06.6_7_presenter.fastapi:app --reload --port 8000 7 | ``` 8 | 9 | ### 2. API 테스트 10 | 11 | #### POST 요청 (각 형식별 엔드포인트) 12 | ```bash 13 | # API 형식 14 | curl -X POST "http://localhost:8000/calculate/api" \ 15 | -H "Content-Type: application/json" \ 16 | -d '{"student_id": "student001", "calculation_type": "average"}' 17 | 18 | # Web 형식 (HTML) 19 | curl -X POST "http://localhost:8000/calculate/web" \ 20 | -H "Content-Type: application/json" \ 21 | -d '{"student_id": "student001", "calculation_type": "average"}' 22 | ``` 23 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/database/models/order.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import JSON, Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from .base import Base 5 | 6 | 7 | class OrderModel(Base): 8 | """주문 테이블 모델""" 9 | 10 | order_id: Mapped[str] = mapped_column(String(36), unique=True, nullable=False) 11 | customer_id: Mapped[str] = mapped_column(String(36), nullable=False) 12 | menu_id: Mapped[str] = mapped_column(String(36), nullable=False) 13 | quantity: Mapped[int] = mapped_column(Integer, nullable=False) 14 | status: Mapped[str] = mapped_column(String(20), nullable=False) 15 | options: Mapped[dict] = mapped_column(JSON, nullable=True) 16 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/entities/menu.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | 6 | @dataclass 7 | class Menu: 8 | menu_id: str 9 | name: str 10 | price: int 11 | category: str 12 | stock: int 13 | created_at: datetime 14 | updated_at: Optional[datetime] = None 15 | 16 | def is_available(self, quantity: int) -> bool: 17 | return self.stock >= quantity 18 | 19 | def decrease_stock(self, quantity: int) -> None: 20 | if not self.is_available(quantity): 21 | raise ValueError("Insufficient stock") 22 | self.stock -= quantity 23 | self.updated_at = datetime.utcnow() 24 | -------------------------------------------------------------------------------- /ch06/6_6_entity/dependency_injection.py: -------------------------------------------------------------------------------- 1 | from .controllers import CalculateScoreController 2 | from .usecase import CalculateAverageUseCase 3 | from .presenter import ConsolePresenter 4 | from .repository import DdbScoreRepository 5 | from .infrastructure import DynamoDBClient 6 | 7 | 8 | def initialize_application() -> CalculateScoreController: 9 | # 인프라스트럭쳐 초기화 10 | dynamodb_client = DynamoDBClient(region="ap-northeast-2") 11 | 12 | # 인터페이스 어댑터 초기화 13 | repository = DdbScoreRepository(dynamodb_client) 14 | presenter = ConsolePresenter() 15 | 16 | # 유스케이스 초기화 17 | usecase = CalculateAverageUseCase(repository, presenter) 18 | 19 | # 컨트롤러 초기화 20 | controller = CalculateScoreController(usecase) 21 | return controller 22 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt/src/interfaces/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional, Dict 3 | from datetime import datetime 4 | 5 | 6 | class MenuResponse(BaseModel): 7 | menu_id: str 8 | name: str 9 | price: int 10 | category: str 11 | 12 | 13 | class OrderOptions(BaseModel): 14 | size: str 15 | temperature: str 16 | 17 | 18 | class CreateOrderRequest(BaseModel): 19 | customer_id: str 20 | menu_id: str 21 | quantity: int 22 | options: OrderOptions 23 | 24 | 25 | class OrderResponse(BaseModel): 26 | order_id: str 27 | status: str 28 | created_at: datetime 29 | 30 | 31 | class APIResponse(BaseModel): 32 | status: str 33 | data: Optional[dict] = None 34 | message: str -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/presentation/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | from ..controller.calculater_score_controller import CalculateScoreController, ScoreRequestDTO 3 | 4 | 5 | @click.command() 6 | @click.option("--student-id", required=True, type=str, help="Student ID") 7 | @click.option("--calculation-type", required=True, type=str, help="Calculation type (average)") 8 | def calculate_score(student_id: str, calculation_type: str): 9 | controller = CalculateScoreController(...) # 의존성 주입은 별도 모듈에서 처리 10 | request_dto = ScoreRequestDTO(student_id=student_id, calculation_type=calculation_type) 11 | response_dto = controller.execute(request_dto) 12 | print(f"Student ID: {response_dto.student_id}, Result: {response_dto.result}, Status: {response_dto.status}") 13 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/presenter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | # 출력 인터페이스 (Presenter) 6 | class OutputBoundary(ABC): 7 | @abstractmethod 8 | def set_result(self, output_dto) -> None: 9 | pass 10 | 11 | @abstractmethod 12 | def present(self) -> Any: 13 | pass 14 | 15 | 16 | class ConsolePresenter(OutputBoundary): 17 | def __init__(self): 18 | self.contents = {} 19 | 20 | def set_result(self, output_dto: "ScoreOutputDTO"): 21 | self.contents = { 22 | "student_id": output_dto.student_id, 23 | "average": output_dto.average, 24 | "status": output_dto.status, 25 | } 26 | 27 | def present(self) -> Any: 28 | return self.contents 29 | -------------------------------------------------------------------------------- /ch06/6_6_entity/presenter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | # 출력 인터페이스 (Presenter) 6 | class OutputBoundary(ABC): 7 | @abstractmethod 8 | def set_result(self, output_dto) -> None: 9 | pass 10 | 11 | @abstractmethod 12 | def present(self) -> Any: 13 | pass 14 | 15 | 16 | class ConsolePresenter(OutputBoundary): 17 | def __init__(self): 18 | self.contents = {} 19 | 20 | def set_result(self, output_dto: "ScoreOutputDTO"): 21 | self.contents = { 22 | "student_id": output_dto.student_id, 23 | "average": output_dto.average, 24 | "status": output_dto.status, 25 | } 26 | 27 | def present(self) -> Any: 28 | return self.contents 29 | -------------------------------------------------------------------------------- /ch07/domain/entities/coffee.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from ..value_objects.money import Money 3 | 4 | 5 | class Coffee: 6 | def __init__(self, id: str, name: str, price: Money, description: Optional[str] = None, stock: int = 0): 7 | self.id = id 8 | self.name = name 9 | self.price = price 10 | self.description = description 11 | self.stock = stock 12 | 13 | def is_available(self) -> bool: 14 | return self.stock > 0 15 | 16 | def reserve_stock(self, quantity: int) -> None: 17 | if quantity <= 0: 18 | raise ValueError("수량은 1 이상이어야 합니다") 19 | if quantity > self.stock: 20 | raise ValueError(f"재고 부족: {self.name} (남은 수량: {self.stock})") 21 | self.stock -= quantity 22 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/presentation/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from ..controller.calculater_score_controller import CalculateScoreController, ScoreRequestDTO 4 | 5 | app = FastAPI() 6 | 7 | 8 | class ScoreRequest(BaseModel): 9 | student_id: str 10 | calculation_type: str 11 | 12 | 13 | @app.post("/calculate") 14 | async def calculate_score(request: ScoreRequest): 15 | controller = CalculateScoreController(...) # 의존성 주입은 별도 모듈에서 처리 16 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 17 | response_dto = controller.execute(request_dto) 18 | return {"student_id": response_dto.student_id, "result": response_dto.result, "status": response_dto.status} 19 | -------------------------------------------------------------------------------- /ch06/6_5_external/presenter.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | # 출력 인터페이스 (Presenter) 6 | class OutputBoundary(ABC): 7 | @abstractmethod 8 | def set_result(self, output_dto) -> None: 9 | pass 10 | 11 | @abstractmethod 12 | def present(self) -> Any: 13 | pass 14 | 15 | 16 | class ConsolePresenter(OutputBoundary): 17 | def __init__(self): 18 | self.contents = {} 19 | 20 | def set_result(self, output_dto: "ScoreOutputDTO"): 21 | self.contents = { 22 | "student_id": output_dto.student_id, 23 | "average": output_dto.average, 24 | "status": output_dto.status, 25 | } 26 | 27 | def present(self) -> Any: 28 | return self.contents 29 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/usecases/menu_usecases.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from ..interfaces.boundaries import GetMenusInputBoundary, MenuOutputDTO 3 | from ..interfaces.repositories import MenuRepository 4 | 5 | 6 | class GetMenusUseCase(GetMenusInputBoundary): 7 | def __init__(self, menu_repository: MenuRepository): 8 | self._menu_repository = menu_repository 9 | 10 | def execute(self) -> List[MenuOutputDTO]: 11 | menus = self._menu_repository.get_all() 12 | return [ 13 | MenuOutputDTO( 14 | menu_id=menu.menu_id, 15 | name=menu.name, 16 | price=menu.price, 17 | category=menu.category 18 | ) 19 | for menu in menus 20 | ] -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | script_location = alembic 3 | sqlalchemy.url = mysql://root:password@localhost:3306/coffee_order 4 | 5 | [loggers] 6 | keys = root,sqlalchemy,alembic 7 | 8 | [handlers] 9 | keys = console 10 | 11 | [formatters] 12 | keys = generic 13 | 14 | [logger_root] 15 | level = WARN 16 | handlers = console 17 | qualname = 18 | 19 | [logger_sqlalchemy] 20 | level = WARN 21 | handlers = 22 | qualname = sqlalchemy.engine 23 | 24 | [logger_alembic] 25 | level = INFO 26 | handlers = 27 | qualname = alembic 28 | 29 | [handler_console] 30 | class = StreamHandler 31 | args = (sys.stderr,) 32 | level = NOTSET 33 | formatter = generic 34 | 35 | [formatter_generic] 36 | format = %(levelname)-5.5s [%(name)s] %(message)s 37 | datefmt = %H:%M:%S -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/entities/menu.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | from datetime import datetime 4 | 5 | @dataclass 6 | class Menu: 7 | menu_id: str 8 | name: str 9 | price: int 10 | category: str 11 | stock: int 12 | created_at: Optional[datetime] = None 13 | updated_at: Optional[datetime] = None 14 | 15 | def is_available(self, quantity: int) -> bool: 16 | """주문 가능한 재고가 있는지 확인""" 17 | return self.stock >= quantity 18 | 19 | def decrease_stock(self, quantity: int) -> None: 20 | """재고 감소""" 21 | if not self.is_available(quantity): 22 | raise ValueError("Insufficient stock") 23 | self.stock -= quantity 24 | self.updated_at = datetime.utcnow() -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/usecases/menu_usecases.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from ..interfaces.boundaries import MenuInputBoundary, MenuOutputDTO 3 | from ..interfaces.repositories import MenuRepository 4 | 5 | class MenuUseCases(MenuInputBoundary): 6 | def __init__(self, menu_repository: MenuRepository): 7 | self._menu_repository = menu_repository 8 | 9 | def get_all_menus(self) -> List[MenuOutputDTO]: 10 | """모든 메뉴 조회""" 11 | menus = self._menu_repository.get_all() 12 | return [ 13 | MenuOutputDTO( 14 | menu_id=menu.menu_id, 15 | name=menu.name, 16 | price=menu.price, 17 | category=menu.category 18 | ) 19 | for menu in menus 20 | ] -------------------------------------------------------------------------------- /ch06/6_1_framework/fastapi/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | 4 | app = FastAPI() 5 | 6 | 7 | class ScoreRequest(BaseModel): 8 | student_id: str 9 | calculation_type: str # 예: "average", "total" 10 | 11 | 12 | @app.post("/calculate") 13 | async def calculate_score(request: ScoreRequest): 14 | # 컨트롤러로 요청 전달 (6.2 섹션에서 구현) 15 | result = {"student_id": request.student_id, "calculation_type": request.calculation_type} 16 | return {"status": "success", "data": result} 17 | 18 | 19 | # 실행 방법 20 | # root에서 uvicorn ch06.framework:app --reload 21 | 22 | # 호출 예시 23 | # curl -X POST "http://127.0.0.1:8000/calculate" -H "Content-Type: application/json" -d '{"student_id": "test_id", "calculation_type": "total"}' 24 | 25 | # 호출할때 경로는 project root에서 26 | -------------------------------------------------------------------------------- /ch07/application/ports/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional, List 3 | 4 | from domain.entities.coffee import Coffee 5 | from domain.entities.order import Order 6 | 7 | 8 | class CoffeeRepository(ABC): 9 | @abstractmethod 10 | def find_by_id(self, coffee_id: str) -> Optional[Coffee]: 11 | pass 12 | 13 | @abstractmethod 14 | def find_all_available(self) -> List[Coffee]: 15 | pass 16 | 17 | @abstractmethod 18 | def save(self, coffee: Coffee) -> None: 19 | pass 20 | 21 | 22 | class OrderRepository(ABC): 23 | @abstractmethod 24 | def find_by_id(self, order_id: str) -> Optional[Order]: 25 | pass 26 | 27 | @abstractmethod 28 | def save(self, order: Order) -> None: 29 | pass 30 | -------------------------------------------------------------------------------- /ch09/application/ports/repository/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional, List 3 | 4 | from domain.entities.coffee import Coffee 5 | from domain.entities.order import Order 6 | 7 | 8 | class CoffeeRepository(ABC): 9 | @abstractmethod 10 | def find_by_id(self, coffee_id: str) -> Optional[Coffee]: 11 | pass 12 | 13 | @abstractmethod 14 | def find_all_available(self) -> List[Coffee]: 15 | pass 16 | 17 | @abstractmethod 18 | def save(self, coffee: Coffee) -> None: 19 | pass 20 | 21 | 22 | class OrderRepository(ABC): 23 | @abstractmethod 24 | def find_by_id(self, order_id: str) -> Optional[Order]: 25 | pass 26 | 27 | @abstractmethod 28 | def save(self, order: Order) -> None: 29 | pass 30 | -------------------------------------------------------------------------------- /ch07/tests/scripts/run_unit_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running pre-commit tests (UseCase and Controller, Entity)..." 4 | 5 | if command -v pipenv >/dev/null 2>&1; then 6 | pipenv run python -m unittest \ 7 | tests/unit/usecase/test_create_order_usecase.py \ 8 | tests/unit/controller/test_create_order_controller.py \ 9 | tests/unit/entity/test_order.py -v 10 | else 11 | # pipenv가 없으면 python3 시도 12 | python3 -m unittest \ 13 | tests/unit/usecase/test_create_order_usecase.py \ 14 | tests/unit/controller/test_create_order_controller.py \ 15 | tests/unit/entity/test_order.py -v 16 | fi 17 | 18 | # 테스트 결과 확인 19 | if [ $? -eq 0 ]; then 20 | echo "Pre-commit tests passed!" 21 | exit 0 22 | else 23 | echo "Pre-commit tests failed!" 24 | exit 1 25 | fi -------------------------------------------------------------------------------- /ch09/tests/scripts/run_unit_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running pre-commit tests (UseCase and Controller, Entity)..." 4 | 5 | if command -v pipenv >/dev/null 2>&1; then 6 | pipenv run python -m unittest \ 7 | tests/unit/usecase/test_create_order_usecase.py \ 8 | tests/unit/controller/test_create_order_controller.py \ 9 | tests/unit/entity/test_order.py -v 10 | else 11 | # pipenv가 없으면 python3 시도 12 | python3 -m unittest \ 13 | tests/unit/usecase/test_create_order_usecase.py \ 14 | tests/unit/controller/test_create_order_controller.py \ 15 | tests/unit/entity/test_order.py -v 16 | fi 17 | 18 | # 테스트 결과 확인 19 | if [ $? -eq 0 ]; then 20 | echo "Pre-commit tests passed!" 21 | exit 0 22 | else 23 | echo "Pre-commit tests failed!" 24 | exit 1 25 | fi -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="coffee-order-api", 5 | version="0.1.0", 6 | packages=find_packages(), 7 | install_requires=[ 8 | "fastapi>=0.109.0", 9 | "pydantic>=2.6.0", 10 | "mangum>=0.17.0", 11 | "uvicorn>=0.27.0", 12 | "pynamodb>=5.5.0", 13 | "python-dotenv>=1.0.0", 14 | ], 15 | extras_require={ 16 | "dev": [ 17 | "pytest>=8.0.0", 18 | "pytest-mock>=3.12.0", 19 | "httpx>=0.26.0", 20 | "pre-commit>=3.6.0", 21 | "black>=24.1.0", 22 | "isort>=5.13.2", 23 | "ruff>=0.2.0", 24 | "mypy>=1.8.0", 25 | "bandit>=1.7.7", 26 | ], 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /ch07/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | annotated-types = "==0.7.0" 8 | anyio = "==4.10.0" 9 | boto3 = "==1.40.5" 10 | botocore = "==1.40.5" 11 | certifi = "==2025.8.3" 12 | charset-normalizer = "==3.4.2" 13 | click = "==8.2.1" 14 | fastapi = "==0.88.0" 15 | h11 = "==0.16.0" 16 | idna = "==3.10" 17 | jmespath = "==1.0.1" 18 | mangum = "==0.19.0" 19 | pydantic = "==1.10.12" 20 | pynamodb = "==6.1.0" 21 | python-dateutil = "==2.9.0.post0" 22 | requests = "==2.32.4" 23 | s3transfer = "==0.13.1" 24 | six = "==1.17.0" 25 | sniffio = "==1.3.1" 26 | starlette = "==0.47.2" 27 | typing-inspection = "==0.4.1" 28 | typing-extensions = "==4.14.1" 29 | urllib3 = "==2.5.0" 30 | uvicorn = "==0.35.0" 31 | 32 | [dev-packages] 33 | 34 | [requires] 35 | python_version = "3.13" 36 | -------------------------------------------------------------------------------- /ch09/domain/entities/coffee.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from ..value_objects.money import Money 3 | 4 | 5 | class Coffee: 6 | def __init__(self, id: str, name: str, price: Money, description: Optional[str] = None, stock: int = 0): 7 | self.id = id 8 | self.name = name 9 | self.price = price 10 | self.description = description 11 | self.stock = stock 12 | 13 | def is_available(self) -> bool: 14 | """고객이 주문 가능한 재고가 있는지 확인한다""" 15 | return self.stock > 0 16 | 17 | def reserve_stock(self, quantity: int) -> None: 18 | """주문 처리를 위해 요청된 수량만큼 재고를 예약한다""" 19 | if quantity <= 0: 20 | raise ValueError("수량은 1 이상이어야 합니다") 21 | if quantity > self.stock: 22 | raise ValueError(f"재고 부족: {self.name} (남은 수량: {self.stock})") 23 | self.stock -= quantity 24 | -------------------------------------------------------------------------------- /ch10/simple-prompt/src/domain/repositories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | from .entities import Menu, Order 4 | 5 | class MenuRepository(ABC): 6 | @abstractmethod 7 | async def get_all(self) -> List[Menu]: 8 | pass 9 | 10 | @abstractmethod 11 | async def get_by_id(self, menu_id: str) -> Optional[Menu]: 12 | pass 13 | 14 | @abstractmethod 15 | async def update_stock(self, menu_id: str, quantity: int) -> bool: 16 | pass 17 | 18 | class OrderRepository(ABC): 19 | @abstractmethod 20 | async def create(self, order: Order) -> Order: 21 | pass 22 | 23 | @abstractmethod 24 | async def get_by_id(self, order_id: str) -> Optional[Order]: 25 | pass 26 | 27 | @abstractmethod 28 | async def update(self, order: Order) -> Order: 29 | pass -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/database/models/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy.ext.declarative import declared_attr 4 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 5 | from sqlalchemy.sql import func 6 | 7 | 8 | class Base(DeclarativeBase): 9 | """모든 SQLAlchemy 모델의 기본 클래스""" 10 | 11 | @declared_attr 12 | @classmethod 13 | def __tablename__(cls) -> str: 14 | """테이블 이름을 클래스 이름의 소문자 버전으로 자동 생성""" 15 | return cls.__name__.lower() 16 | 17 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 18 | created_at: Mapped[datetime] = mapped_column(server_default=func.now()) 19 | updated_at: Mapped[datetime] = mapped_column( 20 | server_default=func.now(), 21 | onupdate=func.now(), 22 | nullable=True, 23 | ) 24 | -------------------------------------------------------------------------------- /ch06/6_5_external/repository.py: -------------------------------------------------------------------------------- 1 | # Interface Adapters Layer 2 | from abc import ABC, abstractmethod 3 | from typing import List 4 | from .infrastructure import DynamoDBClient 5 | 6 | 7 | class ScoreRepository(ABC): 8 | @abstractmethod 9 | def get_scores(self, student_id: str) -> List[float]: 10 | pass 11 | 12 | 13 | class DdbScoreRepository(ScoreRepository): 14 | def __init__(self, dynamodb_client: DynamoDBClient): 15 | self.dynamodb_client = dynamodb_client 16 | self.table_name = "scores" 17 | 18 | def get_scores(self, student_id: str) -> List[float]: 19 | response = self.dynamodb_client.get_item(table_name=self.table_name, key={"student_id": student_id}) 20 | if "Item" in response and "scores" in response["Item"]: 21 | return [float(score) for score in response["Item"]["scores"]["L"]] 22 | return [] 23 | -------------------------------------------------------------------------------- /ch06/6_6_entity/repository.py: -------------------------------------------------------------------------------- 1 | # Interface Adapters Layer 2 | from abc import ABC, abstractmethod 3 | from typing import List 4 | from .infrastructure import DynamoDBClient 5 | 6 | 7 | class ScoreRepository(ABC): 8 | @abstractmethod 9 | def get_scores(self, student_id: str) -> List[float]: 10 | pass 11 | 12 | 13 | class DdbScoreRepository(ScoreRepository): 14 | def __init__(self, dynamodb_client: DynamoDBClient): 15 | self.dynamodb_client = dynamodb_client 16 | self.table_name = "scores" 17 | 18 | def get_scores(self, student_id: str) -> List[float]: 19 | response = self.dynamodb_client.get_item(table_name=self.table_name, key={"student_id": student_id}) 20 | if "Item" in response and "scores" in response["Item"]: 21 | return [float(score) for score in response["Item"]["scores"]["L"]] 22 | return [] 23 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/repository.py: -------------------------------------------------------------------------------- 1 | # Interface Adapters Layer 2 | from abc import ABC, abstractmethod 3 | from typing import List 4 | from .infrastructure import DynamoDBClient 5 | 6 | 7 | class ScoreRepository(ABC): 8 | @abstractmethod 9 | def get_scores(self, student_id: str) -> List[float]: 10 | pass 11 | 12 | 13 | class DdbScoreRepository(ScoreRepository): 14 | def __init__(self, dynamodb_client: DynamoDBClient): 15 | self.dynamodb_client = dynamodb_client 16 | self.table_name = "scores" 17 | 18 | def get_scores(self, student_id: str) -> List[float]: 19 | response = self.dynamodb_client.get_item(table_name=self.table_name, key={"student_id": student_id}) 20 | if "Item" in response and "scores" in response["Item"]: 21 | return [float(score) for score in response["Item"]["scores"]["L"]] 22 | return [] 23 | -------------------------------------------------------------------------------- /ch07/tests/unit/entity/test_order.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from domain.entities.order import Order 4 | from domain.entities.coffee import Coffee 5 | from domain.value_objects.money import Money 6 | 7 | 8 | class TestOrder(unittest.TestCase): 9 | def test_add_coffee(self): 10 | # Given 11 | coffee = Coffee(id="coffee-1", name="Latte", price=Money(500), stock=10) 12 | order = Order(id="order-1", customer_id="cust-1") 13 | 14 | # When 15 | order.add_coffee(coffee, 2) 16 | 17 | # Then 18 | self.assertEqual(len(order.items), 1) # (1) 항목 추가 확인 19 | self.assertEqual(order.items[0].quantity, 2) # (2) 수량 확인 20 | self.assertEqual(order.total_amount.amount, 1000) # (3) 총액 확인 21 | self.assertEqual(coffee.stock, 8) # (4) 재고 감소 확인 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /ch09/tests/unit/entity/test_order.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from domain.entities.order import Order 4 | from domain.entities.coffee import Coffee 5 | from domain.value_objects.money import Money 6 | 7 | 8 | class TestOrder(unittest.TestCase): 9 | def test_add_coffee(self): 10 | # Given 11 | coffee = Coffee(id="coffee-1", name="Latte", price=Money(500), stock=10) 12 | order = Order(id="order-1", customer_id="cust-1") 13 | 14 | # When 15 | order.add_coffee(coffee, 2) 16 | 17 | # Then 18 | self.assertEqual(len(order.items), 1) # (1) 항목 추가 확인 19 | self.assertEqual(order.items[0].quantity, 2) # (2) 수량 확인 20 | self.assertEqual(order.total_amount.amount, 1000) # (3) 총액 확인 21 | self.assertEqual(coffee.stock, 8) # (4) 재고 감소 확인 22 | 23 | 24 | if __name__ == "__main__": 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /ch07/application/dtos/__init__.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class InputDto: 7 | pass 8 | 9 | 10 | @dataclass 11 | class OutputDto: 12 | pass 13 | 14 | 15 | @dataclass 16 | class CreateOrderInputDto(InputDto): 17 | customer_id: str 18 | coffee_id: str 19 | quantity: int 20 | 21 | 22 | @dataclass 23 | class CreateOrderOutputDto(OutputDto): 24 | order_id: str 25 | customer_id: str 26 | total_amount: int 27 | currency: str 28 | status: str 29 | items: List[dict] 30 | 31 | 32 | @dataclass 33 | class GetOrderInputDto(InputDto): 34 | order_id: str 35 | 36 | 37 | @dataclass 38 | class GetOrderOutputDto(OutputDto): 39 | order_id: str 40 | customer_id: str 41 | total_amount: int 42 | currency: str 43 | status: str 44 | items: List[dict] 45 | created_at: str 46 | -------------------------------------------------------------------------------- /ch09/application/dtos/__init__.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class InputDto: 7 | pass 8 | 9 | 10 | @dataclass 11 | class OutputDto: 12 | pass 13 | 14 | 15 | @dataclass 16 | class CreateOrderInputDto(InputDto): 17 | customer_id: str 18 | coffee_id: str 19 | quantity: int 20 | 21 | 22 | @dataclass 23 | class CreateOrderOutputDto(OutputDto): 24 | order_id: str 25 | customer_id: str 26 | total_amount: int 27 | currency: str 28 | status: str 29 | items: List[dict] 30 | 31 | 32 | @dataclass 33 | class GetOrderInputDto(InputDto): 34 | order_id: str 35 | 36 | 37 | @dataclass 38 | class GetOrderOutputDto(OutputDto): 39 | order_id: str 40 | customer_id: str 41 | total_amount: int 42 | currency: str 43 | status: str 44 | items: List[dict] 45 | created_at: str 46 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/interfaces/repositories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | from ..entities.menu import Menu 4 | from ..entities.order import Order 5 | 6 | 7 | class MenuRepository(ABC): 8 | @abstractmethod 9 | def get_all(self) -> List[Menu]: 10 | pass 11 | 12 | @abstractmethod 13 | def get_by_id(self, menu_id: str) -> Optional[Menu]: 14 | pass 15 | 16 | @abstractmethod 17 | def update(self, menu: Menu) -> None: 18 | pass 19 | 20 | 21 | class OrderRepository(ABC): 22 | @abstractmethod 23 | def create(self, order: Order) -> None: 24 | pass 25 | 26 | @abstractmethod 27 | def get_by_id(self, order_id: str) -> Optional[Order]: 28 | pass 29 | 30 | @abstractmethod 31 | def update(self, order: Order) -> None: 32 | pass -------------------------------------------------------------------------------- /ch10/simple-prompt/src/infrastructure/models.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import UnicodeAttribute, NumberAttribute, UTCDateTimeAttribute 3 | from datetime import datetime 4 | 5 | class MenuModel(Model): 6 | class Meta: 7 | table_name = 'Menu' 8 | region = 'ap-northeast-2' 9 | 10 | id = UnicodeAttribute(hash_key=True) 11 | name = UnicodeAttribute() 12 | price = NumberAttribute() 13 | category = UnicodeAttribute() 14 | stock = NumberAttribute() 15 | 16 | class OrderModel(Model): 17 | class Meta: 18 | table_name = 'Order' 19 | region = 'ap-northeast-2' 20 | 21 | id = UnicodeAttribute(hash_key=True) 22 | customer_id = UnicodeAttribute() 23 | menu_id = UnicodeAttribute() 24 | quantity = NumberAttribute() 25 | status = UnicodeAttribute() 26 | created_at = UTCDateTimeAttribute() 27 | updated_at = UTCDateTimeAttribute(null=True) -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/entities/order.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional, Dict 4 | 5 | 6 | @dataclass 7 | class Order: 8 | order_id: str 9 | customer_id: str 10 | menu_id: str 11 | quantity: int 12 | status: str 13 | options: Dict[str, str] 14 | created_at: datetime 15 | 16 | @classmethod 17 | def create(cls, order_id: str, customer_id: str, menu_id: str, 18 | quantity: int, options: Dict[str, str]) -> 'Order': 19 | return cls( 20 | order_id=order_id, 21 | customer_id=customer_id, 22 | menu_id=menu_id, 23 | quantity=quantity, 24 | status="created", 25 | options=options, 26 | created_at=datetime.utcnow() 27 | ) 28 | 29 | def update_status(self, new_status: str) -> None: 30 | self.status = new_status -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/application/controllers/menu_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from ...domain.interfaces.boundaries import GetMenusInputBoundary, OutputBoundary 3 | 4 | router = APIRouter() 5 | 6 | 7 | class MenuController: 8 | def __init__( 9 | self, 10 | get_menus_usecase: GetMenusInputBoundary, 11 | presenter: OutputBoundary 12 | ): 13 | self._get_menus_usecase = get_menus_usecase 14 | self._presenter = presenter 15 | 16 | async def get_menus(self): 17 | try: 18 | menus = self._get_menus_usecase.execute() 19 | return self._presenter.present_success( 20 | data=menus, 21 | message="Menus retrieved successfully" 22 | ) 23 | except Exception as e: 24 | return self._presenter.present_error( 25 | message="Database error occurred" 26 | ) -------------------------------------------------------------------------------- /ch06/6_2_controller/type2/presentation/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, HTTPException 2 | from pydantic import BaseModel 3 | from .usecases import CalculateAverageUseCase # CalculateAverageUseCase 클래스가 존재한다고 가정 4 | 5 | app = FastAPI() 6 | 7 | 8 | class ScoreRequest(BaseModel): 9 | student_id: str 10 | calculation_type: str 11 | 12 | 13 | @app.post("/calculate") 14 | async def calculate_score(request: ScoreRequest): 15 | try: 16 | if request.calculation_type != "average": 17 | raise HTTPException(status_code=400, detail="Unsupported calculation type") 18 | 19 | # 유스케이스 직접 호출 20 | usecase = CalculateAverageUseCase() 21 | average = usecase.execute(request.student_id) 22 | 23 | # 응답 변환 24 | return {"student_id": request.student_id, "result": average, "status": "success"} 25 | except Exception as e: 26 | raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") 27 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/controllers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from .usecase import ScoreInputDTO 3 | 4 | 5 | @dataclass 6 | class ScoreRequestDTO: 7 | student_id: str 8 | calculation_type: str 9 | 10 | 11 | @dataclass 12 | class ScoreResponseDTO: 13 | student_id: str 14 | result: float 15 | status: str 16 | 17 | 18 | class CalculateScoreController: 19 | def __init__(self, calculate_average_usecase: "CalculateAverageUseCase"): 20 | self.calculate_average_usecase = calculate_average_usecase 21 | 22 | def execute(self, request): 23 | if request.calculation_type != "average": 24 | raise ValueError("Unsupported calculation type") 25 | 26 | # 입력 DTO를 유스케이스에 전달 27 | input_dto = ScoreInputDTO(student_id=request.student_id) 28 | self.calculate_average_usecase.execute(input_dto) 29 | 30 | # 프레젠터에서 결과 가져와서 응답 31 | return self.calculate_average_usecase.presenter.present() 32 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/main.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncGenerator 3 | 4 | from fastapi import FastAPI 5 | from mangum import Mangum 6 | 7 | from .config.database import get_db 8 | from .config.dependencies import Dependencies 9 | 10 | 11 | @asynccontextmanager 12 | async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: 13 | # Startup: Register routes 14 | db = next(get_db()) 15 | menu_controller, order_controller, customer_controller = Dependencies.configure(db) 16 | app.include_router(menu_controller.register_routes()) 17 | app.include_router(order_controller.register_routes()) 18 | app.include_router(customer_controller.register_routes()) 19 | yield 20 | # Shutdown: Clean up resources 21 | db.close() 22 | 23 | 24 | app = FastAPI( 25 | title="Coffee Order API", 26 | lifespan=lifespan, 27 | ) 28 | 29 | # AWS Lambda handler 30 | handler = Mangum(app) 31 | -------------------------------------------------------------------------------- /ch09/application/ports/outbound/__init__.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod, ABC 2 | 3 | from application.dtos import OutputDto 4 | from domain.shared.aggregate_root import DomainEvent 5 | 6 | 7 | class OutputBoundary(metaclass=ABCMeta): 8 | @abstractmethod 9 | def set_result(self, output_dto: OutputDto) -> None: 10 | pass 11 | 12 | @abstractmethod 13 | def present(self) -> dict: 14 | pass 15 | 16 | 17 | class DomainEventPublisher(ABC): 18 | """도메인 이벤트를 버스/구독자에게 발행하기 위한 아웃바운드 포트입니다.""" 19 | 20 | @abstractmethod 21 | def publish(self, event: DomainEvent) -> None: 22 | pass 23 | 24 | @abstractmethod 25 | def publish_all(self, events: list[DomainEvent]) -> None: 26 | pass 27 | 28 | 29 | class PaymentGateway(ABC): 30 | """외부 결제 게이트웨이 연동을 위한 아웃바운드 포트입니다.""" 31 | 32 | @abstractmethod 33 | def approve(self, order_id: str, amount: int, currency: str) -> bool: 34 | """결제 승인 시도. 성공 여부를 반환합니다.""" 35 | pass 36 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt/src/infrastructure/models.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import ( 3 | UnicodeAttribute, 4 | NumberAttribute, 5 | UTCDateTimeAttribute, 6 | MapAttribute, 7 | ) 8 | from datetime import datetime 9 | 10 | 11 | class MenuModel(Model): 12 | class Meta: 13 | table_name = "menus" 14 | region = "ap-northeast-2" 15 | 16 | menu_id = UnicodeAttribute(hash_key=True) 17 | name = UnicodeAttribute() 18 | price = NumberAttribute() 19 | category = UnicodeAttribute() 20 | stock = NumberAttribute() 21 | 22 | 23 | class OrderModel(Model): 24 | class Meta: 25 | table_name = "orders" 26 | region = "ap-northeast-2" 27 | 28 | order_id = UnicodeAttribute(hash_key=True) 29 | customer_id = UnicodeAttribute() 30 | menu_id = UnicodeAttribute() 31 | quantity = NumberAttribute() 32 | options = MapAttribute() 33 | status = UnicodeAttribute() 34 | created_at = UTCDateTimeAttribute() -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/usecases/menu_usecases.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from ..dtos.menu_dtos import MenuListResponseDTO, MenuResponseDTO 4 | from ..repositories.menu_repository import MenuRepository 5 | 6 | 7 | class GetMenuListUseCase(ABC): 8 | @abstractmethod 9 | def execute(self) -> MenuListResponseDTO: 10 | pass 11 | 12 | 13 | class GetMenuListUseCaseImpl(GetMenuListUseCase): 14 | def __init__(self, menu_repository: MenuRepository): 15 | self.menu_repository = menu_repository 16 | 17 | def execute(self) -> MenuListResponseDTO: 18 | menus = self.menu_repository.get_all() 19 | menu_dtos = [ 20 | MenuResponseDTO( 21 | menu_id=menu.menu_id, 22 | name=menu.name, 23 | price=menu.price, 24 | category=menu.category, 25 | ) 26 | for menu in menus 27 | ] 28 | return MenuListResponseDTO(menus=menu_dtos) 29 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt/README.md: -------------------------------------------------------------------------------- 1 | # Coffee Order API 2 | 3 | 클린 아키텍처를 적용한 온라인 커피 주문 API 서비스입니다. 4 | 5 | ## 기술 스택 6 | - Python 3.9+ 7 | - FastAPI 8 | - DynamoDB (pynamodb) 9 | - AWS Lambda 10 | - Serverless Framework 11 | 12 | ## 프로젝트 구조 13 | ``` 14 | src/ 15 | ├── domain/ # 엔티티와 리포지토리 인터페이스 16 | ├── infrastructure/ # 리포지토리 구현체 17 | ├── application/ # 유스케이스 18 | └── interfaces/ # API 엔드포인트와 스키마 19 | tests/ 20 | ├── unit/ # 단위 테스트 21 | └── integration/ # 통합 테스트 22 | ``` 23 | 24 | ## 설치 및 실행 25 | 26 | 1. 의존성 설치: 27 | ```bash 28 | pip install -r requirements.txt 29 | ``` 30 | 31 | 2. 환경 변수 설정: 32 | ```bash 33 | cp .env.example .env 34 | # .env 파일을 적절히 수정 35 | ``` 36 | 37 | 3. 로컬 실행: 38 | ```bash 39 | uvicorn src.interfaces.api:app --reload 40 | ``` 41 | 42 | 4. 배포: 43 | ```bash 44 | serverless deploy 45 | ``` 46 | 47 | ## API 엔드포인트 48 | 49 | - GET /menu - 메뉴 목록 조회 50 | - POST /order - 커피 주문 생성 51 | - GET /order/{orderId} - 주문 상태 확인 52 | 53 | ## 테스트 실행 54 | 55 | ```bash 56 | pytest tests/ 57 | ``` -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/interfaces/repositories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | from ..entities.menu import Menu 4 | from ..entities.order import Order 5 | 6 | class MenuRepository(ABC): 7 | @abstractmethod 8 | def get_all(self) -> List[Menu]: 9 | """모든 메뉴 조회""" 10 | pass 11 | 12 | @abstractmethod 13 | def get_by_id(self, menu_id: str) -> Optional[Menu]: 14 | """ID로 메뉴 조회""" 15 | pass 16 | 17 | @abstractmethod 18 | def update(self, menu: Menu) -> None: 19 | """메뉴 정보 업데이트""" 20 | pass 21 | 22 | class OrderRepository(ABC): 23 | @abstractmethod 24 | def create(self, order: Order) -> None: 25 | """주문 생성""" 26 | pass 27 | 28 | @abstractmethod 29 | def get_by_id(self, order_id: str) -> Optional[Order]: 30 | """ID로 주문 조회""" 31 | pass 32 | 33 | @abstractmethod 34 | def update(self, order: Order) -> None: 35 | """주문 정보 업데이트""" 36 | pass -------------------------------------------------------------------------------- /ch06/6_6_entity/README.md: -------------------------------------------------------------------------------- 1 | ## 실행 방법 2 | 3 | ### 1. FastAPI 서버 실행 4 | ```bash 5 | cd /Users/alphahacker/clean-architecture-with-python-test-3 6 | python -m uvicorn ch06.6_6_entity.fastapi:app --reload --port 8000 7 | ``` 8 | 9 | ### 2. API 테스트 10 | ```bash 11 | curl -X POST "http://localhost:8000/calculate" \ 12 | -H "Content-Type: application/json" \ 13 | -d '{"student_id": "student001", "calculation_type": "average"}' 14 | 15 | # 유효하지 않은 점수 (음수) 16 | curl -X POST "http://localhost:8000/calculate" \ 17 | -H "Content-Type: application/json" \ 18 | -d '{"student_id": "student_invalid", "calculation_type": "average"}' 19 | 20 | # 유효하지 않은 점수 (100점 초과) 21 | curl -X POST "http://localhost:8000/calculate" \ 22 | -H "Content-Type: application/json" \ 23 | -d '{"student_id": "student_over100", "calculation_type": "average"}' 24 | 25 | # 빈 데이터 26 | curl -X POST "http://localhost:8000/calculate" \ 27 | -H "Content-Type: application/json" \ 28 | -d '{"student_id": "student_empty", "calculation_type": "average"}' 29 | ``` -------------------------------------------------------------------------------- /ch09/adapter/events/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict, List, DefaultDict 2 | from collections import defaultdict 3 | 4 | from application.ports.outbound import DomainEventPublisher 5 | from domain.shared.aggregate_root import DomainEvent 6 | 7 | 8 | class InMemoryEventBus(DomainEventPublisher): 9 | """데모/테스트를 위한 최소한의 인메모리 퍼브/섭(pub/sub) 버스입니다. 10 | 11 | 프로덕션 용도는 아니며, 이벤트 드리븐 흐름을 설명하기 위한 간단한 구현입니다. 12 | """ 13 | 14 | def __init__(self) -> None: 15 | self._subscribers: DefaultDict[str, List[Callable[[DomainEvent], None]]] = defaultdict(list) 16 | 17 | def subscribe(self, event_name: str, handler: Callable[[DomainEvent], None]) -> None: 18 | self._subscribers[event_name].append(handler) 19 | 20 | def publish(self, event: DomainEvent) -> None: 21 | for handler in list(self._subscribers.get(event.name, [])): 22 | handler(event) 23 | 24 | def publish_all(self, events: List[DomainEvent]) -> None: 25 | for event in events: 26 | self.publish(event) 27 | 28 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/views.py: -------------------------------------------------------------------------------- 1 | # Views Layer 2 | from .viewmodels import ScoreViewModel 3 | 4 | 5 | class WebScoreView: 6 | def display(self, view_model: ScoreViewModel) -> str: 7 | styled_grade = f"{view_model.grade}" 8 | return f""" 9 |
10 |

성적 결과: {view_model.student_id}

11 |
12 |

평균: {view_model.average}

13 |

등급: {styled_grade}

14 |

상태: {view_model.status}

15 |
16 |
17 | """ 18 | 19 | 20 | class ApiScoreView: 21 | def display(self, view_model: ScoreViewModel) -> dict: 22 | return { 23 | "student_id": view_model.student_id, 24 | "average": view_model.average, 25 | "grade": view_model.grade, 26 | "status": view_model.status, 27 | } 28 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/fastapi.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from pydantic import BaseModel 3 | from .dependency_injection import initialize_application, initialize_web_application 4 | from .controllers import ScoreRequestDTO 5 | 6 | app = FastAPI() 7 | 8 | # 각 형식별로 별도의 컨트롤러 초기화 9 | api_controller = initialize_application() 10 | web_controller = initialize_web_application() 11 | 12 | 13 | class ScoreRequest(BaseModel): 14 | student_id: str 15 | calculation_type: str 16 | 17 | 18 | @app.post("/calculate/api") 19 | async def calculate_score_api(request: ScoreRequest): 20 | """API 형식으로 응답""" 21 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 22 | return api_controller.execute(request_dto) 23 | 24 | 25 | @app.post("/calculate/web") 26 | async def calculate_score_web(request: ScoreRequest): 27 | """HTML 형식으로 응답""" 28 | request_dto = ScoreRequestDTO(student_id=request.student_id, calculation_type=request.calculation_type) 29 | return web_controller.execute(request_dto) 30 | -------------------------------------------------------------------------------- /ch07/adapter/presenter/create_order_presenter.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from application.dtos import CreateOrderOutputDto 4 | from application.ports.outbound import OutputBoundary 5 | 6 | 7 | class CreateOrderPresenter(OutputBoundary): 8 | def __init__(self): 9 | self._result = None 10 | 11 | def set_result(self, output_dto: CreateOrderOutputDto) -> None: 12 | self._result = output_dto 13 | 14 | def present(self) -> Dict: 15 | if not self._result: 16 | return {"status": "error", "message": "No result to present"} 17 | 18 | return { 19 | "status": "success", 20 | "data": { 21 | "orderId": self._result.order_id, 22 | "customerId": self._result.customer_id, 23 | "totalAmount": self._result.total_amount, 24 | "currency": self._result.currency, 25 | "status": self._result.status, 26 | "items": self._result.items, 27 | }, 28 | "message": "order created successfully", 29 | } 30 | -------------------------------------------------------------------------------- /ch09/adapter/presenter/create_order_presenter.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from application.dtos import CreateOrderOutputDto 4 | from application.ports.outbound import OutputBoundary 5 | 6 | 7 | class CreateOrderPresenter(OutputBoundary): 8 | def __init__(self): 9 | self._result = None 10 | 11 | def set_result(self, output_dto: CreateOrderOutputDto) -> None: 12 | self._result = output_dto 13 | 14 | def present(self) -> Dict: 15 | if not self._result: 16 | return {"status": "error", "message": "No result to present"} 17 | 18 | return { 19 | "status": "success", 20 | "data": { 21 | "orderId": self._result.order_id, 22 | "customerId": self._result.customer_id, 23 | "totalAmount": self._result.total_amount, 24 | "currency": self._result.currency, 25 | "status": self._result.status, 26 | "items": self._result.items, 27 | }, 28 | "message": "order created successfully", 29 | } 30 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/infrastructure/repositories/dynamodb_models.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import ( 3 | UnicodeAttribute, NumberAttribute, UTCDateTimeAttribute, MapAttribute 4 | ) 5 | import os 6 | 7 | 8 | class MenuModel(Model): 9 | class Meta: 10 | table_name = f"coffee-order-api-{os.getenv('STAGE', 'dev')}-menus" 11 | region = 'ap-northeast-2' 12 | 13 | menu_id = UnicodeAttribute(hash_key=True) 14 | name = UnicodeAttribute() 15 | price = NumberAttribute() 16 | category = UnicodeAttribute() 17 | stock = NumberAttribute() 18 | 19 | 20 | class OrderModel(Model): 21 | class Meta: 22 | table_name = f"coffee-order-api-{os.getenv('STAGE', 'dev')}-orders" 23 | region = 'ap-northeast-2' 24 | 25 | order_id = UnicodeAttribute(hash_key=True) 26 | customer_id = UnicodeAttribute() 27 | menu_id = UnicodeAttribute() 28 | quantity = NumberAttribute() 29 | status = UnicodeAttribute() 30 | options = MapAttribute() 31 | created_at = UTCDateTimeAttribute() -------------------------------------------------------------------------------- /ch09/adapter/gateway/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from application.ports.outbound import PaymentGateway 4 | 5 | 6 | class InMemoryPaymentGateway(PaymentGateway): 7 | """데모를 위한 인메모리 결제 게이트웨이. 항상 승인합니다.""" 8 | 9 | def approve(self, order_id: str, amount: int, currency: str) -> bool: # noqa: ARG002 (단순 데모) 10 | # 실제로는 외부 API 호출 11 | # return requests.post("https://pg.example.com/approve", ...) 12 | return True # 데모용 항상 성공 13 | 14 | 15 | class TozzPaymentGateway(PaymentGateway): 16 | """데모를 위한 Tozz 결제 게이트웨이 예시""" 17 | def approve(self, order_id: str, amount: int, currency: str) -> bool: 18 | try: 19 | response = requests.post( 20 | "https://api.tozzpayments.com/v1/payments", 21 | json={"amount": amount, "orderId": order_id}, 22 | timeout=5 # 5초 제한 23 | ) 24 | return response.status_code == 200 25 | except (requests.Timeout, requests.ConnectionError): 26 | return False # 네트워크 장애 27 | except Exception: 28 | return False # PG사 서버 오류 29 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/entities/order.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Dict, Optional 4 | 5 | 6 | @dataclass 7 | class Order: 8 | order_id: str 9 | customer_id: str 10 | menu_id: str 11 | quantity: int 12 | status: str 13 | options: Dict[str, str] 14 | created_at: datetime 15 | updated_at: Optional[datetime] = None 16 | 17 | def update_status(self, new_status: str) -> None: 18 | self.status = new_status 19 | self.updated_at = datetime.utcnow() 20 | 21 | @staticmethod 22 | def create( 23 | order_id: str, 24 | customer_id: str, 25 | menu_id: str, 26 | quantity: int, 27 | options: Dict[str, str], 28 | ) -> "Order": 29 | return Order( 30 | order_id=order_id, 31 | customer_id=customer_id, 32 | menu_id=menu_id, 33 | quantity=quantity, 34 | status="created", 35 | options=options, 36 | created_at=datetime.utcnow(), 37 | ) 38 | -------------------------------------------------------------------------------- /ch06/6_6_entity/controllers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from .usecase import ScoreInputDTO 3 | 4 | 5 | @dataclass 6 | class ScoreRequestDTO: 7 | student_id: str 8 | calculation_type: str 9 | 10 | 11 | @dataclass 12 | class ScoreResponseDTO: 13 | student_id: str 14 | result: float 15 | status: str 16 | 17 | 18 | class CalculateScoreController: 19 | def __init__(self, calculate_average_usecase: "CalculateAverageUseCase"): 20 | self.calculate_average_usecase = calculate_average_usecase 21 | 22 | def execute(self, request): 23 | try: 24 | if request.calculation_type != "average": 25 | raise ValueError("Unsupported calculation type") 26 | 27 | # 입력 DTO를 유스케이스에 전달 28 | input_dto = ScoreInputDTO(student_id=request.student_id) 29 | self.calculate_average_usecase.execute(input_dto) 30 | 31 | # 프레젠터에서 결과 가져와서 응답 32 | return self.calculate_average_usecase.presenter.present() 33 | except Exception: 34 | import traceback 35 | 36 | print(traceback.format_exc()) 37 | -------------------------------------------------------------------------------- /ch06/6_5_external/controllers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from .usecase import ScoreInputDTO 3 | 4 | 5 | @dataclass 6 | class ScoreRequestDTO: 7 | student_id: str 8 | calculation_type: str 9 | 10 | 11 | @dataclass 12 | class ScoreResponseDTO: 13 | student_id: str 14 | result: float 15 | status: str 16 | 17 | 18 | class CalculateScoreController: 19 | def __init__(self, calculate_average_usecase: "CalculateAverageUseCase"): 20 | self.calculate_average_usecase = calculate_average_usecase 21 | 22 | def execute(self, request): 23 | try: 24 | if request.calculation_type != "average": 25 | raise ValueError("Unsupported calculation type") 26 | 27 | # 입력 DTO를 유스케이스에 전달 28 | input_dto = ScoreInputDTO(student_id=request.student_id) 29 | self.calculate_average_usecase.execute(input_dto) 30 | 31 | # 프레젠터에서 결과 가져와서 응답 32 | return self.calculate_average_usecase.presenter.present() 33 | except Exception: 34 | import traceback 35 | 36 | print(traceback.format_exc()) 37 | -------------------------------------------------------------------------------- /ch09/domain/shared/aggregate_root.py: -------------------------------------------------------------------------------- 1 | from typing import List, Any 2 | from datetime import datetime, UTC 3 | 4 | 5 | class DomainEvent: 6 | """DDD 도메인 이벤트의 기본 타입. 7 | 8 | 의도적으로 단순하게 유지합니다: 이벤트 이름, 발생 시각(UTC), 임의의 페이로드. 9 | """ 10 | 11 | def __init__(self, name: str, payload: Any): 12 | self.name = name 13 | self.payload = payload 14 | self.occurred_on = datetime.now(UTC) 15 | 16 | def __repr__(self) -> str: 17 | return f"DomainEvent(name={self.name}, occurred_on={self.occurred_on.isoformat()}, payload={self.payload})" 18 | 19 | 20 | class AggregateRoot: 21 | """도메인 이벤트를 수집하는 애그리게이트 루트 기본 클래스. 22 | 23 | 애플리케이션 계층에서 `pull_domain_events()`를 호출해 이벤트를 발행/전달할 수 있습니다. 24 | """ 25 | 26 | def __init__(self) -> None: 27 | self._domain_events: List[DomainEvent] = [] 28 | 29 | def _record_event(self, event: DomainEvent) -> None: 30 | self._domain_events.append(event) 31 | 32 | def pull_domain_events(self) -> List[DomainEvent]: 33 | events = list(self._domain_events) 34 | self._domain_events.clear() 35 | return events 36 | 37 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt/src/domain/repositories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | 4 | from .entities import Menu, Order 5 | 6 | 7 | class MenuRepository(ABC): 8 | @abstractmethod 9 | async def get_all(self) -> List[Menu]: 10 | pass 11 | 12 | @abstractmethod 13 | async def get_by_id(self, menu_id: str) -> Optional[Menu]: 14 | pass 15 | 16 | @abstractmethod 17 | async def update_stock(self, menu_id: str, quantity: int) -> bool: 18 | """ 19 | 메뉴의 재고를 업데이트합니다. 20 | 21 | Args: 22 | menu_id: 메뉴 ID 23 | quantity: 차감할 수량 (음수) 24 | 25 | Returns: 26 | bool: 재고 업데이트 성공 여부 27 | """ 28 | pass 29 | 30 | 31 | class OrderRepository(ABC): 32 | @abstractmethod 33 | async def create(self, order: Order) -> Order: 34 | pass 35 | 36 | @abstractmethod 37 | async def get_by_id(self, order_id: str) -> Optional[Order]: 38 | pass 39 | 40 | @abstractmethod 41 | async def update_status(self, order_id: str, status: str) -> bool: 42 | pass -------------------------------------------------------------------------------- /ch09/adapter/events/sns_publisher.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import List 4 | 5 | import boto3 6 | 7 | from application.ports.outbound import DomainEventPublisher 8 | from domain.shared.aggregate_root import DomainEvent 9 | 10 | 11 | class SnsDomainEventPublisher(DomainEventPublisher): 12 | """AWS SNS로 도메인 이벤트를 발행하는 어댑터. 13 | 14 | 환경 변수 `SNS_TOPIC_ARN`에서 토픽 ARN을 읽습니다. 15 | """ 16 | 17 | def __init__(self) -> None: 18 | topic_arn = os.getenv("SNS_TOPIC_ARN") 19 | if not topic_arn: 20 | raise RuntimeError("SNS_TOPIC_ARN 환경 변수가 설정되어 있지 않습니다") 21 | self._topic_arn = topic_arn 22 | self._client = boto3.client("sns") 23 | 24 | def publish(self, event: DomainEvent) -> None: 25 | message = { 26 | "name": event.name, 27 | "occurred_on": event.occurred_on.isoformat(), 28 | "payload": event.payload, 29 | } 30 | self._client.publish(TopicArn=self._topic_arn, Message=json.dumps(message)) 31 | 32 | def publish_all(self, events: List[DomainEvent]) -> None: 33 | for e in events: 34 | self.publish(e) 35 | 36 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/mysql_customer_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from ...domain.entities.customer import Customer 6 | from ...domain.repositories.customer_repository import CustomerRepository 7 | from ..database.models.customer import CustomerModel 8 | 9 | 10 | class MySQLCustomerRepository(CustomerRepository): 11 | def __init__(self, db: Session) -> None: 12 | self.db = db 13 | 14 | def get_by_id(self, customer_id: str) -> Optional[Customer]: 15 | customer_model = ( 16 | self.db.query(CustomerModel) 17 | .filter(CustomerModel.customer_id == customer_id) 18 | .first() 19 | ) 20 | if not customer_model: 21 | return None 22 | 23 | return Customer( 24 | customer_id=customer_model.customer_id, 25 | name=customer_model.name, 26 | email=customer_model.email, 27 | phone=customer_model.phone, 28 | created_at=customer_model.created_at, 29 | updated_at=customer_model.updated_at, 30 | ) 31 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/domain/usecases/customer_usecases.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Optional 3 | 4 | from ..dtos.customer_dtos import CustomerResponseDTO 5 | from ..repositories.customer_repository import CustomerRepository 6 | 7 | 8 | class GetCustomerUseCase(ABC): 9 | @abstractmethod 10 | def execute(self, customer_id: str) -> Optional[CustomerResponseDTO]: 11 | pass 12 | 13 | 14 | class GetCustomerUseCaseImpl(GetCustomerUseCase): 15 | def __init__(self, customer_repository: CustomerRepository) -> None: 16 | self.customer_repository = customer_repository 17 | 18 | def execute(self, customer_id: str) -> Optional[CustomerResponseDTO]: 19 | customer = self.customer_repository.get_by_id(customer_id) 20 | if not customer: 21 | return None 22 | 23 | return CustomerResponseDTO( 24 | customer_id=customer.customer_id, 25 | name=customer.name, 26 | email=customer.email, 27 | phone=customer.phone, 28 | created_at=customer.created_at, 29 | updated_at=customer.updated_at, 30 | ) 31 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/controllers.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from .usecase import ScoreInputDTO 3 | 4 | 5 | @dataclass 6 | class ScoreRequestDTO: 7 | student_id: str 8 | calculation_type: str 9 | 10 | 11 | @dataclass 12 | class ScoreResponseDTO: 13 | student_id: str 14 | result: float 15 | status: str 16 | 17 | 18 | class CalculateScoreController: 19 | def __init__(self, calculate_average_usecase: "CalculateAverageUseCase"): 20 | self.calculate_average_usecase = calculate_average_usecase 21 | 22 | def execute(self, request): 23 | try: 24 | if request.calculation_type != "average": 25 | raise ValueError("Unsupported calculation type") 26 | 27 | # 입력 DTO를 유스케이스에 전달 28 | input_dto = ScoreInputDTO(student_id=request.student_id) 29 | self.calculate_average_usecase.execute(input_dto) 30 | 31 | # 프레젠터에서 결과 가져와서 응답 32 | return self.calculate_average_usecase.presenter.present() 33 | 34 | except Exception: 35 | import traceback 36 | 37 | print(traceback.format_exc()) 38 | return {"error": "Internal server error"} 39 | -------------------------------------------------------------------------------- /ch07/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import UnicodeAttribute, NumberAttribute 3 | 4 | 5 | class MenuItemModel(Model): 6 | class Meta: 7 | table_name = "MenuItems" 8 | region = "us-east-1" 9 | 10 | id = UnicodeAttribute(hash_key=True) 11 | name = UnicodeAttribute() 12 | price = NumberAttribute() 13 | description = UnicodeAttribute() 14 | stock = NumberAttribute() 15 | 16 | 17 | class OrderModel(Model): 18 | class Meta: 19 | table_name = "Orders" 20 | region = "us-east-1" 21 | 22 | id = UnicodeAttribute(hash_key=True) 23 | customer_id = UnicodeAttribute() 24 | item_id = UnicodeAttribute(null=True) # 단일 OrderItem ID 25 | total_amount = NumberAttribute() 26 | status = UnicodeAttribute() 27 | created_at = UnicodeAttribute() 28 | 29 | 30 | class OrderItemModel(Model): 31 | class Meta: 32 | table_name = "OrderItems" 33 | region = "us-east-1" 34 | 35 | id = UnicodeAttribute(hash_key=True) 36 | order_id = UnicodeAttribute(range_key=True) 37 | menu_item_id = UnicodeAttribute() 38 | quantity = NumberAttribute() 39 | unit_price = NumberAttribute() 40 | -------------------------------------------------------------------------------- /ch09/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import UnicodeAttribute, NumberAttribute 3 | 4 | 5 | class MenuItemModel(Model): 6 | class Meta: 7 | table_name = "MenuItems" 8 | region = "us-east-1" 9 | 10 | id = UnicodeAttribute(hash_key=True) 11 | name = UnicodeAttribute() 12 | price = NumberAttribute() 13 | description = UnicodeAttribute() 14 | stock = NumberAttribute() 15 | 16 | 17 | class OrderModel(Model): 18 | class Meta: 19 | table_name = "Orders" 20 | region = "us-east-1" 21 | 22 | id = UnicodeAttribute(hash_key=True) 23 | customer_id = UnicodeAttribute() 24 | item_id = UnicodeAttribute(null=True) # 단일 OrderItem ID 25 | total_amount = NumberAttribute() 26 | status = UnicodeAttribute() 27 | created_at = UnicodeAttribute() 28 | 29 | 30 | class OrderItemModel(Model): 31 | class Meta: 32 | table_name = "OrderItems" 33 | region = "us-east-1" 34 | 35 | id = UnicodeAttribute(hash_key=True) 36 | order_id = UnicodeAttribute(range_key=True) 37 | menu_item_id = UnicodeAttribute() 38 | quantity = NumberAttribute() 39 | unit_price = NumberAttribute() 40 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/README.md: -------------------------------------------------------------------------------- 1 | # 커피 주문 API 2 | 3 | 클린 아키텍처를 적용한 커피 주문 API 프로젝트입니다. 4 | 5 | ## 기술 스택 6 | 7 | - Python 3.9 8 | - FastAPI 9 | - DynamoDB (pynamodb) 10 | - AWS Lambda 11 | - Serverless Framework 12 | 13 | ## 프로젝트 구조 14 | 15 | ``` 16 | src/ 17 | ├── application/ # 애플리케이션 계층 18 | │ └── controllers/ # API 컨트롤러 19 | ├── domain/ # 도메인 계층 20 | │ ├── entities/ # 엔티티 21 | │ ├── interfaces/ # 인터페이스 22 | │ └── usecases/ # 유스케이스 23 | ├── infrastructure/ # 인프라스트럭처 계층 24 | │ ├── presenters/ # 프레젠터 25 | │ └── repositories/ # 레포지토리 구현체 26 | └── config/ # 설정 27 | └── dependencies.py # 의존성 주입 28 | 29 | tests/ 30 | ├── integration/ # 통합 테스트 31 | └── unit/ # 단위 테스트 32 | ``` 33 | 34 | ## API 엔드포인트 35 | 36 | - GET /menu: 메뉴 목록 조회 37 | - POST /order: 주문 생성 38 | - GET /order/{orderId}: 주문 상태 조회 39 | 40 | ## 설치 및 실행 41 | 42 | 1. 의존성 설치: 43 | ```bash 44 | pip install -r requirements.txt 45 | ``` 46 | 47 | 2. 로컬 실행: 48 | ```bash 49 | uvicorn src.main:app --reload 50 | ``` 51 | 52 | 3. 배포: 53 | ```bash 54 | serverless deploy 55 | ``` 56 | 57 | ## 테스트 실행 58 | 59 | ```bash 60 | python -m unittest discover tests 61 | ``` -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/infrastructure/repositories/dynamodb_models.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import ( 3 | UnicodeAttribute, NumberAttribute, 4 | UTCDateTimeAttribute, MapAttribute 5 | ) 6 | from datetime import datetime 7 | 8 | class MenuModel(Model): 9 | class Meta: 10 | table_name = 'menus' 11 | region = 'ap-northeast-2' 12 | 13 | menu_id = UnicodeAttribute(hash_key=True) 14 | name = UnicodeAttribute() 15 | price = NumberAttribute() 16 | category = UnicodeAttribute() 17 | stock = NumberAttribute() 18 | created_at = UTCDateTimeAttribute(default=datetime.utcnow) 19 | updated_at = UTCDateTimeAttribute(default=datetime.utcnow) 20 | 21 | class OrderModel(Model): 22 | class Meta: 23 | table_name = 'orders' 24 | region = 'ap-northeast-2' 25 | 26 | order_id = UnicodeAttribute(hash_key=True) 27 | customer_id = UnicodeAttribute() 28 | menu_id = UnicodeAttribute() 29 | quantity = NumberAttribute() 30 | status = UnicodeAttribute() 31 | options = MapAttribute() 32 | created_at = UTCDateTimeAttribute(default=datetime.utcnow) 33 | updated_at = UTCDateTimeAttribute(default=datetime.utcnow) -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/presentation/schemas/api_response.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Generic, Optional, TypeVar 3 | 4 | from pydantic import BaseModel 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | class APIResponse(BaseModel, Generic[T]): 10 | status: str 11 | message: str 12 | data: Optional[T] = None 13 | 14 | 15 | class MenuResponse(BaseModel): 16 | menu_id: str 17 | name: str 18 | price: int 19 | category: str 20 | 21 | 22 | class MenuListResponse(BaseModel): 23 | menus: list[MenuResponse] 24 | 25 | 26 | class CreateOrderRequest(BaseModel): 27 | customer_id: str 28 | menu_id: str 29 | quantity: int 30 | options: Dict[str, str] 31 | 32 | 33 | class OrderResponse(BaseModel): 34 | order_id: str 35 | status: str 36 | created_at: datetime 37 | updated_at: Optional[datetime] = None 38 | 39 | 40 | class CustomerResponse(BaseModel): 41 | customer_id: str 42 | name: str 43 | email: str 44 | phone: str 45 | created_at: datetime 46 | updated_at: Optional[datetime] = None 47 | 48 | 49 | class DeleteOrderResponse(BaseModel): 50 | order_id: str 51 | success: bool 52 | message: str 53 | -------------------------------------------------------------------------------- /ch07/domain/value_objects/money.py: -------------------------------------------------------------------------------- 1 | class Money: 2 | def __init__(self, amount: int, currency: str = "KRW"): 3 | if amount < 0: 4 | raise ValueError("금액은 0 이상이어야 합니다") 5 | if currency not in ["USD", "KRW"]: 6 | raise ValueError("지원되지 않는 통화입니다") 7 | self.amount = amount 8 | self.currency = currency 9 | 10 | def __add__(self, other: "Money") -> "Money": 11 | if self.currency != other.currency: 12 | raise ValueError("통화가 일치하지 않습니다") 13 | return Money(amount=self.amount + other.amount, currency=self.currency) 14 | 15 | def __mul__(self, quantity: int) -> "Money": 16 | if quantity < 0: 17 | raise ValueError("수량은 0 이상이어야 합니다") 18 | return Money(amount=self.amount * quantity, currency=self.currency) 19 | 20 | def __str__(self) -> str: 21 | return f"{self.amount} {self.currency}" 22 | 23 | def __repr__(self) -> str: 24 | return f"Money(amount={self.amount}, currency='{self.currency}')" 25 | 26 | def __eq__(self, other: object) -> bool: 27 | if not isinstance(other, Money): 28 | return False 29 | return self.amount == other.amount and self.currency == other.currency 30 | -------------------------------------------------------------------------------- /ch09/domain/value_objects/money.py: -------------------------------------------------------------------------------- 1 | class Money: 2 | def __init__(self, amount: int, currency: str = "KRW"): 3 | if amount < 0: 4 | raise ValueError("금액은 0 이상이어야 합니다") 5 | if currency not in ["USD", "KRW"]: 6 | raise ValueError("지원되지 않는 통화입니다") 7 | self.amount = amount 8 | self.currency = currency 9 | 10 | def __add__(self, other: "Money") -> "Money": 11 | if self.currency != other.currency: 12 | raise ValueError("통화가 일치하지 않습니다") 13 | return Money(amount=self.amount + other.amount, currency=self.currency) 14 | 15 | def __mul__(self, quantity: int) -> "Money": 16 | if quantity < 0: 17 | raise ValueError("수량은 0 이상이어야 합니다") 18 | return Money(amount=self.amount * quantity, currency=self.currency) 19 | 20 | def __str__(self) -> str: 21 | return f"{self.amount} {self.currency}" 22 | 23 | def __repr__(self) -> str: 24 | return f"Money(amount={self.amount}, currency='{self.currency}')" 25 | 26 | def __eq__(self, other: object) -> bool: 27 | if not isinstance(other, Money): 28 | return False 29 | return self.amount == other.amount and self.currency == other.currency 30 | -------------------------------------------------------------------------------- /ch06/6_2_controller/type1/controller/calculater_score_controller.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, Any 3 | from .usecases import UseCase # UseCase라는 추상클래스가 존재한다고 가정 4 | 5 | 6 | @dataclass 7 | class ScoreRequestDTO: 8 | student_id: str 9 | calculation_type: str 10 | 11 | 12 | @dataclass 13 | class ScoreResponseDTO: 14 | student_id: str 15 | result: float 16 | status: str 17 | 18 | 19 | class CalculateScoreController: 20 | def __init__(self, calculate_average_usecase: UseCase): 21 | self.calculate_average_usecase = calculate_average_usecase 22 | 23 | def execute(self, request: ScoreRequestDTO) -> ScoreResponseDTO: 24 | try: 25 | if request.calculation_type != "average": 26 | raise ValueError("Unsupported calculation type") 27 | 28 | # 입력 DTO를 유스케이스에 전달 29 | average = self.calculate_average_usecase.execute(request.student_id) 30 | 31 | # 출력 DTO로 변환 32 | return ScoreResponseDTO(student_id=request.student_id, result=average, status="success") 33 | except Exception as e: 34 | return ScoreResponseDTO(student_id=request.student_id, result=0.0, status=f"error: {str(e)}") 35 | -------------------------------------------------------------------------------- /ch09/application/usecases/process_payment_usecase.py: -------------------------------------------------------------------------------- 1 | from application.ports.repository import OrderRepository 2 | from application.ports.outbound import PaymentGateway 3 | from domain.entities.order import OrderStatus 4 | 5 | 6 | class ProcessPaymentUseCase: 7 | """주문 생성 이벤트를 받아 결제를 시도하고 주문 상태를 갱신하는 애플리케이션 서비스.""" 8 | 9 | def __init__(self, order_repo: OrderRepository, payment_gateway: PaymentGateway) -> None: 10 | self.order_repo = order_repo 11 | self.payment_gateway = payment_gateway 12 | 13 | def execute(self, order_id: str) -> None: 14 | order = self.order_repo.find_by_id(order_id) 15 | if order is None: 16 | return 17 | 18 | approved = self.payment_gateway.approve( 19 | order_id=order.id, 20 | amount=order.total_amount.amount, 21 | currency=order.total_amount.currency, 22 | ) 23 | 24 | if approved and order.status == OrderStatus.PENDING: 25 | order.change_status(OrderStatus.PREPARING) 26 | self.order_repo.save(order) 27 | elif not approved and order.status == OrderStatus.PENDING: 28 | order.change_status(OrderStatus.CANCELLED) 29 | self.order_repo.save(order) 30 | 31 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/tests/unit/test_menu_usecase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | from src.domain.entities.menu import Menu 4 | from src.domain.usecases.menu_usecases import GetMenusUseCase 5 | 6 | 7 | class TestGetMenusUseCase(unittest.TestCase): 8 | def setUp(self): 9 | self.menu_repository = Mock() 10 | self.usecase = GetMenusUseCase(self.menu_repository) 11 | 12 | def test_execute_returns_menu_list(self): 13 | # Arrange 14 | mock_menus = [ 15 | Menu(menu_id="1", name="Americano", price=3000, category="coffee", stock=10), 16 | Menu(menu_id="2", name="Latte", price=4000, category="coffee", stock=10) 17 | ] 18 | self.menu_repository.get_all.return_value = mock_menus 19 | 20 | # Act 21 | result = self.usecase.execute() 22 | 23 | # Assert 24 | self.assertEqual(len(result), 2) 25 | self.assertEqual(result[0].menu_id, "1") 26 | self.assertEqual(result[0].name, "Americano") 27 | self.assertEqual(result[0].price, 3000) 28 | self.assertEqual(result[0].category, "coffee") 29 | 30 | self.menu_repository.get_all.assert_called_once() -------------------------------------------------------------------------------- /ch06/6_7_presenter/presenter.py: -------------------------------------------------------------------------------- 1 | # Presenters Layer 2 | from abc import ABC, abstractmethod 3 | from typing import Any 4 | from .viewmodels import ScoreViewModel 5 | 6 | 7 | # 출력 인터페이스 (Presenter) 8 | class OutputBoundary(ABC): 9 | @abstractmethod 10 | def set_result(self, output_dto) -> None: 11 | pass 12 | 13 | @abstractmethod 14 | def present(self) -> Any: 15 | pass 16 | 17 | 18 | class ScorePresenter(OutputBoundary): 19 | def __init__(self, view): 20 | self.view = view 21 | self.output_dto = None 22 | 23 | def set_result(self, output_dto): 24 | self.output_dto = output_dto 25 | 26 | def present(self) -> Any: 27 | if not self.output_dto: 28 | raise ValueError("No result to present") 29 | 30 | # 등급 계산 31 | grade = "A" if self.output_dto.average >= 90 else "B" if self.output_dto.average >= 80 else "C" 32 | 33 | # ViewModel 생성 34 | view_model = ScoreViewModel( 35 | student_id=self.output_dto.student_id, 36 | average=f"{self.output_dto.average:.2f}점", 37 | status="성공" if self.output_dto.status == "success" else "실패", 38 | grade=grade, 39 | ) 40 | 41 | # View를 통해 표시 42 | return self.view.display(view_model) 43 | -------------------------------------------------------------------------------- /ch06/6_5_external/test_mocks.py: -------------------------------------------------------------------------------- 1 | # Test Mocks for Clean Architecture 2 | from typing import Dict, Any, List 3 | from .infrastructure import DynamoDBClient 4 | from .repository import ScoreRepository 5 | 6 | 7 | class MockDynamoDBClient(DynamoDBClient): 8 | """테스트용 Mock DynamoDB 클라이언트""" 9 | 10 | def __init__(self): 11 | self.mock_data = { 12 | "student001": {"Item": {"student_id": "student001", "scores": {"L": [85.5, 92.0, 78.5, 88.0]}}}, 13 | "student002": {"Item": {"student_id": "student002", "scores": {"L": [95.0, 87.5, 91.0]}}}, 14 | } 15 | 16 | def get_item(self, table_name: str, key: Dict[str, Any]) -> Dict[str, Any]: 17 | student_id = key.get("student_id") 18 | if student_id in self.mock_data: 19 | return self.mock_data[student_id] 20 | return {} 21 | 22 | 23 | class MockScoreRepository(ScoreRepository): 24 | """테스트용 Mock Repository""" 25 | 26 | def __init__(self): 27 | self.mock_scores = { 28 | "student001": [85.5, 92.0, 78.5, 88.0], 29 | "student002": [95.0, 87.5, 91.0], 30 | "student003": [76.0, 82.5, 79.0, 85.0], 31 | } 32 | 33 | def get_scores(self, student_id: str) -> List[float]: 34 | return self.mock_scores.get(student_id, []) 35 | -------------------------------------------------------------------------------- /ch09/infrastructure/handlers/sns_payment_handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Dict 3 | 4 | from adapter.repository import DynamoDBOrderRepository 5 | from adapter.gateway import InMemoryPaymentGateway 6 | from application.usecases.process_payment_usecase import ProcessPaymentUseCase 7 | 8 | 9 | def handler(event: Dict[str, Any], _context: Any) -> None: 10 | """SNS 구독 람다 핸들러 11 | 12 | - SNS 메시지를 수신하여 `OrderCreated` 이벤트를 식별합니다. 13 | - 결제 유스케이스를 호출하여 결제 승인/실패에 따른 주문 상태를 갱신합니다. 14 | """ 15 | 16 | order_repo = DynamoDBOrderRepository() 17 | payment_gateway = InMemoryPaymentGateway() # 데모용: 항상 승인 18 | usecase = ProcessPaymentUseCase(order_repo=order_repo, payment_gateway=payment_gateway) 19 | 20 | for record in event.get("Records", []): 21 | message_str = record.get("Sns", {}).get("Message") 22 | if not message_str: 23 | continue 24 | try: 25 | message = json.loads(message_str) 26 | except json.JSONDecodeError: 27 | continue 28 | 29 | name = message.get("name") 30 | payload = message.get("payload", {}) 31 | 32 | if name == "OrderCreated": 33 | order_id = payload.get("order_id") 34 | if order_id: 35 | usecase.execute(order_id) 36 | 37 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/README.md: -------------------------------------------------------------------------------- 1 | # 커피 주문 API 2 | 3 | 클린 아키텍처 원칙을 따르는 커피 주문 API 서비스입니다. 4 | 5 | ## 기술 스택 6 | 7 | - Python 3.9 8 | - FastAPI 9 | - DynamoDB (pynamodb) 10 | - AWS Lambda 11 | - Serverless Framework 12 | 13 | ## 프로젝트 구조 14 | 15 | ``` 16 | src/ 17 | ├── domain/ 18 | │ ├── entities/ # 비즈니스 엔티티 19 | │ ├── interfaces/ # 인터페이스 정의 20 | │ └── usecases/ # 비즈니스 유스케이스 21 | ├── infrastructure/ 22 | │ ├── repositories/ # 데이터베이스 구현체 23 | │ └── presenters/ # 프레젠터 구현체 24 | ├── application/ 25 | │ └── controllers/ # API 컨트롤러 26 | └── config/ # 의존성 주입 설정 27 | ``` 28 | 29 | ## API 엔드포인트 30 | 31 | ### GET /menus 32 | - 메뉴 목록 조회 33 | - 응답: 메뉴 ID, 이름, 가격, 카테고리 34 | 35 | ### POST /orders 36 | - 커피 주문 생성 37 | - 요청: 고객 ID, 커피 ID, 수량, 옵션 38 | - 응답: 주문 ID, 상태 39 | 40 | ### GET /orders/{orderId} 41 | - 주문 상태 조회 42 | - 응답: 주문 ID, 상태, 생성 시간 43 | 44 | ## 설치 및 실행 45 | 46 | 1. 의존성 설치: 47 | ```bash 48 | pip install -r requirements.txt 49 | ``` 50 | 51 | 2. 로컬 실행: 52 | ```bash 53 | uvicorn src.main:app --reload 54 | ``` 55 | 56 | 3. 배포: 57 | ```bash 58 | serverless deploy 59 | ``` 60 | 61 | ## 테스트 62 | 63 | 단위 테스트 실행: 64 | ```bash 65 | python -m pytest tests/unit 66 | ``` 67 | 68 | 통합 테스트 실행: 69 | ```bash 70 | python -m pytest tests/integration 71 | ``` -------------------------------------------------------------------------------- /ch07/infrastructure/persistence/tables.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import UnicodeAttribute, NumberAttribute 3 | 4 | 5 | class CoffeeModel(Model): 6 | class Meta: 7 | table_name = "Coffees" 8 | region = "ap-northeast-2" 9 | 10 | id = UnicodeAttribute(hash_key=True) 11 | name = UnicodeAttribute() 12 | price = NumberAttribute() 13 | currency = UnicodeAttribute(default="KRW") 14 | description = UnicodeAttribute(null=True) 15 | stock = NumberAttribute() 16 | 17 | 18 | class OrderItemModel(Model): 19 | class Meta: 20 | table_name = "OrderItems" 21 | region = "ap-northeast-2" 22 | 23 | id = UnicodeAttribute(hash_key=True) 24 | order_id = UnicodeAttribute() 25 | coffee_id = UnicodeAttribute() 26 | quantity = NumberAttribute() 27 | unit_price = NumberAttribute() 28 | currency = UnicodeAttribute(default="KRW") 29 | 30 | 31 | class OrderModel(Model): 32 | class Meta: 33 | table_name = "Orders" 34 | region = "ap-northeast-2" 35 | 36 | id = UnicodeAttribute(hash_key=True) 37 | customer_id = UnicodeAttribute() 38 | total_amount = NumberAttribute() 39 | currency = UnicodeAttribute(default="KRW") 40 | status = UnicodeAttribute() 41 | created_at = UnicodeAttribute() 42 | -------------------------------------------------------------------------------- /ch09/infrastructure/persistence/tables.py: -------------------------------------------------------------------------------- 1 | from pynamodb.models import Model 2 | from pynamodb.attributes import UnicodeAttribute, NumberAttribute 3 | 4 | 5 | class CoffeeModel(Model): 6 | class Meta: 7 | table_name = "Coffees" 8 | region = "ap-northeast-2" 9 | 10 | id = UnicodeAttribute(hash_key=True) 11 | name = UnicodeAttribute() 12 | price = NumberAttribute() 13 | currency = UnicodeAttribute(default="KRW") 14 | description = UnicodeAttribute(null=True) 15 | stock = NumberAttribute() 16 | 17 | 18 | class OrderItemModel(Model): 19 | class Meta: 20 | table_name = "OrderItems" 21 | region = "ap-northeast-2" 22 | 23 | id = UnicodeAttribute(hash_key=True) 24 | order_id = UnicodeAttribute() 25 | coffee_id = UnicodeAttribute() 26 | quantity = NumberAttribute() 27 | unit_price = NumberAttribute() 28 | currency = UnicodeAttribute(default="KRW") 29 | 30 | 31 | class OrderModel(Model): 32 | class Meta: 33 | table_name = "Orders" 34 | region = "ap-northeast-2" 35 | 36 | id = UnicodeAttribute(hash_key=True) 37 | customer_id = UnicodeAttribute() 38 | total_amount = NumberAttribute() 39 | currency = UnicodeAttribute(default="KRW") 40 | status = UnicodeAttribute() 41 | created_at = UnicodeAttribute() 42 | -------------------------------------------------------------------------------- /ch09/domain/events/order_events.py: -------------------------------------------------------------------------------- 1 | from domain.shared.aggregate_root import DomainEvent 2 | 3 | 4 | class OrderCreated(DomainEvent): 5 | def __init__(self, order_id: str, customer_id: str): 6 | super().__init__(name="OrderCreated", payload={"order_id": order_id, "customer_id": customer_id}) 7 | self.order_id = order_id 8 | self.customer_id = customer_id 9 | 10 | 11 | class OrderItemAdded(DomainEvent): 12 | def __init__(self, order_id: str, item_id: str, coffee_id: str, quantity: int): 13 | super().__init__( 14 | name="OrderItemAdded", 15 | payload={"order_id": order_id, "item_id": item_id, "coffee_id": coffee_id, "quantity": quantity}, 16 | ) 17 | self.order_id = order_id 18 | self.item_id = item_id 19 | self.coffee_id = coffee_id 20 | self.quantity = quantity 21 | 22 | 23 | class OrderStatusChanged(DomainEvent): 24 | def __init__(self, order_id: str, previous_status: str, new_status: str): 25 | super().__init__( 26 | name="OrderStatusChanged", 27 | payload={"order_id": order_id, "previous_status": previous_status, "new_status": new_status}, 28 | ) 29 | self.order_id = order_id 30 | self.previous_status = previous_status 31 | self.new_status = new_status 32 | 33 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/entities/order.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, Dict 3 | from datetime import datetime 4 | from enum import Enum, auto 5 | 6 | class OrderStatus(Enum): 7 | CREATED = auto() 8 | PREPARING = auto() 9 | COMPLETED = auto() 10 | CANCELLED = auto() 11 | 12 | @dataclass 13 | class Order: 14 | order_id: str 15 | customer_id: str 16 | menu_id: str 17 | quantity: int 18 | status: OrderStatus 19 | options: Dict[str, str] 20 | created_at: datetime 21 | updated_at: Optional[datetime] = None 22 | 23 | @classmethod 24 | def create(cls, order_id: str, customer_id: str, menu_id: str, 25 | quantity: int, options: Dict[str, str]) -> 'Order': 26 | """새로운 주문 생성""" 27 | now = datetime.utcnow() 28 | return cls( 29 | order_id=order_id, 30 | customer_id=customer_id, 31 | menu_id=menu_id, 32 | quantity=quantity, 33 | status=OrderStatus.CREATED, 34 | options=options, 35 | created_at=now, 36 | updated_at=now 37 | ) 38 | 39 | def update_status(self, status: OrderStatus) -> None: 40 | """주문 상태 업데이트""" 41 | self.status = status 42 | self.updated_at = datetime.utcnow() -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/dynamodb_customer_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pynamodb.attributes import UnicodeAttribute, UTCDateTimeAttribute 4 | from pynamodb.models import Model 5 | 6 | from ...domain.entities.customer import Customer 7 | from ...domain.repositories.customer_repository import CustomerRepository 8 | 9 | 10 | class CustomerModel(Model): 11 | class Meta: 12 | table_name = "customers" 13 | region = "ap-northeast-2" 14 | 15 | customer_id = UnicodeAttribute(hash_key=True) 16 | name = UnicodeAttribute() 17 | email = UnicodeAttribute() 18 | phone = UnicodeAttribute() 19 | created_at = UTCDateTimeAttribute() 20 | updated_at = UTCDateTimeAttribute(null=True) 21 | 22 | 23 | class DynamoDBCustomerRepository(CustomerRepository): 24 | def get_by_id(self, customer_id: str) -> Optional[Customer]: 25 | try: 26 | item = CustomerModel.get(customer_id) 27 | return Customer( 28 | customer_id=item.customer_id, 29 | name=item.name, 30 | email=item.email, 31 | phone=item.phone, 32 | created_at=item.created_at, 33 | updated_at=item.updated_at, 34 | ) 35 | except CustomerModel.DoesNotExist: 36 | return None 37 | -------------------------------------------------------------------------------- /ch06/6_5_external/dependency_injection.py: -------------------------------------------------------------------------------- 1 | from .controllers import CalculateScoreController 2 | from .usecase import CalculateAverageUseCase 3 | from .presenter import ConsolePresenter 4 | from .repository import DdbScoreRepository 5 | from .infrastructure import DynamoDBClient 6 | 7 | 8 | def initialize_application() -> CalculateScoreController: 9 | # 인프라스트럭쳐 초기화 10 | dynamodb_client = DynamoDBClient(region="ap-northeast-2") 11 | 12 | # 인터페이스 어댑터 초기화 13 | repository = DdbScoreRepository(dynamodb_client) 14 | presenter = ConsolePresenter() 15 | 16 | # 유스케이스 초기화 17 | usecase = CalculateAverageUseCase(repository, presenter) 18 | 19 | # 컨트롤러 초기화 20 | controller = CalculateScoreController(usecase) 21 | return controller 22 | 23 | 24 | def initialize_test_application() -> CalculateScoreController: 25 | """테스트용 애플리케이션 초기화 (Mock 사용)""" 26 | from .test_mocks import MockDynamoDBClient, MockScoreRepository 27 | 28 | # Infrastructure Layer 초기화 (Mock) 29 | mock_dynamodb_client = MockDynamoDBClient() 30 | 31 | # Interface Adapters Layer 초기화 (Mock) 32 | mock_repository = MockScoreRepository() 33 | presenter = ConsolePresenter() 34 | 35 | # Use Cases Layer 초기화 36 | usecase = CalculateAverageUseCase(mock_repository, presenter) 37 | 38 | # Controllers Layer 초기화 39 | controller = CalculateScoreController(usecase) 40 | return controller 41 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/dependency_injection.py: -------------------------------------------------------------------------------- 1 | from .controllers import CalculateScoreController 2 | from .usecase import CalculateAverageUseCase 3 | from .presenter import ScorePresenter 4 | from .views import WebScoreView, ApiScoreView 5 | from .repository import DdbScoreRepository 6 | from .infrastructure import DynamoDBClient 7 | 8 | 9 | def initialize_application() -> CalculateScoreController: 10 | """기본 API 형식(JSON) 응답용 애플리케이션 초기화""" 11 | # 인프라스트럭쳐 초기화 12 | dynamodb_client = DynamoDBClient(region="ap-northeast-2") 13 | 14 | # 인터페이스 어댑터 초기화 15 | repository = DdbScoreRepository(dynamodb_client) 16 | presenter = ScorePresenter(ApiScoreView()) # 기본 API 형식 17 | 18 | # 유스케이스 초기화 19 | usecase = CalculateAverageUseCase(repository, presenter) 20 | 21 | # 컨트롤러 초기화 22 | controller = CalculateScoreController(usecase) 23 | return controller 24 | 25 | 26 | def initialize_web_application() -> CalculateScoreController: 27 | """Web 형식 응답용 애플리케이션 초기화""" 28 | # 인프라스트럭쳐 초기화 29 | dynamodb_client = DynamoDBClient(region="ap-northeast-2") 30 | 31 | # 인터페이스 어댑터 초기화 32 | repository = DdbScoreRepository(dynamodb_client) 33 | presenter = ScorePresenter(WebScoreView()) # Web 형식 34 | 35 | # 유스케이스 초기화 36 | usecase = CalculateAverageUseCase(repository, presenter) 37 | 38 | # 컨트롤러 초기화 39 | controller = CalculateScoreController(usecase) 40 | return controller 41 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | target-version = ['py39'] 4 | include = '\.pyi?$' 5 | 6 | [tool.isort] 7 | profile = "black" 8 | multi_line_output = 3 9 | line_length = 88 10 | include_trailing_comma = true 11 | force_grid_wrap = 0 12 | use_parentheses = true 13 | ensure_newline_before_comments = true 14 | 15 | [tool.ruff] 16 | line-length = 88 17 | target-version = "py39" 18 | 19 | [tool.ruff.lint] 20 | select = [ 21 | "E", # pycodestyle errors 22 | "W", # pycodestyle warnings 23 | "F", # pyflakes 24 | "I", # isort 25 | "C", # flake8-comprehensions 26 | "B", # flake8-bugbear 27 | ] 28 | ignore = [] 29 | 30 | [tool.ruff.lint.per-file-ignores] 31 | "__init__.py" = ["F401"] 32 | 33 | [tool.mypy] 34 | python_version = "3.9" 35 | disallow_untyped_defs = true 36 | disallow_incomplete_defs = true 37 | check_untyped_defs = true 38 | disallow_untyped_decorators = false 39 | no_implicit_optional = true 40 | warn_redundant_casts = true 41 | warn_unused_ignores = true 42 | warn_return_any = true 43 | strict_optional = true 44 | ignore_missing_imports = true 45 | 46 | [[tool.mypy.overrides]] 47 | module = ["alembic", "alembic.*"] 48 | ignore_missing_imports = true 49 | follow_imports = "skip" 50 | follow_imports_for_stubs = false 51 | disallow_untyped_defs = false 52 | 53 | [tool.bandit] 54 | exclude_dirs = ["tests"] 55 | skips = ["B101"] # ignore assert warnings in tests -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/serverless.yml: -------------------------------------------------------------------------------- 1 | service: coffee-order-api 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.9 6 | region: ap-northeast-2 7 | environment: 8 | STAGE: ${opt:stage, 'dev'} 9 | iam: 10 | role: 11 | statements: 12 | - Effect: Allow 13 | Action: 14 | - dynamodb:* 15 | Resource: 16 | - !GetAtt MenuTable.Arn 17 | - !GetAtt OrderTable.Arn 18 | 19 | functions: 20 | api: 21 | handler: src/main.handler 22 | events: 23 | - http: 24 | path: /{proxy+} 25 | method: ANY 26 | cors: true 27 | 28 | resources: 29 | Resources: 30 | MenuTable: 31 | Type: AWS::DynamoDB::Table 32 | Properties: 33 | TableName: ${self:service}-${opt:stage, 'dev'}-menus 34 | AttributeDefinitions: 35 | - AttributeName: menu_id 36 | AttributeType: S 37 | KeySchema: 38 | - AttributeName: menu_id 39 | KeyType: HASH 40 | BillingMode: PAY_PER_REQUEST 41 | 42 | OrderTable: 43 | Type: AWS::DynamoDB::Table 44 | Properties: 45 | TableName: ${self:service}-${opt:stage, 'dev'}-orders 46 | AttributeDefinitions: 47 | - AttributeName: order_id 48 | AttributeType: S 49 | KeySchema: 50 | - AttributeName: order_id 51 | KeyType: HASH 52 | BillingMode: PAY_PER_REQUEST -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/application/controllers/menu_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | from typing import Dict, Any 3 | from ...domain.interfaces.boundaries import ( 4 | MenuInputBoundary, OutputBoundary 5 | ) 6 | 7 | router = APIRouter() 8 | 9 | class MenuController: 10 | def __init__( 11 | self, 12 | menu_usecase: MenuInputBoundary, 13 | presenter: OutputBoundary 14 | ): 15 | self.menu_usecase = menu_usecase 16 | self.presenter = presenter 17 | 18 | def register_routes(self, router: APIRouter) -> None: 19 | """라우트 등록""" 20 | router.add_api_route( 21 | "/menu", 22 | self.get_menus, 23 | methods=["GET"], 24 | response_model=Dict[str, Any] 25 | ) 26 | 27 | async def get_menus(self) -> Dict[str, Any]: 28 | """메뉴 목록 조회""" 29 | try: 30 | menus = self.menu_usecase.get_all_menus() 31 | response = self.presenter.present_success( 32 | data=menus, 33 | message="Menus retrieved successfully" 34 | ) 35 | return response.__dict__ 36 | except Exception as e: 37 | response = self.presenter.present_error( 38 | message=str(e) 39 | ) 40 | raise HTTPException( 41 | status_code=500, 42 | detail=response.__dict__ 43 | ) -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/config/dependencies.py: -------------------------------------------------------------------------------- 1 | from ..domain.usecases.menu_usecases import MenuUseCases 2 | from ..domain.usecases.order_usecases import OrderUseCases 3 | from ..infrastructure.repositories.dynamodb_repositories import ( 4 | DynamoDBMenuRepository, 5 | DynamoDBOrderRepository 6 | ) 7 | from ..infrastructure.presenters.json_presenter import JSONPresenter 8 | from ..application.controllers.menu_controller import MenuController 9 | from ..application.controllers.order_controller import OrderController 10 | 11 | class Dependencies: 12 | """의존성 주입 컨테이너""" 13 | 14 | @staticmethod 15 | def configure(): 16 | """의존성 설정""" 17 | # 레포지토리 생성 18 | menu_repository = DynamoDBMenuRepository() 19 | order_repository = DynamoDBOrderRepository() 20 | 21 | # 프레젠터 생성 22 | presenter = JSONPresenter() 23 | 24 | # 유스케이스 생성 25 | menu_usecase = MenuUseCases(menu_repository) 26 | order_usecase = OrderUseCases( 27 | order_repository=order_repository, 28 | menu_repository=menu_repository 29 | ) 30 | 31 | # 컨트롤러 생성 32 | menu_controller = MenuController( 33 | menu_usecase=menu_usecase, 34 | presenter=presenter 35 | ) 36 | order_controller = OrderController( 37 | order_usecase=order_usecase, 38 | presenter=presenter 39 | ) 40 | 41 | return menu_controller, order_controller -------------------------------------------------------------------------------- /ch10/few-shot-prompt/src/domain/entities.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from enum import Enum 4 | from typing import Dict, Optional 5 | 6 | 7 | class OrderStatus(Enum): 8 | CREATED = "created" 9 | PREPARING = "preparing" 10 | COMPLETED = "completed" 11 | CANCELLED = "cancelled" 12 | 13 | 14 | class MenuCategory(Enum): 15 | COFFEE = "coffee" 16 | TEA = "tea" 17 | DESSERT = "dessert" 18 | 19 | 20 | @dataclass 21 | class Menu: 22 | menu_id: str 23 | name: str 24 | price: int 25 | category: MenuCategory 26 | stock: int 27 | 28 | 29 | @dataclass 30 | class OrderOptions: 31 | size: str 32 | temperature: str 33 | 34 | 35 | @dataclass 36 | class Order: 37 | order_id: str 38 | customer_id: str 39 | menu_id: str 40 | quantity: int 41 | options: OrderOptions 42 | status: OrderStatus 43 | created_at: datetime 44 | 45 | @classmethod 46 | def create( 47 | cls, 48 | order_id: str, 49 | customer_id: str, 50 | menu_id: str, 51 | quantity: int, 52 | options: Dict[str, str], 53 | ) -> "Order": 54 | return cls( 55 | order_id=order_id, 56 | customer_id=customer_id, 57 | menu_id=menu_id, 58 | quantity=quantity, 59 | options=OrderOptions(**options), 60 | status=OrderStatus.CREATED, 61 | created_at=datetime.utcnow(), 62 | ) -------------------------------------------------------------------------------- /ch10/simple-prompt/serverless.yml: -------------------------------------------------------------------------------- 1 | service: coffee-order-api 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.9 6 | region: ap-northeast-2 7 | environment: 8 | STAGE: ${opt:stage, 'dev'} 9 | iamRoleStatements: 10 | - Effect: Allow 11 | Action: 12 | - dynamodb:Query 13 | - dynamodb:Scan 14 | - dynamodb:GetItem 15 | - dynamodb:PutItem 16 | - dynamodb:UpdateItem 17 | - dynamodb:DeleteItem 18 | Resource: 19 | - "arn:aws:dynamodb:${self:provider.region}:*:table/Menu" 20 | - "arn:aws:dynamodb:${self:provider.region}:*:table/Order" 21 | 22 | functions: 23 | api: 24 | handler: src.interfaces.api.handler 25 | events: 26 | - http: 27 | path: /{proxy+} 28 | method: ANY 29 | cors: true 30 | 31 | resources: 32 | Resources: 33 | MenuTable: 34 | Type: AWS::DynamoDB::Table 35 | Properties: 36 | TableName: Menu 37 | AttributeDefinitions: 38 | - AttributeName: id 39 | AttributeType: S 40 | KeySchema: 41 | - AttributeName: id 42 | KeyType: HASH 43 | BillingMode: PAY_PER_REQUEST 44 | 45 | OrderTable: 46 | Type: AWS::DynamoDB::Table 47 | Properties: 48 | TableName: Order 49 | AttributeDefinitions: 50 | - AttributeName: id 51 | AttributeType: S 52 | KeySchema: 53 | - AttributeName: id 54 | KeyType: HASH 55 | BillingMode: PAY_PER_REQUEST -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config, pool 4 | 5 | from alembic import context 6 | from src.infrastructure.database.models.base import Base 7 | 8 | config = context.config 9 | 10 | if config.config_file_name is not None: 11 | fileConfig(config.config_file_name) 12 | 13 | target_metadata = Base.metadata 14 | 15 | 16 | def run_migrations_offline() -> None: 17 | """Run migrations in 'offline' mode.""" 18 | url = config.get_main_option("sqlalchemy.url") 19 | context.configure( 20 | url=url, 21 | target_metadata=target_metadata, 22 | literal_binds=True, 23 | dialect_opts={"paramstyle": "named"}, 24 | ) 25 | 26 | with context.begin_transaction(): 27 | context.run_migrations() 28 | 29 | 30 | def run_migrations_online() -> None: 31 | """Run migrations in 'online' mode.""" 32 | connectable = engine_from_config( 33 | config.get_section(config.config_ini_section, {}), 34 | prefix="sqlalchemy.", 35 | poolclass=pool.NullPool, 36 | ) 37 | 38 | with connectable.connect() as connection: 39 | context.configure( 40 | connection=connection, 41 | target_metadata=target_metadata, 42 | ) 43 | 44 | with context.begin_transaction(): 45 | context.run_migrations() 46 | 47 | 48 | if context.is_offline_mode(): 49 | run_migrations_offline() 50 | else: 51 | run_migrations_online() 52 | -------------------------------------------------------------------------------- /ch06/6_4_gateway/repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List 3 | import boto3 4 | from botocore.exceptions import ClientError 5 | import requests 6 | 7 | 8 | # 게이트웨이 인터페이스 (응용 계층) 9 | class ScoreRepository(ABC): 10 | @abstractmethod 11 | def get_scores(self, student_id: str) -> List[float]: 12 | pass 13 | 14 | 15 | # DynamoDB 구현체 (인터페이스 어댑터 계층) 16 | class DdbScoreRepository(ScoreRepository): 17 | def __init__(self): 18 | self.client = boto3.resource("dynamodb") 19 | self.table = self.client.Table("scores") 20 | 21 | def get_scores(self, student_id: str) -> List[float]: 22 | try: 23 | response = self.table.get_item(Key={"student_id": student_id}) 24 | if "Item" in response and "scores" in response["Item"]: 25 | # DynamoDB 응답에서 성적 데이터 추출 및 변환 26 | return [float(score) for score in response["Item"]["scores"]["L"]] 27 | return [] 28 | except ClientError as e: 29 | raise ValueError(f"Failed to retrieve scores: {str(e)}") 30 | 31 | 32 | # 외부 저장소 구현체 (인터페이스 어댑터 계층) 33 | class ApiScoreRepository(ScoreRepository): 34 | def __init__(self, api_url: str): 35 | self.api_url = api_url 36 | 37 | def get_scores(self, student_id: str) -> List[float]: 38 | response = requests.get(f"{self.api_url}/students/{student_id}/scores") 39 | if response.status_code == 200: 40 | return [float(score) for score in response.json().get("scores", [])] 41 | return [] 42 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/config/dependencies.py: -------------------------------------------------------------------------------- 1 | from ..domain.usecases.menu_usecases import GetMenusUseCase 2 | from ..domain.usecases.order_usecases import CreateOrderUseCase, GetOrderUseCase 3 | from ..infrastructure.repositories.dynamodb_repositories import ( 4 | DynamoDBMenuRepository, DynamoDBOrderRepository 5 | ) 6 | from ..infrastructure.presenters.json_presenter import JSONPresenter 7 | from ..application.controllers.menu_controller import MenuController 8 | from ..application.controllers.order_controller import OrderController 9 | 10 | 11 | def create_menu_controller() -> MenuController: 12 | menu_repository = DynamoDBMenuRepository() 13 | presenter = JSONPresenter() 14 | get_menus_usecase = GetMenusUseCase(menu_repository) 15 | 16 | return MenuController( 17 | get_menus_usecase=get_menus_usecase, 18 | presenter=presenter 19 | ) 20 | 21 | 22 | def create_order_controller() -> OrderController: 23 | menu_repository = DynamoDBMenuRepository() 24 | order_repository = DynamoDBOrderRepository() 25 | presenter = JSONPresenter() 26 | 27 | create_order_usecase = CreateOrderUseCase( 28 | order_repository=order_repository, 29 | menu_repository=menu_repository 30 | ) 31 | get_order_usecase = GetOrderUseCase( 32 | order_repository=order_repository 33 | ) 34 | 35 | return OrderController( 36 | create_order_usecase=create_order_usecase, 37 | get_order_usecase=get_order_usecase, 38 | presenter=presenter 39 | ) -------------------------------------------------------------------------------- /ch06/6_4_gateway/usecase.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from abc import ABC, abstractmethod 3 | 4 | from .repository import ScoreRepository 5 | from .presenter import OutputBoundary 6 | 7 | 8 | # 입력 DTO 9 | @dataclass 10 | class ScoreInputDTO: 11 | student_id: str 12 | 13 | 14 | # 출력 DTO 15 | @dataclass 16 | class ScoreOutputDTO: 17 | student_id: str 18 | average: float 19 | status: str 20 | 21 | 22 | # 유스케이스 인터페이스 23 | class InputBoundary(ABC): 24 | @abstractmethod 25 | def execute(self, input_dto: ScoreInputDTO) -> None: 26 | pass 27 | 28 | 29 | # 유스케이스 구현 30 | class CalculateAverageUseCase(InputBoundary): 31 | def __init__(self, repository: ScoreRepository, presenter: OutputBoundary): 32 | self.repository = repository 33 | self.presenter = presenter 34 | 35 | def execute(self, input_dto: ScoreInputDTO) -> None: 36 | try: 37 | # 저장소에서 성적 데이터 조회 38 | scores = self.repository.get_scores(input_dto.student_id) 39 | 40 | # 비즈니스 로직: 평균 계산 41 | average = sum(scores) / len(scores) if scores else 0.0 42 | 43 | # 출력 DTO 생성 44 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=average, status="success") 45 | 46 | # 프레젠터로 결과 전달 47 | self.presenter.set_result(output_dto) 48 | 49 | except ValueError as e: 50 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=0.0, status=f"error: {str(e)}") 51 | self.presenter.set_result(output_dto) 52 | -------------------------------------------------------------------------------- /ch06/6_5_external/usecase.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from abc import ABC, abstractmethod 3 | 4 | from .repository import ScoreRepository 5 | from .presenter import OutputBoundary 6 | 7 | 8 | # 입력 DTO 9 | @dataclass 10 | class ScoreInputDTO: 11 | student_id: str 12 | 13 | 14 | # 출력 DTO 15 | @dataclass 16 | class ScoreOutputDTO: 17 | student_id: str 18 | average: float 19 | status: str 20 | 21 | 22 | # 유스케이스 인터페이스 23 | class InputBoundary(ABC): 24 | @abstractmethod 25 | def execute(self, input_dto: ScoreInputDTO) -> None: 26 | pass 27 | 28 | 29 | # 유스케이스 구현 30 | class CalculateAverageUseCase(InputBoundary): 31 | def __init__(self, repository: ScoreRepository, presenter: OutputBoundary): 32 | self.repository = repository 33 | self.presenter = presenter 34 | 35 | def execute(self, input_dto: ScoreInputDTO) -> None: 36 | try: 37 | # 저장소에서 성적 데이터 조회 38 | scores = self.repository.get_scores(input_dto.student_id) 39 | 40 | # 비즈니스 로직: 평균 계산 41 | average = sum(scores) / len(scores) if scores else 0.0 42 | 43 | # 출력 DTO 생성 44 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=average, status="success") 45 | 46 | # 프레젠터로 결과 전달 47 | self.presenter.set_result(output_dto) 48 | 49 | except ValueError as e: 50 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=0.0, status=f"error: {str(e)}") 51 | self.presenter.set_result(output_dto) 52 | -------------------------------------------------------------------------------- /ch06/6_6_entity/entity.py: -------------------------------------------------------------------------------- 1 | # Entities Layer (Enterprise Business Rules) 2 | from abc import ABC, abstractmethod 3 | from typing import List 4 | 5 | 6 | class ScoreValidationPolicy(ABC): 7 | @abstractmethod 8 | def validate(self, scores: List[float]) -> None: 9 | pass 10 | 11 | 12 | class StandardScoreValidationPolicy(ScoreValidationPolicy): 13 | def validate(self, scores: List[float]) -> None: 14 | if any(score < 0 for score in scores): 15 | raise ValueError("음수 점수는 허용되지 않습니다") 16 | if any(score > 100 for score in scores): 17 | raise ValueError("100점을 초과하는 점수는 허용되지 않습니다") 18 | 19 | 20 | class Scores: 21 | def __init__(self, values: List[float], validation_policy: ScoreValidationPolicy): 22 | self.values = values 23 | self.validation_policy = validation_policy 24 | 25 | def validate(self) -> None: 26 | """유효성 검증 정책을 적용한다.""" 27 | self.validation_policy.validate(self.values) 28 | 29 | def calculate_average(self) -> float: 30 | """성적의 평균을 계산한다.""" 31 | if not self.values: 32 | return 0.0 33 | return sum(self.values) / len(self.values) 34 | 35 | 36 | class Student: 37 | def __init__(self, student_id: str, scores: Scores): 38 | self.student_id = student_id 39 | self.scores = scores 40 | 41 | def get_average_score(self) -> float: 42 | """학생의 평균 성적을 계산한다.""" 43 | return self.scores.calculate_average() 44 | 45 | def validate_scores(self) -> None: 46 | """학생의 성적을 검증한다.""" 47 | self.scores.validate() 48 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/entity.py: -------------------------------------------------------------------------------- 1 | # Entities Layer (Enterprise Business Rules) 2 | from abc import ABC, abstractmethod 3 | from typing import List 4 | 5 | 6 | class ScoreValidationPolicy(ABC): 7 | @abstractmethod 8 | def validate(self, scores: List[float]) -> None: 9 | pass 10 | 11 | 12 | class StandardScoreValidationPolicy(ScoreValidationPolicy): 13 | def validate(self, scores: List[float]) -> None: 14 | if any(score < 0 for score in scores): 15 | raise ValueError("음수 점수는 허용되지 않습니다") 16 | if any(score > 100 for score in scores): 17 | raise ValueError("100점을 초과하는 점수는 허용되지 않습니다") 18 | 19 | 20 | class Scores: 21 | def __init__(self, values: List[float], validation_policy: ScoreValidationPolicy): 22 | self.values = values 23 | self.validation_policy = validation_policy 24 | 25 | def validate(self) -> None: 26 | """유효성 검증 정책을 적용한다.""" 27 | self.validation_policy.validate(self.values) 28 | 29 | def calculate_average(self) -> float: 30 | """성적의 평균을 계산한다.""" 31 | if not self.values: 32 | return 0.0 33 | return sum(self.values) / len(self.values) 34 | 35 | 36 | class Student: 37 | def __init__(self, student_id: str, scores: Scores): 38 | self.student_id = student_id 39 | self.scores = scores 40 | 41 | def get_average_score(self) -> float: 42 | """학생의 평균 성적을 계산한다.""" 43 | return self.scores.calculate_average() 44 | 45 | def validate_scores(self) -> None: 46 | """학생의 성적을 검증한다.""" 47 | self.scores.validate() 48 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/interfaces/boundaries.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from typing import List, Dict, Optional 4 | from datetime import datetime 5 | from ..entities.order import OrderStatus 6 | 7 | # Input DTOs 8 | @dataclass 9 | class CreateOrderInputDTO: 10 | customer_id: str 11 | menu_id: str 12 | quantity: int 13 | options: Dict[str, str] 14 | 15 | @dataclass 16 | class GetOrderInputDTO: 17 | order_id: str 18 | 19 | # Output DTOs 20 | @dataclass 21 | class MenuOutputDTO: 22 | menu_id: str 23 | name: str 24 | price: int 25 | category: str 26 | 27 | @dataclass 28 | class OrderOutputDTO: 29 | order_id: str 30 | status: OrderStatus 31 | created_at: datetime 32 | 33 | @dataclass 34 | class ResponseDTO: 35 | status: str 36 | data: Optional[any] 37 | message: str 38 | 39 | # Input Boundaries (Use Cases) 40 | class MenuInputBoundary(ABC): 41 | @abstractmethod 42 | def get_all_menus(self) -> List[MenuOutputDTO]: 43 | pass 44 | 45 | class OrderInputBoundary(ABC): 46 | @abstractmethod 47 | def create_order(self, input_dto: CreateOrderInputDTO) -> OrderOutputDTO: 48 | pass 49 | 50 | @abstractmethod 51 | def get_order(self, input_dto: GetOrderInputDTO) -> OrderOutputDTO: 52 | pass 53 | 54 | # Output Boundaries (Presenters) 55 | class OutputBoundary(ABC): 56 | @abstractmethod 57 | def present_success(self, data: any, message: str) -> ResponseDTO: 58 | pass 59 | 60 | @abstractmethod 61 | def present_error(self, message: str) -> ResponseDTO: 62 | pass -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/interfaces/boundaries.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from typing import List, Dict, Optional 4 | from datetime import datetime 5 | 6 | 7 | # Input DTOs 8 | @dataclass 9 | class CreateOrderInputDTO: 10 | customer_id: str 11 | menu_id: str 12 | quantity: int 13 | options: Dict[str, str] 14 | 15 | 16 | @dataclass 17 | class GetOrderInputDTO: 18 | order_id: str 19 | 20 | 21 | # Output DTOs 22 | @dataclass 23 | class MenuOutputDTO: 24 | menu_id: str 25 | name: str 26 | price: int 27 | category: str 28 | 29 | 30 | @dataclass 31 | class OrderOutputDTO: 32 | order_id: str 33 | status: str 34 | created_at: datetime 35 | 36 | 37 | @dataclass 38 | class ResponseDTO: 39 | status: str 40 | data: Optional[any] 41 | message: str 42 | 43 | 44 | # Input Boundaries 45 | class GetMenusInputBoundary(ABC): 46 | @abstractmethod 47 | def execute(self) -> List[MenuOutputDTO]: 48 | pass 49 | 50 | 51 | class CreateOrderInputBoundary(ABC): 52 | @abstractmethod 53 | def execute(self, input_dto: CreateOrderInputDTO) -> OrderOutputDTO: 54 | pass 55 | 56 | 57 | class GetOrderInputBoundary(ABC): 58 | @abstractmethod 59 | def execute(self, input_dto: GetOrderInputDTO) -> OrderOutputDTO: 60 | pass 61 | 62 | 63 | # Output Boundaries 64 | class OutputBoundary(ABC): 65 | @abstractmethod 66 | def present_success(self, data: any, message: str) -> ResponseDTO: 67 | pass 68 | 69 | @abstractmethod 70 | def present_error(self, message: str) -> ResponseDTO: 71 | pass -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/tests/unit/test_menu_usecase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timezone 3 | from unittest.mock import Mock 4 | 5 | from src.domain.entities.menu import Menu 6 | from src.domain.usecases.menu_usecases import GetMenuListUseCaseImpl 7 | 8 | 9 | class TestMenuUseCase(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.menu_repository = Mock() 12 | self.get_menu_list_usecase = GetMenuListUseCaseImpl( 13 | menu_repository=self.menu_repository 14 | ) 15 | 16 | def test_get_menu_list_success(self) -> None: 17 | # Arrange 18 | menu1 = Menu( 19 | menu_id="menu-1", 20 | name="Americano", 21 | price=3000, 22 | category="coffee", 23 | stock=100, 24 | created_at=datetime.now(timezone.utc), 25 | ) 26 | menu2 = Menu( 27 | menu_id="menu-2", 28 | name="Latte", 29 | price=4000, 30 | category="coffee", 31 | stock=100, 32 | created_at=datetime.now(timezone.utc), 33 | ) 34 | self.menu_repository.get_all.return_value = [menu1, menu2] 35 | 36 | # Act 37 | result = self.get_menu_list_usecase.execute() 38 | 39 | # Assert 40 | self.assertEqual(len(result.menus), 2) 41 | self.assertEqual(result.menus[0].menu_id, "menu-1") 42 | self.assertEqual(result.menus[0].name, "Americano") 43 | self.assertEqual(result.menus[0].price, 3000) 44 | self.assertEqual(result.menus[0].category, "coffee") 45 | self.menu_repository.get_all.assert_called_once() 46 | -------------------------------------------------------------------------------- /ch10/simple-prompt/src/application/usecases.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import uuid 3 | from typing import List, Optional 4 | from src.domain.entities import Menu, Order, OrderStatus 5 | from src.domain.repositories import MenuRepository, OrderRepository 6 | 7 | class MenuService: 8 | def __init__(self, menu_repository: MenuRepository): 9 | self.menu_repository = menu_repository 10 | 11 | async def get_all_menus(self) -> List[Menu]: 12 | return await self.menu_repository.get_all() 13 | 14 | class OrderService: 15 | def __init__( 16 | self, 17 | order_repository: OrderRepository, 18 | menu_repository: MenuRepository 19 | ): 20 | self.order_repository = order_repository 21 | self.menu_repository = menu_repository 22 | 23 | async def create_order( 24 | self, 25 | customer_id: str, 26 | menu_id: str, 27 | quantity: int 28 | ) -> Optional[Order]: 29 | menu = await self.menu_repository.get_by_id(menu_id) 30 | if not menu: 31 | return None 32 | 33 | if not await self.menu_repository.update_stock(menu_id, quantity): 34 | return None 35 | 36 | order = Order( 37 | id=str(uuid.uuid4()), 38 | customer_id=customer_id, 39 | menu_id=menu_id, 40 | quantity=quantity, 41 | status=OrderStatus.PENDING, 42 | created_at=datetime.utcnow() 43 | ) 44 | 45 | return await self.order_repository.create(order) 46 | 47 | async def get_order(self, order_id: str) -> Optional[Order]: 48 | return await self.order_repository.get_by_id(order_id) -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/presentation/controllers/menu_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | from ...domain.usecases.menu_usecases import GetMenuListUseCase 4 | from ..schemas.api_response import APIResponse, MenuListResponse 5 | 6 | 7 | class MenuController: 8 | def __init__(self, get_menu_list_usecase: GetMenuListUseCase) -> None: 9 | self.get_menu_list_usecase = get_menu_list_usecase 10 | self.router = APIRouter(tags=["menu"]) 11 | 12 | def register_routes(self) -> APIRouter: 13 | @self.router.get("/menu", response_model=APIResponse[MenuListResponse]) 14 | async def get_menu_list() -> APIResponse[MenuListResponse]: 15 | try: 16 | result = self.get_menu_list_usecase.execute() 17 | return APIResponse( 18 | status="success", 19 | message="Menus retrieved successfully", 20 | data=MenuListResponse( 21 | menus=[ 22 | { 23 | "menu_id": menu.menu_id, 24 | "name": menu.name, 25 | "price": menu.price, 26 | "category": menu.category, 27 | } 28 | for menu in result.menus 29 | ] 30 | ), 31 | ) 32 | except Exception as e: 33 | raise HTTPException( 34 | status_code=500, 35 | detail=str(e), 36 | ) from e 37 | 38 | return self.router 39 | -------------------------------------------------------------------------------- /ch07/infrastructure/web/fastapi.py: -------------------------------------------------------------------------------- 1 | import json 2 | from fastapi import FastAPI, Depends 3 | from fastapi.responses import JSONResponse 4 | from pydantic import BaseModel 5 | from mangum import Mangum 6 | 7 | from application.dtos import CreateOrderInputDto 8 | from application.usecases.create_order_usecase import CreateOrderUseCase 9 | from adapter.repository import DynamoDBCoffeeRepository, DynamoDBOrderRepository 10 | from adapter.presenter.create_order_presenter import CreateOrderPresenter 11 | from domain.exceptions import OnlineCafeException 12 | 13 | app = FastAPI() 14 | 15 | 16 | class OrderRequest(BaseModel): 17 | customer_id: str 18 | coffee_id: str 19 | quantity: int 20 | 21 | 22 | def get_create_order_use_case() -> CreateOrderUseCase: 23 | presenter = CreateOrderPresenter() 24 | coffee_repo = DynamoDBCoffeeRepository() 25 | order_repo = DynamoDBOrderRepository() 26 | 27 | return CreateOrderUseCase(presenter, coffee_repo, order_repo) 28 | 29 | 30 | @app.post("/order") 31 | def create_order(request: OrderRequest, usecase: CreateOrderUseCase = Depends(get_create_order_use_case)): 32 | try: 33 | input_dto = CreateOrderInputDto(request.customer_id, request.coffee_id, request.quantity) 34 | usecase.execute(input_dto) 35 | return JSONResponse(status_code=200, content=usecase.output_boundary.present()) 36 | except OnlineCafeException as e: 37 | return JSONResponse(status_code=e.get_status_code(), content=json.loads(e.get_response_body())) 38 | except Exception: 39 | return JSONResponse(status_code=500, content="Internal Server Error") 40 | 41 | 42 | @app.get("/test") 43 | def hello_world(): 44 | return {"status": "success", "data": {"message": "Hello, World!"}} 45 | 46 | 47 | handler = Mangum(app) 48 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import JSONResponse 3 | from mangum import Mangum 4 | from .config.dependencies import create_menu_controller, create_order_controller 5 | from typing import Dict, Any 6 | 7 | app = FastAPI() 8 | 9 | menu_controller = create_menu_controller() 10 | order_controller = create_order_controller() 11 | 12 | 13 | @app.get("/menus") 14 | async def get_menus(): 15 | response = await menu_controller.get_menus() 16 | return JSONResponse( 17 | status_code=200 if response.status == "success" else 500, 18 | content=response.__dict__ 19 | ) 20 | 21 | 22 | @app.post("/orders") 23 | async def create_order(request: Request): 24 | body = await request.json() 25 | response = await order_controller.create_order( 26 | customer_id=body["customer_id"], 27 | menu_id=body["menu_id"], 28 | quantity=body["quantity"], 29 | options=body.get("options", {}) 30 | ) 31 | 32 | status_code = 201 if response.status == "success" else ( 33 | 404 if response.message == "Invalid menu ID" else 34 | 400 if response.message == "Insufficient stock" else 500 35 | ) 36 | 37 | return JSONResponse( 38 | status_code=status_code, 39 | content=response.__dict__ 40 | ) 41 | 42 | 43 | @app.get("/orders/{order_id}") 44 | async def get_order(order_id: str): 45 | response = await order_controller.get_order(order_id) 46 | 47 | status_code = 200 if response.status == "success" else ( 48 | 404 if response.message == "Order not found" else 500 49 | ) 50 | 51 | return JSONResponse( 52 | status_code=status_code, 53 | content=response.__dict__ 54 | ) 55 | 56 | 57 | handler = Mangum(app) -------------------------------------------------------------------------------- /ch07/domain/entities/order.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | from ..value_objects.money import Money 4 | from ..value_objects.order_status import OrderStatus 5 | from .order_item import OrderItem 6 | from .coffee import Coffee 7 | 8 | 9 | class Order: 10 | def __init__(self, id: str, customer_id: str): 11 | self.id = id 12 | self.customer_id = customer_id 13 | self.items: List[OrderItem] = [] 14 | self.total_amount = Money(amount=0) 15 | self.status = OrderStatus.PENDING 16 | self.created_at = datetime.now() 17 | 18 | def add_coffee(self, coffee: Coffee, quantity: int) -> None: 19 | if quantity <= 0 or quantity > 100: 20 | raise ValueError("수량은 1~100 사이여야 합니다") 21 | coffee.reserve_stock(quantity) 22 | order_item = OrderItem( 23 | id=f"item-{self.id}-{coffee.id}", 24 | order_id=self.id, 25 | coffee_id=coffee.id, 26 | quantity=quantity, 27 | unit_price=coffee.price, 28 | ) 29 | self.items.append(order_item) 30 | self.calculate_total() 31 | 32 | def calculate_total(self) -> Money: 33 | total = Money(amount=0) 34 | for item in self.items: 35 | total = total + item.calculate_subtotal() 36 | self.total_amount = total 37 | return self.total_amount 38 | 39 | def change_status(self, new_status: OrderStatus) -> None: 40 | if self.status in [OrderStatus.COMPLETED, OrderStatus.CANCELLED]: 41 | raise ValueError("완료/취소된 주문은 상태 변경 불가") 42 | if new_status == OrderStatus.PENDING: 43 | raise ValueError("PENDING으로 되돌릴 수 없습니다") 44 | self.status = new_status 45 | 46 | def can_cancel(self) -> bool: 47 | return self.status == OrderStatus.PENDING 48 | -------------------------------------------------------------------------------- /ch07/domain/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class OnlineCafeException(Exception): 5 | _CODE = "Undefined" 6 | _STATUS_CODE = 500 7 | _MESSAGE = "Undefined" 8 | _DESCRIPTION = "Unknown error" 9 | 10 | def __init__(self, debug_message=None): 11 | self._debug_message = debug_message 12 | super(OnlineCafeException, self).__init__(self.get_response_body()) 13 | 14 | @classmethod 15 | def get_error_code(cls): 16 | return cls._CODE 17 | 18 | @classmethod 19 | def get_status_code(cls): 20 | return cls._STATUS_CODE 21 | 22 | def get_debug_message(self): 23 | return self._debug_message 24 | 25 | def get_error_message(self): 26 | return self._MESSAGE 27 | 28 | def get_response_body(self) -> str: 29 | return json.dumps( 30 | { 31 | "status": "failure", 32 | "errorCode": self._CODE, 33 | "message": self._MESSAGE, 34 | **({"debugMessage": self.get_debug_message()} if self._debug_message else {}), 35 | } 36 | ) 37 | 38 | 39 | # Client Error 40 | class ClientError(OnlineCafeException): 41 | _MESSAGE = "Bad Request" 42 | _STATUS_CODE = 400 43 | _DESCRIPTION = "요청 에러" 44 | 45 | 46 | class InvalidInputParameter(ClientError): 47 | _CODE = "1001" 48 | _DESCRIPTION = "유효하지 않은 입력 파라미터" 49 | 50 | 51 | class MalformedRequestError(ClientError): 52 | _CODE = "1002" 53 | _DESCRIPTION = "요청 형식이 잘못된 경우" 54 | 55 | 56 | # Server Error 57 | class ServerError(OnlineCafeException): 58 | """HTTP 5xx Errors""" 59 | 60 | _MESSAGE = "Internal Server Error" 61 | _STATUS_CODE = 500 62 | _DESCRIPTION = "서버 에러" 63 | 64 | 65 | class DatabaseSaveError(ServerError): 66 | _CODE = "1010" 67 | _DESCRIPTION = "데이터베이스 저장 실패" 68 | -------------------------------------------------------------------------------- /ch09/domain/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class OnlineCafeException(Exception): 5 | _CODE = "Undefined" 6 | _STATUS_CODE = 500 7 | _MESSAGE = "Undefined" 8 | _DESCRIPTION = "Unknown error" 9 | 10 | def __init__(self, debug_message=None): 11 | self._debug_message = debug_message 12 | super(OnlineCafeException, self).__init__(self.get_response_body()) 13 | 14 | @classmethod 15 | def get_error_code(cls): 16 | return cls._CODE 17 | 18 | @classmethod 19 | def get_status_code(cls): 20 | return cls._STATUS_CODE 21 | 22 | def get_debug_message(self): 23 | return self._debug_message 24 | 25 | def get_error_message(self): 26 | return self._MESSAGE 27 | 28 | def get_response_body(self) -> str: 29 | return json.dumps( 30 | { 31 | "status": "failure", 32 | "errorCode": self._CODE, 33 | "message": self._MESSAGE, 34 | **({"debugMessage": self.get_debug_message()} if self._debug_message else {}), 35 | } 36 | ) 37 | 38 | 39 | # Client Error 40 | class ClientError(OnlineCafeException): 41 | _MESSAGE = "Bad Request" 42 | _STATUS_CODE = 400 43 | _DESCRIPTION = "요청 에러" 44 | 45 | 46 | class InvalidInputParameter(ClientError): 47 | _CODE = "1001" 48 | _DESCRIPTION = "유효하지 않은 입력 파라미터" 49 | 50 | 51 | class MalformedRequestError(ClientError): 52 | _CODE = "1002" 53 | _DESCRIPTION = "요청 형식이 잘못된 경우" 54 | 55 | 56 | # Server Error 57 | class ServerError(OnlineCafeException): 58 | """HTTP 5xx Errors""" 59 | 60 | _MESSAGE = "Internal Server Error" 61 | _STATUS_CODE = 500 62 | _DESCRIPTION = "서버 에러" 63 | 64 | 65 | class DatabaseSaveError(ServerError): 66 | _CODE = "1010" 67 | _DESCRIPTION = "데이터베이스 저장 실패" 68 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/tests/unit/test_customer_usecase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timezone 3 | from unittest.mock import Mock 4 | 5 | from src.domain.entities.customer import Customer 6 | from src.domain.usecases.customer_usecases import GetCustomerUseCaseImpl 7 | 8 | 9 | class TestCustomerUseCase(unittest.TestCase): 10 | def setUp(self) -> None: 11 | self.customer_repository = Mock() 12 | self.get_customer_usecase = GetCustomerUseCaseImpl( 13 | customer_repository=self.customer_repository 14 | ) 15 | 16 | def test_get_customer_success(self) -> None: 17 | # Arrange 18 | customer = Customer( 19 | customer_id="cust-1", 20 | name="John Doe", 21 | email="john@example.com", 22 | phone="+1234567890", 23 | created_at=datetime.now(timezone.utc), 24 | ) 25 | self.customer_repository.get_by_id.return_value = customer 26 | 27 | # Act 28 | result = self.get_customer_usecase.execute("cust-1") 29 | 30 | # Assert 31 | assert result is not None # Type guard 32 | self.assertEqual(result.customer_id, "cust-1") 33 | self.assertEqual(result.name, "John Doe") 34 | self.assertEqual(result.email, "john@example.com") 35 | self.assertEqual(result.phone, "+1234567890") 36 | self.customer_repository.get_by_id.assert_called_once_with("cust-1") 37 | 38 | def test_get_customer_not_found(self) -> None: 39 | # Arrange 40 | self.customer_repository.get_by_id.return_value = None 41 | 42 | # Act 43 | result = self.get_customer_usecase.execute("non-existent-id") 44 | 45 | # Assert 46 | self.assertIsNone(result) 47 | self.customer_repository.get_by_id.assert_called_once_with("non-existent-id") 48 | -------------------------------------------------------------------------------- /ch06/6_3_usecase/calculate_average_usecase.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | # 저장소 인터페이스 7 | class ScoreRepository(ABC): 8 | @abstractmethod 9 | def get_scores(self, student_id: str) -> List[float]: 10 | pass 11 | 12 | 13 | # 출력 인터페이스 (Presenter) 14 | class OutputBoundary(ABC): 15 | @abstractmethod 16 | def set_result(self, output_dto: "ScoreOutputDTO") -> None: 17 | pass 18 | 19 | @abstractmethod 20 | def present(self) -> None: 21 | pass 22 | 23 | 24 | # 입력 DTO 25 | @dataclass 26 | class ScoreInputDTO: 27 | student_id: str 28 | 29 | 30 | # 출력 DTO 31 | @dataclass 32 | class ScoreOutputDTO: 33 | student_id: str 34 | average: float 35 | status: str 36 | 37 | 38 | # 유스케이스 인터페이스 39 | class InputBoundary(ABC): 40 | @abstractmethod 41 | def execute(self, input_dto: ScoreInputDTO) -> None: 42 | pass 43 | 44 | 45 | # 유스케이스 구현 46 | class CalculateAverageUseCase(InputBoundary): 47 | def __init__(self, repository: ScoreRepository, presenter: OutputBoundary): 48 | self.repository = repository 49 | self.presenter = presenter 50 | 51 | def execute(self, input_dto: ScoreInputDTO) -> None: 52 | try: 53 | # 저장소에서 성적 데이터 조회 54 | scores = self.repository.get_scores(input_dto.student_id) 55 | 56 | # 비즈니스 로직: 평균 계산 57 | average = sum(scores) / len(scores) if scores else 0.0 58 | 59 | # 출력 DTO 생성 60 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=average, status="success") 61 | 62 | # 프레젠터로 결과 전달 63 | self.presenter.set_result(output_dto) 64 | 65 | except ValueError as e: 66 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=0.0, status=f"error: {str(e)}") 67 | self.presenter.set_result(output_dto) 68 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/serverless.yml: -------------------------------------------------------------------------------- 1 | service: coffee-shop-api 2 | 3 | frameworkVersion: '3' 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.9 8 | region: ap-northeast-2 9 | environment: 10 | STAGE: ${opt:stage, 'dev'} 11 | iam: 12 | role: 13 | statements: 14 | - Effect: Allow 15 | Action: 16 | - dynamodb:Query 17 | - dynamodb:Scan 18 | - dynamodb:GetItem 19 | - dynamodb:PutItem 20 | - dynamodb:UpdateItem 21 | - dynamodb:DeleteItem 22 | Resource: 23 | - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/menus" 24 | - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/orders" 25 | 26 | functions: 27 | api: 28 | handler: src.main.handler 29 | events: 30 | - http: 31 | path: /menu 32 | method: get 33 | - http: 34 | path: /order 35 | method: post 36 | - http: 37 | path: /order/{orderId} 38 | method: get 39 | 40 | resources: 41 | Resources: 42 | MenusTable: 43 | Type: AWS::DynamoDB::Table 44 | Properties: 45 | TableName: menus 46 | AttributeDefinitions: 47 | - AttributeName: menu_id 48 | AttributeType: S 49 | KeySchema: 50 | - AttributeName: menu_id 51 | KeyType: HASH 52 | BillingMode: PAY_PER_REQUEST 53 | 54 | OrdersTable: 55 | Type: AWS::DynamoDB::Table 56 | Properties: 57 | TableName: orders 58 | AttributeDefinitions: 59 | - AttributeName: order_id 60 | AttributeType: S 61 | KeySchema: 62 | - AttributeName: order_id 63 | KeyType: HASH 64 | BillingMode: PAY_PER_REQUEST 65 | 66 | plugins: 67 | - serverless-python-requirements 68 | 69 | package: 70 | patterns: 71 | - '!./**' 72 | - src/** 73 | - requirements.txt -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/presentation/controllers/customer_controller.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, HTTPException 2 | 3 | from ...domain.usecases.customer_usecases import GetCustomerUseCase 4 | from ..schemas.api_response import APIResponse, CustomerResponse 5 | 6 | 7 | class CustomerController: 8 | def __init__(self, get_customer_usecase: GetCustomerUseCase) -> None: 9 | self.get_customer_usecase = get_customer_usecase 10 | self.router = APIRouter(tags=["customer"]) 11 | 12 | def register_routes(self) -> APIRouter: 13 | @self.router.get( 14 | "/customer/{customer_id}", 15 | response_model=APIResponse[CustomerResponse], 16 | ) 17 | async def get_customer(customer_id: str) -> APIResponse[CustomerResponse]: 18 | try: 19 | result = self.get_customer_usecase.execute(customer_id) 20 | if not result: 21 | raise HTTPException( 22 | status_code=404, 23 | detail="Customer not found", 24 | ) from None 25 | return APIResponse( 26 | status="success", 27 | message="Customer retrieved successfully", 28 | data=CustomerResponse( 29 | customer_id=result.customer_id, 30 | name=result.name, 31 | email=result.email, 32 | phone=result.phone, 33 | created_at=result.created_at, 34 | updated_at=result.updated_at, 35 | ), 36 | ) 37 | except HTTPException as e: 38 | raise e 39 | except Exception as e: 40 | raise HTTPException( 41 | status_code=500, 42 | detail=str(e), 43 | ) from e 44 | 45 | return self.router 46 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/mysql_order_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sqlalchemy.orm import Session 4 | 5 | from ...domain.entities.order import Order 6 | from ...domain.repositories.order_repository import OrderRepository 7 | from ..database.models.order import OrderModel 8 | 9 | 10 | class MySQLOrderRepository(OrderRepository): 11 | def __init__(self, db: Session) -> None: 12 | self.db = db 13 | 14 | def save(self, order: Order) -> None: 15 | order_model = OrderModel( 16 | order_id=order.order_id, 17 | customer_id=order.customer_id, 18 | menu_id=order.menu_id, 19 | quantity=order.quantity, 20 | status=order.status, 21 | options=order.options, 22 | ) 23 | self.db.add(order_model) 24 | self.db.commit() 25 | 26 | def get_by_id(self, order_id: str) -> Optional[Order]: 27 | order_model = ( 28 | self.db.query(OrderModel).filter(OrderModel.order_id == order_id).first() 29 | ) 30 | if not order_model: 31 | return None 32 | 33 | return Order( 34 | order_id=order_model.order_id, 35 | customer_id=order_model.customer_id, 36 | menu_id=order_model.menu_id, 37 | quantity=order_model.quantity, 38 | status=order_model.status, 39 | options=order_model.options, 40 | created_at=order_model.created_at, 41 | updated_at=order_model.updated_at, 42 | ) 43 | 44 | def delete(self, order_id: str) -> bool: 45 | try: 46 | result: int = ( 47 | self.db.query(OrderModel) 48 | .filter(OrderModel.order_id == order_id) 49 | .delete() 50 | ) 51 | self.db.commit() 52 | return result > 0 53 | except Exception: 54 | self.db.rollback() 55 | return False 56 | -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/dynamodb_order_repository.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pynamodb.attributes import ( 4 | MapAttribute, 5 | NumberAttribute, 6 | UnicodeAttribute, 7 | UTCDateTimeAttribute, 8 | ) 9 | from pynamodb.models import Model 10 | 11 | from ...domain.entities.order import Order 12 | from ...domain.repositories.order_repository import OrderRepository 13 | 14 | 15 | class OrderModel(Model): 16 | class Meta: 17 | table_name = "orders" 18 | region = "ap-northeast-2" 19 | 20 | order_id = UnicodeAttribute(hash_key=True) 21 | customer_id = UnicodeAttribute() 22 | menu_id = UnicodeAttribute() 23 | quantity = NumberAttribute() 24 | status = UnicodeAttribute() 25 | options = MapAttribute() 26 | created_at = UTCDateTimeAttribute() 27 | updated_at = UTCDateTimeAttribute(null=True) 28 | 29 | 30 | class DynamoDBOrderRepository(OrderRepository): 31 | def save(self, order: Order) -> None: 32 | item = OrderModel( 33 | order_id=order.order_id, 34 | customer_id=order.customer_id, 35 | menu_id=order.menu_id, 36 | quantity=order.quantity, 37 | status=order.status, 38 | options=order.options, 39 | created_at=order.created_at, 40 | updated_at=order.updated_at, 41 | ) 42 | item.save() 43 | 44 | def get_by_id(self, order_id: str) -> Optional[Order]: 45 | try: 46 | item = OrderModel.get(order_id) 47 | return Order( 48 | order_id=item.order_id, 49 | customer_id=item.customer_id, 50 | menu_id=item.menu_id, 51 | quantity=item.quantity, 52 | status=item.status, 53 | options=item.options, 54 | created_at=item.created_at, 55 | updated_at=item.updated_at, 56 | ) 57 | except OrderModel.DoesNotExist: 58 | return None 59 | -------------------------------------------------------------------------------- /ch10/few-shot-prompt/serverless.yml: -------------------------------------------------------------------------------- 1 | service: coffee-order-api 2 | 3 | frameworkVersion: '3' 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.9 8 | region: ap-northeast-2 9 | memorySize: 256 10 | timeout: 30 11 | environment: 12 | STAGE: ${opt:stage, 'dev'} 13 | iam: 14 | role: 15 | statements: 16 | - Effect: Allow 17 | Action: 18 | - dynamodb:Query 19 | - dynamodb:Scan 20 | - dynamodb:GetItem 21 | - dynamodb:PutItem 22 | - dynamodb:UpdateItem 23 | - dynamodb:DeleteItem 24 | Resource: 25 | - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/menus" 26 | - "arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/orders" 27 | 28 | functions: 29 | api: 30 | handler: src.interfaces.api.handler 31 | events: 32 | - http: 33 | path: /menu 34 | method: get 35 | - http: 36 | path: /order 37 | method: post 38 | - http: 39 | path: /order/{orderId} 40 | method: get 41 | 42 | resources: 43 | Resources: 44 | MenusTable: 45 | Type: AWS::DynamoDB::Table 46 | Properties: 47 | TableName: menus 48 | AttributeDefinitions: 49 | - AttributeName: menu_id 50 | AttributeType: S 51 | KeySchema: 52 | - AttributeName: menu_id 53 | KeyType: HASH 54 | BillingMode: PAY_PER_REQUEST 55 | 56 | OrdersTable: 57 | Type: AWS::DynamoDB::Table 58 | Properties: 59 | TableName: orders 60 | AttributeDefinitions: 61 | - AttributeName: order_id 62 | AttributeType: S 63 | KeySchema: 64 | - AttributeName: order_id 65 | KeyType: HASH 66 | BillingMode: PAY_PER_REQUEST 67 | 68 | plugins: 69 | - serverless-python-requirements 70 | 71 | package: 72 | patterns: 73 | - '!.git/**' 74 | - '!.gitignore' 75 | - '!.pytest_cache/**' 76 | - '!tests/**' 77 | - '!README.md' -------------------------------------------------------------------------------- /ch06/6_6_entity/usecase.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from abc import ABC, abstractmethod 3 | 4 | from .repository import ScoreRepository 5 | from .presenter import OutputBoundary 6 | from .entity import Student, Scores, StandardScoreValidationPolicy 7 | 8 | 9 | # 입력 DTO 10 | @dataclass 11 | class ScoreInputDTO: 12 | student_id: str 13 | 14 | 15 | # 출력 DTO 16 | @dataclass 17 | class ScoreOutputDTO: 18 | student_id: str 19 | average: float 20 | status: str 21 | 22 | 23 | # 유스케이스 인터페이스 24 | class InputBoundary(ABC): 25 | @abstractmethod 26 | def execute(self, input_dto: ScoreInputDTO) -> None: 27 | pass 28 | 29 | 30 | # 유스케이스 구현 (Entity 활용) 31 | class CalculateAverageUseCase(InputBoundary): 32 | def __init__(self, repository: ScoreRepository, presenter: OutputBoundary): 33 | self.repository = repository 34 | self.presenter = presenter 35 | 36 | def execute(self, input_dto: ScoreInputDTO) -> None: 37 | try: 38 | # 저장소에서 성적 데이터 조회 39 | raw_scores = self.repository.get_scores(input_dto.student_id) 40 | 41 | # Entity 생성 및 비즈니스 로직 적용 42 | validation_policy = StandardScoreValidationPolicy() 43 | scores_entity = Scores(values=raw_scores, validation_policy=validation_policy) 44 | 45 | # Student Entity 생성 46 | student = Student(student_id=input_dto.student_id, scores=scores_entity) 47 | 48 | # Entity를 통한 유효성 검증 49 | student.validate_scores() 50 | 51 | # Entity를 통한 평균 계산 52 | average = student.get_average_score() 53 | 54 | # 출력 DTO 생성 55 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=average, status="success") 56 | 57 | # 프레젠터로 결과 전달 58 | self.presenter.set_result(output_dto) 59 | 60 | except ValueError as e: 61 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=0.0, status=f"error: {str(e)}") 62 | self.presenter.set_result(output_dto) 63 | -------------------------------------------------------------------------------- /ch06/6_7_presenter/usecase.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from abc import ABC, abstractmethod 3 | 4 | from .repository import ScoreRepository 5 | from .presenter import OutputBoundary 6 | from .entity import Student, Scores, StandardScoreValidationPolicy 7 | 8 | 9 | # 입력 DTO 10 | @dataclass 11 | class ScoreInputDTO: 12 | student_id: str 13 | 14 | 15 | # 출력 DTO 16 | @dataclass 17 | class ScoreOutputDTO: 18 | student_id: str 19 | average: float 20 | status: str 21 | 22 | 23 | # 유스케이스 인터페이스 24 | class InputBoundary(ABC): 25 | @abstractmethod 26 | def execute(self, input_dto: ScoreInputDTO) -> None: 27 | pass 28 | 29 | 30 | # 유스케이스 구현 (Entity 활용) 31 | class CalculateAverageUseCase(InputBoundary): 32 | def __init__(self, repository: ScoreRepository, presenter: OutputBoundary): 33 | self.repository = repository 34 | self.presenter = presenter 35 | 36 | def execute(self, input_dto: ScoreInputDTO) -> None: 37 | try: 38 | # 저장소에서 성적 데이터 조회 39 | raw_scores = self.repository.get_scores(input_dto.student_id) 40 | 41 | # Entity 생성 및 비즈니스 로직 적용 42 | validation_policy = StandardScoreValidationPolicy() 43 | scores_entity = Scores(values=raw_scores, validation_policy=validation_policy) 44 | 45 | # Student Entity 생성 46 | student = Student(student_id=input_dto.student_id, scores=scores_entity) 47 | 48 | # Entity를 통한 유효성 검증 49 | student.validate_scores() 50 | 51 | # Entity를 통한 평균 계산 52 | average = student.get_average_score() 53 | 54 | # 출력 DTO 생성 55 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=average, status="success") 56 | 57 | # 프레젠터로 결과 전달 58 | self.presenter.set_result(output_dto) 59 | 60 | except ValueError as e: 61 | output_dto = ScoreOutputDTO(student_id=input_dto.student_id, average=0.0, status=f"error: {str(e)}") 62 | self.presenter.set_result(output_dto) 63 | -------------------------------------------------------------------------------- /ch07/domain_class_diagram.puml: -------------------------------------------------------------------------------- 1 | @startuml Domain Class Diagram 2 | 3 | ' 스타일 설정 4 | skinparam class { 5 | BackgroundColor<> LightBlue 6 | BackgroundColor<> LightGreen 7 | BorderColor Black 8 | ArrowColor Black 9 | } 10 | 11 | ' Money 값 객체 12 | class Money <> { 13 | - amount: int 14 | - currency: str 15 | 16 | + __init__(amount: int, currency: str = "KRW") 17 | + __add__(other: Money): Money 18 | + __mul__(quantity: int): Money 19 | + __str__(): str 20 | + __repr__(): str 21 | + __eq__(other: object): bool 22 | } 23 | 24 | ' OrderStatus 값 객체 25 | enum OrderStatus <> { 26 | PENDING = "pending" 27 | PREPARING = "preparing" 28 | COMPLETED = "completed" 29 | CANCELLED = "cancelled" 30 | } 31 | 32 | ' Coffee 엔티티 33 | class Coffee <> { 34 | - id: str 35 | - name: str 36 | - price: Money 37 | - description: Optional[str] 38 | - stock: int 39 | 40 | + __init__(id: str, name: str, price: Money, description: Optional[str], stock: int) 41 | + is_available(): bool 42 | + reserve_stock(quantity: int): None 43 | } 44 | 45 | ' OrderItem 엔티티 46 | class OrderItem <> { 47 | - id: str 48 | - order_id: str 49 | - coffee_id: str 50 | - quantity: int 51 | - unit_price: Money 52 | 53 | + __init__(id: str, order_id: str, coffee_id: str, quantity: int, unit_price: Money) 54 | + calculate_subtotal(): Money 55 | } 56 | 57 | ' Order 엔티티 58 | class Order <> { 59 | - id: str 60 | - customer_id: str 61 | - items: List[OrderItem] 62 | - total_amount: Money 63 | - status: OrderStatus 64 | - created_at: datetime 65 | 66 | + __init__(id: str, customer_id: str) 67 | + add_coffee(coffee: Coffee, quantity: int): None 68 | + calculate_total(): Money 69 | + change_status(new_status: OrderStatus): None 70 | + can_cancel(): bool 71 | } 72 | 73 | ' 관계 정의 74 | Order o--> "many" OrderItem : contains 75 | Order --> OrderStatus : uses 76 | Order --> Coffee : references 77 | OrderItem --> Coffee : references 78 | OrderItem --> Money : uses 79 | Coffee --> Money : uses 80 | 81 | @enduml -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt/src/domain/usecases/order_usecases.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import Optional 3 | from ..interfaces.boundaries import ( 4 | OrderInputBoundary, CreateOrderInputDTO, 5 | GetOrderInputDTO, OrderOutputDTO 6 | ) 7 | from ..interfaces.repositories import OrderRepository, MenuRepository 8 | from ..entities.order import Order 9 | 10 | class OrderUseCases(OrderInputBoundary): 11 | def __init__( 12 | self, 13 | order_repository: OrderRepository, 14 | menu_repository: MenuRepository 15 | ): 16 | self._order_repository = order_repository 17 | self._menu_repository = menu_repository 18 | 19 | def create_order(self, input_dto: CreateOrderInputDTO) -> OrderOutputDTO: 20 | """주문 생성""" 21 | # 메뉴 존재 여부 확인 22 | menu = self._menu_repository.get_by_id(input_dto.menu_id) 23 | if not menu: 24 | raise ValueError("Menu not found") 25 | 26 | # 재고 확인 27 | if not menu.is_available(input_dto.quantity): 28 | raise ValueError("Insufficient stock") 29 | 30 | # 주문 생성 31 | order = Order.create( 32 | order_id=str(uuid.uuid4()), 33 | customer_id=input_dto.customer_id, 34 | menu_id=input_dto.menu_id, 35 | quantity=input_dto.quantity, 36 | options=input_dto.options 37 | ) 38 | 39 | # 재고 감소 40 | menu.decrease_stock(input_dto.quantity) 41 | self._menu_repository.update(menu) 42 | 43 | # 주문 저장 44 | self._order_repository.create(order) 45 | 46 | return OrderOutputDTO( 47 | order_id=order.order_id, 48 | status=order.status, 49 | created_at=order.created_at 50 | ) 51 | 52 | def get_order(self, input_dto: GetOrderInputDTO) -> OrderOutputDTO: 53 | """주문 조회""" 54 | order = self._order_repository.get_by_id(input_dto.order_id) 55 | if not order: 56 | raise ValueError("Order not found") 57 | 58 | return OrderOutputDTO( 59 | order_id=order.order_id, 60 | status=order.status, 61 | created_at=order.created_at 62 | ) -------------------------------------------------------------------------------- /ch10/few-shot-prompt-with-constraints/src/domain/usecases/order_usecases.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from ..interfaces.boundaries import ( 3 | CreateOrderInputBoundary, GetOrderInputBoundary, 4 | CreateOrderInputDTO, GetOrderInputDTO, OrderOutputDTO 5 | ) 6 | from ..interfaces.repositories import OrderRepository, MenuRepository 7 | 8 | 9 | class CreateOrderUseCase(CreateOrderInputBoundary): 10 | def __init__(self, order_repository: OrderRepository, menu_repository: MenuRepository): 11 | self._order_repository = order_repository 12 | self._menu_repository = menu_repository 13 | 14 | def execute(self, input_dto: CreateOrderInputDTO) -> OrderOutputDTO: 15 | menu = self._menu_repository.get_by_id(input_dto.menu_id) 16 | if not menu: 17 | raise ValueError("Invalid menu ID") 18 | 19 | if not menu.has_sufficient_stock(input_dto.quantity): 20 | raise ValueError("Insufficient stock") 21 | 22 | order = Order.create( 23 | order_id=f"order-{uuid.uuid4()}", 24 | customer_id=input_dto.customer_id, 25 | menu_id=input_dto.menu_id, 26 | quantity=input_dto.quantity, 27 | options=input_dto.options 28 | ) 29 | 30 | menu.decrease_stock(input_dto.quantity) 31 | self._menu_repository.update(menu) 32 | self._order_repository.create(order) 33 | 34 | return OrderOutputDTO( 35 | order_id=order.order_id, 36 | status=order.status, 37 | created_at=order.created_at 38 | ) 39 | 40 | 41 | class GetOrderUseCase(GetOrderInputBoundary): 42 | def __init__(self, order_repository: OrderRepository): 43 | self._order_repository = order_repository 44 | 45 | def execute(self, input_dto: GetOrderInputDTO) -> OrderOutputDTO: 46 | order = self._order_repository.get_by_id(input_dto.order_id) 47 | if not order: 48 | raise ValueError("Order not found") 49 | 50 | return OrderOutputDTO( 51 | order_id=order.order_id, 52 | status=order.status, 53 | created_at=order.created_at 54 | ) -------------------------------------------------------------------------------- /ch07/application/usecases/create_order_usecase.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from application.dtos import CreateOrderInputDto, CreateOrderOutputDto 4 | from application.ports.inbound import CreateOrderInputBoundary 5 | from application.ports.outbound import OutputBoundary 6 | from application.ports.repository import CoffeeRepository, OrderRepository 7 | from domain.entities.order import Order 8 | from domain.exceptions import InvalidInputParameter 9 | 10 | 11 | class CreateOrderUseCase(CreateOrderInputBoundary): 12 | def __init__(self, output_boundary: OutputBoundary, coffee_repo: CoffeeRepository, order_repo: OrderRepository): 13 | self.output_boundary = output_boundary 14 | self.coffee_repo = coffee_repo 15 | self.order_repo = order_repo 16 | 17 | def execute(self, input_dto: CreateOrderInputDto) -> None: 18 | coffee = self.coffee_repo.find_by_id(input_dto.coffee_id) 19 | if not coffee or not coffee.is_available(): 20 | raise InvalidInputParameter("주문 불가능한 커피입니다") 21 | 22 | order_id = f"order-{input_dto.customer_id}-{int(datetime.datetime.now().timestamp())}" 23 | order = Order(id=order_id, customer_id=input_dto.customer_id) 24 | order.add_coffee(coffee, input_dto.quantity) 25 | 26 | # 주문과 커피 정보 저장 27 | self.order_repo.save(order) 28 | self.coffee_repo.save(coffee) 29 | 30 | # 출력 DTO 생성 31 | items = [] 32 | for item in order.items: 33 | items.append( 34 | { 35 | "id": item.id, 36 | "coffee_id": item.coffee_id, 37 | "quantity": item.quantity, 38 | "unit_price": str(item.unit_price), 39 | "subtotal": str(item.calculate_subtotal()), 40 | } 41 | ) 42 | 43 | output_dto = CreateOrderOutputDto( 44 | order_id=order.id, 45 | customer_id=order.customer_id, 46 | total_amount=order.total_amount.amount, 47 | currency=order.total_amount.currency, 48 | status=order.status.value, 49 | items=items, 50 | ) 51 | 52 | self.output_boundary.set_result(output_dto) 53 | -------------------------------------------------------------------------------- /ch10/simple-prompt/tests/unit/test_usecases.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import Mock, AsyncMock 3 | from datetime import datetime 4 | from src.domain.entities import Menu, Order, OrderStatus 5 | from src.application.usecases import MenuService, OrderService 6 | 7 | @pytest.fixture 8 | def menu_repository(): 9 | return Mock() 10 | 11 | @pytest.fixture 12 | def order_repository(): 13 | return Mock() 14 | 15 | @pytest.fixture 16 | def menu_service(menu_repository): 17 | return MenuService(menu_repository) 18 | 19 | @pytest.fixture 20 | def order_service(order_repository, menu_repository): 21 | return OrderService(order_repository, menu_repository) 22 | 23 | @pytest.mark.asyncio 24 | async def test_get_all_menus(menu_service, menu_repository): 25 | # Given 26 | expected_menus = [ 27 | Menu(id="1", name="아메리카노", price=4500, category="커피", stock=100), 28 | Menu(id="2", name="카페라떼", price=5000, category="커피", stock=100) 29 | ] 30 | menu_repository.get_all = AsyncMock(return_value=expected_menus) 31 | 32 | # When 33 | menus = await menu_service.get_all_menus() 34 | 35 | # Then 36 | assert menus == expected_menus 37 | menu_repository.get_all.assert_called_once() 38 | 39 | @pytest.mark.asyncio 40 | async def test_create_order_success(order_service, menu_repository, order_repository): 41 | # Given 42 | menu = Menu(id="1", name="아메리카노", price=4500, category="커피", stock=100) 43 | menu_repository.get_by_id = AsyncMock(return_value=menu) 44 | menu_repository.update_stock = AsyncMock(return_value=True) 45 | 46 | expected_order = Order( 47 | id="test-id", 48 | customer_id="customer-1", 49 | menu_id="1", 50 | quantity=2, 51 | status=OrderStatus.PENDING, 52 | created_at=datetime.utcnow() 53 | ) 54 | order_repository.create = AsyncMock(return_value=expected_order) 55 | 56 | # When 57 | order = await order_service.create_order("customer-1", "1", 2) 58 | 59 | # Then 60 | assert order == expected_order 61 | menu_repository.get_by_id.assert_called_once_with("1") 62 | menu_repository.update_stock.assert_called_once_with("1", 2) 63 | order_repository.create.assert_called_once() -------------------------------------------------------------------------------- /ch10/chain-of-thought-prompt-with-guardrail/src/infrastructure/repositories/dynamodb_menu_repository.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from pynamodb.attributes import NumberAttribute, UnicodeAttribute, UTCDateTimeAttribute 4 | from pynamodb.models import Model 5 | 6 | from ...domain.entities.menu import Menu 7 | from ...domain.repositories.menu_repository import MenuRepository 8 | 9 | 10 | class MenuModel(Model): 11 | class Meta: 12 | table_name = "menus" 13 | region = "ap-northeast-2" 14 | 15 | menu_id = UnicodeAttribute(hash_key=True) 16 | name = UnicodeAttribute() 17 | price = NumberAttribute() 18 | category = UnicodeAttribute() 19 | stock = NumberAttribute() 20 | created_at = UTCDateTimeAttribute() 21 | updated_at = UTCDateTimeAttribute(null=True) 22 | 23 | 24 | class DynamoDBMenuRepository(MenuRepository): 25 | def get_all(self) -> List[Menu]: 26 | return [ 27 | Menu( 28 | menu_id=item.menu_id, 29 | name=item.name, 30 | price=item.price, 31 | category=item.category, 32 | stock=item.stock, 33 | created_at=item.created_at, 34 | updated_at=item.updated_at, 35 | ) 36 | for item in MenuModel.scan() 37 | ] 38 | 39 | def get_by_id(self, menu_id: str) -> Optional[Menu]: 40 | try: 41 | item = MenuModel.get(menu_id) 42 | return Menu( 43 | menu_id=item.menu_id, 44 | name=item.name, 45 | price=item.price, 46 | category=item.category, 47 | stock=item.stock, 48 | created_at=item.created_at, 49 | updated_at=item.updated_at, 50 | ) 51 | except MenuModel.DoesNotExist: 52 | return None 53 | 54 | def update(self, menu: Menu) -> None: 55 | item = MenuModel( 56 | menu_id=menu.menu_id, 57 | name=menu.name, 58 | price=menu.price, 59 | category=menu.category, 60 | stock=menu.stock, 61 | created_at=menu.created_at, 62 | updated_at=menu.updated_at, 63 | ) 64 | item.save() 65 | --------------------------------------------------------------------------------