Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.8'

services:
web:
build: .
Expand All @@ -11,6 +9,8 @@ services:
- db
networks:
- web_network
env_file:
- .env

db:
image: postgres:13
Expand Down
2 changes: 1 addition & 1 deletion src/common/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions src/users/models/pydantic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Union
from typing import Optional

from pydantic import (
BaseModel,
Expand All @@ -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
26 changes: 24 additions & 2 deletions src/users/repository.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional
from typing import List, Optional

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -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]):
Expand All @@ -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)
1 change: 1 addition & 0 deletions src/users/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

class UserManagementRoutesPrefixes:
user: str = '/user'
addresses: str = '/addresses'


class UserRoutesPrefixes(BaseCrudPrefixes):
Expand Down
22 changes: 21 additions & 1 deletion src/users/services.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
from typing import Optional
from typing import List, Optional

from fastapi import Depends

from src.authentication.security import verify_password
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,
)


Expand All @@ -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)
11 changes: 10 additions & 1 deletion src/users/views/__init__.py
Original file line number Diff line number Diff line change
@@ -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
76 changes: 35 additions & 41 deletions src/users/views/user_address.py
Original file line number Diff line number Diff line change
@@ -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