Skip to content

Commit 44df8de

Browse files
committed
create brand crud operations and endpoints. Also, fix model errors
1 parent 30a1d8d commit 44df8de

File tree

12 files changed

+175
-52
lines changed

12 files changed

+175
-52
lines changed

src/api/routers.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
from fastapi import APIRouter
22

3-
from api.v1 import ( # noqa
4-
admin,
5-
auth,
6-
category,
7-
dashboard,
8-
social_auth,
9-
users,
10-
)
3+
from api.v1 import admin, auth, brand, category, dashboard, social_auth, users # noqa
114
from core.config import settings
125

136
api_router = APIRouter(
@@ -22,5 +15,6 @@
2215
api_router.include_router(users.router, prefix="/users", tags=["Users"])
2316
api_router.include_router(admin.router, prefix="/admin", tags=["Admin"])
2417
api_router.include_router(category.router, prefix="/categories", tags=["Categories"])
18+
api_router.include_router(brand.router, prefix="/brands", tags=["Brands"])
2519

2620
__all__ = ("api_router",)

src/api/v1/brand.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from fastapi import APIRouter, Depends, HTTPException, status
2+
3+
from db.crud.brand import BrandCRUD
4+
from db.dependencies.auth import ADMIN_ROLE, SELLER_ROLE, require_roles
5+
from schemas.brand import (
6+
BrandCreateScheme,
7+
BrandOutScheme,
8+
BrandUpdateScheme,
9+
)
10+
11+
router = APIRouter(prefix="/brands", tags=["Brands"])
12+
13+
14+
@router.get("/", response_model=list[BrandOutScheme])
15+
async def get_brands(brands_crud: BrandCRUD = Depends(BrandCRUD)):
16+
return await brands_crud.get_all()
17+
18+
19+
@router.get("/{slug}", response_model=BrandOutScheme)
20+
async def get_brand(slug: str, brands_crud: BrandCRUD = Depends(BrandCRUD)):
21+
brand = await brands_crud.get_by_slug(slug)
22+
if not brand:
23+
raise HTTPException(
24+
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
25+
)
26+
return brand
27+
28+
29+
@router.post(
30+
"/",
31+
response_model=BrandOutScheme,
32+
status_code=status.HTTP_201_CREATED,
33+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
34+
)
35+
async def create_brand(
36+
data: BrandCreateScheme, brands_crud: BrandCRUD = Depends(BrandCRUD)
37+
):
38+
return await brands_crud.create(data)
39+
40+
41+
@router.put(
42+
"/{slug}",
43+
response_model=BrandOutScheme,
44+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
45+
)
46+
async def update_brand(
47+
slug: str, data: BrandUpdateScheme, brands_crud: BrandCRUD = Depends(BrandCRUD)
48+
):
49+
brand = await brands_crud.get_by_slug(slug)
50+
if not brand:
51+
raise HTTPException(
52+
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
53+
)
54+
return await brands_crud.update(brand, data)
55+
56+
57+
@router.delete(
58+
"/{slug}",
59+
status_code=status.HTTP_204_NO_CONTENT,
60+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
61+
)
62+
async def delete_brand(slug: str, brands_crud: BrandCRUD = Depends(BrandCRUD)):
63+
brand = await brands_crud.get_by_slug(slug)
64+
if not brand:
65+
raise HTTPException(
66+
status_code=status.HTTP_404_NOT_FOUND, detail="Brand not found"
67+
)
68+
await brands_crud.delete(brand)

src/api/v1/category.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from fastapi import APIRouter, Depends, HTTPException, status
2-
from sqlalchemy.exc import IntegrityError
32

43
from db.crud.category import CategoryCRUD
5-
from db.dependencies.auth import require_admin
4+
from db.dependencies.auth import ADMIN_ROLE, SELLER_ROLE, require_roles
65
from schemas.category import (
76
CategoryCreateScheme,
87
CategoryOutScheme,
@@ -12,7 +11,7 @@
1211
router = APIRouter(prefix="/categories", tags=["Categories"])
1312

1413

15-
@router.get("/", response_model=CategoryOutScheme)
14+
@router.get("/", response_model=list[CategoryOutScheme])
1615
async def get_categories(categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)):
1716
return await categoryies_crud.get_all()
1817

@@ -31,24 +30,18 @@ async def get_category(
3130
"/",
3231
response_model=CategoryOutScheme,
3332
status_code=status.HTTP_201_CREATED,
34-
dependencies=[Depends(require_admin)],
33+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
3534
)
3635
async def create_category(
3736
data: CategoryCreateScheme, categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)
3837
):
39-
try:
40-
return await categoryies_crud.create(data)
41-
except HTTPException:
42-
raise
43-
except IntegrityError:
44-
raise HTTPException(
45-
status_code=status.HTTP_400_BAD_REQUEST,
46-
detail="Slug or name already exists",
47-
)
38+
return await categoryies_crud.create(data)
4839

4940

5041
@router.put(
51-
"/{slug}", response_model=CategoryOutScheme, dependencies=[Depends(require_admin)]
42+
"/{slug}",
43+
response_model=CategoryOutScheme,
44+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
5245
)
5346
async def update_category(
5447
slug: str,
@@ -59,13 +52,13 @@ async def update_category(
5952
if not category:
6053
raise HTTPException(status_code=404, detail="Category not found")
6154

62-
raise await categoryies_crud.update(category, data)
55+
return await categoryies_crud.update(category, data)
6356

6457

6558
@router.delete(
66-
"/{category_id}",
59+
"/{slug}",
6760
status_code=status.HTTP_204_NO_CONTENT,
68-
dependencies=[Depends(require_admin)],
61+
dependencies=[Depends(require_roles(ADMIN_ROLE, SELLER_ROLE))],
6962
)
7063
async def delete_category(
7164
slug: str, categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)

src/db/crud/brand.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Optional, Sequence
2+
3+
from fastapi import Depends, HTTPException
4+
from sqlalchemy import select
5+
from sqlalchemy.exc import IntegrityError
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from db.dependencies.sessions import get_db_session
9+
from db.models.brands import Brand
10+
from schemas.brand import BrandCreateScheme, BrandUpdateScheme
11+
12+
13+
class BrandCRUD:
14+
def __init__(self, session: AsyncSession = Depends(get_db_session)):
15+
self.session = session
16+
17+
async def get_all(self) -> Sequence[Brand]:
18+
result = await self.session.execute(select(Brand))
19+
return result.scalars().all()
20+
21+
async def get_by_id(self, brand_id: int) -> Optional[Brand]:
22+
result = await self.session.execute(select(Brand).where(Brand.id == brand_id))
23+
return result.scalars().first()
24+
25+
async def get_by_slug(self, brand_slug: int) -> Optional[Brand]:
26+
result = await self.session.execute(
27+
select(Brand).where(Brand.slug == brand_slug)
28+
)
29+
return result.scalars().first()
30+
31+
async def create(self, data: BrandCreateScheme) -> Brand:
32+
new_brand = Brand(**data.dict())
33+
self.session.add(new_brand)
34+
try:
35+
await self.session.commit()
36+
await self.session.refresh(new_brand)
37+
return new_brand
38+
except IntegrityError:
39+
await self.session.rollback()
40+
raise HTTPException(status_code=400, detail="Brand already exists")
41+
42+
async def update(self, brand: Brand, data: BrandUpdateScheme) -> Brand:
43+
for field, value in data.dict(exclude_unset=True).items():
44+
setattr(brand, field, value)
45+
46+
try:
47+
self.session.add(brand)
48+
await self.session.commit()
49+
await self.session.refresh(brand)
50+
except IntegrityError:
51+
await self.session.rollback()
52+
raise HTTPException(
53+
status_code=400, detail="Update would violate constraints"
54+
)
55+
56+
async def delete(self, brand: Brand) -> None:
57+
await self.session.delete(brand)
58+
await self.session.commit()

src/db/dependencies/auth.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ async def role_checker(user: User = Depends(get_current_user)):
8585

8686

8787
# READY-TO-USE SHORTCUTS
88-
require_admin = require_roles(UserRole.admin.value)
89-
require_seller = require_roles(UserRole.seller.value)
90-
require_buyer = require_roles(UserRole.buyer.value)
91-
require_courier = require_roles(UserRole.courier.value)
88+
ADMIN_ROLE = UserRole.admin.value
89+
SELLER_ROLE = UserRole.seller.value
90+
BUYER_ROLE = UserRole.buyer.value
91+
COURIER_ROLE = UserRole.courier.value
92+
93+
require_admin = require_roles(ADMIN_ROLE)
94+
require_seller = require_roles(SELLER_ROLE)
95+
require_buyer = require_roles(BUYER_ROLE)
96+
require_courier = require_roles(COURIER_ROLE)

src/db/migrations/versions/cfa02231b685_.py renamed to src/db/migrations/versions/3d224d0e6065_.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""empty message
22
3-
Revision ID: cfa02231b685
3+
Revision ID: 3d224d0e6065
44
Revises:
5-
Create Date: 2025-12-07 16:47:32.384365
5+
Create Date: 2025-12-08 12:48:54.428901
66
77
"""
88

@@ -12,7 +12,7 @@
1212
from alembic import op
1313

1414
# revision identifiers, used by Alembic.
15-
revision: str = 'cfa02231b685'
15+
revision: str = '3d224d0e6065'
1616
down_revision: Union[str, Sequence[str], None] = None
1717
branch_labels: Union[str, Sequence[str], None] = None
1818
depends_on: Union[str, Sequence[str], None] = None
@@ -24,6 +24,7 @@ def upgrade() -> None:
2424
op.create_table(
2525
'brands',
2626
sa.Column('name', sa.String(length=100), nullable=False),
27+
sa.Column('slug', sa.String(length=200), nullable=False),
2728
sa.Column('description', sa.String(length=255), nullable=True),
2829
sa.Column('logo_url', sa.String(length=255), nullable=True),
2930
sa.Column('website_url', sa.String(length=255), nullable=True),
@@ -43,6 +44,7 @@ def upgrade() -> None:
4344
),
4445
sa.PrimaryKeyConstraint('id', name=op.f('pk_brands')),
4546
sa.UniqueConstraint('name', name=op.f('uq_brands_name')),
47+
sa.UniqueConstraint('slug', name=op.f('uq_brands_slug')),
4648
)
4749
op.create_table(
4850
'categories',
@@ -149,7 +151,7 @@ def upgrade() -> None:
149151
)
150152
op.create_index(op.f('ix_carts_user_id'), 'carts', ['user_id'], unique=True)
151153
op.create_table(
152-
'courier_profiles',
154+
'couriers',
153155
sa.Column('user_id', sa.Integer(), nullable=False),
154156
sa.Column(
155157
'transport_type',
@@ -170,10 +172,10 @@ def upgrade() -> None:
170172
nullable=False,
171173
),
172174
sa.ForeignKeyConstraint(
173-
['user_id'], ['users.id'], name=op.f('fk_courier_profiles_user_id_users')
175+
['user_id'], ['users.id'], name=op.f('fk_couriers_user_id_users')
174176
),
175-
sa.PrimaryKeyConstraint('id', name=op.f('pk_courier_profiles')),
176-
sa.UniqueConstraint('user_id', name=op.f('uq_courier_profiles_user_id')),
177+
sa.PrimaryKeyConstraint('id', name=op.f('pk_couriers')),
178+
sa.UniqueConstraint('user_id', name=op.f('uq_couriers_user_id')),
177179
)
178180
op.create_table(
179181
'delivery_addresses',
@@ -275,7 +277,9 @@ def upgrade() -> None:
275277
name=op.f('fk_deliveries_address_id_delivery_addresses'),
276278
),
277279
sa.ForeignKeyConstraint(
278-
['courier_id'], ['users.id'], name=op.f('fk_deliveries_courier_id_users')
280+
['courier_id'],
281+
['couriers.id'],
282+
name=op.f('fk_deliveries_courier_id_couriers'),
279283
),
280284
sa.ForeignKeyConstraint(
281285
['order_id'], ['orders.id'], name=op.f('fk_deliveries_order_id_orders')
@@ -503,7 +507,7 @@ def downgrade() -> None:
503507
op.drop_index(op.f('ix_orders_user_id'), table_name='orders')
504508
op.drop_table('orders')
505509
op.drop_table('delivery_addresses')
506-
op.drop_table('courier_profiles')
510+
op.drop_table('couriers')
507511
op.drop_index(op.f('ix_carts_user_id'), table_name='carts')
508512
op.drop_table('carts')
509513
op.drop_index(op.f('ix_users_username'), table_name='users')

src/db/migrations/versions/12ac55d9de47_.py renamed to src/db/migrations/versions/81fafec580d5_something.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
"""empty message
1+
"""something
22
3-
Revision ID: 12ac55d9de47
4-
Revises: cfa02231b685
5-
Create Date: 2025-12-07 16:50:13.657790
3+
Revision ID: 81fafec580d5
4+
Revises: 3d224d0e6065
5+
Create Date: 2025-12-08 13:04:04.688986
66
77
"""
88

99
from typing import Sequence, Union
1010

1111
# revision identifiers, used by Alembic.
12-
revision: str = '12ac55d9de47'
13-
down_revision: Union[str, Sequence[str], None] = 'cfa02231b685'
12+
revision: str = '81fafec580d5'
13+
down_revision: Union[str, Sequence[str], None] = '3d224d0e6065'
1414
branch_labels: Union[str, Sequence[str], None] = None
1515
depends_on: Union[str, Sequence[str], None] = None
1616

src/db/models/brands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Brand(BaseModel):
1010
__tablename__ = "brands"
1111

1212
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
13+
slug: Mapped[str] = mapped_column(String(200), nullable=False, unique=True)
1314
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
1415
logo_url: Mapped[str | None] = mapped_column(String(255), nullable=True)
1516
website_url: Mapped[str | None] = mapped_column(String(255), nullable=True)
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ class TransportType(str, enum.Enum):
1515
moto = "moto"
1616

1717

18-
class CourierProfile(BaseModel):
19-
__tablename__ = "courier_profiles"
18+
class Courier(BaseModel):
19+
__tablename__ = "couriers"
2020

2121
user_id: Mapped[int] = mapped_column(
2222
ForeignKey("users.id"), unique=True, nullable=False
@@ -37,4 +37,4 @@ class CourierProfile(BaseModel):
3737
latitude: Mapped[float | None] = mapped_column(Float, nullable=True)
3838
longitude: Mapped[float | None] = mapped_column(Float, nullable=True)
3939

40-
user: Mapped["User"] = relationship("User", back_populates="courier_profile") # type: ignore # noqa: F821
40+
user: Mapped["User"] = relationship("User", back_populates="couriers") # type: ignore # noqa: F821

src/db/models/delivery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class Delivery(BaseModel):
2222
__tablename__ = "deliveries"
2323

2424
order_id: Mapped[int] = mapped_column(ForeignKey("orders.id"), unique=True)
25-
courier_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
25+
courier_id: Mapped[int] = mapped_column(ForeignKey("couriers.id"))
2626
address_id: Mapped[int] = mapped_column(
2727
ForeignKey("delivery_addresses.id"), nullable=False
2828
)
@@ -37,5 +37,5 @@ class Delivery(BaseModel):
3737
delivered_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
3838

3939
order: Mapped["Order"] = relationship("Order", back_populates="delivery") # type: ignore # noqa: F821
40-
courier: Mapped["User"] = relationship("User") # type: ignore # noqa: F821
40+
courier: Mapped["Courier"] = relationship("Courier") # type: ignore # noqa: F821
4141
address: Mapped["DeliveryAddress"] = relationship("DeliveryAddress") # type: ignore # noqa: F821

0 commit comments

Comments
 (0)