Merge branch 'main' of gitlab.prodcontest.com:team-39/backend

* 'main' of gitlab.prodcontest.com:team-39/backend:
  feat(): update resume contracts
This commit is contained in:
ITQ
2025-11-22 17:52:13 +03:00
16 changed files with 847 additions and 64 deletions
@@ -23,8 +23,6 @@ class KeySkillsDataGateway:
return result.scalars().all() return result.scalars().all()
async def add_skills(self, name: list[str]) -> None: async def add_skills(self, name: list[str]) -> None:
insert_statement = insert(key_skills_table).values( insert_statement = insert(key_skills_table).values([{"name": _} for _ in name])
[{"name": _} for _ in name]
)
with contextlib.suppress(IntegrityError): with contextlib.suppress(IntegrityError):
await self._session.execute(insert_statement) await self._session.execute(insert_statement)
@@ -4,9 +4,29 @@ from typing import override
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from template_project.adapters.data_gateways.tables import resume_prediction_table, resume_table from template_project.adapters.data_gateways.tables import (
from template_project.application.resume.data_gateway import ResumeDataGateway, ResumePredictionDataGateway resume_education_table,
from template_project.application.resume.entity import Resume, ResumeEmbeddingId, ResumeId, ResumePrediction resume_experience_table,
resume_prediction_table,
resume_project_table,
resume_table,
)
from template_project.application.resume.data_gateway import (
ResumeDataGateway,
ResumeEducationDataGateway,
ResumeExperienceDataGateway,
ResumePredictionDataGateway,
ResumeProjectDataGateway,
)
from template_project.application.resume.entity import (
Resume,
ResumeEducation,
ResumeEmbeddingId,
ResumeExperience,
ResumeId,
ResumePrediction,
ResumeProject,
)
from template_project.application.resume.errors import ResumeNotFoundError from template_project.application.resume.errors import ResumeNotFoundError
from template_project.application.user.entity import UserId from template_project.application.user.entity import UserId
@@ -29,12 +49,7 @@ class DefaultResumeDataGateway(ResumeDataGateway):
@override @override
async def list_by_user_id(self, user_id: UserId, limit: int, offset: int) -> Sequence[Resume]: async def list_by_user_id(self, user_id: UserId, limit: int, offset: int) -> Sequence[Resume]:
statement = ( statement = select(Resume).where(resume_table.c.user_id == user_id).limit(limit).offset(offset)
select(Resume)
.where(resume_table.c.user_id == user_id)
.limit(limit)
.offset(offset)
)
result = await self._session.execute(statement) result = await self._session.execute(statement)
return result.scalars().all() return result.scalars().all()
@@ -73,3 +88,36 @@ class DefaultResumePredictionDataGateway(ResumePredictionDataGateway):
statement = select(ResumePrediction).where(resume_prediction_table.c.resume_id == resume_id) statement = select(ResumePrediction).where(resume_prediction_table.c.resume_id == resume_id)
result = await self._session.execute(statement) result = await self._session.execute(statement)
return result.scalar() return result.scalar()
class DefaultResumeExperienceDataGateway(ResumeExperienceDataGateway):
def __init__(self, session: AsyncSession) -> None:
self._session = session
@override
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeExperience]:
statement = select(ResumeExperience).where(resume_experience_table.c.resume_id == resume_id)
result = await self._session.execute(statement)
return result.scalars().all()
class DefaultResumeEducationDataGateway(ResumeEducationDataGateway):
def __init__(self, session: AsyncSession) -> None:
self._session = session
@override
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeEducation]:
statement = select(ResumeEducation).where(resume_education_table.c.resume_id == resume_id)
result = await self._session.execute(statement)
return result.scalars().all()
class DefaultResumeProjectDataGateway(ResumeProjectDataGateway):
def __init__(self, session: AsyncSession) -> None:
self._session = session
@override
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeProject]:
statement = select(ResumeProject).where(resume_project_table.c.resume_id == resume_id)
result = await self._session.execute(statement)
return result.scalars().all()
@@ -3,7 +3,6 @@ from typing import Any, Final, override
from pgvector.sqlalchemy import Vector from pgvector.sqlalchemy import Vector
from sqlalchemy import ( from sqlalchemy import (
ARRAY,
Boolean, Boolean,
Column, Column,
DateTime, DateTime,
@@ -23,8 +22,16 @@ from sqlalchemy.orm import registry
from template_project.application.access_token.entity import AccessToken from template_project.application.access_token.entity import AccessToken
from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod
from template_project.application.common.enums import EducationGrade
from template_project.application.notification_device.entity import NotificationDevice from template_project.application.notification_device.entity import NotificationDevice
from template_project.application.resume.entity import Resume, ResumeEmbedding, ResumePrediction from template_project.application.resume.entity import (
Resume,
ResumeEducation,
ResumeEmbedding,
ResumeExperience,
ResumePrediction,
ResumeProject,
)
from template_project.application.user.entity import User from template_project.application.user.entity import User
from template_project.application.user.profile.entity import Profile from template_project.application.user.profile.entity import Profile
@@ -127,6 +134,7 @@ resume_table: Final = Table(
Column("created_at", DateTime(timezone=True), nullable=False), Column("created_at", DateTime(timezone=True), nullable=False),
Column("user_id", UUID, ForeignKey("users.id", ondelete="CASCADE"), nullable=False), Column("user_id", UUID, ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
Column("position", String, nullable=False), Column("position", String, nullable=False),
Column("location", String, nullable=False),
Column("about_me", String, nullable=False), Column("about_me", String, nullable=False),
Column("key_skills", StringArrayType(), nullable=False, server_default=text("'[]'::jsonb")), Column("key_skills", StringArrayType(), nullable=False, server_default=text("'[]'::jsonb")),
Column("experience_type", String, nullable=False), Column("experience_type", String, nullable=False),
@@ -158,7 +166,43 @@ key_skills_table: Final = Table(
"key_skills", "key_skills",
meta_data, meta_data,
Column("id", Integer, autoincrement=True, primary_key=True), Column("id", Integer, autoincrement=True, primary_key=True),
Column("name", String, nullable=False, unique=True) Column("name", String, nullable=False, unique=True),
)
resume_experience_table: Final = Table(
"resume_experience",
meta_data,
Column("id", UUID, primary_key=True),
Column("deleted_at", DateTime(timezone=True)),
Column("created_at", DateTime(timezone=True), nullable=False),
Column("resume_id", UUID, ForeignKey("resume.id", ondelete="CASCADE"), nullable=False),
Column("place", String, nullable=False),
Column("description", String, nullable=False),
Column("months_duration", Integer, nullable=False),
)
resume_education_table: Final = Table(
"resume_education",
meta_data,
Column("id", UUID, primary_key=True),
Column("deleted_at", DateTime(timezone=True)),
Column("created_at", DateTime(timezone=True), nullable=False),
Column("resume_id", UUID, ForeignKey("resume.id", ondelete="CASCADE"), nullable=False),
Column("place", String, nullable=False),
Column("grade", Enum(EducationGrade, name="education_grade"), nullable=False),
Column("specialization", String, nullable=False),
Column("description", String, nullable=True),
)
resume_project_table: Final = Table(
"resume_project",
meta_data,
Column("id", UUID, primary_key=True),
Column("deleted_at", DateTime(timezone=True)),
Column("created_at", DateTime(timezone=True), nullable=False),
Column("resume_id", UUID, ForeignKey("resume.id", ondelete="CASCADE"), nullable=False),
Column("name", String, nullable=False),
Column("description", String, nullable=False),
) )
@@ -182,3 +226,6 @@ mapper_registry.map_imperatively(
"recommended_skills": resume_prediction_table.c.recommended_skills, "recommended_skills": resume_prediction_table.c.recommended_skills,
}, },
) )
mapper_registry.map_imperatively(ResumeExperience, resume_experience_table)
mapper_registry.map_imperatively(ResumeEducation, resume_education_table)
mapper_registry.map_imperatively(ResumeProject, resume_project_table)
@@ -6,3 +6,14 @@ class ExperienceType(StrEnum):
BETWEEN_1_AND_3 = "between1And3" BETWEEN_1_AND_3 = "between1And3"
BETWEEN_3_AND_6 = "between3And6" BETWEEN_3_AND_6 = "between3And6"
MORE_THAN_6 = "moreThan6" MORE_THAN_6 = "moreThan6"
class EducationGrade(StrEnum):
BASIC_GENERAL_EDUCATION = "basic_general_education"
SECONDARY_GENERAL_EDUCATION = "secondary_general_education"
SECONDARY_PROFESSIONAL_EDUCATION = "secondary_professional_education"
BACHELOR = "bachelor"
SPECIALIST = "specialist"
MASTER = "master"
POSTGRADUATE_STUDIES = "postgraduate_studies"
OTHER = "other"
@@ -4,9 +4,12 @@ from typing import Protocol
from template_project.application.resume.entity import ( from template_project.application.resume.entity import (
Resume, Resume,
ResumeEducation,
ResumeEmbeddingId, ResumeEmbeddingId,
ResumeExperience,
ResumeId, ResumeId,
ResumePrediction, ResumePrediction,
ResumeProject,
) )
from template_project.application.user.entity import UserId from template_project.application.user.entity import UserId
@@ -37,3 +40,21 @@ class ResumePredictionDataGateway(Protocol):
@abstractmethod @abstractmethod
async def load_by_resume_id(self, resume_id: ResumeId) -> ResumePrediction | None: async def load_by_resume_id(self, resume_id: ResumeId) -> ResumePrediction | None:
raise NotImplementedError raise NotImplementedError
class ResumeExperienceDataGateway(Protocol):
@abstractmethod
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeExperience]:
raise NotImplementedError
class ResumeEducationDataGateway(Protocol):
@abstractmethod
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeEducation]:
raise NotImplementedError
class ResumeProjectDataGateway(Protocol):
@abstractmethod
async def load_by_resume_id(self, resume_id: ResumeId) -> Sequence[ResumeProject]:
raise NotImplementedError
@@ -6,19 +6,22 @@ from uuid import UUID
from uuid_utils.compat import uuid7 from uuid_utils.compat import uuid7
from template_project.application.common.entity import Entity, to_entity from template_project.application.common.entity import Entity, to_entity
from template_project.application.common.enums import ExperienceType from template_project.application.common.enums import EducationGrade, ExperienceType
from template_project.application.user.entity import UserId from template_project.application.user.entity import UserId
ResumeId = NewType("ResumeId", UUID) ResumeId = NewType("ResumeId", UUID)
ResumeEmbeddingId = NewType("ResumeEmbeddingId", UUID) ResumeEmbeddingId = NewType("ResumeEmbeddingId", UUID)
ResumePredictionId = NewType("ResumePredictionId", UUID) ResumePredictionId = NewType("ResumePredictionId", UUID)
ResumeExperienceId = NewType("ResumeExperienceId", UUID)
ResumeEducationId = NewType("ResumeEducationId", UUID)
ResumeProjectId = NewType("ResumeProjectId", UUID)
@to_entity @to_entity
class Resume(Entity[ResumeId]): class Resume(Entity[ResumeId]):
user_id: UserId user_id: UserId
position: str position: str
# location: str location: str
about_me: str about_me: str
key_skills: list[str] key_skills: list[str]
experience_type: ExperienceType experience_type: ExperienceType
@@ -30,6 +33,7 @@ class Resume(Entity[ResumeId]):
cls, cls,
user_id: UserId, user_id: UserId,
position: str, position: str,
location: str,
about_me: str, about_me: str,
key_skills: list[str], key_skills: list[str],
experience_type: ExperienceType, experience_type: ExperienceType,
@@ -41,6 +45,7 @@ class Resume(Entity[ResumeId]):
created_at=datetime.now(tz=UTC), created_at=datetime.now(tz=UTC),
user_id=user_id, user_id=user_id,
position=position, position=position,
location=location,
about_me=about_me, about_me=about_me,
key_skills=key_skills, key_skills=key_skills,
experience_type=experience_type, experience_type=experience_type,
@@ -74,4 +79,78 @@ class ResumePrediction(Entity[ResumePredictionId]):
from_salary: Decimal from_salary: Decimal
to_salary: Decimal to_salary: Decimal
recommended_skills: list[str] recommended_skills: list[str]
# common_recommended: str # TODO
@to_entity
class ResumeExperience(Entity[ResumeExperienceId]):
resume_id: ResumeId
place: str
description: str
months_duration: int
@classmethod
def factory(
cls,
resume_id: ResumeId,
place: str,
description: str,
months_duration: int,
) -> Self:
return cls(
id=ResumeExperienceId(uuid7()),
created_at=datetime.now(tz=UTC),
resume_id=resume_id,
place=place,
description=description,
months_duration=months_duration,
)
@to_entity
class ResumeEducation(Entity[ResumeEducationId]):
resume_id: ResumeId
place: str
grade: EducationGrade
specialization: str
description: str | None = None
@classmethod
def factory(
cls,
resume_id: ResumeId,
place: str,
grade: EducationGrade,
specialization: str,
description: str | None = None,
) -> Self:
return cls(
id=ResumeEducationId(uuid7()),
created_at=datetime.now(tz=UTC),
resume_id=resume_id,
place=place,
grade=grade,
specialization=specialization,
description=description,
)
@to_entity
class ResumeProject(Entity[ResumeProjectId]):
resume_id: ResumeId
name: str
description: str
@classmethod
def factory(
cls,
resume_id: ResumeId,
name: str,
description: str,
) -> Self:
return cls(
id=ResumeProjectId(uuid7()),
created_at=datetime.now(tz=UTC),
resume_id=resume_id,
name=name,
description=description,
)
@@ -1,8 +1,36 @@
from template_project.application.common.enums import ExperienceType from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.enums import EducationGrade, ExperienceType
from template_project.application.common.identity_provider import IdentityProvider from template_project.application.common.identity_provider import IdentityProvider
from template_project.application.common.interactor import to_interactor from template_project.application.common.interactor import to_interactor
from template_project.application.common.unit_of_work import UnitOfWork from template_project.application.common.unit_of_work import UnitOfWork
from template_project.application.resume.entity import Resume, ResumeId from template_project.application.resume.entity import (
Resume,
ResumeEducation,
ResumeExperience,
ResumeId,
ResumeProject,
)
@to_data_structure
class ExperienceInput:
place: str
description: str
months_duration: int
@to_data_structure
class EducationInput:
place: str
grade: EducationGrade
specialization: str
description: str | None = None
@to_data_structure
class ProjectInput:
name: str
description: str
@to_interactor @to_interactor
@@ -13,15 +41,20 @@ class AddResumeInteractor:
async def execute( async def execute(
self, self,
position: str, position: str,
location: str,
about_me: str, about_me: str,
key_skills: list[str], key_skills: list[str],
experience_type: ExperienceType, experience_type: ExperienceType,
experience: list[ExperienceInput] | None = None,
education: list[EducationInput] | None = None,
projects: list[ProjectInput] | None = None,
) -> ResumeId: ) -> ResumeId:
user = await self.identity_provider.get_current_user() user = await self.identity_provider.get_current_user()
resume = Resume.factory( resume = Resume.factory(
user_id=user.id, user_id=user.id,
position=position, position=position,
location=location,
about_me=about_me, about_me=about_me,
key_skills=key_skills, key_skills=key_skills,
experience_type=experience_type, experience_type=experience_type,
@@ -29,9 +62,38 @@ class AddResumeInteractor:
up_resume_id=None, up_resume_id=None,
) )
# TODO: тут надо сделать запуск фоновой таски для вычитывания подходящей вакансии
await self.unit_of_work.add(resume) await self.unit_of_work.add(resume)
if experience:
for exp in experience:
resume_experience = ResumeExperience.factory(
resume_id=resume.id,
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
await self.unit_of_work.add(resume_experience)
if education:
for edu in education:
resume_education = ResumeEducation.factory(
resume_id=resume.id,
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
await self.unit_of_work.add(resume_education)
if projects:
for proj in projects:
resume_project = ResumeProject.factory(
resume_id=resume.id,
name=proj.name,
description=proj.description,
)
await self.unit_of_work.add(resume_project)
await self.unit_of_work.commit() await self.unit_of_work.commit()
return resume.id return resume.id
@@ -1,20 +1,77 @@
from template_project.application.common.data_structure import to_data_structure from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.enums import ExperienceType from template_project.application.common.enums import EducationGrade, ExperienceType
from template_project.application.common.identity_provider import IdentityProvider from template_project.application.common.identity_provider import IdentityProvider
from template_project.application.common.interactor import to_interactor from template_project.application.common.interactor import to_interactor
from template_project.application.common.unit_of_work import UnitOfWork from template_project.application.common.unit_of_work import UnitOfWork
from template_project.application.resume.data_gateway import ResumeDataGateway from template_project.application.resume.data_gateway import (
from template_project.application.resume.entity import Resume, ResumeId ResumeDataGateway,
ResumeEducationDataGateway,
ResumeExperienceDataGateway,
ResumeProjectDataGateway,
)
from template_project.application.resume.entity import (
Resume,
ResumeEducation,
ResumeExperience,
ResumeId,
ResumeProject,
)
from template_project.application.resume.errors import ResumeDoesBelongUserError from template_project.application.resume.errors import ResumeDoesBelongUserError
@to_data_structure @to_data_structure
class _Response: class ExperienceInput:
place: str
description: str
months_duration: int
@to_data_structure
class EducationInput:
place: str
grade: EducationGrade
specialization: str
description: str | None = None
@to_data_structure
class ProjectInput:
name: str
description: str
@to_data_structure
class ExperienceItemResponse:
place: str
description: str
months_duration: int
@to_data_structure
class EducationItemResponse:
place: str
grade: EducationGrade
specialization: str
description: str | None
@to_data_structure
class ProjectItemResponse:
name: str
description: str
@to_data_structure
class EditResumeResponse:
id: ResumeId id: ResumeId
position: str position: str
location: str
about_me: str about_me: str
key_skills: list[str] key_skills: list[str]
experience_type: ExperienceType experience_type: ExperienceType
experience: list[ExperienceItemResponse]
education: list[EducationItemResponse]
projects: list[ProjectItemResponse]
@to_interactor @to_interactor
@@ -22,21 +79,29 @@ class EditResumeInteractor:
unit_of_work: UnitOfWork unit_of_work: UnitOfWork
identity_provider: IdentityProvider identity_provider: IdentityProvider
resume_data_gateway: ResumeDataGateway resume_data_gateway: ResumeDataGateway
resume_experience_data_gateway: ResumeExperienceDataGateway
resume_education_data_gateway: ResumeEducationDataGateway
resume_project_data_gateway: ResumeProjectDataGateway
async def execute( async def execute(
self, self,
resume_id: ResumeId, resume_id: ResumeId,
position: str | None, position: str | None,
location: str | None,
about_me: str | None, about_me: str | None,
key_skills: list[str] | None, key_skills: list[str] | None,
experience_type: ExperienceType | None, experience_type: ExperienceType | None,
) -> _Response: experience: list[ExperienceInput] | None = None,
education: list[EducationInput] | None = None,
projects: list[ProjectInput] | None = None,
) -> EditResumeResponse:
user = await self.identity_provider.get_current_user() user = await self.identity_provider.get_current_user()
old_resume = await self.resume_data_gateway.load(resume_id) old_resume = await self.resume_data_gateway.load(resume_id)
if old_resume.user_id != user.id: if old_resume.user_id != user.id:
raise ResumeDoesBelongUserError raise ResumeDoesBelongUserError
new_position = position if position is not None else old_resume.position new_position = position if position is not None else old_resume.position
new_location = location if location is not None else old_resume.location
new_about_me = about_me if about_me is not None else old_resume.about_me new_about_me = about_me if about_me is not None else old_resume.about_me
new_key_skills = key_skills if key_skills is not None else old_resume.key_skills new_key_skills = key_skills if key_skills is not None else old_resume.key_skills
new_experience_type = experience_type if experience_type is not None else old_resume.experience_type new_experience_type = experience_type if experience_type is not None else old_resume.experience_type
@@ -44,6 +109,7 @@ class EditResumeInteractor:
new_resume = Resume.factory( new_resume = Resume.factory(
user_id=user.id, user_id=user.id,
position=new_position, position=new_position,
location=new_location,
about_me=new_about_me, about_me=new_about_me,
key_skills=new_key_skills, key_skills=new_key_skills,
experience_type=new_experience_type, experience_type=new_experience_type,
@@ -53,15 +119,74 @@ class EditResumeInteractor:
await self.unit_of_work.add(new_resume) await self.unit_of_work.add(new_resume)
if experience is not None:
for exp in experience:
resume_experience = ResumeExperience.factory(
resume_id=new_resume.id,
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
await self.unit_of_work.add(resume_experience)
if education is not None:
for edu in education:
resume_education = ResumeEducation.factory(
resume_id=new_resume.id,
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
await self.unit_of_work.add(resume_education)
if projects is not None:
for proj in projects:
resume_project = ResumeProject.factory(
resume_id=new_resume.id,
name=proj.name,
description=proj.description,
)
await self.unit_of_work.add(resume_project)
if old_resume.up_resume_id is None: if old_resume.up_resume_id is None:
old_resume.up_resume_id = new_resume.id old_resume.up_resume_id = new_resume.id
await self.unit_of_work.commit() await self.unit_of_work.commit()
return _Response( new_experiences = await self.resume_experience_data_gateway.load_by_resume_id(new_resume.id)
new_educations = await self.resume_education_data_gateway.load_by_resume_id(new_resume.id)
new_projects = await self.resume_project_data_gateway.load_by_resume_id(new_resume.id)
return EditResumeResponse(
id=new_resume.id, id=new_resume.id,
position=new_resume.position, position=new_resume.position,
location=new_resume.location,
about_me=new_resume.about_me, about_me=new_resume.about_me,
key_skills=new_resume.key_skills, key_skills=new_resume.key_skills,
experience_type=new_resume.experience_type, experience_type=new_resume.experience_type,
experience=[
ExperienceItemResponse(
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
for exp in new_experiences
],
education=[
EducationItemResponse(
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
for edu in new_educations
],
projects=[
ProjectItemResponse(
name=proj.name,
description=proj.description,
)
for proj in new_projects
],
) )
@@ -1,10 +1,16 @@
from decimal import Decimal from decimal import Decimal
from template_project.application.common.data_structure import to_data_structure from template_project.application.common.data_structure import to_data_structure
from template_project.application.common.enums import ExperienceType from template_project.application.common.enums import EducationGrade, ExperienceType
from template_project.application.common.identity_provider import IdentityProvider from template_project.application.common.identity_provider import IdentityProvider
from template_project.application.common.interactor import to_interactor from template_project.application.common.interactor import to_interactor
from template_project.application.resume.data_gateway import ResumeDataGateway, ResumePredictionDataGateway from template_project.application.resume.data_gateway import (
ResumeDataGateway,
ResumeEducationDataGateway,
ResumeExperienceDataGateway,
ResumePredictionDataGateway,
ResumeProjectDataGateway,
)
from template_project.application.resume.entity import ResumeId from template_project.application.resume.entity import ResumeId
from template_project.application.resume.errors import ResumeDoesBelongUserError from template_project.application.resume.errors import ResumeDoesBelongUserError
@@ -17,12 +23,37 @@ class ResumePredictionResponse:
@to_data_structure @to_data_structure
class _Response: class ExperienceItemResponse:
place: str
description: str
months_duration: int
@to_data_structure
class EducationItemResponse:
place: str
grade: EducationGrade
specialization: str
description: str | None
@to_data_structure
class ProjectItemResponse:
name: str
description: str
@to_data_structure
class GetResumeResponse:
id: ResumeId id: ResumeId
position: str position: str
location: str
about_me: str about_me: str
key_skills: list[str] key_skills: list[str]
experience_type: ExperienceType experience_type: ExperienceType
experience: list[ExperienceItemResponse]
education: list[EducationItemResponse]
projects: list[ProjectItemResponse]
prediction: ResumePredictionResponse | None prediction: ResumePredictionResponse | None
@@ -31,11 +62,14 @@ class GetResumeInteractor:
identity_provider: IdentityProvider identity_provider: IdentityProvider
resume_data_gateway: ResumeDataGateway resume_data_gateway: ResumeDataGateway
resume_prediction_data_gateway: ResumePredictionDataGateway resume_prediction_data_gateway: ResumePredictionDataGateway
resume_experience_data_gateway: ResumeExperienceDataGateway
resume_education_data_gateway: ResumeEducationDataGateway
resume_project_data_gateway: ResumeProjectDataGateway
async def execute( async def execute(
self, self,
resume_id: ResumeId, resume_id: ResumeId,
) -> _Response: ) -> GetResumeResponse:
user = await self.identity_provider.get_current_user() user = await self.identity_provider.get_current_user()
resume = await self.resume_data_gateway.load(resume_id) resume = await self.resume_data_gateway.load(resume_id)
@@ -53,12 +87,41 @@ class GetResumeInteractor:
else: else:
prediction = None prediction = None
return _Response( experiences = await self.resume_experience_data_gateway.load_by_resume_id(resume.id)
educations = await self.resume_education_data_gateway.load_by_resume_id(resume.id)
projects = await self.resume_project_data_gateway.load_by_resume_id(resume.id)
return GetResumeResponse(
id=resume.id, id=resume.id,
position=resume.position, position=resume.position,
location=resume.location,
about_me=resume.about_me, about_me=resume.about_me,
key_skills=resume.key_skills, key_skills=resume.key_skills,
experience_type=resume.experience_type, experience_type=resume.experience_type,
experience=[
ExperienceItemResponse(
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
for exp in experiences
],
education=[
EducationItemResponse(
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
for edu in educations
],
projects=[
ProjectItemResponse(
name=proj.name,
description=proj.description,
)
for proj in projects
],
prediction=prediction, prediction=prediction,
) )
@@ -67,6 +130,7 @@ class GetResumeInteractor:
class ResumeListItemResponse: class ResumeListItemResponse:
id: ResumeId id: ResumeId
position: str position: str
location: str
about_me: str about_me: str
key_skills: list[str] key_skills: list[str]
experience_type: ExperienceType experience_type: ExperienceType
@@ -86,6 +150,7 @@ class GetResumeListInteractor:
ResumeListItemResponse( ResumeListItemResponse(
id=r.id, id=r.id,
position=r.position, position=r.position,
location=r.location,
about_me=r.about_me, about_me=r.about_me,
key_skills=r.key_skills, key_skills=r.key_skills,
experience_type=r.experience_type, experience_type=r.experience_type,
@@ -112,6 +177,7 @@ class GetResumeHistoryInteractor:
ResumeListItemResponse( ResumeListItemResponse(
id=r.id, id=r.id,
position=r.position, position=r.position,
location=r.location,
about_me=r.about_me, about_me=r.about_me,
key_skills=r.key_skills, key_skills=r.key_skills,
experience_type=r.experience_type, experience_type=r.experience_type,
@@ -32,8 +32,7 @@ class PredictSalaryResponse:
class PredictSalaryInteractor: class PredictSalaryInteractor:
async def execute(self, request: PredictSalaryRequest) -> PredictSalaryResponse: async def execute(self, request: PredictSalaryRequest) -> PredictSalaryResponse:
return PredictSalaryResponse( return PredictSalaryResponse(
salary_from=Decimal("50000"), salary_from=Decimal(50000),
salary_to=Decimal("80000"), salary_to=Decimal(80000),
recommended_skills=["python", "django", "postgresql"], recommended_skills=["python", "django", "postgresql"],
) )
@@ -9,4 +9,3 @@ class InteractorProvider(Provider):
interactors = provide_all( interactors = provide_all(
PredictSalaryInteractor, PredictSalaryInteractor,
) )
+2 -6
View File
@@ -2,14 +2,13 @@ from decimal import Decimal
from dishka import FromDishka from dishka import FromDishka
from dishka.integrations.fastapi import DishkaRoute from dishka.integrations.fastapi import DishkaRoute
from fastapi import APIRouter, status from fastapi import APIRouter
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from template_project.application.resume.entity import ResumeId from template_project.application.resume.entity import ResumeId
from template_project.ml.interactors.predict_salary import ( from template_project.ml.interactors.predict_salary import (
PredictSalaryInteractor, PredictSalaryInteractor,
PredictSalaryRequest, PredictSalaryRequest,
PredictSalaryResponse,
VacancyInput, VacancyInput,
) )
@@ -43,9 +42,7 @@ class PredictSalaryRequestModel(BaseModel):
key_skills: list[str] = Field( key_skills: list[str] = Field(
min_length=1, description="List of key skills from resume", examples=[["Python", "FastAPI", "PostgreSQL"]] min_length=1, description="List of key skills from resume", examples=[["Python", "FastAPI", "PostgreSQL"]]
) )
vacancies: list[VacancyInputModel] = Field( vacancies: list[VacancyInputModel] = Field(min_length=1, description="List of relevant vacancies", examples=[[]])
min_length=1, description="List of relevant vacancies", examples=[[]]
)
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
@@ -120,4 +117,3 @@ async def predict_salary(
salary_to=response.salary_to, salary_to=response.salary_to,
recommended_skills=response.recommended_skills, recommended_skills=response.recommended_skills,
) )
@@ -5,7 +5,13 @@ from template_project.adapters.data_gateways.auth_identity import DefaultAuthIde
from template_project.adapters.data_gateways.key_skills import KeySkillsDataGateway from template_project.adapters.data_gateways.key_skills import KeySkillsDataGateway
from template_project.adapters.data_gateways.notification_device import DefaultNotificationDeviceDataGateway from template_project.adapters.data_gateways.notification_device import DefaultNotificationDeviceDataGateway
from template_project.adapters.data_gateways.profile import DefaultProfileDataGateway from template_project.adapters.data_gateways.profile import DefaultProfileDataGateway
from template_project.adapters.data_gateways.resume import DefaultResumeDataGateway, DefaultResumePredictionDataGateway from template_project.adapters.data_gateways.resume import (
DefaultResumeDataGateway,
DefaultResumeEducationDataGateway,
DefaultResumeExperienceDataGateway,
DefaultResumePredictionDataGateway,
DefaultResumeProjectDataGateway,
)
from template_project.adapters.data_gateways.user import DefaultUserDataGateway from template_project.adapters.data_gateways.user import DefaultUserDataGateway
from template_project.adapters.unit_of_work import DefaultUnitOfWork from template_project.adapters.unit_of_work import DefaultUnitOfWork
@@ -23,4 +29,7 @@ class DataGatewayProvider(Provider):
WithParents[DefaultNotificationDeviceDataGateway], WithParents[DefaultNotificationDeviceDataGateway],
WithParents[DefaultResumeDataGateway], WithParents[DefaultResumeDataGateway],
WithParents[DefaultResumePredictionDataGateway], WithParents[DefaultResumePredictionDataGateway],
WithParents[DefaultResumeExperienceDataGateway],
WithParents[DefaultResumeEducationDataGateway],
WithParents[DefaultResumeProjectDataGateway],
) )
+287 -4
View File
@@ -8,11 +8,21 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.security import HTTPBearer from fastapi.security import HTTPBearer
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from template_project.application.common.enums import ExperienceType from template_project.application.common.enums import EducationGrade, ExperienceType
from template_project.application.resume.entity import ResumeId from template_project.application.resume.entity import ResumeId
from template_project.application.resume.errors import ResumeDoesBelongUserError, ResumeNotFoundError from template_project.application.resume.errors import ResumeDoesBelongUserError, ResumeNotFoundError
from template_project.application.resume.interactors.add import AddResumeInteractor from template_project.application.resume.interactors.add import (
from template_project.application.resume.interactors.edit import EditResumeInteractor AddResumeInteractor,
EducationInput,
ExperienceInput,
ProjectInput,
)
from template_project.application.resume.interactors.edit import (
EditResumeInteractor,
EducationInput as EditEducationInput,
ExperienceInput as EditExperienceInput,
ProjectInput as EditProjectInput,
)
from template_project.application.resume.interactors.get import ( from template_project.application.resume.interactors.get import (
GetResumeHistoryInteractor, GetResumeHistoryInteractor,
GetResumeInteractor, GetResumeInteractor,
@@ -23,18 +33,81 @@ security = HTTPBearer()
router = APIRouter(route_class=DishkaRoute, tags=["Resume"], dependencies=[Depends(security)]) router = APIRouter(route_class=DishkaRoute, tags=["Resume"], dependencies=[Depends(security)])
class ExperienceItem(BaseModel):
place: str = Field(min_length=1, max_length=200, description="Company or organization name", examples=["T-bank"])
description: str = Field(
min_length=1, max_length=2000, description="Job description", examples=["some description lorem ipsum"]
)
months_duration: int = Field(ge=1, description="Duration in months", examples=[12])
model_config = {
"json_schema_extra": {
"example": {
"place": "T-bank",
"description": "some description lorem ipsum",
"months_duration": 12,
}
}
}
class EducationItem(BaseModel):
place: str = Field(
min_length=1, max_length=200, description="Educational institution name", examples=["Central university"]
)
grade: EducationGrade = Field(description="Education grade", examples=[EducationGrade.BACHELOR])
specialization: str = Field(min_length=1, max_length=200, description="Field of study", examples=["IT guy"])
description: str | None = Field(
None,
max_length=2000,
description="Additional description (optional)",
examples=["optional field, if user want add something"],
)
model_config = {
"json_schema_extra": {
"example": {
"place": "Central university",
"grade": "bachelor",
"specialization": "IT guy",
"description": "optional field, if user want add something",
}
}
}
class ProjectItem(BaseModel):
name: str = Field(min_length=1, max_length=200, description="Project name", examples=["Rekomenci fluon"])
description: str = Field(
min_length=1, max_length=2000, description="Project description", examples=["fucking shit"]
)
model_config = {
"json_schema_extra": {
"example": {
"name": "Rekomenci fluon",
"description": "fucking shit",
}
}
}
class CreateResumeRequest(BaseModel): class CreateResumeRequest(BaseModel):
position: str = Field(min_length=1, max_length=200, description="Job position", examples=["Python Developer"]) position: str = Field(min_length=1, max_length=200, description="Job position", examples=["Python Developer"])
about_me: str = Field( about_me: str = Field(
min_length=1, min_length=1,
max_length=2000, max_length=2000,
description="About me section", description="About me section",
examples=["Experienced Python developer"], examples=["Experienced Python developer with 5 years of experience"],
) )
key_skills: list[str] = Field( key_skills: list[str] = Field(
min_length=1, description="List of key skills", examples=[["Python", "FastAPI", "PostgreSQL"]] min_length=1, description="List of key skills", examples=[["Python", "FastAPI", "PostgreSQL"]]
) )
experience_type: ExperienceType = Field(description="Experience type", examples=[ExperienceType.BETWEEN_3_AND_6]) experience_type: ExperienceType = Field(description="Experience type", examples=[ExperienceType.BETWEEN_3_AND_6])
location: str = Field(min_length=1, max_length=100, description="Location", examples=["Moscow"])
experience: list[ExperienceItem] = Field(default_factory=list, description="Work experience list")
education: list[EducationItem] = Field(default_factory=list, description="Education list")
projects: list[ProjectItem] = Field(default_factory=list, description="Projects list")
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
@@ -43,6 +116,28 @@ class CreateResumeRequest(BaseModel):
"about_me": "Experienced Python developer with 5 years of experience", "about_me": "Experienced Python developer with 5 years of experience",
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"], "key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
"experience_type": "between3And6", "experience_type": "between3And6",
"location": "Moscow",
"experience": [
{
"place": "T-bank",
"description": "some description lorem ipsum",
"months_duration": 12,
}
],
"education": [
{
"place": "Central university",
"grade": "bachelor",
"specialization": "IT guy",
"description": "optional field, if user want add something",
}
],
"projects": [
{
"name": "Rekomenci fluon",
"description": "fucking shit",
}
],
} }
} }
} }
@@ -73,18 +168,44 @@ class SalaryPrediction(BaseModel):
class ResumeResponse(BaseModel): class ResumeResponse(BaseModel):
id: ResumeId = Field(description="Resume ID") id: ResumeId = Field(description="Resume ID")
position: str = Field(description="Job position") position: str = Field(description="Job position")
location: str = Field(description="Location")
about_me: str = Field(description="About me section") about_me: str = Field(description="About me section")
key_skills: list[str] = Field(description="List of key skills") key_skills: list[str] = Field(description="List of key skills")
experience_type: ExperienceType = Field(description="Experience type") experience_type: ExperienceType = Field(description="Experience type")
experience: list[ExperienceItem] = Field(description="Work experience list")
education: list[EducationItem] = Field(description="Education list")
projects: list[ProjectItem] = Field(description="Projects list")
prediction: SalaryPrediction | None = Field(None, description="Salary prediction (can be null)") prediction: SalaryPrediction | None = Field(None, description="Salary prediction (can be null)")
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
"example": { "example": {
"position": "Python Developer", "position": "Python Developer",
"location": "Moscow",
"about_me": "Experienced Python developer with 5 years of experience", "about_me": "Experienced Python developer with 5 years of experience",
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"], "key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
"experience_type": "between3And6", "experience_type": "between3And6",
"experience": [
{
"place": "T-bank",
"description": "some description lorem ipsum",
"months_duration": 12,
}
],
"education": [
{
"place": "Central university",
"grade": "bachelor",
"specialization": "IT guy",
"description": "optional field, if user want add something",
}
],
"projects": [
{
"name": "Rekomenci fluon",
"description": "fucking shit",
}
],
"prediction": { "prediction": {
"from_salary": "100000", "from_salary": "100000",
"to_salary": "150000", "to_salary": "150000",
@@ -98,6 +219,7 @@ class ResumeResponse(BaseModel):
class ResumeListItem(BaseModel): class ResumeListItem(BaseModel):
id: ResumeId = Field(description="Resume ID") id: ResumeId = Field(description="Resume ID")
position: str = Field(description="Job position") position: str = Field(description="Job position")
location: str = Field(description="Location")
about_me: str = Field(description="About me section") about_me: str = Field(description="About me section")
key_skills: list[str] = Field(description="List of key skills") key_skills: list[str] = Field(description="List of key skills")
experience_type: ExperienceType = Field(description="Experience type") experience_type: ExperienceType = Field(description="Experience type")
@@ -106,6 +228,7 @@ class ResumeListItem(BaseModel):
"json_schema_extra": { "json_schema_extra": {
"example": { "example": {
"position": "Python Developer", "position": "Python Developer",
"location": "Moscow",
"about_me": "Experienced Python developer with 5 years of experience", "about_me": "Experienced Python developer with 5 years of experience",
"key_skills": ["Python", "FastAPI", "PostgreSQL"], "key_skills": ["Python", "FastAPI", "PostgreSQL"],
"experience_type": "between3And6", "experience_type": "between3And6",
@@ -175,11 +298,39 @@ async def create_resume(
request: CreateResumeRequest, request: CreateResumeRequest,
interactor: FromDishka[AddResumeInteractor], interactor: FromDishka[AddResumeInteractor],
) -> CreateResumeResponse: ) -> CreateResumeResponse:
experience = (
[
ExperienceInput(place=exp.place, description=exp.description, months_duration=exp.months_duration)
for exp in request.experience
]
if request.experience
else None
)
education = (
[
EducationInput(
place=edu.place, grade=edu.grade, specialization=edu.specialization, description=edu.description
)
for edu in request.education
]
if request.education
else None
)
projects = (
[ProjectInput(name=proj.name, description=proj.description) for proj in request.projects]
if request.projects
else None
)
interactor_response = await interactor.execute( interactor_response = await interactor.execute(
position=request.position, position=request.position,
location=request.location,
about_me=request.about_me, about_me=request.about_me,
key_skills=request.key_skills, key_skills=request.key_skills,
experience_type=request.experience_type, experience_type=request.experience_type,
experience=experience,
education=education,
projects=projects,
) )
return CreateResumeResponse( return CreateResumeResponse(
resume_id=interactor_response, resume_id=interactor_response,
@@ -206,6 +357,7 @@ async def get_resume_list(
ResumeListItem( ResumeListItem(
id=r.id, id=r.id,
position=r.position, position=r.position,
location=r.location,
about_me=r.about_me, about_me=r.about_me,
key_skills=r.key_skills, key_skills=r.key_skills,
experience_type=r.experience_type, experience_type=r.experience_type,
@@ -246,9 +398,34 @@ async def get_resume(
return ResumeResponse( return ResumeResponse(
id=interactor_response.id, id=interactor_response.id,
position=interactor_response.position, position=interactor_response.position,
location=interactor_response.location,
about_me=interactor_response.about_me, about_me=interactor_response.about_me,
key_skills=interactor_response.key_skills, key_skills=interactor_response.key_skills,
experience_type=interactor_response.experience_type, experience_type=interactor_response.experience_type,
experience=[
ExperienceItem(
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
for exp in interactor_response.experience
],
education=[
EducationItem(
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
for edu in interactor_response.education
],
projects=[
ProjectItem(
name=proj.name,
description=proj.description,
)
for proj in interactor_response.projects
],
prediction=SalaryPrediction( prediction=SalaryPrediction(
from_salary=interactor_response.prediction.from_salary, from_salary=interactor_response.prediction.from_salary,
to_salary=interactor_response.prediction.to_salary, to_salary=interactor_response.prediction.to_salary,
@@ -263,6 +440,7 @@ class PatchResumeRequest(BaseModel):
position: str | None = Field( position: str | None = Field(
None, min_length=1, max_length=200, description="Job position", examples=["Python Developer"] None, min_length=1, max_length=200, description="Job position", examples=["Python Developer"]
) )
location: str | None = Field(None, min_length=1, max_length=100, description="Location", examples=["Moscow"])
about_me: str | None = Field( about_me: str | None = Field(
None, min_length=1, max_length=2000, description="About me section", examples=["Experienced Python developer"] None, min_length=1, max_length=2000, description="About me section", examples=["Experienced Python developer"]
) )
@@ -272,14 +450,39 @@ class PatchResumeRequest(BaseModel):
experience_type: ExperienceType | None = Field( experience_type: ExperienceType | None = Field(
None, description="Experience type", examples=[ExperienceType.BETWEEN_3_AND_6] None, description="Experience type", examples=[ExperienceType.BETWEEN_3_AND_6]
) )
experience: list[ExperienceItem] | None = Field(None, description="Work experience list")
education: list[EducationItem] | None = Field(None, description="Education list")
projects: list[ProjectItem] | None = Field(None, description="Projects list")
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
"example": { "example": {
"position": "Python Developer", "position": "Python Developer",
"location": "Moscow",
"about_me": "Experienced Python developer with 5 years of experience", "about_me": "Experienced Python developer with 5 years of experience",
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"], "key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
"experience_type": "between3And6", "experience_type": "between3And6",
"experience": [
{
"place": "T-bank",
"description": "some description lorem ipsum",
"months_duration": 12,
}
],
"education": [
{
"place": "Central university",
"grade": "bachelor",
"specialization": "IT guy",
"description": "optional field, if user want add something",
}
],
"projects": [
{
"name": "Rekomenci fluon",
"description": "fucking shit",
}
],
} }
} }
} }
@@ -288,17 +491,43 @@ class PatchResumeRequest(BaseModel):
class PatchResumeResponse(BaseModel): class PatchResumeResponse(BaseModel):
id: ResumeId = Field(description="Resume ID") id: ResumeId = Field(description="Resume ID")
position: str = Field(description="Job position") position: str = Field(description="Job position")
location: str = Field(description="Location")
about_me: str = Field(description="About me section") about_me: str = Field(description="About me section")
key_skills: list[str] = Field(description="List of key skills") key_skills: list[str] = Field(description="List of key skills")
experience_type: ExperienceType = Field(description="Experience type") experience_type: ExperienceType = Field(description="Experience type")
experience: list[ExperienceItem] = Field(description="Work experience list")
education: list[EducationItem] = Field(description="Education list")
projects: list[ProjectItem] = Field(description="Projects list")
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
"example": { "example": {
"position": "Python Developer", "position": "Python Developer",
"location": "Moscow",
"about_me": "Experienced Python developer with 5 years of experience", "about_me": "Experienced Python developer with 5 years of experience",
"key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"], "key_skills": ["Python", "FastAPI", "PostgreSQL", "Docker"],
"experience_type": "between3And6", "experience_type": "between3And6",
"experience": [
{
"place": "T-bank",
"description": "some description lorem ipsum",
"months_duration": 12,
}
],
"education": [
{
"place": "Central university",
"grade": "bachelor",
"specialization": "IT guy",
"description": "optional field, if user want add something",
}
],
"projects": [
{
"name": "Rekomenci fluon",
"description": "fucking shit",
}
],
} }
} }
} }
@@ -337,6 +566,7 @@ async def get_resume_history(
ResumeListItem( ResumeListItem(
id=r.id, id=r.id,
position=r.position, position=r.position,
location=r.location,
about_me=r.about_me, about_me=r.about_me,
key_skills=r.key_skills, key_skills=r.key_skills,
experience_type=r.experience_type, experience_type=r.experience_type,
@@ -363,12 +593,40 @@ async def patch_resume(
interactor: FromDishka[EditResumeInteractor], interactor: FromDishka[EditResumeInteractor],
) -> PatchResumeResponse: ) -> PatchResumeResponse:
try: try:
experience = (
[
EditExperienceInput(place=exp.place, description=exp.description, months_duration=exp.months_duration)
for exp in request.experience
]
if request.experience is not None
else None
)
education = (
[
EditEducationInput(
place=edu.place, grade=edu.grade, specialization=edu.specialization, description=edu.description
)
for edu in request.education
]
if request.education is not None
else None
)
projects = (
[EditProjectInput(name=proj.name, description=proj.description) for proj in request.projects]
if request.projects is not None
else None
)
interactor_response = await interactor.execute( interactor_response = await interactor.execute(
resume_id=resume_id, resume_id=resume_id,
position=request.position, position=request.position,
location=request.location,
about_me=request.about_me, about_me=request.about_me,
key_skills=request.key_skills, key_skills=request.key_skills,
experience_type=request.experience_type, experience_type=request.experience_type,
experience=experience,
education=education,
projects=projects,
) )
except ResumeDoesBelongUserError as error: except ResumeDoesBelongUserError as error:
raise HTTPException( raise HTTPException(
@@ -384,7 +642,32 @@ async def patch_resume(
return PatchResumeResponse( return PatchResumeResponse(
id=interactor_response.id, id=interactor_response.id,
position=interactor_response.position, position=interactor_response.position,
location=interactor_response.location,
about_me=interactor_response.about_me, about_me=interactor_response.about_me,
key_skills=interactor_response.key_skills, key_skills=interactor_response.key_skills,
experience_type=interactor_response.experience_type, experience_type=interactor_response.experience_type,
experience=[
ExperienceItem(
place=exp.place,
description=exp.description,
months_duration=exp.months_duration,
)
for exp in interactor_response.experience
],
education=[
EducationItem(
place=edu.place,
grade=edu.grade,
specialization=edu.specialization,
description=edu.description,
)
for edu in interactor_response.education
],
projects=[
ProjectItem(
name=proj.name,
description=proj.description,
)
for proj in interactor_response.projects
],
) )
+16
View File
@@ -33,6 +33,7 @@ async def test_success_add_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
assert is_success_response(response) assert is_success_response(response)
assert response.json() == IsDict( assert response.json() == IsDict(
@@ -54,6 +55,7 @@ async def test_unauthorized_add_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
assert is_unauthorized_response(response) assert is_unauthorized_response(response)
@@ -75,6 +77,7 @@ async def test_success_get_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
response = await test_api_gateway.get_resume( response = await test_api_gateway.get_resume(
@@ -84,9 +87,13 @@ async def test_success_get_resume(
assert is_success_response(response) assert is_success_response(response)
assert response.json() == IsPartialDict( assert response.json() == IsPartialDict(
position="Position", position="Position",
location="Moscow",
about_me="About me", about_me="About me",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
experience_type="noExperience", experience_type="noExperience",
experience=[],
education=[],
projects=[],
prediction=None, prediction=None,
) )
@@ -108,6 +115,7 @@ async def test_unauthorized_get_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
response = await test_api_gateway.get_resume( response = await test_api_gateway.get_resume(
@@ -152,6 +160,7 @@ async def test_success_edit_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
resume_id = response.json()["resume_id"] resume_id = response.json()["resume_id"]
response = await test_api_gateway.edit_resume( response = await test_api_gateway.edit_resume(
@@ -161,13 +170,18 @@ async def test_success_edit_resume(
experience_type="between1And3", experience_type="between1And3",
key_skills=["i love python"], key_skills=["i love python"],
position="Updated Position", position="Updated Position",
location="St. Petersburg",
) )
assert is_success_response(response) assert is_success_response(response)
assert response.json() == IsPartialDict( assert response.json() == IsPartialDict(
position="Updated Position", position="Updated Position",
location="St. Petersburg",
about_me="Updated about me", about_me="Updated about me",
key_skills=["i love python"], key_skills=["i love python"],
experience_type="between1And3", experience_type="between1And3",
experience=[],
education=[],
projects=[],
) )
@@ -188,6 +202,7 @@ async def test_unauthorized_edit_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
resume_id = response.json()["resume_id"] resume_id = response.json()["resume_id"]
response = await test_api_gateway.edit_resume( response = await test_api_gateway.edit_resume(
@@ -239,6 +254,7 @@ async def test_forbidden_edit_resume(
experience_type="noExperience", experience_type="noExperience",
key_skills=["i love lisp", "i love rust"], key_skills=["i love lisp", "i love rust"],
position="Position", position="Position",
location="Moscow",
) )
response_other_sign_up = await test_api_gateway.sign_up_email("f" + unique_email, DEFAULT_PASSWORD) response_other_sign_up = await test_api_gateway.sign_up_email("f" + unique_email, DEFAULT_PASSWORD)
+40 -16
View File
@@ -58,18 +58,30 @@ class TestApiGateway:
self, self,
access_token: str, access_token: str,
position: str, position: str,
location: str,
about_me: str, about_me: str,
key_skills: list[str], key_skills: list[str],
experience_type: str, experience_type: str,
experience: list[dict[str, Any]] | None = None,
education: list[dict[str, Any]] | None = None,
projects: list[dict[str, Any]] | None = None,
) -> Response: ) -> Response:
json_data: dict[str, Any] = {
"position": position,
"location": location,
"about_me": about_me,
"key_skills": key_skills,
"experience_type": experience_type,
}
if experience is not None:
json_data["experience"] = experience
if education is not None:
json_data["education"] = education
if projects is not None:
json_data["projects"] = projects
return await self._client.post( return await self._client.post(
"/resume", "/resume",
json={ json=json_data,
"position": position,
"about_me": about_me,
"key_skills": key_skills,
"experience_type": experience_type,
},
headers=make_auth_headers(access_token), headers=make_auth_headers(access_token),
) )
@@ -84,22 +96,34 @@ class TestApiGateway:
access_token: str, access_token: str,
resume_id: str, resume_id: str,
position: str | None = None, position: str | None = None,
location: str | None = None,
about_me: str | None = None, about_me: str | None = None,
key_skills: list[str] | None = None, key_skills: list[str] | None = None,
experience_type: str | None = None, experience_type: str | None = None,
experience: list[dict[str, Any]] | None = None,
education: list[dict[str, Any]] | None = None,
projects: list[dict[str, Any]] | None = None,
) -> Response: ) -> Response:
json_data: dict[str, Any] = {}
if position is not None:
json_data["position"] = position
if location is not None:
json_data["location"] = location
if about_me is not None:
json_data["about_me"] = about_me
if key_skills is not None:
json_data["key_skills"] = key_skills
if experience_type is not None:
json_data["experience_type"] = experience_type
if experience is not None:
json_data["experience"] = experience
if education is not None:
json_data["education"] = education
if projects is not None:
json_data["projects"] = projects
return await self._client.patch( return await self._client.patch(
f"/resume/{resume_id}", f"/resume/{resume_id}",
json={ json=json_data,
key: value
for key, value in {
"position": position,
"about_me": about_me,
"key_skills": key_skills,
"experience_type": experience_type,
}.items()
if value is not None
},
headers=make_auth_headers(access_token), headers=make_auth_headers(access_token),
) )