add Readme per layer
move db calls to interface_adapters
implement product example
add tests
add __init__ files for easier to read imports
uebung_entities
michael 2025-01-16 00:36:59 +01:00
parent b3ff7894c6
commit 53523d46b2
36 changed files with 308 additions and 61 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
__pycache__/
*.py[cod]
*$py.class
/env

View File

@ -0,0 +1,11 @@
# ENTITIES
Entities encapsulate enterprise-wide Critical Business Rules. An entity can be an
object with methods, or it can be a set of data structures and functions. It doesnt
matter so long as the entities can be used by many different applications in the
enterprise.
If you dont have an enterprise and are writing just a single application, then these
entities are the business objects of the application. They encapsulate the most general
and high-level rules. They are the least likely to change when something external
changes. For example, you would not expect these objects to be affected by a change
to page navigation or security. No operational change to any particular application
should affect the entity layer.

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -3,8 +3,8 @@ from typing import List
from dataclasses import dataclass
#dependency imports
from entities.cart_item import CartItem
from entities.product import Product
from .cart_item import CartItem
from .product import Product
@dataclass
class Cart:

View File

@ -0,0 +1,8 @@
# FRAMEWORKS AND DRIVERS
The outermost layer of the model in Figure 22.1 is generally composed of
frameworks and tools such as the database and the web framework. Generally you
dont write much code in this layer, other than glue code that communicates to the
next circle inward.
The frameworks and drivers layer is where all the details go. The web is a detail. The
database is a detail. We keep these things on the outside where they can do little
harm.

View File

@ -0,0 +1,17 @@
# INTERFACE ADAPTERS
The software in the interface adapters layer is a set of adapters that convert data from
the format most convenient for the use cases and entities, to the format most
convenient for some external agency such as the database or the web. It is this layer,
for example, that will wholly contain the MVC architecture of a GUI. The
presenters, views, and controllers all belong in the interface adapters layer. The
models are likely just data structures that are passed from the controllers to the use
cases, and then back from the use cases to the presenters and views.
Similarly, data is converted, in this layer, from the form most convenient for entities
and use cases, to the form most convenient for whatever persistence framework is
being used (i.e., the database). No code inward of this circle should know anything at
all about the database. If the database is a SQL database, then all SQL should be
restricted to this layer—and in particular to the parts of this layer that have to do with
the database.
Also in this layer is any other adapter necessary to convert data from some external
form, such as an external service, to the internal form used by the use cases and
entities.

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -8,21 +8,16 @@ from fastapi.templating import Jinja2Templates
import httpx
#dependency imports
from use_cases.view_cart import ViewCart
from interface_adapters.dtos.view_cart_response import ViewCartResponseDTO, CartItemDTO
from interface_adapters.dtos.view_cart_request import ViewCartRequestDTO
from interface_adapters.repositories.cart_repository import CartRepository
#from interface_adapters.mocks.cart_db import CartDatabase
from framework_driver.db.db_cart import CartDatabase
from use_cases import ViewCart
from interface_adapters.dtos import ViewCartRequestDTO, ViewCartResponseDTO, CartItemDTO
from interface_adapters.repositories import SQLCartRepository
# Set up templates
templates = Jinja2Templates(directory="framework_driver/ui/templates")
# Initialize components
cart_database = CartDatabase() # Concrete database implementation
cart_repository = CartRepository(cart_database) # Repository depends on gateway
view_cart_use_case = ViewCart(cart_repository) # Use case depends on repository
cart_repository = SQLCartRepository()
view_cart_use_case = ViewCart(cart_repository)
router = APIRouter()
@ -35,7 +30,7 @@ async def view_cart(request_dto: ViewCartRequestDTO):
raise HTTPException(status_code=404, detail="Cart not found")
response_dto = ViewCartResponseDTO(
items=[CartItemDTO(product_id=item.product_id, quantity=item.quantity, price=item.price) for item in cart.items],
items=[CartItemDTO(product_id=item.product_id, name=item.name, quantity=item.quantity, price=item.price) for item in cart.items],
total_price=sum(item.price * item.quantity for item in cart.items)
)

View File

@ -0,0 +1,33 @@
#python imports
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import httpx
#dependency imports
from use_cases import ViewProduct
from interface_adapters.dtos import ViewProductRequestDTO, ViewProductResponseDTO, ProductDTO
from interface_adapters.repositories import SQLProductRepository
# Initialize components
product_repository = SQLProductRepository()
view_product_use_case = ViewProduct(product_repository)
router = APIRouter()
@router.post("/view_product", response_model=ViewProductResponseDTO)
async def view_product(request_dto: ViewProductRequestDTO):
product = view_product_use_case.execute(request_dto.product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
response_dto = ViewProductResponseDTO(
product=ProductDTO(id=product.id, name=product.name, description=product.description, price=product.price)
)
return response_dto

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -4,6 +4,7 @@ from typing import List, Optional
class CartItemDTO(BaseModel):
product_id: int
name: str
quantity: int
price: float

View File

@ -0,0 +1,6 @@
#python imports
from pydantic import BaseModel
from typing import List, Optional
class ViewProductRequestDTO(BaseModel):
product_id: int

View File

@ -0,0 +1,12 @@
#python imports
from pydantic import BaseModel
from typing import List, Optional
class ProductDTO(BaseModel):
id: int
name: str
description: str
price: float
class ViewProductResponseDTO(BaseModel):
product: ProductDTO

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -1,9 +0,0 @@
#python imports
from typing import Optional, List
class CartDatabaseInterface:
def fetch_cart_items(self, user_id: int) -> Optional[List[dict]]:
"""
Abstract method to fetch cart items from the database for a given user ID.
"""
raise NotImplementedError("fetch_cart_items must be implemented by a subclass")

View File

@ -1,13 +0,0 @@
#python imports
from typing import Optional, List
#dependency imports
from use_cases.cart_repository_interface import CartRepositoryInterface
from interface_adapters.repositories.cart_database_interface import CartDatabaseInterface
class CartRepository(CartRepositoryInterface):
def __init__(self, database_gateway: CartDatabaseInterface):
self.database_gateway = database_gateway
def fetch_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]:
return self.database_gateway.fetch_cart_items(user_id)

View File

@ -1,8 +1,11 @@
import sqlite3
#python imports
from typing import Optional, List
from interface_adapters.repositories.cart_database_interface import CartDatabaseInterface
import sqlite3
class CartDatabase(CartDatabaseInterface):
#dependency imports
from use_cases import ICartRepository
class SQLCartRepository(ICartRepository):
def __init__(self, db_file="framework_driver/db/shop.db"):
"""Initialize the CartDatabase with a connection to the SQLite database."""
self.conn = sqlite3.connect(db_file)
@ -18,7 +21,7 @@ class CartDatabase(CartDatabaseInterface):
except sqlite3.Error as e:
print(e)
def fetch_cart_items(self, user_id: int) -> Optional[List[dict]]:
def get_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]:
"""Fetch cart items from the database for a given user ID."""
sql = "SELECT product_id, name, quantity, price FROM cart WHERE user_id = ?"
try:

View File

@ -1,7 +1,12 @@
#python imports
from typing import Optional, List
import sqlite3
class ProductDatabase:
def __init__(self, db_file):
#dependency imports
from use_cases import IProductRepository
class SQLProductRepository(IProductRepository):
def __init__(self, db_file="framework_driver/db/shop.db"):
"""Initialize the ProductDatabase with a connection to the SQLite database."""
self.conn = sqlite3.connect(db_file)
@ -22,7 +27,8 @@ class ProductDatabase:
try:
c = self.conn.cursor()
c.execute(sql, (product_id,))
return c.fetchone()
r = c.fetchone()
return {'id':r[0], 'name':str(r[1]), 'description':str(r[2]), 'price':r[3]}
except sqlite3.Error as e:
print(e)
return None

View File

@ -1,7 +1,12 @@
#python imports
from typing import Optional, List
import sqlite3
class UserDatabase:
def __init__(self, db_file):
#dependency imports
from use_cases import IUserRepository
class SQLUserRepository(IUserRepository):
def __init__(self, db_file="framework_driver/db/shop.db"):
"""Initialize the UserDatabase with a connection to the SQLite database."""
self.conn = sqlite3.connect(db_file)

View File

@ -1,4 +1,5 @@
from interface_adapters.controllers.cart_controller import router as cart_router
from interface_adapters.controllers.product_controller import router as product_router
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@ -14,6 +15,7 @@ import os
app = FastAPI()
app.include_router(cart_router)
app.include_router(product_router)
# Allow CORS for all origins (for simplicity)
app.add_middleware(

View File

@ -0,0 +1,2 @@
# Tests
To run tests: python -m unittest discover

View File

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -2,9 +2,9 @@
from typing import Optional, List
#dependency imports
from interface_adapters.repositories.cart_database_interface import CartDatabaseInterface
from use_cases import ICartRepository
class CartDatabase(CartDatabaseInterface):
class CartDatabase(ICartRepository):
def __init__(self):
# Mock database setup
self.mock_db = {
@ -17,5 +17,5 @@ class CartDatabase(CartDatabaseInterface):
],
}
def fetch_cart_items(self, user_id: int) -> Optional[List[dict]]:
def get_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]:
return self.mock_db.get(user_id)

View File

@ -0,0 +1,16 @@
#python imports
from typing import Optional, List
#dependency imports
from use_cases import IProductRepository
class ProductDatabase(IProductRepository):
def __init__(self):
# Mock database setup
self.mock_db = {
1: {"id": 1, "name": "Apple", "description": "gsdg", "price": 0.5},
2: {"id": 2, "name": "Bread", "description": "sfg", "price": 2.0},
}
def get_product_by_id(self, product_id: int) -> Optional[List[dict]]:
return self.mock_db.get(product_id)

View File

View File

@ -0,0 +1,19 @@
from use_cases import ViewCart
from tests.mocks import CartDatabase
import unittest
class TestViewCart(unittest.TestCase):
def setUp(self):
self.cart_database = CartDatabase()
self.view_cart_use_case = ViewCart(self.cart_database)
def test_view_cart_valid_id(self):
result = self.view_cart_use_case.execute(1)
self.assertIsNotNone(result)
#self.assertEqual(result['id'], 1)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,19 @@
from use_cases import ViewProduct
from tests.mocks import ProductDatabase
import unittest
class TestViewProduct(unittest.TestCase):
def setUp(self):
self.product_database = ProductDatabase()
self.view_product_use_case = ViewProduct(self.product_database)
def test_view_product_valid_id(self):
result = self.view_product_use_case.execute(1)
self.assertIsNotNone(result)
#self.assertEqual(result['id'], 1)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,11 @@
# USE CASES
The software in the use cases layer contains application-specific business rules. It
encapsulates and implements all of the use cases of the system. These use cases
orchestrate the flow of data to and from the entities, and direct those entities to use
their Critical Business Rules to achieve the goals of the use case.
We do not expect changes in this layer to affect the entities. We also do not expect
this layer to be affected by changes to externalities such as the database, the UI, or
any of the common frameworks. The use cases layer is isolated from such concerns.
We do, however, expect that changes to the operation of the application will affect
the use cases and, therefore, the software in this layer. If the details of a use case
change, then some code in this layer will certainly be affected.

View File

@ -0,0 +1,11 @@
import importlib
import pkgutil
import inspect
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
module = importlib.import_module(f".{module_name}", __name__)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ == module.__name__:
globals()[name] = obj
__all__.append(name)

View File

@ -0,0 +1,8 @@
from typing import Optional, List
class ICartRepository:
def get_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]:
"""
Abstract method to fetch cart data by user ID.
"""
raise NotImplementedError("get_cart_by_user_id must be implemented by a subclass")

View File

@ -1,8 +0,0 @@
from typing import Optional, List
class CartRepositoryInterface:
def fetch_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]:
"""
Abstract method to fetch cart data by user ID.
"""
raise NotImplementedError("fetch_cart_by_user_id must be implemented by a subclass")

View File

@ -0,0 +1,8 @@
from typing import Optional, List
class IProductRepository:
def get_product_by_id(self, product_id: int):
"""
Abstract method to fetch product data by user ID.
"""
raise NotImplementedError("get_product_by_id must be implemented by a subclass")

View File

@ -0,0 +1,8 @@
from typing import Optional, List
class IUserRepository:
def get_user_by_id(self, user_id: int):
"""
Abstract method to fetch user data by user ID.
"""
raise NotImplementedError("get_user_by_id must be implemented by a subclass")

View File

@ -2,18 +2,18 @@
from typing import Optional
#dependency imports
from entities.cart import Cart, CartItem
from use_cases.cart_repository_interface import CartRepositoryInterface
from entities import Cart, CartItem
from .cart_repository import ICartRepository
class ViewCart:
def __init__(self, cart_repository: CartRepositoryInterface):
def __init__(self, cart_repository: ICartRepository):
self.cart_repository = cart_repository
def execute(self, user_id: int) -> Optional[Cart]:
"""
Fetches the cart data from the repository, converts it to Cart entity.
"""
cart_data = self.cart_repository.fetch_cart_by_user_id(user_id)
cart_data = self.cart_repository.get_cart_by_user_id(user_id)
if not cart_data:
return None

View File

@ -0,0 +1,19 @@
#python imports
from typing import Optional
#dependency imports
from entities import Product
from .product_repository import IProductRepository
class ViewProduct:
def __init__(self, product_repository: IProductRepository):
self.product_repository = product_repository
def execute(self, product_id: int) -> Optional[Product]:
"""
Fetches the product data from the repository, converts it to Product entity.
"""
product_data = self.product_repository.get_product_by_id(product_id)
if not product_data:
return None
return Product(**product_data)