From 25859b36b0d37569b653c3b24832466f01d8e067 Mon Sep 17 00:00:00 2001 From: vadym Date: Tue, 2 Sep 2025 14:48:28 +0300 Subject: [PATCH] hw lesson 22 --- Dockerfile | 8 ++-- docker-compose.yml | 4 +- src/common/service.py | 2 +- src/users/models/pydantic.py | 22 ++++++++++ src/users/repository.py | 26 ++++++++++- src/users/routes.py | 1 + src/users/services.py | 22 +++++++++- src/users/views/__init__.py | 11 ++++- src/users/views/user_address.py | 76 +++++++++++++++------------------ 9 files changed, 120 insertions(+), 52 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1410224..f13754d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,14 @@ FROM python:3.11 WORKDIR /app COPY pyproject.toml pyproject.toml +COPY poetry.lock poetry.lock -RUN pip install poetry -RUN poetry config virtualenvs.create false -RUN poetry install --no-dev +RUN pip install poetry \ + && poetry config virtualenvs.create false +RUN poetry install --no-interaction --no-ansi --no-root COPY . /app RUN chmod +x ops/start-api.sh - CMD ["sh", "ops/start-api.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 9c4431fc..d78afa96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: web: build: . @@ -11,6 +9,8 @@ services: - db networks: - web_network + env_file: + - .env db: image: postgres:13 diff --git a/src/common/service.py b/src/common/service.py index 8af5ea9a..4349fd49 100644 --- a/src/common/service.py +++ b/src/common/service.py @@ -26,7 +26,7 @@ async def create(self, instance_data: PType) -> PType: return await self.repository.create(instance_data) async def update(self, pk: int, update_data: PType) -> PType: - return await self.repository.update(id, update_data) + return await self.repository.update(pk, update_data) async def delete(self, pk: int): await self.repository.delete(pk=pk) diff --git a/src/users/models/pydantic.py b/src/users/models/pydantic.py index cda96f2a..a2658d3c 100644 --- a/src/users/models/pydantic.py +++ b/src/users/models/pydantic.py @@ -1,4 +1,5 @@ from typing import Union +from typing import Optional from pydantic import ( BaseModel, @@ -19,3 +20,24 @@ class UserModel(BaseModel): class UserWithPassword(UserModel): hashed_password: str + + +class UserAddressListItem(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + title: Optional[str] = None + + +class UserAddressDetail(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: int + title: Optional[str] = None + city: str + street: str + house: str + apartment: Optional[str] = None + post_code: Optional[str] = None + floor: Optional[str] = None + additional_info: Optional[str] = None \ No newline at end of file diff --git a/src/users/repository.py b/src/users/repository.py index e53dab38..2b9d4c52 100644 --- a/src/users/repository.py +++ b/src/users/repository.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession @@ -9,8 +9,10 @@ from src.users.models.pydantic import ( UserModel, UserWithPassword, + UserAddressDetail, + UserAddressListItem, ) -from src.users.models.sqlalchemy import User +from src.users.models.sqlalchemy import User, UserAddress class UserRepository(BaseSQLAlchemyRepository[User, UserModel]): @@ -35,3 +37,23 @@ async def get_by_email(self, email: str) -> Optional[UserWithPassword]: def get_user_repository(session: AsyncSession = Depends(get_session)) -> UserRepository: return UserRepository(session=session) + + +class UserAddressRepository(BaseSQLAlchemyRepository[UserAddress, UserAddressDetail]): + def __init__(self, session: AsyncSession): + super().__init__(model=UserAddress, pydantic_model=UserAddressDetail, session=session) + + async def list_by_user(self, user_id: int) -> List[UserAddressListItem]: + stmt = select(self.model).where(self.model.user_id == user_id) + result = await self.session.execute(stmt) + instances = result.scalars().all() + return [UserAddressListItem.model_validate(instance) for instance in instances] + + async def get_sqlalchemy(self, pk: int) -> Optional[UserAddress]: + stmt = select(self.model).where(self.model.id == pk) + result = await self.session.execute(stmt) + return result.scalar_one_or_none() + + +def get_user_address_repository(session: AsyncSession = Depends(get_session)) -> UserAddressRepository: + return UserAddressRepository(session=session) diff --git a/src/users/routes.py b/src/users/routes.py index 1350e5c2..f7e4e138 100644 --- a/src/users/routes.py +++ b/src/users/routes.py @@ -3,6 +3,7 @@ class UserManagementRoutesPrefixes: user: str = '/user' + addresses: str = '/addresses' class UserRoutesPrefixes(BaseCrudPrefixes): diff --git a/src/users/services.py b/src/users/services.py index ba7a2d37..7359c15e 100644 --- a/src/users/services.py +++ b/src/users/services.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional from fastapi import Depends @@ -6,10 +6,14 @@ from src.common.service import BaseService from src.users.models.pydantic import ( UserModel, + UserAddressDetail, + UserAddressListItem, ) from src.users.repository import ( UserRepository, get_user_repository, + UserAddressRepository, + get_user_address_repository, ) @@ -31,3 +35,19 @@ async def authenticate(self, email: str, password: str) -> Optional[UserModel]: def get_user_service(repo: UserRepository = Depends(get_user_repository)) -> UserService: return UserService(repository=repo) + + +class UserAddressService(BaseService[UserAddressDetail]): + def __init__(self, repository: UserAddressRepository): + super().__init__(repository) + self.repository: UserAddressRepository + + async def list_by_user(self, user_id: int) -> List[UserAddressListItem]: + return await self.repository.list_by_user(user_id=user_id) + + async def get_sqlalchemy(self, pk: int): + return await self.repository.get_sqlalchemy(pk=pk) + + +def get_user_address_service(repo: UserAddressRepository = Depends(get_user_address_repository)) -> 'UserAddressService': + return UserAddressService(repository=repo) diff --git a/src/users/views/__init__.py b/src/users/views/__init__.py index 6cd7a856..4a38d04a 100644 --- a/src/users/views/__init__.py +++ b/src/users/views/__init__.py @@ -1 +1,10 @@ -from .user import router as user_router +from fastapi import APIRouter + +from .user import router as _user_router +from .user_address import router as _address_router + +router = APIRouter() +router.include_router(_user_router) +router.include_router(_address_router) + +user_router = router diff --git a/src/users/views/user_address.py b/src/users/views/user_address.py index 5b05453d..901c6532 100644 --- a/src/users/views/user_address.py +++ b/src/users/views/user_address.py @@ -1,67 +1,61 @@ -from typing import ( - Annotated, - Union, -) +from typing import Annotated, Union -from fastapi import ( - APIRouter, - Depends, - Response, - status, -) +from fastapi import APIRouter, Depends, Response, status -from src.catalogue.models.pydantic import ProductModel -from src.catalogue.routes import ( - CatalogueRoutesPrefixes, - ProductRoutesPrefixes, +from src.authentication.utils import get_current_user +from src.common.schemas.common import ErrorResponse +from src.users.models.pydantic import ( + UserModel, + UserAddressDetail, + UserAddressListItem, ) -from src.catalogue.services import get_product_service +from src.users.routes import UserManagementRoutesPrefixes, UserRoutesPrefixes +from src.users.services import get_user_address_service from src.common.exceptions.base import ObjectDoesNotExistException -from src.common.schemas.common import ErrorResponse -router = APIRouter(prefix=CatalogueRoutesPrefixes.product) +router = APIRouter(prefix=f"{UserManagementRoutesPrefixes.user}{UserManagementRoutesPrefixes.addresses}") @router.get( - ProductRoutesPrefixes.root, + UserRoutesPrefixes.root, status_code=status.HTTP_200_OK, - response_model=list[ProductModel], + response_model=list[UserAddressListItem], ) -async def product_list(product_service: Annotated[get_product_service, Depends()]) -> list[ProductModel]: - """ - Get list of products. - - Returns: - Response with list of products. - """ - return await product_service.list() +async def user_address_list( + current_user: Annotated[UserModel, Depends(get_current_user)], + service: Annotated[get_user_address_service, Depends()], +) -> list[UserAddressListItem]: + return await service.list_by_user(user_id=current_user.id) @router.get( - ProductRoutesPrefixes.detail, + UserRoutesPrefixes.detail, responses={ - status.HTTP_200_OK: {'model': ProductModel}, + status.HTTP_200_OK: {'model': UserAddressDetail}, + status.HTTP_403_FORBIDDEN: {'model': ErrorResponse}, status.HTTP_404_NOT_FOUND: {'model': ErrorResponse}, }, status_code=status.HTTP_200_OK, - response_model=Union[ProductModel, ErrorResponse], + response_model=Union[UserAddressDetail, ErrorResponse], ) -async def product_detail( +async def user_address_detail( response: Response, pk: int, - service: Annotated[get_product_service, Depends()], -) -> Union[Response, ErrorResponse]: - """ - Retrieve product. + current_user: Annotated[UserModel, Depends(get_current_user)], + service: Annotated[get_user_address_service, Depends()], +) -> Union[UserAddressDetail, ErrorResponse]: + instance = await service.get_sqlalchemy(pk=pk) + if instance is None: + response.status_code = status.HTTP_404_NOT_FOUND + return ErrorResponse(message="Address does not exist") + + if instance.user_id != current_user.id: + response.status_code = status.HTTP_403_FORBIDDEN + return ErrorResponse(message="You do not have permission to access this address") - Returns: - Response with product details. - """ try: - response = await service.detail(pk=pk) + return await service.detail(pk=pk) except ObjectDoesNotExistException as exc: response.status_code = status.HTTP_404_NOT_FOUND return ErrorResponse(message=exc.message) - - return response