Skip to content

Commit

Permalink
Add create person from url endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
shri committed Aug 30, 2024
1 parent 853fe6c commit 1b7217c
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 8 deletions.
110 changes: 107 additions & 3 deletions src/app/domain/people/controllers/persons.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

from __future__ import annotations

import structlog
from datetime import date, datetime, timezone, timedelta
from typing import TYPE_CHECKING, Annotated

from advanced_alchemy.filters import SearchFilter, LimitOffset
from litestar import Controller, delete, get, patch, post
from litestar.di import Provide

from app.config import constants
from app.lib.schema import Location, WorkExperience, SocialActivity
from app.lib.pdl import get_person_details
from app.domain.accounts.guards import requires_active_user
from app.domain.companies.dependencies import provide_companies_service
from app.domain.companies.schemas import CompanyCreate
from app.domain.companies.services import CompanyService
from app.domain.people import urls
from app.domain.people.dependencies import provide_persons_service
from app.domain.people.schemas import Person, PersonCreate, PersonUpdate
from app.domain.people.schemas import Person, PersonCreate, PersonCreateFromURL, PersonUpdate
from app.domain.people.services import PersonService

if TYPE_CHECKING:
Expand All @@ -22,12 +30,17 @@

from app.lib.dependencies import FilterTypes

logger = structlog.get_logger()


class PersonController(Controller):
"""Person operations."""

tags = ["Persons"]
dependencies = {"persons_service": Provide(provide_persons_service)}
dependencies = {
"companies_service": Provide(provide_companies_service),
"persons_service": Provide(provide_persons_service),
}
guards = [requires_active_user]
signature_namespace = {
"PersonService": PersonService,
Expand Down Expand Up @@ -60,12 +73,103 @@ async def create_person(
self,
persons_service: PersonService,
data: PersonCreate,
) -> PersonCreate:
) -> Person:
"""Create a new person."""
obj = data.to_dict()
db_obj = await persons_service.create(obj)
return persons_service.to_schema(schema_type=Person, data=db_obj)

@post(
operation_id="CreatePersonFromURL",
name="persons:create-from-url",
summary="Create a new person from URL.",
path=urls.PERSON_CREATE_FROM_URL,
)
async def create_person_from_url(
self,
companies_service: CompanyService,
persons_service: PersonService,
data: PersonCreateFromURL,
) -> Person:
"""Create a new person from URL."""
# Check if person already exists in the database
filters = [
SearchFilter(field_name="linkedin_profile_url", value=data.url.rstrip("/"), ignore_case=True),
LimitOffset(limit=1, offset=0),
]
results, count = await persons_service.list_and_count(*filters)

now = datetime.now(timezone.utc)
four_weeks_ago = now - timedelta(weeks=4)

if count > 0 and results[0].updated_at > four_weeks_ago:
await logger.ainfo("Person already exists and is up-to-date", person=results[0])
return persons_service.to_schema(schema_type=Person, data=results[0])

# Extract person from data provider
person_details = await get_person_details(data.url)

linkedin_profile_url = None
twitter_profile_url = None
github_profile_url = None
birth_date = None

if person_details.get("linkedin_url"):
linkedin_profile_url = "https://" + person_details.get("linkedin_url").rstrip("/")
if person_details.get("twitter_url"):
twitter_profile_url = "https://" + person_details.get("twitter_url").rstrip("/")
if person_details.get("github_url"):
github_profile_url = "https://" + person_details.get("github_url").rstrip("/")
if person_details.get("birth_date"):
birth_date = datetime.strptime(person_details.get("birth_date"), "%Y-%m-%d").date()

# Add or update company
company = CompanyCreate(
name=person_details.get("job_company_name"),
url=person_details.get("job_company_website"),
linkedin_profile_url=person_details.get("job_company_linkedin_url"),
)
company_db_obj = await companies_service.create(company.to_dict())

# Add person
# TODO: Move this code into a provider specific code
obj = PersonCreate(
first_name=person_details.get("first_name"),
last_name=person_details.get("last_name"),
full_name=person_details.get("full_name"),
title=person_details.get("job_title"),
occupation=person_details.get("job_title_role"),
industry=person_details.get("industry"),
linkedin_profile_url=linkedin_profile_url,
twitter_profile_url=twitter_profile_url,
github_profile_url=github_profile_url,
location=Location(
country=person_details.get("location_country"),
region=person_details.get("location_region"),
city=person_details.get("location_locality"),
),
personal_emails=person_details.get("personal_emails", []),
work_email=person_details.get("work_email"),
personal_numbers=person_details.get("personal_numbers", []),
birth_date=birth_date,
work_experiences=[
WorkExperience(
starts_at=datetime.strptime(work_ex.get("start_date"), "%Y-%m").date(),
title=work_ex.get("title", {}).get("name"),
company_name=work_ex.get("company", {}).get("name"),
company_url=work_ex.get("company", {}).get("website"),
company_linkedin_profile_url=work_ex.get("linkedin_url"),
ends_at=datetime.strptime(work_ex.get("end_date"), "%Y-%m").date()
if work_ex.get("end_date")
else None,
)
for work_ex in person_details.get("experience", [])
],
company_id=company_db_obj.id,
)
db_obj = await persons_service.upsert(obj.to_dict(), item_id=results[0].id if count > 0 else None)
return persons_service.to_schema(schema_type=Person, data=db_obj)

@get(
operation_id="GetPerson",
name="persons:get",
Expand Down
12 changes: 11 additions & 1 deletion src/app/domain/people/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Person(CamelizedBaseStruct):
last_name: str | None = None
full_name: str | None = None
headline: str | None = None
title: str | None = None
summary: str | None = None
occupation: str | None = None
industry: str | None = None
Expand All @@ -46,6 +47,7 @@ class PersonCreate(CamelizedBaseStruct):
last_name: str | None = None
full_name: str | None = None
headline: str | None = None
title: str | None = None
summary: str | None = None
occupation: str | None = None
industry: str | None = None
Expand All @@ -56,13 +58,20 @@ class PersonCreate(CamelizedBaseStruct):
github_profile_url: str | None = None
location: Location | None = None
personal_emails: list[str] | None = None
work_emails: list[str] | None = None
work_email: str | None = None
personal_numbers: list[str] | None = None
birth_date: date | None = None
gender: str | None = None
languages: list[str] | None = None
work_experiences: list[WorkExperience] | None = None
social_activities: list[SocialActivity] | None = None
company_id: str | None = None


class PersonCreateFromURL(CamelizedBaseStruct):
"""A person create from URL schema."""

url: str


class PersonUpdate(CamelizedBaseStruct, omit_defaults=True):
Expand All @@ -73,6 +82,7 @@ class PersonUpdate(CamelizedBaseStruct, omit_defaults=True):
last_name: str | None | msgspec.UnsetType = msgspec.UNSET
full_name: str | None | msgspec.UnsetType = msgspec.UNSET
headline: str | None | msgspec.UnsetType = msgspec.UNSET
title: str | None | msgspec.UnsetType = msgspec.UNSET
summary: str | None | msgspec.UnsetType = msgspec.UNSET
occupation: str | None | msgspec.UnsetType = msgspec.UNSET
industry: str | None | msgspec.UnsetType = msgspec.UNSET
Expand Down
10 changes: 6 additions & 4 deletions src/app/domain/people/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
from msgspec import Struct
from sqlalchemy.orm import InstrumentedAttribute

__all__ = (
"PersonService",
)
__all__ = ("PersonService",)


class PersonService(SQLAlchemyAsyncRepositoryService[Person]):
Expand All @@ -41,8 +39,12 @@ async def to_model(self, data: Person | dict[str, Any] | Struct, operation: str
data.slug = await self.repository.get_available_slug(data.name) # type: ignore[union-attr]
if (is_msgspec_model(data) or is_pydantic_model(data)) and operation == "update" and data.slug is None: # type: ignore[union-attr]
data.slug = await self.repository.get_available_slug(data.name) # type: ignore[union-attr]
if (is_msgspec_model(data) or is_pydantic_model(data)) and operation == "upsert" and data.slug is None: # type: ignore[union-attr]
data.slug = await self.repository.get_available_slug(data.name) # type: ignore[union-attr]
if is_dict(data) and "slug" not in data and operation == "create":
data["slug"] = await self.repository.get_available_slug(data["full_name"])
if is_dict(data) and "slug" not in data and "name" in data and operation == "update":
if is_dict(data) and "slug" not in data and "full_name" in data and operation == "update":
data["slug"] = await self.repository.get_available_slug(data["full_name"])
if is_dict(data) and "slug" not in data and "full_name" in data and operation == "upsert":
data["slug"] = await self.repository.get_available_slug(data["full_name"])
return await super().to_model(data, operation)
1 change: 1 addition & 0 deletions src/app/domain/people/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
PERSON_DETAIL = "/api/persons/{company_id:uuid}"
PERSON_UPDATE = "/api/persons/{company_id:uuid}"
PERSON_CREATE = "/api/persons"
PERSON_CREATE_FROM_URL = "/api/persons/from-url"
PERSON_INDEX = "/api/persons/{company_id:uuid}"

0 comments on commit 1b7217c

Please sign in to comment.