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,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
MetaData,
|
||||
Numeric,
|
||||
String,
|
||||
@@ -118,6 +119,12 @@ resume_prediction_table: Final = Table(
|
||||
Column("to_salary", Numeric, 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)
|
||||
|
||||
@@ -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.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, resume, storage
|
||||
from template_project.web_api.routes import auth, healthcheck, key_skills, notification, profile, resume, storage
|
||||
|
||||
LOG_CONFIG: Final = {
|
||||
"version": 1,
|
||||
@@ -82,6 +82,7 @@ def make_asgi_application(
|
||||
app.include_router(auth.router)
|
||||
app.include_router(healthcheck.router)
|
||||
app.include_router(profile.router)
|
||||
app.include_router(key_skills.router)
|
||||
app.include_router(notification.router)
|
||||
app.include_router(storage.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.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.profile import DefaultProfileDataGateway
|
||||
from template_project.adapters.data_gateways.resume import DefaultResumeDataGateway, DefaultResumePredictionDataGateway
|
||||
@@ -14,6 +15,7 @@ class DataGatewayProvider(Provider):
|
||||
|
||||
unit_of_work = provide(WithParents[DefaultUnitOfWork])
|
||||
data_gateways = provide_all(
|
||||
KeySkillsDataGateway,
|
||||
WithParents[DefaultUserDataGateway],
|
||||
WithParents[DefaultAccessTokenDataGateway],
|
||||
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),
|
||||
)
|
||||
|
||||
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