You've already forked RekomenciBackend
add skills endpoints
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import insert, select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from template_project.adapters.data_gateways.tables import key_skills_table
|
||||||
|
|
||||||
|
|
||||||
|
class KeySkillsDataGateway:
|
||||||
|
def __init__(self, session: AsyncSession) -> None:
|
||||||
|
self._session = session
|
||||||
|
|
||||||
|
async def query(self, query: str) -> Sequence[str]:
|
||||||
|
statement = (
|
||||||
|
select(key_skills_table.c.name)
|
||||||
|
.where(key_skills_table.c.name.ilike(f"{query}%"))
|
||||||
|
.order_by(key_skills_table.c.name)
|
||||||
|
.limit(30)
|
||||||
|
)
|
||||||
|
result = await self._session.execute(statement)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
async def add_skills(self, name: list[str]) -> None:
|
||||||
|
insert_statement = insert(key_skills_table).values(
|
||||||
|
[{"name": _} for _ in name]
|
||||||
|
)
|
||||||
|
await self._session.execute(insert_statement)
|
||||||
@@ -8,6 +8,7 @@ from sqlalchemy import (
|
|||||||
DateTime,
|
DateTime,
|
||||||
Enum,
|
Enum,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
MetaData,
|
MetaData,
|
||||||
Numeric,
|
Numeric,
|
||||||
String,
|
String,
|
||||||
@@ -118,6 +119,12 @@ resume_prediction_table: Final = Table(
|
|||||||
Column("to_salary", Numeric, nullable=False),
|
Column("to_salary", Numeric, nullable=False),
|
||||||
Column("recommended_skills", ARRAY(String, as_tuple=True), nullable=False),
|
Column("recommended_skills", ARRAY(String, as_tuple=True), nullable=False),
|
||||||
)
|
)
|
||||||
|
key_skills_table: Final = Table(
|
||||||
|
"key_skills",
|
||||||
|
meta_data,
|
||||||
|
Column("id", Integer, autoincrement=True, primary_key=True),
|
||||||
|
Column("name", String, nullable=False, unique=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
mapper_registry.map_imperatively(User, user_table)
|
mapper_registry.map_imperatively(User, user_table)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""add key skills
|
||||||
|
|
||||||
|
Revision ID: 2ebcb2592cab
|
||||||
|
Revises: 5b7a1ca1f06b
|
||||||
|
Create Date: 2025-11-22 03:59:55.147083
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2ebcb2592cab'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '5b7a1ca1f06b'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('key_skills',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('key_skills')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -21,7 +21,7 @@ from prometheus_fastapi_instrumentator import Instrumentator
|
|||||||
from template_project.application.auth_identity.errors import AuthError
|
from template_project.application.auth_identity.errors import AuthError
|
||||||
from template_project.web_api.configuration import Configuration, load_configuration
|
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.ioc.make import make_ioc
|
||||||
from template_project.web_api.routes import auth, healthcheck, notification, profile, resume, storage
|
from template_project.web_api.routes import auth, healthcheck, key_skills, notification, profile, resume, storage
|
||||||
|
|
||||||
LOG_CONFIG: Final = {
|
LOG_CONFIG: Final = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@@ -82,6 +82,7 @@ def make_asgi_application(
|
|||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
app.include_router(healthcheck.router)
|
app.include_router(healthcheck.router)
|
||||||
app.include_router(profile.router)
|
app.include_router(profile.router)
|
||||||
|
app.include_router(key_skills.router)
|
||||||
app.include_router(notification.router)
|
app.include_router(notification.router)
|
||||||
app.include_router(storage.router)
|
app.include_router(storage.router)
|
||||||
app.include_router(resume.router)
|
app.include_router(resume.router)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from dishka import BaseScope, Provider, Scope, WithParents, provide, provide_all
|
|||||||
|
|
||||||
from template_project.adapters.data_gateways.access_token import DefaultAccessTokenDataGateway
|
from template_project.adapters.data_gateways.access_token import DefaultAccessTokenDataGateway
|
||||||
from template_project.adapters.data_gateways.auth_identity import DefaultAuthIdentityDataGateway
|
from template_project.adapters.data_gateways.auth_identity import DefaultAuthIdentityDataGateway
|
||||||
|
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, DefaultResumePredictionDataGateway
|
||||||
@@ -14,6 +15,7 @@ class DataGatewayProvider(Provider):
|
|||||||
|
|
||||||
unit_of_work = provide(WithParents[DefaultUnitOfWork])
|
unit_of_work = provide(WithParents[DefaultUnitOfWork])
|
||||||
data_gateways = provide_all(
|
data_gateways = provide_all(
|
||||||
|
KeySkillsDataGateway,
|
||||||
WithParents[DefaultUserDataGateway],
|
WithParents[DefaultUserDataGateway],
|
||||||
WithParents[DefaultAccessTokenDataGateway],
|
WithParents[DefaultAccessTokenDataGateway],
|
||||||
WithParents[DefaultAuthIdentityDataGateway],
|
WithParents[DefaultAuthIdentityDataGateway],
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from dishka import FromDishka
|
||||||
|
from dishka.integrations.fastapi import DishkaRoute
|
||||||
|
from fastapi import APIRouter, Query
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from template_project.adapters.data_gateways.key_skills import KeySkillsDataGateway
|
||||||
|
from template_project.application.common.unit_of_work import UnitOfWork
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
route_class=DishkaRoute,
|
||||||
|
tags=["Key Skills"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KeySkill(BaseModel):
|
||||||
|
name: str = Field(description="The name of the key skill")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/key_skills")
|
||||||
|
async def query_key_skills(
|
||||||
|
query: Annotated[str, Query()],
|
||||||
|
data_gateway: FromDishka[KeySkillsDataGateway],
|
||||||
|
) -> Sequence[KeySkill]:
|
||||||
|
skills = await data_gateway.query(query)
|
||||||
|
return [KeySkill(name=skill) for skill in skills]
|
||||||
|
|
||||||
|
|
||||||
|
class AddKeySkillsRequest(BaseModel):
|
||||||
|
key_skills: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/key_skills")
|
||||||
|
async def add_key_skill(
|
||||||
|
request: AddKeySkillsRequest,
|
||||||
|
unit_of_work: FromDishka[UnitOfWork],
|
||||||
|
data_gateway: FromDishka[KeySkillsDataGateway],
|
||||||
|
) -> None:
|
||||||
|
await data_gateway.add_skills(request.key_skills)
|
||||||
|
await unit_of_work.commit()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from dishka import FromDishka
|
||||||
|
|
||||||
|
from tests.web_api.helpers import is_success_response
|
||||||
|
from tests.web_api.ioc import DatabaseClearer, inject
|
||||||
|
from tests.web_api.test_api_gateway import TestApiGateway
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_add_key_skill(
|
||||||
|
client: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
response = await client.add_key_skill(key_skills=["Python", "Django", "REST APIs"])
|
||||||
|
assert is_success_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_search_key_skills(
|
||||||
|
client: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
await client.add_key_skill(key_skills=["Python", "Django", "Python3.12", "REST APIs"])
|
||||||
|
response = await client.search_key_skills("p")
|
||||||
|
assert is_success_response(response)
|
||||||
|
assert {name["name"] for name in response.json()} == {"Python", "Python3.12"}
|
||||||
@@ -102,3 +102,18 @@ class TestApiGateway:
|
|||||||
},
|
},
|
||||||
headers=make_auth_headers(access_token),
|
headers=make_auth_headers(access_token),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def add_key_skill(
|
||||||
|
self,
|
||||||
|
key_skills: list[str],
|
||||||
|
) -> Response:
|
||||||
|
return await self._client.post(
|
||||||
|
"/key_skills",
|
||||||
|
json={"key_skills": key_skills},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def search_key_skills(self, query: str) -> Response:
|
||||||
|
return await self._client.get(
|
||||||
|
"/key_skills",
|
||||||
|
params={"query": query},
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user