diff --git a/.gitignore b/.gitignore index e928de1..7ec1667 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*$py.class \ No newline at end of file +*$py.class + +/env \ No newline at end of file diff --git a/src/entities/README.md b/src/entities/README.md new file mode 100644 index 0000000..0f7a616 --- /dev/null +++ b/src/entities/README.md @@ -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 doesn’t +matter so long as the entities can be used by many different applications in the +enterprise. +If you don’t 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. \ No newline at end of file diff --git a/src/entities/__init__.py b/src/entities/__init__.py new file mode 100644 index 0000000..10727b3 --- /dev/null +++ b/src/entities/__init__.py @@ -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) \ No newline at end of file diff --git a/src/entities/cart.py b/src/entities/cart.py index b849bd7..805143c 100644 --- a/src/entities/cart.py +++ b/src/entities/cart.py @@ -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: diff --git a/src/framework_driver/README.md b/src/framework_driver/README.md new file mode 100644 index 0000000..a484b2d --- /dev/null +++ b/src/framework_driver/README.md @@ -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 +don’t 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. \ No newline at end of file diff --git a/src/interface_adapters/README.md b/src/interface_adapters/README.md new file mode 100644 index 0000000..f73bae9 --- /dev/null +++ b/src/interface_adapters/README.md @@ -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. \ No newline at end of file diff --git a/src/interface_adapters/controllers/__init__.py b/src/interface_adapters/controllers/__init__.py new file mode 100644 index 0000000..cb0cf8a --- /dev/null +++ b/src/interface_adapters/controllers/__init__.py @@ -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) diff --git a/src/interface_adapters/controllers/cart_controller.py b/src/interface_adapters/controllers/cart_controller.py index 9ba18cc..57fe7ac 100644 --- a/src/interface_adapters/controllers/cart_controller.py +++ b/src/interface_adapters/controllers/cart_controller.py @@ -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) ) diff --git a/src/interface_adapters/controllers/product_controller.py b/src/interface_adapters/controllers/product_controller.py new file mode 100644 index 0000000..d602936 --- /dev/null +++ b/src/interface_adapters/controllers/product_controller.py @@ -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 diff --git a/src/interface_adapters/dtos/__init__.py b/src/interface_adapters/dtos/__init__.py new file mode 100644 index 0000000..cb0cf8a --- /dev/null +++ b/src/interface_adapters/dtos/__init__.py @@ -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) diff --git a/src/interface_adapters/dtos/view_cart_response.py b/src/interface_adapters/dtos/view_cart_response.py index 87b6af0..6bc21d2 100644 --- a/src/interface_adapters/dtos/view_cart_response.py +++ b/src/interface_adapters/dtos/view_cart_response.py @@ -4,6 +4,7 @@ from typing import List, Optional class CartItemDTO(BaseModel): product_id: int + name: str quantity: int price: float diff --git a/src/interface_adapters/dtos/view_product_request.py b/src/interface_adapters/dtos/view_product_request.py new file mode 100644 index 0000000..a7367e2 --- /dev/null +++ b/src/interface_adapters/dtos/view_product_request.py @@ -0,0 +1,6 @@ +#python imports +from pydantic import BaseModel +from typing import List, Optional + +class ViewProductRequestDTO(BaseModel): + product_id: int \ No newline at end of file diff --git a/src/interface_adapters/dtos/view_product_response.py b/src/interface_adapters/dtos/view_product_response.py new file mode 100644 index 0000000..b092a3e --- /dev/null +++ b/src/interface_adapters/dtos/view_product_response.py @@ -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 \ No newline at end of file diff --git a/src/interface_adapters/repositories/__init__.py b/src/interface_adapters/repositories/__init__.py new file mode 100644 index 0000000..cb0cf8a --- /dev/null +++ b/src/interface_adapters/repositories/__init__.py @@ -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) diff --git a/src/interface_adapters/repositories/cart_database_interface.py b/src/interface_adapters/repositories/cart_database_interface.py deleted file mode 100644 index e5a95cd..0000000 --- a/src/interface_adapters/repositories/cart_database_interface.py +++ /dev/null @@ -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") \ No newline at end of file diff --git a/src/interface_adapters/repositories/cart_repository.py b/src/interface_adapters/repositories/cart_repository.py deleted file mode 100644 index e5b9f34..0000000 --- a/src/interface_adapters/repositories/cart_repository.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/src/framework_driver/db/db_cart.py b/src/interface_adapters/repositories/sql_cart_repository.py similarity index 93% rename from src/framework_driver/db/db_cart.py rename to src/interface_adapters/repositories/sql_cart_repository.py index 426b888..675c478 100644 --- a/src/framework_driver/db/db_cart.py +++ b/src/interface_adapters/repositories/sql_cart_repository.py @@ -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: diff --git a/src/framework_driver/db/db_product.py b/src/interface_adapters/repositories/sql_product_repository.py similarity index 87% rename from src/framework_driver/db/db_product.py rename to src/interface_adapters/repositories/sql_product_repository.py index f43ab74..5c78df7 100644 --- a/src/framework_driver/db/db_product.py +++ b/src/interface_adapters/repositories/sql_product_repository.py @@ -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 diff --git a/src/framework_driver/db/db_user.py b/src/interface_adapters/repositories/sql_user_repository.py similarity index 91% rename from src/framework_driver/db/db_user.py rename to src/interface_adapters/repositories/sql_user_repository.py index 764a2ab..c372c9f 100644 --- a/src/framework_driver/db/db_user.py +++ b/src/interface_adapters/repositories/sql_user_repository.py @@ -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) diff --git a/src/main.py b/src/main.py index 1def017..872d6f2 100644 --- a/src/main.py +++ b/src/main.py @@ -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( diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 0000000..951b104 --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,2 @@ +# Tests +To run tests: python -m unittest discover \ No newline at end of file diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/mocks/__init__.py b/src/tests/mocks/__init__.py new file mode 100644 index 0000000..cb0cf8a --- /dev/null +++ b/src/tests/mocks/__init__.py @@ -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) diff --git a/src/interface_adapters/mocks/cart_db.py b/src/tests/mocks/cart_db.py similarity index 66% rename from src/interface_adapters/mocks/cart_db.py rename to src/tests/mocks/cart_db.py index f57c667..dae874a 100644 --- a/src/interface_adapters/mocks/cart_db.py +++ b/src/tests/mocks/cart_db.py @@ -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]]: - return self.mock_db.get(user_id) \ No newline at end of file + def get_cart_by_user_id(self, user_id: int) -> Optional[List[dict]]: + return self.mock_db.get(user_id) diff --git a/src/tests/mocks/product_db.py b/src/tests/mocks/product_db.py new file mode 100644 index 0000000..66596e5 --- /dev/null +++ b/src/tests/mocks/product_db.py @@ -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) diff --git a/src/tests/use_cases/__init__.py b/src/tests/use_cases/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/use_cases/test_view_cart.py b/src/tests/use_cases/test_view_cart.py new file mode 100644 index 0000000..4d9128f --- /dev/null +++ b/src/tests/use_cases/test_view_cart.py @@ -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() \ No newline at end of file diff --git a/src/tests/use_cases/test_view_product.py b/src/tests/use_cases/test_view_product.py new file mode 100644 index 0000000..041cdc8 --- /dev/null +++ b/src/tests/use_cases/test_view_product.py @@ -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() \ No newline at end of file diff --git a/src/use_cases/README.md b/src/use_cases/README.md new file mode 100644 index 0000000..2822cc6 --- /dev/null +++ b/src/use_cases/README.md @@ -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. \ No newline at end of file diff --git a/src/use_cases/__init__.py b/src/use_cases/__init__.py new file mode 100644 index 0000000..10727b3 --- /dev/null +++ b/src/use_cases/__init__.py @@ -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) \ No newline at end of file diff --git a/src/use_cases/cart_repository.py b/src/use_cases/cart_repository.py new file mode 100644 index 0000000..5cb7452 --- /dev/null +++ b/src/use_cases/cart_repository.py @@ -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") diff --git a/src/use_cases/cart_repository_interface.py b/src/use_cases/cart_repository_interface.py deleted file mode 100644 index 5bf005a..0000000 --- a/src/use_cases/cart_repository_interface.py +++ /dev/null @@ -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") diff --git a/src/use_cases/product_repository.py b/src/use_cases/product_repository.py new file mode 100644 index 0000000..63dd8df --- /dev/null +++ b/src/use_cases/product_repository.py @@ -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") diff --git a/src/use_cases/user_repository.py b/src/use_cases/user_repository.py new file mode 100644 index 0000000..1952ccc --- /dev/null +++ b/src/use_cases/user_repository.py @@ -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") \ No newline at end of file diff --git a/src/use_cases/view_cart.py b/src/use_cases/view_cart.py index 62b8e2f..3ade8b6 100644 --- a/src/use_cases/view_cart.py +++ b/src/use_cases/view_cart.py @@ -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 diff --git a/src/use_cases/view_product.py b/src/use_cases/view_product.py new file mode 100644 index 0000000..54a3938 --- /dev/null +++ b/src/use_cases/view_product.py @@ -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) \ No newline at end of file