Skip to content

Commit 661d3c8

Browse files
committed
fix: change relationships between models and add some models optimize style
1 parent b0e1a82 commit 661d3c8

26 files changed

Lines changed: 484 additions & 119 deletions
File renamed without changes.

src/api/v1/auth.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
RegistrationScheme,
1313
TokenScheme,
1414
)
15-
from schemas.user import UserCreateScheme
1615
from services.auth_service import AuthService
1716
from services.password_service import PasswordService, get_password_service
1817

@@ -25,11 +24,11 @@ async def get_auth_service(
2524
return AuthService(session)
2625

2726

28-
@router.post("/register", response_model=UserCreateScheme)
27+
@router.post("/register", response_model=RegisterOutScheme)
2928
async def register_user(
3029
data: RegistrationScheme,
3130
auth_service: AuthService = Depends(get_auth_service),
32-
) -> RegisterOutScheme:
31+
):
3332
"""
3433
User registration endpoint
3534
"""
@@ -40,7 +39,7 @@ async def register_user(
4039
async def login_user(
4140
data: LoginScheme,
4241
auth_service: AuthService = Depends(get_auth_service),
43-
) -> LoginOutScheme:
42+
):
4443
"""
4544
User login endpoint
4645
"""

src/api/v1/category.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from typing import List
2-
31
from fastapi import APIRouter, Depends, HTTPException, status
2+
from sqlalchemy.exc import IntegrityError
43

54
from db.crud.category import CategoryCRUD
65
from db.dependencies.auth import require_admin
@@ -10,10 +9,10 @@
109
CategoryUpdateScheme,
1110
)
1211

13-
router = APIRouter()
12+
router = APIRouter(prefix="/categories", tags=["Categories"])
1413

1514

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

@@ -37,7 +36,15 @@ async def get_category(
3736
async def create_category(
3837
data: CategoryCreateScheme, categoryies_crud: CategoryCRUD = Depends(CategoryCRUD)
3938
):
40-
return await categoryies_crud.create(data)
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+
)
4148

4249

4350
@router.put(

src/core/exceptions.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,51 @@
1-
from fastapi import FastAPI, status
1+
from typing import Any
2+
3+
from fastapi import FastAPI
24
from fastapi.responses import JSONResponse
5+
from pydantic import BaseModel
6+
7+
8+
class ErrorResponse(BaseModel):
9+
code: str
10+
detail: str
11+
values: dict[str, Any] = {}
312

413

514
class APIException(Exception):
15+
"""
16+
Base application exception use in services.
17+
message/detail and numeric status_code stored here.
18+
code - machine-readable error code (eg. 'user_exists').
19+
"""
20+
621
def __init__(
722
self,
8-
message: str,
23+
detail: str | None = None,
924
*,
10-
status_code: int = status.HTTP_400_BAD_REQUEST,
11-
details: dict | None = None,
25+
status_code: int = 500,
26+
code: str | None = None,
27+
values: dict[str, Any] | None = None,
1228
):
13-
self.message = message
29+
self.detail = detail or "Internal server error"
1430
self.status_code = status_code
15-
self.details = details or {}
16-
17-
def to_dict(self):
18-
return {"message": self.message, "details": self.details}
31+
self.code = code or "error"
32+
self.values = values or {}
33+
super().__init__(self.detail)
1934

2035

21-
def register_error_handler(app: FastAPI):
36+
def register_error_handler(app: FastAPI) -> None:
2237

2338
@app.exception_handler(APIException)
2439
async def api_exception_handler(_, exc: APIException):
25-
return JSONResponse(status_code=exc.status_code, content=exc.to_dict())
40+
if isinstance(exc, APIException):
41+
payload = {"code": exc.code, "detail": exc.detail, "values": exc.values}
42+
return JSONResponse(status_code=exc.status_code, content=payload)
43+
44+
# Fallback: unexpected exception -> 500 but not leak internals
45+
return JSONResponse(
46+
status_code=500,
47+
content={
48+
"code": "internal_server_error",
49+
"detail": "Internal server error",
50+
},
51+
)

src/core/security.py

Lines changed: 40 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from datetime import datetime, timedelta, timezone
33
from typing import Any, Dict, Optional, Union
44

5-
from fastapi import HTTPException
65
from jose import ExpiredSignatureError, JWTError, jwt
76
from passlib.context import CryptContext
87

@@ -30,77 +29,66 @@ def _jti() -> str:
3029
return str(uuid.uuid4())
3130

3231

33-
# Token factories
34-
def create_access_token(
32+
def _to_ts(dt: datetime) -> int:
33+
return int(dt.timestamp())
34+
35+
36+
def create_token(
3537
subject: Union[str, int, Dict[str, Any]],
38+
token_type: str = "access",
3639
extra: Optional[Dict[str, Any]] = None,
37-
expires_delta: timedelta | None = None,
38-
) -> str:
39-
"""
40-
Create access token (JWT) with proper iat and exp timestamps.
41-
"""
42-
40+
expires_delta: Optional[timedelta] = None,
41+
):
4342
if isinstance(subject, dict):
44-
payload: Dict[str, Any] = subject.copy()
43+
payload = subject.copy()
4544
else:
46-
payload: Dict[str, Any] = {"sub": str(subject)} # type: ignore
45+
payload = {"sub": str(subject)}
4746

48-
payload.setdefault("type", "access")
47+
payload.setdefault("type", token_type)
4948
payload.setdefault("jti", _jti())
50-
51-
now_ts = int(_now().timestamp())
52-
payload["iat"] = now_ts
49+
payload.setdefault("iat", _to_ts(_now()))
5350

5451
if extra:
5552
payload.update(extra)
5653

57-
exp_ts = int(
58-
(
59-
_now()
60-
+ (
61-
expires_delta
62-
or timedelta(minutes=settings.PASSWORD_RESET_TOKEN_EXPIRE_MINUTES)
54+
if expires_delta:
55+
exp = _now() + expires_delta
56+
else:
57+
if token_type == "access":
58+
exp = _now() + timedelta(
59+
minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES or 15
6360
)
64-
).timestamp()
65-
)
66-
payload["exp"] = exp_ts
61+
else:
62+
exp = _now() + timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRES_DAYS or 30)
6763

64+
payload["exp"] = _to_ts(exp)
6865
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
6966

7067

71-
def create_refresh_token(
68+
# Token factories
69+
def create_access_token(
7270
subject: Union[str, int, Dict[str, Any]],
7371
extra: Optional[Dict[str, Any]] = None,
74-
expires_delta: timedelta | None = None,
72+
expires_delta: Optional[timedelta] | None = None,
7573
) -> str:
7674
"""
77-
Create refresh token (JWT) with proper iat and exp timestamps.
75+
Create access token (JWT) with proper iat and exp timestamps.
7876
"""
77+
return create_token(subject, "access", extra, expires_delta)
7978

80-
if isinstance(subject, dict):
81-
payload: Dict[str, Any] = subject.copy()
82-
else:
83-
payload: Dict[str, Any] = {"sub": str(subject)} # type: ignore
84-
85-
payload.setdefault("type", "refresh")
86-
payload.setdefault("jti", _jti())
87-
payload["iat"] = int(_now().timestamp())
88-
89-
if extra:
90-
payload.update(extra)
91-
92-
exp_ts = int(
93-
(
94-
_now()
95-
+ (expires_delta or timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRES_DAYS))
96-
).timestamp()
97-
)
98-
payload["exp"] = exp_ts
9979

100-
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
80+
def create_refresh_token(
81+
subject: Union[str, int, Dict[str, Any]],
82+
extra: Optional[Dict[str, Any]] = None,
83+
expires_delta: Optional[timedelta] | None = None,
84+
) -> str:
85+
"""
86+
Create refresh token (JWT) with proper iat and exp timestamps.
87+
"""
88+
return create_token(subject, "refresh", extra, expires_delta)
10189

10290

103-
def decode_token(token: str) -> dict:
91+
def decode_token(token: str) -> Dict[str, Any]:
10492
"""
10593
Decode JWT token. By default, disables exp verification for internal inspection.
10694
Use jose.decode(token, ..., options={"verify_exp": True}) when verifying token lifetime.
@@ -113,7 +101,7 @@ def decode_token(token: str) -> dict:
113101
options={"verify_exp": True},
114102
)
115103
return payload
116-
except ExpiredSignatureError:
117-
raise HTTPException(status_code=400, detail="Token has expired")
118-
except JWTError:
119-
raise HTTPException(status_code=400, detail="Invalid token")
104+
except ExpiredSignatureError as e:
105+
raise ExpiredSignatureError("token_expired") from e
106+
except JWTError as e:
107+
raise JWTError("invalid_token") from e

src/db/crud/category.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from typing import Optional, Sequence
22

3-
from fastapi import Depends
3+
from fastapi import Depends, HTTPException
44
from sqlalchemy import select
5+
from sqlalchemy.exc import IntegrityError
56
from sqlalchemy.ext.asyncio import AsyncSession
67

78
from db.dependencies.sessions import get_db_session
@@ -24,24 +25,35 @@ async def get_by_id(self, category_id: int) -> Optional[Category]:
2425
return result.scalars().first()
2526

2627
async def get_by_slug(self, slug: str) -> Optional[Category]:
27-
stmt = select(Category).where(Category.slug == slug)
28-
result = await self.session.execute(stmt)
28+
result = await self.session.execute(
29+
select(Category).where(Category.slug == slug)
30+
)
2931
return result.scalars().first()
3032

3133
async def create(self, data: CategoryCreateScheme) -> Category:
3234
new_category = Category(**data.dict())
3335
self.session.add(new_category)
34-
await self.session.commit()
35-
await self.session.refresh(new_category)
36-
return new_category
36+
try:
37+
await self.session.commit()
38+
await self.session.refresh(new_category)
39+
return new_category
40+
except IntegrityError:
41+
await self.session.rollback()
42+
raise HTTPException(status_code=400, detail="Category already exists")
3743

3844
async def update(self, category: Category, data: CategoryUpdateScheme) -> Category:
3945
for field, value in data.dict(exclude_unset=True).items():
4046
setattr(category, field, value)
41-
42-
await self.session.commit()
43-
await self.session.refresh(category)
44-
return category
47+
try:
48+
self.session.add(category)
49+
await self.session.commit()
50+
await self.session.refresh(category)
51+
return category
52+
except IntegrityError:
53+
await self.session.rollback()
54+
raise HTTPException(
55+
status_code=400, detail="Update would violate constraints"
56+
)
4557

4658
async def delete(self, category: Category) -> None:
4759
await self.session.delete(category)

src/db/crud/courier.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# # src/db/crud/courier.py
2+
# from sqlalchemy import select
3+
# from sqlalchemy.ext.asyncio import AsyncSession
4+
5+
# from db.models.courier_profile import CourierProfile
6+
# from schemas.courier import CourierCreateScheme, CourierUpdateLocationScheme
7+
8+
9+
# class CourierCRUD:
10+
# def __init__(self, session: AsyncSession):
11+
# self.session = session
12+
13+
# async def get_by_user_id(self, user_id: int):
14+
# stmt = select(CourierProfile).where(CourierProfile.user_id == user_id)
15+
# res = await self.session.execute(stmt)
16+
# return res.scalars().first()
17+
18+
# async def create(self, user_id: int, data: CourierCreateScheme):
19+
# courier = CourierProfile(
20+
# user_id=user_id,
21+
# transport_type=data.transport_type,
22+
# )
23+
# self.session.add(courier)
24+
# await self.session.commit()
25+
# await self.session.refresh(courier)
26+
# return courier
27+
28+
# async def update_location(self, courier: CourierProfile, lat: float, lon: float):
29+
# courier.latitude = lat
30+
# courier.longitude = lon
31+
# await self.session.commit()
32+
# return courier
33+
34+
# async def set_availability(self, courier: CourierProfile, available: bool):
35+
# courier.is_available = available
36+
# await self.session.commit()
37+
# return courier

src/db/dependencies/auth.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async def get_current_user(
2727
payload = decode_token(token)
2828
except ValueError as exc:
2929
reason = str(exc)
30-
if reason == "token_expired":
30+
if "token_expired" in reason:
3131
raise HTTPException(
3232
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
3333
)
@@ -47,9 +47,8 @@ async def get_current_user(
4747
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token revoked"
4848
)
4949

50-
user_crud = UserCRUD(session)
51-
user = await user_crud.get_by_id(int(payload.get("sub")))
52-
50+
user_id = int(payload.get("sub"))
51+
user = await UserCRUD(session).get_by_id(user_id)
5352
if not user:
5453
raise HTTPException(
5554
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"

0 commit comments

Comments
 (0)