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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ COPY pyproject.toml pyproject.toml

RUN pip install poetry
RUN poetry config virtualenvs.create false
RUN poetry install --no-dev
RUN poetry install --no-root

COPY . /app

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"""Add product tables
"""init schema

Revision ID: cedcfebfb6ad
Revises: 40f8af84ecb8
Create Date: 2023-12-06 22:33:42.993363
Revision ID: 256b8bcd290c
Revises:
Create Date: 2025-08-25 09:37:06.747222

"""
from typing import (
Sequence,
Union,
)
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = 'cedcfebfb6ad'
down_revision: Union[str, None] = '40f8af84ecb8'
revision: str = '256b8bcd290c'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

Expand Down
53 changes: 0 additions & 53 deletions alembic/versions/3b3e7aef66e3_.py

This file was deleted.

58 changes: 0 additions & 58 deletions alembic/versions/40f8af84ecb8_.py

This file was deleted.

13 changes: 13 additions & 0 deletions reset_db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -e

echo "Dropping and recreating public schema..."
docker compose exec -T db psql -U user -d fastapi_shop -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"

echo "Stamping Alembic to head..."
docker compose exec -T web alembic stamp head

echo "Running migrations..."
docker compose exec -T web alembic upgrade head

echo "✅ Database reset complete!"
8 changes: 8 additions & 0 deletions src/catalogue/models/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@ class ProductModel(BaseModel):

class Config:
from_attributes = True

class CategoryModel(BaseModel):
id: Optional[int]
title: str
description: Optional[str]
image: Optional[str]
is_active: bool
parent_id: Optional[int]
12 changes: 10 additions & 2 deletions src/catalogue/repository.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession

from src.catalogue.models.pydantic import ProductModel
from src.catalogue.models.sqlalchemy import Product
from src.catalogue.models.pydantic import ProductModel, CategoryModel
from src.catalogue.models.sqlalchemy import Product, Category
from src.common.databases.postgres import (
get_session,
)
Expand All @@ -16,3 +16,11 @@ def __init__(self, session: AsyncSession):

def get_product_repository(session: AsyncSession = Depends(get_session)) -> ProductRepository:
return ProductRepository(session=session)

class CategoryRepository(BaseSQLAlchemyRepository[Category, CategoryModel]):
def __init__(self, session: AsyncSession):
super().__init__(model=Category, pydantic_model=CategoryModel, session=session)


def get_category_repository(session: AsyncSession = Depends(get_session)) -> CategoryRepository:
return CategoryRepository(session=session)
4 changes: 4 additions & 0 deletions src/catalogue/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

class CatalogueRoutesPrefixes:
product: str = '/product'
category: str = '/category'


class ProductRoutesPrefixes(BaseCrudPrefixes):
...

class CategoryRoutesPrefixes(BaseCrudPrefixes):
...
13 changes: 12 additions & 1 deletion src/catalogue/services.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from fastapi import Depends

from src.catalogue.models.pydantic import ProductModel
from src.catalogue.models.pydantic import ProductModel, CategoryModel
from src.catalogue.repository import (
ProductRepository,
get_product_repository,
CategoryRepository,
get_category_repository,
)
from src.common.service import BaseService

Expand All @@ -15,3 +17,12 @@ def __init__(self, repository: ProductRepository):

def get_product_service(repo: ProductRepository = Depends(get_product_repository)) -> ProductService:
return ProductService(repository=repo)


class CategoryService(BaseService[CategoryModel]):
def __init__(self, repository: CategoryRepository):
super().__init__(repository)


def get_category_service(repo: CategoryRepository = Depends(get_category_repository)) -> CategoryService:
return CategoryService(repository=repo)
44 changes: 42 additions & 2 deletions src/catalogue/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@
status,
)

from src.catalogue.models.pydantic import ProductModel
from src.catalogue.models.pydantic import ProductModel, CategoryModel
from src.catalogue.routes import (
CatalogueRoutesPrefixes,
ProductRoutesPrefixes,
CategoryRoutesPrefixes
)
from src.catalogue.services import (
get_product_service,
get_product_service, get_category_service,
)
from src.common.exceptions.base import ObjectDoesNotExistException
from src.common.schemas.common import ErrorResponse


product_router = APIRouter(prefix=CatalogueRoutesPrefixes.product)
category_router = APIRouter(prefix=CatalogueRoutesPrefixes.category)


@product_router.get(
Expand Down Expand Up @@ -69,3 +71,41 @@ async def product_detail(
return ErrorResponse(message=exc.message)

return response

@category_router.get(
CategoryRoutesPrefixes.root,
status_code=status.HTTP_200_OK,
response_model=list[CategoryModel],
)
async def category_list(category_service=Depends(get_category_service)) -> list[CategoryModel]:

"""
Get list of categories..

Returns:
Response with list of categories...
"""
return await category_service.list()

@category_router.get(
CategoryRoutesPrefixes.detail,
responses={
status.HTTP_200_OK: {'model': CategoryModel},
status.HTTP_404_NOT_FOUND: {'model': ErrorResponse},
},
status_code=status.HTTP_200_OK,
response_model=Union[CategoryModel, ErrorResponse],
)
async def category_detail(
response: Response,
pk: int,
service: Annotated[get_category_service, Depends()],
) -> Union[Response, ErrorResponse]:
try:
response = await service.detail(pk=pk)
except ObjectDoesNotExistException as exc:
response.status_code = status.HTTP_404_NOT_FOUND
return ErrorResponse(message=exc.message)

return response

6 changes: 3 additions & 3 deletions src/common/repository/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ async def get(self, pk: int) -> PType:
instance = result.scalar_one_or_none()
if not instance:
raise ObjectDoesNotExistException()
return self.pydantic_model.from_orm(instance)
return self.pydantic_model.model_validate(instance, from_attributes=True)

async def create(self, instance_data: PType) -> PType:
instance = self.model(**instance_data.model_dump())
self.session.add(instance)
await self.session.commit()
await self.session.refresh(instance)
return self.pydantic_model.from_orm(instance)
return self.pydantic_model.model_validate(instance, from_attributes=True)

async def update(self, pk: int, update_data: PType) -> PType:
await self.session.execute(
Expand All @@ -56,7 +56,7 @@ async def all(self) -> List[PType]:
stmt = select(self.model)
result = await self.session.execute(stmt)
instances = result.scalars().all()
return [self.pydantic_model.model_validate(instance) for instance in instances]
return [self.pydantic_model.model_validate(instance, from_attributes=True) for instance in instances]

async def filter(self, **kwargs) -> List[PType]:
stmt = select(self.model).filter_by(**kwargs)
Expand Down
8 changes: 7 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from src.admin import register_admin_views
from src.base_settings import base_settings
from src.catalogue.views import product_router
from src.catalogue.views import product_router, category_router
from src.common.databases.postgres import postgres
from src.general.views import router as status_router
from src.routes import BaseRoutesPrefixes
Expand All @@ -19,6 +19,12 @@ def include_routes(application: FastAPI) -> None:
tags=['Catalogue'],
)

application.include_router(
router=category_router,
prefix=BaseRoutesPrefixes.catalogue,
tags=['Catalogue'],
)


def get_application() -> FastAPI:
application = FastAPI(
Expand Down