diff --git a/src/template_project/web_api/entry_point.py b/src/template_project/web_api/entry_point.py index 7b305af..9d584d6 100644 --- a/src/template_project/web_api/entry_point.py +++ b/src/template_project/web_api/entry_point.py @@ -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) diff --git a/src/template_project/web_api/routes/resume.py b/src/template_project/web_api/routes/resume.py new file mode 100644 index 0000000..093b081 --- /dev/null +++ b/src/template_project/web_api/routes/resume.py @@ -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