From 47b99597c39401cb080369750443d3cd70d24489 Mon Sep 17 00:00:00 2001 From: TJ Maynes Date: Wed, 28 Feb 2024 23:46:31 -0600 Subject: [PATCH] chore: upgrade dependencies --- .tool-versions | 1 + Makefile | 10 ++--- README.md | 4 +- api/cart/domain.py | 45 --------------------- {api => app}/__init__.py | 0 {api => app}/cart/__init__.py | 0 app/cart/domain.py | 36 +++++++++++++++++ {api => app}/cart/repository.py | 22 +++++------ {api => app}/cart/service.py | 2 +- {api => app}/core/__init__.py | 8 ++-- {api => app}/core/exceptions.py | 12 +++++- {api => app}/core/repository.py | 0 {api => app}/core/service.py | 4 +- {api => app}/core/types.py | 0 {api => app}/health/__init__.py | 0 {api => app}/health/domain.py | 0 {api => app}/health/service.py | 2 +- {api => app}/main.py | 46 +++++++++++----------- {api => app}/main_test.py | 14 +++---- {api => app}/seed.py | 8 ++-- k8s/shopping-cart-db/persistence.local.yml | 7 ++-- requirements-dev.txt | 2 +- requirements.txt | 9 +++-- 23 files changed, 118 insertions(+), 114 deletions(-) delete mode 100644 api/cart/domain.py rename {api => app}/__init__.py (100%) rename {api => app}/cart/__init__.py (100%) create mode 100644 app/cart/domain.py rename {api => app}/cart/repository.py (83%) rename {api => app}/cart/service.py (65%) rename {api => app}/core/__init__.py (54%) rename {api => app}/core/exceptions.py (70%) rename {api => app}/core/repository.py (100%) rename {api => app}/core/service.py (94%) rename {api => app}/core/types.py (100%) rename {api => app}/health/__init__.py (100%) rename {api => app}/health/domain.py (100%) rename {api => app}/health/service.py (91%) rename {api => app}/main.py (74%) rename {api => app}/main_test.py (94%) rename {api => app}/seed.py (76%) diff --git a/.tool-versions b/.tool-versions index e6ea852..99dbbc5 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ python 3.11.3 +kubectl 1.26.2 diff --git a/Makefile b/Makefile index 0d1458f..106b7f9 100644 --- a/Makefile +++ b/Makefile @@ -15,18 +15,18 @@ test: migrate . .venv/bin/activate; python3 -m pytest lint: - . .venv/bin/activate; mypy api/ + . .venv/bin/activate; mypy app/ start: migrate - . .venv/bin/activate; uvicorn --host 0.0.0.0 --port $(PORT) api.main:app + . .venv/bin/activate; uvicorn --host 0.0.0.0 --port $(PORT) app.main:api seed: migrate - . .venv/bin/activate; python3 -m api.seed + . .venv/bin/activate; python3 -m app.seed -run_local_db: +start_local_db: kubectl apply -f ./k8s/shopping-cart-common/secret.yml kubectl apply -f ./k8s/shopping-cart-db/deployment.yml - kubectl apply -f ./k8s/shopping-cart-db/persistence.local.yml + (mkdir -p /tmp/shopping-cart/data || true) && kubectl apply -f ./k8s/shopping-cart-db/persistence.local.yml connect_localhost_to_remote_db: kubectl port-forward svc/shopping-cart-db 5432:5432 diff --git a/README.md b/README.md index 2965148..18d8df9 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Next, let's make sure the test suite for the application is running as expected. 2. Since our test suite talks to a database, let's make sure that PostgreSQL is running locally via: ```bash -make run_local_db +make start_local_db ``` 3. Finally, let's run our tests via: @@ -101,7 +101,7 @@ make test To make sure the database is running, run the following command: ```bash -make run_local_db +make start_local_db ``` To start the app and database locally, run the following command: diff --git a/api/cart/domain.py b/api/cart/domain.py deleted file mode 100644 index 58a91f6..0000000 --- a/api/cart/domain.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import List, Dict, Callable, Any, Tuple -from dataclasses import dataclass, field -from result import Ok, Err, Result -from pydantic import BaseModel -from datetime import datetime -from api.core import InvalidItemException, CustomException - -class CartItemIn(BaseModel): - name: str - price: int - manufacturer: str - -class CartItem(BaseModel): - id: str - name: str - price: int - manufacturer: str - created_at: datetime - last_modified_at: datetime - -def validate_cart_item(item: CartItemIn) -> Result[CartItemIn, CustomException]: - def empty_value(type: str) -> str: - return f"'{type}' must not be empty" - - errors: List[str] = [] - - if item.name is None: - errors.append(empty_value("name")) - elif len(item.name) == 0: - errors.append(empty_value("name")) - - if item.price is None: - errors.append(empty_value("price")) - if int(item.price) < 99: - errors.append("'price' must be greater than 99") - - if item.manufacturer is None: - errors.append(empty_value("manufacturer")) - if len(item.manufacturer) == 0: - errors.append(empty_value("manufacturer")) - - if len(errors) != 0: - return Err(InvalidItemException(errors)) - else: - return Ok(item) \ No newline at end of file diff --git a/api/__init__.py b/app/__init__.py similarity index 100% rename from api/__init__.py rename to app/__init__.py diff --git a/api/cart/__init__.py b/app/cart/__init__.py similarity index 100% rename from api/cart/__init__.py rename to app/cart/__init__.py diff --git a/app/cart/domain.py b/app/cart/domain.py new file mode 100644 index 0000000..815ce22 --- /dev/null +++ b/app/cart/domain.py @@ -0,0 +1,36 @@ +from typing import List, Dict, Callable, Any, Tuple +from dataclasses import dataclass, field +from result import Ok, Err, Result +from pydantic import BaseModel +from datetime import datetime +from app.core import InvalidItemException, CustomException + +class CartItemIn(BaseModel): + name: str + price: int + manufacturer: str + +class CartItem(BaseModel): + id: str + name: str + price: int + manufacturer: str + created_at: datetime + last_modified_at: datetime + +def validate_cart_item(item: CartItemIn) -> Result[CartItemIn, CustomException]: + errors = [] + + if not item.name or len(item.name) == 0: + errors.append("'name' must not be empty") + + if item.price is None or int(item.price) < 99: + errors.append("'price' must be a non-empty number greater than 99") + + if not item.manufacturer or len(item.manufacturer) == 0: + errors.append("'manufacturer' must not be empty") + + if errors: + return Err(InvalidItemException(errors)) + else: + return Ok(item) \ No newline at end of file diff --git a/api/cart/repository.py b/app/cart/repository.py similarity index 83% rename from api/cart/repository.py rename to app/cart/repository.py index 5eb5cbb..896303a 100644 --- a/api/cart/repository.py +++ b/app/cart/repository.py @@ -1,5 +1,5 @@ from typing import List, TypeVar, Tuple -from api.core import Repository, CustomException, UnknownException, NotFoundException, Connection +from app.core import Repository, CustomException, UnknownException, NotFoundException, Connection from .domain import CartItem, CartItemIn from result import Ok, Err, Result import datetime @@ -19,7 +19,7 @@ def get_all_items(self, page_number: int = 0, page_size: int = 10) -> Result[Lis return Ok([]) except Exception as inst: - return Err(UnknownException(list(inst.args))) + return Err(UnknownException(inst)) def get_item_by_id(self, id: str) -> Result[CartItem, CustomException]: @@ -29,10 +29,10 @@ def get_item_by_id(self, id: str) -> Result[CartItem, CustomException]: row = cursor.fetchone() if row: return Ok(self.__from_tuple(row)) - return Err(NotFoundException()) + return Err(NotFoundException(message="id: {}".format(id))) except Exception as inst: - return Err(UnknownException(list(inst.args))) + return Err(UnknownException(inst)) def add_item(self, item: CartItemIn) -> Result[CartItem, CustomException]: @@ -46,11 +46,11 @@ def add_item(self, item: CartItemIn) -> Result[CartItem, CustomException]: if result: return Ok(self.__from_tuple(result)) - return Err(UnknownException(["Unable to add item at this time!"])) + return Err(UnknownException("Unable to add item at this time!")) except Exception as inst: self.__db_conn.rollback() - return Err(UnknownException(list(inst.args))) + return Err(UnknownException(inst)) def update_item(self, id: str, item: CartItemIn) -> Result[CartItem, CustomException]: @@ -63,11 +63,11 @@ def update_item(self, id: str, item: CartItemIn) -> Result[CartItem, CustomExcep self.__db_conn.commit() if result: return Ok(self.__from_tuple(result)) - return Err(NotFoundException()) + return Err(NotFoundException(message="id: {}".format(id))) except Exception as inst: self.__db_conn.rollback() - return Err(UnknownException(list(inst.args))) + return Err(UnknownException(inst)) def remove_item_by_id(self, id: str) -> Result[str, CustomException]: @@ -79,16 +79,16 @@ def remove_item_by_id(self, id: str) -> Result[str, CustomException]: if rows_deleted == 1: return Ok(id) - return Err(NotFoundException([id])) + return Err(NotFoundException(message="id: {}".format(id))) except Exception as inst: self.__db_conn.rollback() - return Err(UnknownException(list(inst.args))) + return Err(UnknownException(inst)) def __from_tuple(self, tuple: Tuple) -> CartItem: return CartItem( - id=tuple[0], + id="{}".format(tuple[0]), name=tuple[1], price=tuple[2], manufacturer=tuple[3], diff --git a/api/cart/service.py b/app/cart/service.py similarity index 65% rename from api/cart/service.py rename to app/cart/service.py index 096c5e0..de49fd4 100644 --- a/api/cart/service.py +++ b/app/cart/service.py @@ -1,5 +1,5 @@ from typing import Callable, List -from api.core import Service, Repository +from app.core import Service, Repository from .domain import CartItem, CartItemIn, validate_cart_item def CartService(repository: Repository) -> Service[CartItemIn, CartItem]: diff --git a/api/core/__init__.py b/app/core/__init__.py similarity index 54% rename from api/core/__init__.py rename to app/core/__init__.py index 4b78d7a..18e5c8b 100644 --- a/api/core/__init__.py +++ b/app/core/__init__.py @@ -1,6 +1,6 @@ -from api.core.repository import Repository -from api.core.service import Service -from api.core.exceptions import (\ +from app.core.repository import Repository +from app.core.service import Service +from app.core.exceptions import (\ CustomException, \ DBConnectionFailedException, \ NotFoundException, \ @@ -8,4 +8,4 @@ InvalidItemException, \ BadRequestException, \ UnknownException) -from api.core.types import Connection +from app.core.types import Connection diff --git a/api/core/exceptions.py b/app/core/exceptions.py similarity index 70% rename from api/core/exceptions.py rename to app/core/exceptions.py index 33f76fe..18da122 100644 --- a/api/core/exceptions.py +++ b/app/core/exceptions.py @@ -1,17 +1,27 @@ class CustomException(Exception): """Basic exception for errors raised""" + def __init__(self, message): + self.message = message + def __str__(self): + return self.message + pass class DBConnectionFailedException(CustomException): """Unable to connect to repository""" + pass class NotFoundException(CustomException): """Unable to find item in repository""" + pass class InvalidItemException(CustomException): """Given an invalid item""" + pass class BadRequestException(CustomException): """Invalid item given""" + pass class UnknownException(CustomException): - """Unknown""" \ No newline at end of file + """Unknown""" + pass \ No newline at end of file diff --git a/api/core/repository.py b/app/core/repository.py similarity index 100% rename from api/core/repository.py rename to app/core/repository.py diff --git a/api/core/service.py b/app/core/service.py similarity index 94% rename from api/core/service.py rename to app/core/service.py index 5faecb8..9530448 100644 --- a/api/core/service.py +++ b/app/core/service.py @@ -23,7 +23,7 @@ def get_item_by_id(self, id: str) -> Result[T, CustomException]: def add_item(self, item: S) -> Result[T, CustomException]: validation_result = self.__validate_item(item) if isinstance(validation_result, Ok): - return self.__repository.add_item(validation_result.value) + return self.__repository.add_item(validation_result.ok_value) else: return validation_result @@ -31,7 +31,7 @@ def add_item(self, item: S) -> Result[T, CustomException]: def update_item(self, id: str, item: S) -> Result[T, CustomException]: validation_result = self.__validate_item(item) if isinstance(validation_result, Ok): - return self.__repository.update_item(id, validation_result.value) + return self.__repository.update_item(id, validation_result.ok_value) else: return validation_result diff --git a/api/core/types.py b/app/core/types.py similarity index 100% rename from api/core/types.py rename to app/core/types.py diff --git a/api/health/__init__.py b/app/health/__init__.py similarity index 100% rename from api/health/__init__.py rename to app/health/__init__.py diff --git a/api/health/domain.py b/app/health/domain.py similarity index 100% rename from api/health/domain.py rename to app/health/domain.py diff --git a/api/health/service.py b/app/health/service.py similarity index 91% rename from api/health/service.py rename to app/health/service.py index 4255cf0..27e10d5 100644 --- a/api/health/service.py +++ b/app/health/service.py @@ -1,5 +1,5 @@ from psycopg2 import Error as Psycopg2Error -from api.core import Connection +from app.core import Connection from .domain import Health class HealthService: diff --git a/api/main.py b/app/main.py similarity index 74% rename from api/main.py rename to app/main.py index 0572776..8d9262b 100644 --- a/api/main.py +++ b/app/main.py @@ -6,13 +6,13 @@ from typing import List, Dict import json from os import getenv -from api.core import CustomException, InvalidItemException, NotFoundException -from api.cart import CartService, CartRepository, CartItem, CartItemIn -from api.health import HealthService, Health +from app.core import CustomException, InvalidItemException, NotFoundException +from app.cart import CartService, CartRepository, CartItem, CartItemIn +from app.health import HealthService, Health -def build_app() -> FastAPI: +def build_api() -> FastAPI: db_conn = connect(getenv("DATABASE_URL")) - + health_service = HealthService(db_conn = db_conn) cart_repository = CartRepository(db_conn = db_conn) cart_service = CartService(repository = cart_repository) @@ -27,55 +27,55 @@ async def health_endpoint() -> JSONResponse: else: return JSONResponse(status_code=500, content=jsonable_encoder(health)) - @app.get("/cart/", response_model=List[CartItem]) + @app.get("/cart", response_model=List[CartItem]) async def fetch_cart_items(page_number: int = 0, page_size: int = 10) -> JSONResponse: items_result = cart_service.get_items(page_number, page_size) if isinstance(items_result, Ok): - return JSONResponse(status_code=200, content=jsonable_encoder(items_result.value)) + return JSONResponse(status_code=200, content=jsonable_encoder(items_result.ok_value)) else: - return JSONResponse(status_code=500, content=jsonable_encoder(items_result.value)) + return JSONResponse(status_code=500, content=jsonable_encoder(items_result.err_value)) @app.get("/cart/{id}", response_model=CartItem) async def fetch_item_by_id(id: str) -> JSONResponse: item_result = cart_service.get_item_by_id(id) if isinstance(item_result, Ok): - return JSONResponse(status_code=200, content=jsonable_encoder(item_result.value)) + return JSONResponse(status_code=200, content=jsonable_encoder(item_result.ok_value)) else: - return JSONResponse(status_code=500, content=jsonable_encoder(item_result.value)) + return JSONResponse(status_code=500, content=jsonable_encoder(item_result.err_value)) @app.post("/cart/", response_model=CartItem) async def create_cart_item(item: CartItemIn) -> JSONResponse: add_item_result = cart_service.add_item(item) if isinstance(add_item_result, Ok): - return JSONResponse(status_code=201, content=jsonable_encoder(add_item_result.value)) + return JSONResponse(status_code=201, content=jsonable_encoder(add_item_result.ok_value)) else: - if isinstance(add_item_result.value, InvalidItemException): - return JSONResponse(status_code=422, content=str(add_item_result.value)) + if isinstance(add_item_result.err_value, InvalidItemException): + return JSONResponse(status_code=422, content=str(add_item_result.err_value)) else: - return JSONResponse(status_code=500, content=jsonable_encoder(add_item_result.value)) + return JSONResponse(status_code=500, content=jsonable_encoder(add_item_result.err_value)) @app.put("/cart/{id}", response_model=CartItem) async def update_cart_item(id: str, item: CartItemIn) -> JSONResponse: update_item_result = cart_service.update_item(id, item) if isinstance(update_item_result, Ok): - return JSONResponse(status_code=200, content=jsonable_encoder(update_item_result.value)) + return JSONResponse(status_code=200, content=jsonable_encoder(update_item_result.ok_value)) else: - if isinstance(update_item_result.value, InvalidItemException): - return JSONResponse(status_code=422, content=str(update_item_result.value)) + if isinstance(update_item_result.err_value, InvalidItemException): + return JSONResponse(status_code=422, content=str(update_item_result.err_value)) else: - return JSONResponse(status_code=500, content=jsonable_encoder(update_item_result.value)) + return JSONResponse(status_code=500, content=jsonable_encoder(update_item_result.err_value)) @app.delete("/cart/{id}", response_model=Dict) async def delete_cart_item(id: str) -> JSONResponse: delete_item_result = cart_service.remove_item_by_id(id) if isinstance(delete_item_result, Ok): - return JSONResponse(status_code=200, content=jsonable_encoder({ "id": delete_item_result.value })) + return JSONResponse(status_code=200, content=jsonable_encoder({ "id": delete_item_result.ok_value })) else: - if isinstance(delete_item_result.value, NotFoundException): - return JSONResponse(status_code=404, content=str(delete_item_result.value)) + if isinstance(delete_item_result.err_value, NotFoundException): + return JSONResponse(status_code=404, content=str(delete_item_result.err_value)) else: - return JSONResponse(status_code=500, content=jsonable_encoder(delete_item_result.value)) + return JSONResponse(status_code=500, content=jsonable_encoder(delete_item_result.err_value)) return app -app = build_app() \ No newline at end of file +api = build_api() \ No newline at end of file diff --git a/api/main_test.py b/app/main_test.py similarity index 94% rename from api/main_test.py rename to app/main_test.py index 501b54e..046f568 100644 --- a/api/main_test.py +++ b/app/main_test.py @@ -1,19 +1,19 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient -from api.core import Connection -from api.main import app -from api.seed import seed_db +from .core import Connection +from .main import api +from .seed import seed_db from typing import Dict, Any, Iterator from psycopg2 import connect from os import getenv from datetime import datetime -class TestApi: +class TestApp: @pytest.fixture def client(self): - yield TestClient(app) + yield TestClient(app=api) @pytest.fixture def default_cart_item(self) -> Iterator[Dict]: @@ -32,13 +32,13 @@ def test_health_endpoint_returns_200_when_db_conn_is_connected(self, client: Tes def test_cart_index_returns_items_from_cart(self, client: TestClient, db_conn: Connection): seed_db() - response = client.get('/cart/') + response = client.get('/cart') assert 200 == response.status_code assert len(response.json()) == 10 page_size = 20 - response = client.get(f'/cart/?page_number=0&page_size={page_size}') + response = client.get(f'/cart?page_number=0&page_size={page_size}') assert 200 == response.status_code assert len(response.json()) == page_size diff --git a/api/seed.py b/app/seed.py similarity index 76% rename from api/seed.py rename to app/seed.py index a4fa52e..b6e9f84 100644 --- a/api/seed.py +++ b/app/seed.py @@ -3,7 +3,7 @@ from result import Ok, Err, Result from os import getenv from json import load as get_json_data -from api.cart import CartService, CartRepository, CartItemIn +from app.cart import CartService, CartRepository, CartItemIn def safely_load_json(filename: str) -> Result[List[Dict[str, Any]], Exception]: @@ -21,16 +21,16 @@ def seed_db() -> str: db_conn = connect(getenv("DATABASE_URL")) service = CartService(CartRepository(db_conn)) - for item in seed_result.value: + for item in seed_result.ok_value: service.add_item(CartItemIn( name = item["name"], manufacturer = item["manufacturer"], price = item["price"] )) - return f"Seeded database with {len(seed_result.value)} items!" + return f"Seeded database with {len(seed_result.ok_value)} items!" else: - return f"Unable to seed database: {seed_result.value}" + return f"Unable to seed database: {seed_result.err_value}" if __name__ == "__main__": diff --git a/k8s/shopping-cart-db/persistence.local.yml b/k8s/shopping-cart-db/persistence.local.yml index af2b5db..ab75700 100644 --- a/k8s/shopping-cart-db/persistence.local.yml +++ b/k8s/shopping-cart-db/persistence.local.yml @@ -5,7 +5,7 @@ metadata: labels: app: shopping-cart-db spec: - storageClassName: manual + storageClassName: local-path accessModes: - ReadWriteMany resources: @@ -20,10 +20,11 @@ metadata: type: local app: shopping-cart-db spec: - storageClassName: manual + storageClassName: local-path capacity: storage: 5Gi accessModes: - ReadWriteMany hostPath: - path: "/mnt/data" \ No newline at end of file + path: "/tmp/shopping-cart/data" + type: Directory \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index d3f1953..cb49f43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -r requirements.txt -pytest==7.1.2 +pytest==8.0.2 requests==2.31.0 mypy==1.3.0 diff --git a/requirements.txt b/requirements.txt index c47f569..06ecfb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -fastapi==0.70.0 -result==0.6.0 -psycopg2-binary==2.9.3 -uvicorn==0.15.0 \ No newline at end of file +fastapi==0.110.0 +httpx==0.27.0 +result==0.16.0 +psycopg2-binary==2.9.9 +uvicorn==0.27.1