You've already forked RekomenciBackend
Merge branch 'main' of gitlab.prodcontest.com:team-39/backend
* 'main' of gitlab.prodcontest.com:team-39/backend: feat(): openapi docs for resume routes feat(): add validation and documentation to input schemas
This commit is contained in:
@@ -18,6 +18,7 @@ dependencies = [
|
||||
"aioboto3==15.5.0",
|
||||
"prometheus-fastapi-instrumentator>=7.1.0",
|
||||
"python-multipart>=0.0.20",
|
||||
"pydantic[email]>=2.12.4",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -19,7 +19,7 @@ from prometheus_fastapi_instrumentator import Instrumentator
|
||||
|
||||
from template_project.web_api.configuration import Configuration, load_configuration
|
||||
from template_project.web_api.ioc.make import make_ioc
|
||||
from template_project.web_api.routes import auth, healthcheck, notification, profile, storage
|
||||
from template_project.web_api.routes import auth, healthcheck, notification, profile, resume, storage
|
||||
|
||||
LOG_CONFIG: Final = {
|
||||
"version": 1,
|
||||
@@ -74,6 +74,7 @@ def make_asgi_application(
|
||||
app.include_router(profile.router)
|
||||
app.include_router(notification.router)
|
||||
app.include_router(storage.router)
|
||||
app.include_router(resume.router)
|
||||
Instrumentator().instrument(app).expose(app)
|
||||
|
||||
setup_dishka(container=ioc, app=app)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel, SecretStr
|
||||
from pydantic import BaseModel, EmailStr, Field, SecretStr
|
||||
|
||||
from template_project.application.auth_identity.errors import (
|
||||
AuthError,
|
||||
@@ -18,15 +18,29 @@ router = APIRouter(route_class=DishkaRoute, tags=["Auth"])
|
||||
|
||||
|
||||
class EmailSignUpRequest(BaseModel):
|
||||
email: str
|
||||
password: SecretStr
|
||||
email: EmailStr = Field(..., description="User email address", examples=["user@example.com"])
|
||||
password: SecretStr = Field(
|
||||
..., min_length=8, description="User password (minimum 8 characters)", examples=["password123"]
|
||||
)
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"email": "user@example.com", "password": "password123"}}}
|
||||
|
||||
|
||||
class EmailSignUpResponse(BaseModel):
|
||||
access_token: str
|
||||
access_token: str = Field(..., description="JWT access token for authentication")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}
|
||||
|
||||
|
||||
@router.post("/auth/sign_up/email")
|
||||
@router.post(
|
||||
"/auth/sign_up/email",
|
||||
summary="Sign up with email",
|
||||
description="Register a new user with email and password",
|
||||
responses={
|
||||
200: {"description": "User successfully registered", "model": EmailSignUpResponse},
|
||||
409: {"description": "User with this email already exists"},
|
||||
},
|
||||
)
|
||||
async def sign_up_email(
|
||||
request: EmailSignUpRequest,
|
||||
interactor: FromDishka[SignUpInteractor],
|
||||
@@ -44,14 +58,26 @@ async def sign_up_email(
|
||||
|
||||
|
||||
class YandexSignUpRequest(BaseModel):
|
||||
code: str
|
||||
code: str = Field(..., min_length=1, description="Yandex OAuth authorization code", examples=["abc123def456"])
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"code": "abc123def456"}}}
|
||||
|
||||
|
||||
class YandexSignUpResponse(BaseModel):
|
||||
access_token: str
|
||||
access_token: str = Field(..., description="JWT access token for authentication")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}
|
||||
|
||||
|
||||
@router.post("/auth/sign_up/yandex")
|
||||
@router.post(
|
||||
"/auth/sign_up/yandex",
|
||||
summary="Sign up with Yandex",
|
||||
description="Register a new user using Yandex OAuth",
|
||||
responses={
|
||||
200: {"description": "User successfully registered", "model": YandexSignUpResponse},
|
||||
401: {"description": "Authentication failed - invalid code or OAuth error"},
|
||||
},
|
||||
)
|
||||
async def sign_up_yandex(
|
||||
request: YandexSignUpRequest,
|
||||
interactor: FromDishka[SignUpInteractor],
|
||||
@@ -66,15 +92,28 @@ async def sign_up_yandex(
|
||||
|
||||
|
||||
class EmailSignInRequest(BaseModel):
|
||||
email: str
|
||||
password: SecretStr
|
||||
email: EmailStr = Field(..., description="User email address", examples=["user@example.com"])
|
||||
password: SecretStr = Field(..., min_length=1, description="User password", examples=["password123"])
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"email": "user@example.com", "password": "password123"}}}
|
||||
|
||||
|
||||
class EmailSignInResponse(BaseModel):
|
||||
access_token: str
|
||||
access_token: str = Field(..., description="JWT access token for authentication")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}
|
||||
|
||||
|
||||
@router.post("/auth/sign_in/email")
|
||||
@router.post(
|
||||
"/auth/sign_in/email",
|
||||
summary="Sign in with email",
|
||||
description="Authenticate user with email and password",
|
||||
responses={
|
||||
200: {"description": "User successfully authenticated", "model": EmailSignInResponse},
|
||||
401: {"description": "Invalid credentials"},
|
||||
404: {"description": "User not found"},
|
||||
},
|
||||
)
|
||||
async def sign_in_email(
|
||||
request: EmailSignInRequest,
|
||||
interactor: FromDishka[SignInInteractor],
|
||||
@@ -94,14 +133,27 @@ async def sign_in_email(
|
||||
|
||||
|
||||
class YandexSignInRequest(BaseModel):
|
||||
code: str
|
||||
code: str = Field(..., min_length=1, description="Yandex OAuth authorization code", examples=["abc123def456"])
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"code": "abc123def456"}}}
|
||||
|
||||
|
||||
class YandexSignInResponse(BaseModel):
|
||||
access_token: str
|
||||
access_token: str = Field(..., description="JWT access token for authentication")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}}}
|
||||
|
||||
|
||||
@router.post("/auth/sign_in/yandex")
|
||||
@router.post(
|
||||
"/auth/sign_in/yandex",
|
||||
summary="Sign in with Yandex",
|
||||
description="Authenticate user using Yandex OAuth",
|
||||
responses={
|
||||
200: {"description": "User successfully authenticated", "model": YandexSignInResponse},
|
||||
401: {"description": "Invalid authorization code or authentication failed"},
|
||||
404: {"description": "User not found"},
|
||||
},
|
||||
)
|
||||
async def sign_in_yandex(
|
||||
request: YandexSignInRequest,
|
||||
interactor: FromDishka[SignInInteractor],
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
router = APIRouter(route_class=DishkaRoute, tags=["Health"])
|
||||
|
||||
|
||||
class HealthcheckResponse(BaseModel):
|
||||
ok: bool
|
||||
ok: bool = Field(..., description="Service health status")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"ok": True}}}
|
||||
|
||||
|
||||
@router.get("/healthcheck")
|
||||
@router.get(
|
||||
"/healthcheck",
|
||||
summary="Health check",
|
||||
description="Check if the service is running and healthy",
|
||||
responses={
|
||||
200: {"description": "Service is healthy", "model": HealthcheckResponse},
|
||||
},
|
||||
)
|
||||
async def healthcheck() -> HealthcheckResponse:
|
||||
return HealthcheckResponse(ok=True)
|
||||
|
||||
@@ -2,7 +2,7 @@ from dishka import FromDishka
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from template_project.application.notification_device.errors import NotificationDeviceNotFoundError
|
||||
from template_project.application.notification_device.interactors.register_device import (
|
||||
@@ -19,15 +19,36 @@ router = APIRouter(route_class=DishkaRoute, tags=["Notifications"], dependencies
|
||||
|
||||
|
||||
class SendNotificationRequestModel(BaseModel):
|
||||
title: str
|
||||
body: str
|
||||
title: str = Field(..., min_length=1, max_length=100, description="Notification title", examples=["New Message"])
|
||||
body: str = Field(
|
||||
..., min_length=1, max_length=500, description="Notification body text", examples=["You have a new message"]
|
||||
)
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"title": "New Message", "body": "You have a new message"}}}
|
||||
|
||||
|
||||
class RegisterNotificationDeviceRequestModel(BaseModel):
|
||||
device_id: str
|
||||
device_id: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
max_length=200,
|
||||
description="Device ID for push notifications (FCM token)",
|
||||
examples=["fcm_token_123456789"],
|
||||
)
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"device_id": "fcm_token_123456789"}}}
|
||||
|
||||
|
||||
@router.post("/notifications/send")
|
||||
@router.post(
|
||||
"/notifications/send",
|
||||
summary="Send notification",
|
||||
description="Send a push notification to the current user's registered device",
|
||||
responses={
|
||||
200: {"description": "Notification sent successfully"},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
404: {"description": "Notification device not found - user needs to register device first"},
|
||||
},
|
||||
)
|
||||
async def send_notification(
|
||||
request: SendNotificationRequestModel,
|
||||
interactor: FromDishka[NotificationInteractor],
|
||||
@@ -39,7 +60,15 @@ async def send_notification(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notification device not found") from error
|
||||
|
||||
|
||||
@router.post("/notifications/register_device")
|
||||
@router.post(
|
||||
"/notifications/register_device",
|
||||
summary="Register notification device",
|
||||
description="Register a device ID (FCM token) for push notifications for the current user",
|
||||
responses={
|
||||
200: {"description": "Device registered successfully"},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
},
|
||||
)
|
||||
async def register_notification_device(
|
||||
request: RegisterNotificationDeviceRequestModel,
|
||||
interactor: FromDishka[RegisterNotificationDeviceInteractor],
|
||||
|
||||
@@ -2,7 +2,7 @@ from dishka import FromDishka
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, EmailStr, Field, HttpUrl
|
||||
|
||||
from template_project.application.user.entity import UserId
|
||||
from template_project.application.user.profile.interactors.get_profile import GetProfileInteractor
|
||||
@@ -16,16 +16,39 @@ router = APIRouter(route_class=DishkaRoute, tags=["Profile"], dependencies=[Depe
|
||||
|
||||
|
||||
class GetProfileResponse(BaseModel):
|
||||
id: UserId
|
||||
email: str | None
|
||||
display_name: str | None
|
||||
first_name: str | None
|
||||
last_name: str | None
|
||||
avatar_url: str | None
|
||||
phone: str | None
|
||||
id: UserId = Field(..., description="User ID")
|
||||
email: str | None = Field(None, description="User email address")
|
||||
display_name: str | None = Field(None, description="User display name")
|
||||
first_name: str | None = Field(None, description="User first name")
|
||||
last_name: str | None = Field(None, description="User last name")
|
||||
avatar_url: str | None = Field(None, description="URL to user avatar")
|
||||
phone: str | None = Field(None, description="User phone number")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"id": "01234567-89ab-cdef-0123-456789abcdef",
|
||||
"email": "user@example.com",
|
||||
"display_name": "John Doe",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"avatar_url": "https://example.com/avatar.jpg",
|
||||
"phone": "+1234567890",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/profile")
|
||||
@router.get(
|
||||
"/profile",
|
||||
summary="Get user profile",
|
||||
description="Retrieve the current authenticated user's profile information",
|
||||
responses={
|
||||
200: {"description": "Profile retrieved successfully", "model": GetProfileResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
404: {"description": "Profile not found"},
|
||||
},
|
||||
)
|
||||
async def get_profile(
|
||||
interactor: FromDishka[GetProfileInteractor],
|
||||
) -> GetProfileResponse:
|
||||
@@ -44,25 +67,62 @@ async def get_profile(
|
||||
|
||||
|
||||
class PatchProfileRequestModel(BaseModel):
|
||||
email: str | None = None
|
||||
display_name: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
avatar_url: str | None = None
|
||||
phone: str | None = None
|
||||
email: EmailStr | None = Field(None, description="User email address", examples=["user@example.com"])
|
||||
display_name: str | None = Field(None, max_length=100, description="User display name", examples=["John Doe"])
|
||||
first_name: str | None = Field(None, max_length=50, description="User first name", examples=["John"])
|
||||
last_name: str | None = Field(None, max_length=50, description="User last name", examples=["Doe"])
|
||||
avatar_url: HttpUrl | None = Field(
|
||||
None, description="URL to user avatar", examples=["https://example.com/avatar.jpg"]
|
||||
)
|
||||
phone: str | None = Field(None, max_length=20, description="User phone number", examples=["+1234567890"])
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"email": "user@example.com",
|
||||
"display_name": "John Doe",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"avatar_url": "https://example.com/avatar.jpg",
|
||||
"phone": "+1234567890",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PatchProfileResponse(BaseModel):
|
||||
id: UserId
|
||||
email: str | None
|
||||
display_name: str | None
|
||||
first_name: str | None
|
||||
last_name: str | None
|
||||
avatar_url: str | None
|
||||
phone: str | None
|
||||
id: UserId = Field(..., description="User ID")
|
||||
email: str | None = Field(None, description="User email address")
|
||||
display_name: str | None = Field(None, description="User display name")
|
||||
first_name: str | None = Field(None, description="User first name")
|
||||
last_name: str | None = Field(None, description="User last name")
|
||||
avatar_url: str | None = Field(None, description="URL to user avatar")
|
||||
phone: str | None = Field(None, description="User phone number")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"id": "01234567-89ab-cdef-0123-456789abcdef",
|
||||
"email": "user@example.com",
|
||||
"display_name": "John Doe",
|
||||
"first_name": "John",
|
||||
"last_name": "Doe",
|
||||
"avatar_url": "https://example.com/avatar.jpg",
|
||||
"phone": "+1234567890",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.patch("/profile")
|
||||
@router.patch(
|
||||
"/profile",
|
||||
summary="Update user profile",
|
||||
description="Update the current authenticated user's profile information. All fields are optional.",
|
||||
responses={
|
||||
200: {"description": "Profile updated successfully", "model": PatchProfileResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
},
|
||||
)
|
||||
async def patch_profile(
|
||||
request: PatchProfileRequestModel,
|
||||
interactor: FromDishka[PatchProfileInteractor],
|
||||
@@ -72,7 +132,7 @@ async def patch_profile(
|
||||
display_name=request.display_name,
|
||||
first_name=request.first_name,
|
||||
last_name=request.last_name,
|
||||
avatar_url=request.avatar_url,
|
||||
avatar_url=str(request.avatar_url) if request.avatar_url else None,
|
||||
phone=request.phone,
|
||||
)
|
||||
response = await interactor.execute(patch_request)
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
from decimal import Decimal
|
||||
from enum import StrEnum
|
||||
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.security import HTTPBearer
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
security = HTTPBearer()
|
||||
router = APIRouter(route_class=DishkaRoute, tags=["Resume"], dependencies=[Depends(security)])
|
||||
|
||||
|
||||
class ExperienceTypeEnum(StrEnum):
|
||||
NO_EXPERIENCE = "no_experience"
|
||||
BETWEEN_1_AND_3 = "between_1_and_3"
|
||||
BETWEEN_3_AND_6 = "between_3_and_6"
|
||||
MORE_THAN_6 = "more_than_6"
|
||||
|
||||
|
||||
class CreateResumeRequest(BaseModel):
|
||||
position: str = Field(..., min_length=1, max_length=200, description="Job position", examples=["Python Developer"])
|
||||
about_me: str = Field(
|
||||
..., min_length=1, max_length=2000, description="About me section", examples=["Experienced Python developer"]
|
||||
)
|
||||
key_skills: list[str] = Field(
|
||||
..., min_length=1, description="List of key skills", examples=[["Python", "FastAPI", "PostgreSQL"]]
|
||||
)
|
||||
experience_type: ExperienceTypeEnum = Field(
|
||||
..., description="Experience type", examples=[ExperienceTypeEnum.BETWEEN_3_AND_6]
|
||||
)
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"position": "Python Developer",
|
||||
"about_me": "Experienced Python developer with 5 years of experience",
|
||||
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
|
||||
"experience_type": "between_3_and_6",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CreateResumeResponse(BaseModel):
|
||||
resume_id: str = Field(..., description="Created resume ID")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"resume_id": "01234567-89ab-cdef-0123-456789abcdef"}}}
|
||||
|
||||
|
||||
class SalaryPrediction(BaseModel):
|
||||
from_salary: Decimal = Field(..., description="Minimum predicted salary", examples=[Decimal(100000)])
|
||||
to_salary: Decimal = Field(..., description="Maximum predicted salary", examples=[Decimal(150000)])
|
||||
recommended_skills: list[str] = Field(
|
||||
..., description="Recommended skills to add", examples=[["Kubernetes", "Redis"]]
|
||||
)
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"from_salary": "100000",
|
||||
"to_salary": "150000",
|
||||
"recommended_skills": ["Kubernetes", "Redis"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ResumeResponse(BaseModel):
|
||||
position: str = Field(..., description="Job position")
|
||||
about_me: str = Field(..., description="About me section")
|
||||
key_skills: list[str] = Field(..., description="List of key skills")
|
||||
experience_type: ExperienceTypeEnum = Field(..., description="Experience type")
|
||||
prediction: SalaryPrediction | None = Field(None, description="Salary prediction (can be null)")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"position": "Python Developer",
|
||||
"about_me": "Experienced Python developer with 5 years of experience",
|
||||
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
|
||||
"experience_type": "between_3_and_6",
|
||||
"prediction": {
|
||||
"from_salary": "100000",
|
||||
"to_salary": "150000",
|
||||
"recommended_skills": ["Kubernetes", "Redis"],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ResumeListItem(BaseModel):
|
||||
position: str = Field(..., description="Job position")
|
||||
about_me: str = Field(..., description="About me section")
|
||||
key_skills: list[str] = Field(..., description="List of key skills")
|
||||
experience_type: ExperienceTypeEnum = Field(..., description="Experience type")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"position": "Python Developer",
|
||||
"about_me": "Experienced Python developer with 5 years of experience",
|
||||
"key_skills": ["Python", "FastAPI", "PostgreSQL"],
|
||||
"experience_type": "between_3_and_6",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GetResumeListRequest(BaseModel):
|
||||
limit: int = Field(..., ge=1, le=100, description="Number of resumes to return", examples=[10])
|
||||
offset: int = Field(..., ge=0, description="Number of resumes to skip", examples=[0])
|
||||
|
||||
|
||||
class GetResumeListResponse(BaseModel):
|
||||
resumes: list[ResumeListItem] = Field(..., description="List of resumes")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"resumes": [
|
||||
{
|
||||
"position": "Python Developer",
|
||||
"about_me": "Experienced Python developer",
|
||||
"key_skills": ["Python", "FastAPI"],
|
||||
"experience_type": "between_3_and_6",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GetResumeHistoryRequest(BaseModel):
|
||||
limit: int = Field(..., ge=1, le=100, description="Number of resumes to return", examples=[10])
|
||||
offset: int = Field(..., ge=0, description="Number of resumes to skip", examples=[0])
|
||||
|
||||
|
||||
class GetResumeHistoryResponse(BaseModel):
|
||||
resumes: list[ResumeListItem] = Field(..., description="List of resume history items")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"resumes": [
|
||||
{
|
||||
"position": "Python Developer",
|
||||
"about_me": "Experienced Python developer",
|
||||
"key_skills": ["Python", "FastAPI"],
|
||||
"experience_type": "between_3_and_6",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/resume",
|
||||
summary="Create resume",
|
||||
description="Create a new resume with position, about me, key skills and experience type",
|
||||
responses={
|
||||
200: {"description": "Resume created successfully", "model": CreateResumeResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
},
|
||||
)
|
||||
async def create_resume(
|
||||
request: CreateResumeRequest,
|
||||
) -> CreateResumeResponse:
|
||||
# TODO: Implement resume creation
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/resume/{resume_id}",
|
||||
summary="Get resume",
|
||||
description="Get resume by ID with optional salary prediction",
|
||||
responses={
|
||||
200: {"description": "Resume retrieved successfully", "model": ResumeResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
404: {"description": "Resume not found"},
|
||||
},
|
||||
)
|
||||
async def get_resume(
|
||||
resume_id: str = Field(..., description="Resume ID"),
|
||||
) -> ResumeResponse:
|
||||
# TODO: Implement resume retrieval
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/resume/list",
|
||||
summary="Get resume list",
|
||||
description="Get paginated list of resumes",
|
||||
responses={
|
||||
200: {"description": "Resume list retrieved successfully", "model": GetResumeListResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
},
|
||||
)
|
||||
async def get_resume_list(
|
||||
limit: int = Query(..., ge=1, le=100, description="Number of resumes to return", examples=[10]),
|
||||
offset: int = Query(..., ge=0, description="Number of resumes to skip", examples=[0]),
|
||||
) -> GetResumeListResponse:
|
||||
# TODO: Implement resume list retrieval
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@router.get(
|
||||
"/resume/{resume_id}/history",
|
||||
summary="Get resume history",
|
||||
description="Get paginated history of resume changes",
|
||||
responses={
|
||||
200: {"description": "Resume history retrieved successfully", "model": GetResumeHistoryResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
404: {"description": "Resume not found"},
|
||||
},
|
||||
)
|
||||
async def get_resume_history(
|
||||
resume_id: str = Field(..., description="Resume ID"),
|
||||
limit: int = Query(..., ge=1, le=100, description="Number of history items to return", examples=[10]),
|
||||
offset: int = Query(..., ge=0, description="Number of history items to skip", examples=[0]),
|
||||
) -> GetResumeHistoryResponse:
|
||||
# TODO: Implement resume history retrieval
|
||||
raise NotImplementedError
|
||||
@@ -2,9 +2,9 @@ from io import BytesIO
|
||||
|
||||
from dishka import FromDishka
|
||||
from dishka.integrations.fastapi import DishkaRoute
|
||||
from fastapi import APIRouter, Depends, UploadFile, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, Depends, UploadFile
|
||||
from fastapi.security import HTTPBearer
|
||||
from pydantic import BaseModel, Field
|
||||
from uuid_utils.compat import uuid7
|
||||
|
||||
from template_project.application.common.file_storage import FileStorage
|
||||
@@ -13,16 +13,28 @@ security = HTTPBearer()
|
||||
router = APIRouter(route_class=DishkaRoute, tags=["Storage"], dependencies=[Depends(security)])
|
||||
|
||||
|
||||
@router.post("/storage/upload_file")
|
||||
class UploadFileResponse(BaseModel):
|
||||
path: str = Field(..., description="Generated file path (UUID7)")
|
||||
|
||||
model_config = {"json_schema_extra": {"example": {"path": "01234567-89ab-cdef-0123-456789abcdef"}}}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/storage/upload_file",
|
||||
summary="Upload file",
|
||||
description="Upload a file to storage. The file path is automatically generated using UUID7.",
|
||||
responses={
|
||||
200: {"description": "File uploaded successfully", "model": UploadFileResponse},
|
||||
401: {"description": "Unauthorized - invalid or missing token"},
|
||||
413: {"description": "File too large"},
|
||||
},
|
||||
)
|
||||
async def upload_file(
|
||||
file: UploadFile,
|
||||
file_storage: FromDishka[FileStorage],
|
||||
) -> JSONResponse:
|
||||
) -> UploadFileResponse:
|
||||
path = str(uuid7())
|
||||
file_content = await file.read()
|
||||
file_io = BytesIO(file_content)
|
||||
await file_storage.upload(path=path, image=file_io)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK,
|
||||
content={"path": path},
|
||||
)
|
||||
return UploadFileResponse(path=path)
|
||||
|
||||
@@ -632,6 +632,28 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/89381173b4f336e986d72471198614806cd313e0f85c143ccb677c310223/dishka-1.7.2-py3-none-any.whl", hash = "sha256:f6faa6ab321903926b825b3337d77172ee693450279b314434864978d01fbad3", size = 94774, upload-time = "2025-09-24T21:23:03.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.119.0"
|
||||
@@ -1609,6 +1631,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
email = [
|
||||
{ name = "email-validator" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
@@ -1961,6 +1988,7 @@ dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "prometheus-fastapi-instrumentator" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uuid-utils" },
|
||||
@@ -2012,6 +2040,7 @@ requires-dist = [
|
||||
{ name = "httpx", specifier = "==0.28.1" },
|
||||
{ name = "prometheus-fastapi-instrumentator", specifier = ">=7.1.0" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.12" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.12.4" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||
{ name = "sqlalchemy", specifier = "==2.0.44" },
|
||||
{ name = "uuid-utils", specifier = "==0.11.1" },
|
||||
|
||||
Reference in New Issue
Block a user