You've already forked RekomenciBackend
chore
This commit is contained in:
@@ -16,7 +16,7 @@ build:
|
|||||||
[group("Docker")]
|
[group("Docker")]
|
||||||
[doc("Compose start")]
|
[doc("Compose start")]
|
||||||
up: build
|
up: build
|
||||||
docker compose --profile migrations --profile observability up -d --remove-orphans --quiet-pull --force-recreate --build
|
docker compose --profile migrations --profile observabilit --profile backend up -d --remove-orphans --quiet-pull --force-recreate --build
|
||||||
|
|
||||||
# =========
|
# =========
|
||||||
# > Tests
|
# > Tests
|
||||||
@@ -25,9 +25,9 @@ up: build
|
|||||||
[no-cd]
|
[no-cd]
|
||||||
[group("Tests")]
|
[group("Tests")]
|
||||||
[doc("Tests run")]
|
[doc("Tests run")]
|
||||||
tests: up
|
tests:
|
||||||
docker compose --profile migrations --profile tests up tests --remove-orphans --abort-on-container-exit
|
docker compose --profile migrations --profile tests up tests --remove-orphans --abort-on-container-exit
|
||||||
|
|
||||||
# =========
|
# =========
|
||||||
# > Lints
|
# > Lints
|
||||||
# =========
|
# =========
|
||||||
|
|||||||
+12
-12
@@ -18,10 +18,10 @@ services:
|
|||||||
restart: false
|
restart: false
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
required: true
|
required: true
|
||||||
redis:
|
# redis:
|
||||||
restart: false
|
# restart: false
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
required: true
|
# required: true
|
||||||
env_file:
|
env_file:
|
||||||
- path: ./infrastructure/configs/backend/.env.template
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
required: true
|
required: true
|
||||||
@@ -36,6 +36,8 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
profiles:
|
||||||
|
- backend
|
||||||
ports:
|
ports:
|
||||||
- name: web
|
- name: web
|
||||||
target: 8080
|
target: 8080
|
||||||
@@ -68,10 +70,6 @@ services:
|
|||||||
- template-project-tests:latest
|
- template-project-tests:latest
|
||||||
pull: true
|
pull: true
|
||||||
depends_on:
|
depends_on:
|
||||||
backend:
|
|
||||||
restart: false
|
|
||||||
condition: service_healthy
|
|
||||||
required: true
|
|
||||||
migrations:
|
migrations:
|
||||||
restart: false
|
restart: false
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
@@ -80,10 +78,10 @@ services:
|
|||||||
restart: false
|
restart: false
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
required: true
|
required: true
|
||||||
redis:
|
# redis:
|
||||||
restart: false
|
# restart: false
|
||||||
condition: service_healthy
|
# condition: service_healthy
|
||||||
required: true
|
# required: true
|
||||||
env_file:
|
env_file:
|
||||||
- path: ./infrastructure/configs/backend/.env.template
|
- path: ./infrastructure/configs/backend/.env.template
|
||||||
required: true
|
required: true
|
||||||
@@ -248,6 +246,8 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
profiles:
|
||||||
|
- redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
shm_size: 4mb
|
shm_size: 4mb
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -38,6 +38,30 @@ class DefaultResumeDataGateway(ResumeDataGateway):
|
|||||||
result = await self._session.execute(statement)
|
result = await self._session.execute(statement)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def list_latest_by_user_id(self, user_id: UserId, limit: int, offset: int) -> Sequence[Resume]:
|
||||||
|
statement = (
|
||||||
|
select(Resume)
|
||||||
|
.where(Resume.user_id == user_id)
|
||||||
|
.where(Resume.up_resume_id.is_(None))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
)
|
||||||
|
result = await self._session.execute(statement)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def get_history(self, resume_id: ResumeId) -> Sequence[Resume]:
|
||||||
|
history: list[Resume] = []
|
||||||
|
current_resume = await self.load(resume_id)
|
||||||
|
history.append(current_resume)
|
||||||
|
|
||||||
|
while current_resume.down_resume_id is not None:
|
||||||
|
current_resume = await self.load(current_resume.down_resume_id)
|
||||||
|
history.append(current_resume)
|
||||||
|
|
||||||
|
return history
|
||||||
|
|
||||||
|
|
||||||
class DefaultResumePredictionDataGateway(ResumePredictionDataGateway):
|
class DefaultResumePredictionDataGateway(ResumePredictionDataGateway):
|
||||||
def __init__(self, session: AsyncSession) -> None:
|
def __init__(self, session: AsyncSession) -> None:
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ class ResumeDataGateway(Protocol):
|
|||||||
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]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def list_latest_by_user_id(self, user_id: UserId, limit: int, offset: int) -> Sequence[Resume]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_history(self, resume_id: ResumeId) -> Sequence[Resume]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class ResumePredictionDataGateway(Protocol):
|
class ResumePredictionDataGateway(Protocol):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AddResumeInteractor:
|
|||||||
key_skills=key_skills,
|
key_skills=key_skills,
|
||||||
experience_type=experience_type,
|
experience_type=experience_type,
|
||||||
down_resume_id=None,
|
down_resume_id=None,
|
||||||
|
up_resume_id=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: тут надо сделать запуск фоновой таски для вычитывания подходящей вакансии
|
# TODO: тут надо сделать запуск фоновой таски для вычитывания подходящей вакансии
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from template_project.application.common.identity_provider import IdentityProvid
|
|||||||
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 ResumeDataGateway
|
||||||
from template_project.application.resume.entity import ResumeId
|
from template_project.application.resume.entity import Resume, ResumeId
|
||||||
from template_project.application.resume.errors import ResumeDoesBelongUserError
|
from template_project.application.resume.errors import ResumeDoesBelongUserError
|
||||||
|
|
||||||
|
|
||||||
@@ -32,25 +32,36 @@ class EditResumeInteractor:
|
|||||||
experience_type: ExperienceType | None,
|
experience_type: ExperienceType | None,
|
||||||
) -> _Response:
|
) -> _Response:
|
||||||
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)
|
old_resume = await self.resume_data_gateway.load(resume_id)
|
||||||
if resume.user_id != user.id:
|
if old_resume.user_id != user.id:
|
||||||
raise ResumeDoesBelongUserError
|
raise ResumeDoesBelongUserError
|
||||||
|
|
||||||
if position is not None:
|
new_position = position if position is not None else old_resume.position
|
||||||
resume.position = position
|
new_about_me = about_me if about_me is not None else old_resume.about_me
|
||||||
if about_me is not None:
|
new_key_skills = key_skills if key_skills is not None else old_resume.key_skills
|
||||||
resume.about_me = about_me
|
new_experience_type = experience_type if experience_type is not None else old_resume.experience_type
|
||||||
if key_skills is not None:
|
|
||||||
resume.key_skills = key_skills
|
new_resume = Resume.factory(
|
||||||
if experience_type is not None:
|
user_id=user.id,
|
||||||
resume.experience_type = experience_type
|
position=new_position,
|
||||||
|
about_me=new_about_me,
|
||||||
|
key_skills=new_key_skills,
|
||||||
|
experience_type=new_experience_type,
|
||||||
|
down_resume_id=old_resume.id,
|
||||||
|
up_resume_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.unit_of_work.add(new_resume)
|
||||||
|
|
||||||
|
if old_resume.up_resume_id is None:
|
||||||
|
old_resume.up_resume_id = new_resume.id
|
||||||
|
|
||||||
await self.unit_of_work.commit()
|
await self.unit_of_work.commit()
|
||||||
|
|
||||||
return _Response(
|
return _Response(
|
||||||
id=resume.id,
|
id=new_resume.id,
|
||||||
position=resume.position,
|
position=new_resume.position,
|
||||||
about_me=resume.about_me,
|
about_me=new_resume.about_me,
|
||||||
key_skills=resume.key_skills,
|
key_skills=new_resume.key_skills,
|
||||||
experience_type=resume.experience_type,
|
experience_type=new_resume.experience_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class GetResumeInteractor:
|
|||||||
|
|
||||||
@to_data_structure
|
@to_data_structure
|
||||||
class ResumeListItemResponse:
|
class ResumeListItemResponse:
|
||||||
|
id: ResumeId
|
||||||
position: str
|
position: str
|
||||||
about_me: str
|
about_me: str
|
||||||
key_skills: list[str]
|
key_skills: list[str]
|
||||||
@@ -79,10 +80,11 @@ class GetResumeListInteractor:
|
|||||||
async def execute(self, limit: int, offset: int) -> list[ResumeListItemResponse]:
|
async def execute(self, limit: int, offset: int) -> list[ResumeListItemResponse]:
|
||||||
user = await self.identity_provider.get_current_user()
|
user = await self.identity_provider.get_current_user()
|
||||||
|
|
||||||
resumes = await self.resume_data_gateway.list_by_user_id(user.id, limit=limit, offset=offset)
|
resumes = await self.resume_data_gateway.list_latest_by_user_id(user.id, limit=limit, offset=offset)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
ResumeListItemResponse(
|
ResumeListItemResponse(
|
||||||
|
id=r.id,
|
||||||
position=r.position,
|
position=r.position,
|
||||||
about_me=r.about_me,
|
about_me=r.about_me,
|
||||||
key_skills=r.key_skills,
|
key_skills=r.key_skills,
|
||||||
@@ -90,3 +92,29 @@ class GetResumeListInteractor:
|
|||||||
)
|
)
|
||||||
for r in resumes
|
for r in resumes
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@to_interactor
|
||||||
|
class GetResumeHistoryInteractor:
|
||||||
|
identity_provider: IdentityProvider
|
||||||
|
resume_data_gateway: ResumeDataGateway
|
||||||
|
|
||||||
|
async def execute(self, resume_id: ResumeId) -> list[ResumeListItemResponse]:
|
||||||
|
user = await self.identity_provider.get_current_user()
|
||||||
|
|
||||||
|
resume = await self.resume_data_gateway.load(resume_id)
|
||||||
|
if resume.user_id != user.id:
|
||||||
|
raise ResumeDoesBelongUserError
|
||||||
|
|
||||||
|
history = await self.resume_data_gateway.get_history(resume_id)
|
||||||
|
|
||||||
|
return [
|
||||||
|
ResumeListItemResponse(
|
||||||
|
id=r.id,
|
||||||
|
position=r.position,
|
||||||
|
about_me=r.about_me,
|
||||||
|
key_skills=r.key_skills,
|
||||||
|
experience_type=r.experience_type,
|
||||||
|
)
|
||||||
|
for r in history
|
||||||
|
]
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ from template_project.application.notification_device.interactors.register_devic
|
|||||||
from template_project.application.notification_device.interactors.send_notification import NotificationInteractor
|
from template_project.application.notification_device.interactors.send_notification import NotificationInteractor
|
||||||
from template_project.application.resume.interactors.add import AddResumeInteractor
|
from template_project.application.resume.interactors.add import AddResumeInteractor
|
||||||
from template_project.application.resume.interactors.edit import EditResumeInteractor
|
from template_project.application.resume.interactors.edit import EditResumeInteractor
|
||||||
from template_project.application.resume.interactors.get import GetResumeInteractor
|
from template_project.application.resume.interactors.get import (
|
||||||
|
GetResumeHistoryInteractor,
|
||||||
|
GetResumeInteractor,
|
||||||
|
GetResumeListInteractor,
|
||||||
|
)
|
||||||
from template_project.application.user.profile.interactors.get_profile import GetProfileInteractor
|
from template_project.application.user.profile.interactors.get_profile import GetProfileInteractor
|
||||||
from template_project.application.user.profile.interactors.patch_profile import PatchProfileInteractor
|
from template_project.application.user.profile.interactors.patch_profile import PatchProfileInteractor
|
||||||
|
|
||||||
@@ -24,6 +28,8 @@ class InteractorProvider(Provider):
|
|||||||
NotificationInteractor,
|
NotificationInteractor,
|
||||||
RegisterNotificationDeviceInteractor,
|
RegisterNotificationDeviceInteractor,
|
||||||
GetResumeInteractor,
|
GetResumeInteractor,
|
||||||
|
GetResumeListInteractor,
|
||||||
|
GetResumeHistoryInteractor,
|
||||||
AddResumeInteractor,
|
AddResumeInteractor,
|
||||||
EditResumeInteractor,
|
EditResumeInteractor,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ from template_project.web_api.ioc.data_gateway import DataGatewayProvider
|
|||||||
from template_project.web_api.ioc.factory import FactoryProvider
|
from template_project.web_api.ioc.factory import FactoryProvider
|
||||||
from template_project.web_api.ioc.idp import IdPProvider
|
from template_project.web_api.ioc.idp import IdPProvider
|
||||||
from template_project.web_api.ioc.interactor import InteractorProvider
|
from template_project.web_api.ioc.interactor import InteractorProvider
|
||||||
from template_project.web_api.ioc.notifications import NotificationServiceProvider
|
from template_project.web_api.ioc.notifications import (
|
||||||
|
NotificationServiceProvider,
|
||||||
|
)
|
||||||
from template_project.web_api.ioc.oauth import OAuthClientProvider
|
from template_project.web_api.ioc.oauth import OAuthClientProvider
|
||||||
from template_project.web_api.ioc.storage import StorageProvider
|
from template_project.web_api.ioc.storage import StorageProvider
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from template_project.application.resume.errors import ResumeDoesBelongUserError
|
|||||||
from template_project.application.resume.interactors.add import AddResumeInteractor
|
from template_project.application.resume.interactors.add import AddResumeInteractor
|
||||||
from template_project.application.resume.interactors.edit import EditResumeInteractor
|
from template_project.application.resume.interactors.edit import EditResumeInteractor
|
||||||
from template_project.application.resume.interactors.get import (
|
from template_project.application.resume.interactors.get import (
|
||||||
|
GetResumeHistoryInteractor,
|
||||||
GetResumeInteractor,
|
GetResumeInteractor,
|
||||||
GetResumeListInteractor,
|
GetResumeListInteractor,
|
||||||
)
|
)
|
||||||
@@ -188,7 +189,7 @@ async def create_resume(
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/resume/list",
|
"/resume/list",
|
||||||
summary="Get resume list",
|
summary="Get resume list",
|
||||||
description="Get paginated list of resumes",
|
description="Get paginated list of latest resumes (without up_resume_id)",
|
||||||
responses={
|
responses={
|
||||||
200: {"description": "Resume list retrieved successfully", "model": GetResumeListResponse},
|
200: {"description": "Resume list retrieved successfully", "model": GetResumeListResponse},
|
||||||
401: {"description": "Unauthorized - invalid or missing token"},
|
401: {"description": "Unauthorized - invalid or missing token"},
|
||||||
@@ -199,7 +200,19 @@ async def get_resume_list(
|
|||||||
offset: Annotated[int, Query(ge=0)],
|
offset: Annotated[int, Query(ge=0)],
|
||||||
interactor: FromDishka[GetResumeListInteractor],
|
interactor: FromDishka[GetResumeListInteractor],
|
||||||
) -> GetResumeListResponse:
|
) -> GetResumeListResponse:
|
||||||
raise NotImplementedError
|
interactor_response = await interactor.execute(limit=limit, offset=offset)
|
||||||
|
return GetResumeListResponse(
|
||||||
|
resumes=[
|
||||||
|
ResumeListItem(
|
||||||
|
id=r.id,
|
||||||
|
position=r.position,
|
||||||
|
about_me=r.about_me,
|
||||||
|
key_skills=r.key_skills,
|
||||||
|
experience_type=r.experience_type,
|
||||||
|
)
|
||||||
|
for r in interactor_response
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -294,20 +307,43 @@ class PatchResumeResponse(BaseModel):
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/resume/{resume_id}/history",
|
"/resume/{resume_id}/history",
|
||||||
summary="Get resume history",
|
summary="Get resume history",
|
||||||
description="Get paginated history of resume changes",
|
description="Get all versions of a resume by following down_resume_id chain",
|
||||||
responses={
|
responses={
|
||||||
200: {"description": "Resume history retrieved successfully", "model": GetResumeHistoryResponse},
|
200: {"description": "Resume history retrieved successfully", "model": GetResumeHistoryResponse},
|
||||||
401: {"description": "Unauthorized - invalid or missing token"},
|
401: {"description": "Unauthorized - invalid or missing token"},
|
||||||
404: {"description": "Resume not found"},
|
404: {"description": "Resume not found"},
|
||||||
|
403: {"description": "The resume does not belong to you"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def get_resume_history(
|
async def get_resume_history(
|
||||||
resume_id: str,
|
resume_id: ResumeId,
|
||||||
limit: Annotated[int, Query(ge=1, le=100, description="Number of history items to return", examples=[10])],
|
interactor: FromDishka[GetResumeHistoryInteractor],
|
||||||
offset: Annotated[int, Query(ge=0, description="Number of history items to skip", examples=[0])],
|
|
||||||
) -> GetResumeHistoryResponse:
|
) -> GetResumeHistoryResponse:
|
||||||
# TODO: Implement resume history retrieval
|
try:
|
||||||
raise NotImplementedError
|
interactor_response = await interactor.execute(resume_id)
|
||||||
|
except ResumeNotFoundError as error:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Resume not found",
|
||||||
|
) from error
|
||||||
|
except ResumeDoesBelongUserError as error:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="The resume does not belong to you",
|
||||||
|
) from error
|
||||||
|
|
||||||
|
return GetResumeHistoryResponse(
|
||||||
|
resumes=[
|
||||||
|
ResumeListItem(
|
||||||
|
id=r.id,
|
||||||
|
position=r.position,
|
||||||
|
about_me=r.about_me,
|
||||||
|
key_skills=r.key_skills,
|
||||||
|
experience_type=r.experience_type,
|
||||||
|
)
|
||||||
|
for r in interactor_response
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
|
|||||||
Reference in New Issue
Block a user