You've already forked RekomenciBackend
add edit resume
This commit is contained in:
@@ -184,3 +184,27 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/template_project"]
|
packages = ["src/template_project"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
skip_empty = true
|
||||||
|
exclude_also = [
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"self.logger",
|
||||||
|
"def __repr__",
|
||||||
|
"lambda: None",
|
||||||
|
"from .*",
|
||||||
|
"import .*",
|
||||||
|
'@(abc\.)?abstractmethod',
|
||||||
|
"raise NotImplementedError",
|
||||||
|
'raise AssertionError',
|
||||||
|
'logger\..*',
|
||||||
|
"pass",
|
||||||
|
'\.\.\.',
|
||||||
|
]
|
||||||
|
omit = [
|
||||||
|
'*/__about__.py',
|
||||||
|
'*/__main__.py',
|
||||||
|
'*/__init__.py',
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from template_project.application.common.data_structure import to_data_structure
|
||||||
|
from template_project.application.common.enums import ExperienceType
|
||||||
|
from template_project.application.common.identity_provider import IdentityProvider
|
||||||
|
from template_project.application.common.interactor import to_interactor
|
||||||
|
from template_project.application.common.unit_of_work import UnitOfWork
|
||||||
|
from template_project.application.resume.data_gateway import ResumeDataGateway
|
||||||
|
from template_project.application.resume.entity import ResumeId
|
||||||
|
from template_project.application.resume.errors import ResumeDoesBelongUserError
|
||||||
|
|
||||||
|
|
||||||
|
@to_data_structure
|
||||||
|
class _Response:
|
||||||
|
id: ResumeId
|
||||||
|
position: str
|
||||||
|
about_me: str
|
||||||
|
key_skills: list[str]
|
||||||
|
experience_type: ExperienceType
|
||||||
|
|
||||||
|
|
||||||
|
@to_interactor
|
||||||
|
class EditResumeInteractor:
|
||||||
|
unit_of_work: UnitOfWork
|
||||||
|
identity_provider: IdentityProvider
|
||||||
|
resume_data_gateway: ResumeDataGateway
|
||||||
|
|
||||||
|
async def execute(
|
||||||
|
self,
|
||||||
|
resume_id: ResumeId,
|
||||||
|
position: str | None,
|
||||||
|
about_me: str | None,
|
||||||
|
key_skills: list[str] | None,
|
||||||
|
experience_type: ExperienceType | None,
|
||||||
|
) -> _Response:
|
||||||
|
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
|
||||||
|
|
||||||
|
if position is not None:
|
||||||
|
resume.position = position
|
||||||
|
if about_me is not None:
|
||||||
|
resume.about_me = about_me
|
||||||
|
if key_skills is not None:
|
||||||
|
resume.key_skills = key_skills
|
||||||
|
if experience_type is not None:
|
||||||
|
resume.experience_type = experience_type
|
||||||
|
|
||||||
|
await self.unit_of_work.commit()
|
||||||
|
|
||||||
|
return _Response(
|
||||||
|
id=resume.id,
|
||||||
|
position=resume.position,
|
||||||
|
about_me=resume.about_me,
|
||||||
|
key_skills=resume.key_skills,
|
||||||
|
experience_type=resume.experience_type,
|
||||||
|
)
|
||||||
@@ -18,6 +18,7 @@ class ResumePredictionResponse:
|
|||||||
|
|
||||||
@to_data_structure
|
@to_data_structure
|
||||||
class _Response:
|
class _Response:
|
||||||
|
id: ResumeId
|
||||||
position: str
|
position: str
|
||||||
about_me: str
|
about_me: str
|
||||||
key_skills: list[str]
|
key_skills: list[str]
|
||||||
@@ -53,6 +54,7 @@ class GetResumeInteractor:
|
|||||||
prediction = None
|
prediction = None
|
||||||
|
|
||||||
return _Response(
|
return _Response(
|
||||||
|
id=resume.id,
|
||||||
position=resume.position,
|
position=resume.position,
|
||||||
about_me=resume.about_me,
|
about_me=resume.about_me,
|
||||||
key_skills=resume.key_skills,
|
key_skills=resume.key_skills,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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.get import GetResumeInteractor
|
from template_project.application.resume.interactors.get import GetResumeInteractor
|
||||||
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,4 +25,5 @@ class InteractorProvider(Provider):
|
|||||||
RegisterNotificationDeviceInteractor,
|
RegisterNotificationDeviceInteractor,
|
||||||
GetResumeInteractor,
|
GetResumeInteractor,
|
||||||
AddResumeInteractor,
|
AddResumeInteractor,
|
||||||
|
EditResumeInteractor,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from template_project.application.common.enums import 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 AddResumeInteractor
|
||||||
|
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 GetResumeInteractor
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
@@ -66,6 +67,7 @@ class SalaryPrediction(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ResumeResponse(BaseModel):
|
class ResumeResponse(BaseModel):
|
||||||
|
id: ResumeId = Field(description="Resume ID")
|
||||||
position: str = Field(description="Job position")
|
position: str = Field(description="Job position")
|
||||||
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")
|
||||||
@@ -90,6 +92,7 @@ class ResumeResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ResumeListItem(BaseModel):
|
class ResumeListItem(BaseModel):
|
||||||
|
id: ResumeId = Field(description="Resume ID")
|
||||||
position: str = Field(description="Job position")
|
position: str = Field(description="Job position")
|
||||||
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")
|
||||||
@@ -208,6 +211,7 @@ async def get_resume(
|
|||||||
) from error
|
) from error
|
||||||
|
|
||||||
return ResumeResponse(
|
return ResumeResponse(
|
||||||
|
id=interactor_response.id,
|
||||||
position=interactor_response.position,
|
position=interactor_response.position,
|
||||||
about_me=interactor_response.about_me,
|
about_me=interactor_response.about_me,
|
||||||
key_skills=interactor_response.key_skills,
|
key_skills=interactor_response.key_skills,
|
||||||
@@ -266,6 +270,7 @@ class PatchResumeRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PatchResumeResponse(BaseModel):
|
class PatchResumeResponse(BaseModel):
|
||||||
|
id: ResumeId = Field(description="Resume ID")
|
||||||
position: str = Field(description="Job position")
|
position: str = Field(description="Job position")
|
||||||
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")
|
||||||
@@ -310,11 +315,37 @@ async def get_resume_history(
|
|||||||
200: {"description": "Resume updated successfully", "model": PatchResumeResponse},
|
200: {"description": "Resume updated successfully", "model": PatchResumeResponse},
|
||||||
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: {"descriptipn": "The resume does not belong to you"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def patch_resume(
|
async def patch_resume(
|
||||||
resume_id: str,
|
resume_id: ResumeId,
|
||||||
request: PatchResumeRequest,
|
request: PatchResumeRequest,
|
||||||
|
interactor: FromDishka[EditResumeInteractor],
|
||||||
) -> PatchResumeResponse:
|
) -> PatchResumeResponse:
|
||||||
# TODO: Implement resume update
|
try:
|
||||||
raise NotImplementedError
|
interactor_response = await interactor.execute(
|
||||||
|
resume_id=resume_id,
|
||||||
|
position=request.position,
|
||||||
|
about_me=request.about_me,
|
||||||
|
key_skills=request.key_skills,
|
||||||
|
experience_type=request.experience_type,
|
||||||
|
)
|
||||||
|
except ResumeDoesBelongUserError as error:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.FORBIDDEN,
|
||||||
|
detail="The resume does not belong to you",
|
||||||
|
) from error
|
||||||
|
except ResumeNotFoundError as error:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.NOT_FOUND,
|
||||||
|
detail="Resume not found",
|
||||||
|
) from error
|
||||||
|
|
||||||
|
return PatchResumeResponse(
|
||||||
|
id=interactor_response.id,
|
||||||
|
position=interactor_response.position,
|
||||||
|
about_me=interactor_response.about_me,
|
||||||
|
key_skills=interactor_response.key_skills,
|
||||||
|
experience_type=interactor_response.experience_type,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections.abc import AsyncIterable, AsyncIterator
|
from collections.abc import AsyncIterable, AsyncIterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from dishka import AsyncContainer
|
from dishka import AsyncContainer
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from template_project.web_api.configuration import load_configuration
|
from template_project.web_api.configuration import load_configuration
|
||||||
from template_project.web_api.entry_point import make_server
|
from template_project.web_api.entry_point import make_server
|
||||||
@@ -14,20 +16,23 @@ from tests.web_api.ioc import make_ioc
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def dishka_container(backend: Any) -> AsyncIterable[AsyncContainer]:
|
async def dishka_container(backend: FastAPI) -> AsyncIterable[AsyncContainer]:
|
||||||
path = Path(os.environ["CONFIGURATION_PATH"])
|
path = Path(os.environ["CONFIGURATION_PATH"])
|
||||||
configuration = load_configuration(path)
|
configuration = load_configuration(path)
|
||||||
ioc = make_ioc(configuration)
|
ioc = make_ioc(configuration, backend)
|
||||||
yield ioc
|
yield ioc
|
||||||
await ioc.close()
|
await ioc.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def backend() -> AsyncIterator[None]:
|
async def backend() -> AsyncIterator[FastAPI]:
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
|
|
||||||
configuration = load_configuration(Path(os.environ["CONFIGURATION_PATH"]))
|
configuration = load_configuration(Path(os.environ["CONFIGURATION_PATH"]))
|
||||||
server = make_server(configuration)
|
server = make_server(configuration)
|
||||||
asyncio.create_task(server.serve()) # type: ignore[unused-awaitable]
|
asyncio.create_task(server.serve()) # type: ignore[unused-awaitable]
|
||||||
yield
|
yield cast(FastAPI, server.config.app)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from dirty_equals import IsDict, IsUUID
|
from dirty_equals import IsDict, IsPartialDict, IsUUID
|
||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
from uuid_utils.compat import uuid7
|
from uuid_utils.compat import uuid7
|
||||||
|
|
||||||
from tests.web_api.helpers import is_not_found_response, is_success_response, is_unauthorized_response
|
from tests.web_api.helpers import (
|
||||||
|
is_forbidden_response,
|
||||||
|
is_not_found_response,
|
||||||
|
is_success_response,
|
||||||
|
is_unauthorized_response,
|
||||||
|
)
|
||||||
from tests.web_api.ioc import DatabaseClearer, inject
|
from tests.web_api.ioc import DatabaseClearer, inject
|
||||||
from tests.web_api.test_api_gateway import TestApiGateway
|
from tests.web_api.test_api_gateway import TestApiGateway
|
||||||
|
|
||||||
@@ -129,3 +134,123 @@ async def test_not_found_get_resume(
|
|||||||
resume_id=str(uuid7()),
|
resume_id=str(uuid7()),
|
||||||
)
|
)
|
||||||
assert is_not_found_response(response)
|
assert is_not_found_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_success_edit_resume(
|
||||||
|
unique_email: str,
|
||||||
|
test_api_gateway: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
response_sign_up = await test_api_gateway.sign_up_email(unique_email, DEFAULT_PASSWORD)
|
||||||
|
access_token = response_sign_up.json()["access_token"]
|
||||||
|
|
||||||
|
response = await test_api_gateway.create_resume(
|
||||||
|
access_token=access_token,
|
||||||
|
about_me="About me",
|
||||||
|
experience_type="noExperience",
|
||||||
|
key_skills=["i love lisp", "i love rust"],
|
||||||
|
position="Position",
|
||||||
|
)
|
||||||
|
resume_id = response.json()["resume_id"]
|
||||||
|
response = await test_api_gateway.edit_resume(
|
||||||
|
access_token=access_token,
|
||||||
|
resume_id=resume_id,
|
||||||
|
about_me="Updated about me",
|
||||||
|
experience_type="between1And3",
|
||||||
|
key_skills=["i love python"],
|
||||||
|
position="Updated Position",
|
||||||
|
)
|
||||||
|
assert is_success_response(response)
|
||||||
|
assert response.json() == IsPartialDict(
|
||||||
|
position="Updated Position",
|
||||||
|
about_me="Updated about me",
|
||||||
|
key_skills=["i love python"],
|
||||||
|
experience_type="between1And3",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_unauthorized_edit_resume(
|
||||||
|
unique_email: str,
|
||||||
|
test_api_gateway: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
response_sign_up = await test_api_gateway.sign_up_email(unique_email, DEFAULT_PASSWORD)
|
||||||
|
access_token = response_sign_up.json()["access_token"]
|
||||||
|
|
||||||
|
response = await test_api_gateway.create_resume(
|
||||||
|
access_token=access_token,
|
||||||
|
about_me="About me",
|
||||||
|
experience_type="noExperience",
|
||||||
|
key_skills=["i love lisp", "i love rust"],
|
||||||
|
position="Position",
|
||||||
|
)
|
||||||
|
resume_id = response.json()["resume_id"]
|
||||||
|
response = await test_api_gateway.edit_resume(
|
||||||
|
access_token="...",
|
||||||
|
resume_id=resume_id,
|
||||||
|
about_me="Updated about me",
|
||||||
|
experience_type="between1And3",
|
||||||
|
key_skills=["i love python"],
|
||||||
|
position="Updated Position",
|
||||||
|
)
|
||||||
|
assert is_unauthorized_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_not_found_edit_resume(
|
||||||
|
unique_email: str,
|
||||||
|
test_api_gateway: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
response_sign_up = await test_api_gateway.sign_up_email(unique_email, DEFAULT_PASSWORD)
|
||||||
|
access_token = response_sign_up.json()["access_token"]
|
||||||
|
|
||||||
|
response = await test_api_gateway.edit_resume(
|
||||||
|
access_token=access_token,
|
||||||
|
resume_id=str(uuid7()),
|
||||||
|
about_me="Updated about me",
|
||||||
|
experience_type="between1And3",
|
||||||
|
key_skills=["i love python"],
|
||||||
|
position="Updated Position",
|
||||||
|
)
|
||||||
|
assert is_not_found_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
@inject
|
||||||
|
async def test_forbidden_edit_resume(
|
||||||
|
unique_email: str,
|
||||||
|
test_api_gateway: FromDishka[TestApiGateway],
|
||||||
|
database_clearer: FromDishka[DatabaseClearer],
|
||||||
|
) -> None:
|
||||||
|
await database_clearer.clear()
|
||||||
|
|
||||||
|
response_sign_up = await test_api_gateway.sign_up_email(unique_email, DEFAULT_PASSWORD)
|
||||||
|
|
||||||
|
response = await test_api_gateway.create_resume(
|
||||||
|
access_token=response_sign_up.json()["access_token"],
|
||||||
|
about_me="About me",
|
||||||
|
experience_type="noExperience",
|
||||||
|
key_skills=["i love lisp", "i love rust"],
|
||||||
|
position="Position",
|
||||||
|
)
|
||||||
|
|
||||||
|
response_other_sign_up = await test_api_gateway.sign_up_email("f" + unique_email, DEFAULT_PASSWORD)
|
||||||
|
|
||||||
|
response = await test_api_gateway.edit_resume(
|
||||||
|
access_token=response_other_sign_up.json()["access_token"],
|
||||||
|
resume_id=response.json()["resume_id"],
|
||||||
|
about_me="Updated about me",
|
||||||
|
experience_type="between1And3",
|
||||||
|
key_skills=["i love python"],
|
||||||
|
position="Updated Position",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert is_forbidden_response(response)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import AsyncIterable, Callable
|
||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
@@ -13,7 +13,8 @@ from dishka import (
|
|||||||
)
|
)
|
||||||
from dishka.integrations.base import wrap_injection
|
from dishka.integrations.base import wrap_injection
|
||||||
from dishka.integrations.fastapi import FastapiProvider
|
from dishka.integrations.fastapi import FastapiProvider
|
||||||
from httpx import AsyncClient
|
from fastapi import FastAPI
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
@@ -71,11 +72,13 @@ class TestProvider(Provider):
|
|||||||
database_clearer = provide(DatabaseClearer)
|
database_clearer = provide(DatabaseClearer)
|
||||||
|
|
||||||
@provide
|
@provide
|
||||||
def http_client(self) -> AsyncClient:
|
async def http_client(self, app: FastAPI) -> AsyncIterable[AsyncClient]:
|
||||||
return AsyncClient(base_url="http://backend:8080")
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
def make_ioc(configuration: Configuration) -> AsyncContainer:
|
def make_ioc(configuration: Configuration, app: FastAPI) -> AsyncContainer:
|
||||||
return make_async_container(
|
return make_async_container(
|
||||||
IdPProvider(),
|
IdPProvider(),
|
||||||
FactoryProvider(),
|
FactoryProvider(),
|
||||||
@@ -97,6 +100,7 @@ def make_ioc(configuration: Configuration) -> AsyncContainer:
|
|||||||
FirebaseConfiguration: configuration.firebase,
|
FirebaseConfiguration: configuration.firebase,
|
||||||
Configuration: configuration,
|
Configuration: configuration,
|
||||||
S3Config: configuration.s3,
|
S3Config: configuration.s3,
|
||||||
|
FastAPI: app,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,27 @@ class TestApiGateway:
|
|||||||
f"/resume/{resume_id}",
|
f"/resume/{resume_id}",
|
||||||
headers=make_auth_headers(access_token),
|
headers=make_auth_headers(access_token),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def edit_resume(
|
||||||
|
self,
|
||||||
|
access_token: str,
|
||||||
|
resume_id: str,
|
||||||
|
position: str | None = None,
|
||||||
|
about_me: str | None = None,
|
||||||
|
key_skills: list[str] | None = None,
|
||||||
|
experience_type: str | None = None,
|
||||||
|
) -> Response:
|
||||||
|
return await self._client.patch(
|
||||||
|
f"/resume/{resume_id}",
|
||||||
|
json={
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user