Skip to content

Commit

Permalink
Add people domain, person model and APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
shri committed Jul 31, 2024
1 parent de98971 commit 5e5ddfc
Show file tree
Hide file tree
Showing 14 changed files with 515 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# type: ignore
"""Add people model
Revision ID: 698ddcfa9900
Revises: a05c476c0ae9
Create Date: 2024-07-31 09:48:08.467309+00:00
"""
from __future__ import annotations

import warnings
from typing import TYPE_CHECKING

import sqlalchemy as sa
from alembic import op
from advanced_alchemy.types import EncryptedString, EncryptedText, GUID, ORA_JSONB, DateTimeUTC
from sqlalchemy import Text # noqa: F401
from sqlalchemy.dialects import postgresql
from app.db.models.custom_types import LocationType, WorkExperienceType, SocialActivityType
if TYPE_CHECKING:
from collections.abc import Sequence

__all__ = ["downgrade", "upgrade", "schema_upgrades", "schema_downgrades", "data_upgrades", "data_downgrades"]

sa.GUID = GUID
sa.DateTimeUTC = DateTimeUTC
sa.ORA_JSONB = ORA_JSONB
sa.EncryptedString = EncryptedString
sa.EncryptedText = EncryptedText

# revision identifiers, used by Alembic.
revision = '698ddcfa9900'
down_revision = 'a05c476c0ae9'
branch_labels = None
depends_on = None


def upgrade() -> None:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
with op.get_context().autocommit_block():
schema_upgrades()
data_upgrades()

def downgrade() -> None:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
with op.get_context().autocommit_block():
data_downgrades()
schema_downgrades()

def schema_upgrades() -> None:
"""schema upgrade migrations go here."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('person',
sa.Column('id', sa.GUID(length=16), nullable=False),
sa.Column('first_name', sa.String(), nullable=True),
sa.Column('last_name', sa.String(), nullable=True),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('headline', sa.String(length=500), nullable=True),
sa.Column('summary', sa.String(length=2000), nullable=True),
sa.Column('occupation', sa.String(), nullable=True),
sa.Column('industry', sa.String(), nullable=True),
sa.Column('profile_pic_url', sa.String(length=2083), nullable=True),
sa.Column('url', sa.String(length=2083), nullable=True),
sa.Column('linkedin_profile_url', sa.String(length=2083), nullable=True),
sa.Column('twitter_profile_url', sa.String(length=2083), nullable=True),
sa.Column('github_profile_url', sa.String(length=2083), nullable=True),
sa.Column('location', LocationType(), nullable=True),
sa.Column('personal_emails', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('work_emails', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('personal_numbers', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('birth_date', sa.Date(), nullable=True),
sa.Column('gender', sa.String(), nullable=True),
sa.Column('languages', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('work_experiences', WorkExperienceType(), nullable=True),
sa.Column('social_activities', SocialActivityType(), nullable=True),
sa.Column('slug', sa.String(length=100), nullable=False),
sa.Column('sa_orm_sentinel', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTimeUTC(timezone=True), nullable=False),
sa.Column('updated_at', sa.DateTimeUTC(timezone=True), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_person')),
sa.UniqueConstraint('slug', name='uq_person_slug')
)
with op.batch_alter_table('person', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_person_industry'), ['industry'], unique=False)
batch_op.create_index('ix_person_slug_unique', ['slug'], unique=True)

# ### end Alembic commands ###

def schema_downgrades() -> None:
"""schema downgrade migrations go here."""
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('person', schema=None) as batch_op:
batch_op.drop_index('ix_person_slug_unique')
batch_op.drop_index(batch_op.f('ix_person_industry'))

op.drop_table('person')
# ### end Alembic commands ###

def data_upgrades() -> None:
"""Add any optional data upgrade migrations here!"""

def data_downgrades() -> None:
"""Add any optional data downgrade migrations here!"""
4 changes: 3 additions & 1 deletion src/app/db/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .tenant import Tenant
from .company import Company
from .job_post import JobPost
from .person import Person

__all__ = (
"User",
Expand All @@ -25,5 +26,6 @@
"TeamRoles",
"Tenant",
"Company",
"JobPost"
"JobPost",
"Person"
)
17 changes: 16 additions & 1 deletion src/app/db/models/custom_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.types import TypeDecorator, TEXT
from sqlalchemy.dialects.postgresql import JSONB

from app.lib.schema import Location, Funding
from app.lib.schema import Location, Funding, WorkExperience, SocialActivity


class JSONBType(TypeDecorator):
Expand Down Expand Up @@ -41,3 +41,18 @@ def process_result_value(self, value, dialect):
if value and isinstance(value, dict):
return Funding.from_dict(value)
return None


class WorkExperienceType(JSONBType):
def process_result_value(self, value, dialect):
"""Convert JSON format to Python object when reading from the database."""
if value and isinstance(value, dict):
return WorkExperience.from_dict(value)
return None

class SocialActivityType(JSONBType):
def process_result_value(self, value, dialect):
"""Convert JSON format to Python object when reading from the database."""
if value and isinstance(value, dict):
return SocialActivity.from_dict(value)
return None
43 changes: 43 additions & 0 deletions src/app/db/models/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from typing import TYPE_CHECKING
from datetime import date

from advanced_alchemy.base import SlugKey, UUIDAuditBase
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.lib.schema import Location, WorkExperience, SocialActivity
from .custom_types import LocationType, WorkExperienceType, SocialActivityType


class Person(UUIDAuditBase, SlugKey):
"""A person."""

__tablename__ = "person"
__pii_columns__ = {"first_name", "last_name", "full_name", "linkedin_url", "profile_pic_url", "personal_emails", "work_emails", "personal_numbers", "social_activities"}
first_name: Mapped[str] = mapped_column(nullable=True, default=None)
last_name: Mapped[str] = mapped_column(nullable=True, default=None)
full_name: Mapped[str] = mapped_column(nullable=True, default=None)
headline: Mapped[str | None] = mapped_column(String(length=500), nullable=True, default=None)
summary: Mapped[str | None] = mapped_column(String(length=2000), nullable=True, default=None)
occupation: Mapped[str] = mapped_column(nullable=True, default=None)
industry: Mapped[str | None] = mapped_column(nullable=True, default=None, index=True)
profile_pic_url: Mapped[str | None] = mapped_column(String(length=2083), nullable=True, default=None)
url: Mapped[str | None] = mapped_column(String(length=2083), nullable=True, default=None)
linkedin_profile_url: Mapped[str | None] = mapped_column(String(length=2083), nullable=True, default=None)
twitter_profile_url: Mapped[str | None] = mapped_column(String(length=2083), nullable=True, default=None)
github_profile_url: Mapped[str | None] = mapped_column(String(length=2083), nullable=True, default=None)
location: Mapped[Location | None] = mapped_column(LocationType, nullable=True, default=None)
personal_emails: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True, default=None)
work_emails: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True, default=None)
personal_numbers: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True, default=None)
birth_date: Mapped[date | None] = mapped_column(nullable=True, default=None)
gender: Mapped[str | None] = mapped_column(nullable=True, default=None)
languages: Mapped[list[str] | None] = mapped_column(JSONB, nullable=True, default=None)
work_experiences: Mapped[list[WorkExperience] | None] = mapped_column(WorkExperienceType, nullable=True, default=None)
social_activities: Mapped[list[SocialActivity] | None] = mapped_column(SocialActivityType, nullable=True, default=None)
# -----------
# ORM Relationships
# ------------
4 changes: 4 additions & 0 deletions src/app/domain/people/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""People Application Module."""
from . import controllers, dependencies, schemas, services

__all__ = ["controllers", "services", "schemas", "dependencies"]
3 changes: 3 additions & 0 deletions src/app/domain/people/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .persons import PersonController

__all__ = ["PersonController"]
129 changes: 129 additions & 0 deletions src/app/domain/people/controllers/persons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Person Controllers."""

from __future__ import annotations

from typing import TYPE_CHECKING, Annotated

from litestar import Controller, delete, get, patch, post
from litestar.di import Provide

from app.config import constants
from app.domain.accounts.guards import requires_active_user
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.services import PersonService

if TYPE_CHECKING:
from uuid import UUID

from advanced_alchemy.service.pagination import OffsetPagination
from litestar.params import Dependency, Parameter

from app.lib.dependencies import FilterTypes


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

tags = ["Persons"]
dependencies = {"persons_service": Provide(provide_persons_service)}
guards = [requires_active_user]
signature_namespace = {
"PersonService": PersonService,
}
dto = None
return_dto = None

@get(
operation_id="ListPersons",
name="persons:list",
summary="List Persons",
path=urls.PERSON_LIST,
)
async def list_persons(
self,
persons_service: PersonService,
filters: Annotated[list[FilterTypes], Dependency(skip_validation=True)],
) -> OffsetPagination[Person]:
"""List persons that your account can access.."""
results, total = await persons_service.list_and_count(*filters)
return persons_service.to_schema(data=results, total=total, schema_type=Person, filters=filters)

@post(
operation_id="CreatePerson",
name="persons:create",
summary="Create a new person.",
path=urls.PERSON_CREATE,
)
async def create_person(
self,
persons_service: PersonService,
data: PersonCreate,
) -> PersonCreate:
"""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)

@get(
operation_id="GetPerson",
name="persons:get",
summary="Retrieve the details of a person.",
path=urls.PERSON_DETAIL,
)
async def get_person(
self,
persons_service: PersonService,
person_id: Annotated[
UUID,
Parameter(
title="Person ID",
description="The person to retrieve.",
),
],
) -> Person:
"""Get details about a comapny."""
db_obj = await persons_service.get(person_id)
return persons_service.to_schema(schema_type=Person, data=db_obj)

@patch(
operation_id="UpdatePerson",
name="persons:update",
path=urls.PERSON_UPDATE,
)
async def update_person(
self,
data: PersonUpdate,
persons_service: PersonService,
person_id: Annotated[
UUID,
Parameter(
title="Person ID",
description="The person to update.",
),
],
) -> Person:
"""Update a person."""
db_obj = await persons_service.update(
item_id=person_id,
data=data.to_dict(),
)
return persons_service.to_schema(schema_type=Person, data=db_obj)

@delete(
operation_id="DeletePerson",
name="persons:delete",
summary="Remove Person",
path=urls.PERSON_DELETE,
)
async def delete_person(
self,
persons_service: PersonService,
person_id: Annotated[
UUID,
Parameter(title="Person ID", description="The person to delete."),
],
) -> None:
"""Delete a person."""
_ = await persons_service.delete(person_id)
27 changes: 27 additions & 0 deletions src/app/domain/people/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""People Controllers."""

from __future__ import annotations

from typing import TYPE_CHECKING

from sqlalchemy.orm import joinedload, noload, selectinload

from app.db.models import Person
from app.domain.people.services import PersonService

__all__ = ("provide_persons_service", )


if TYPE_CHECKING:
from collections.abc import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession


async def provide_persons_service(db_session: AsyncSession) -> AsyncGenerator[PersonService, None]:
"""Construct repository and service objects for the request."""
async with PersonService.new(
session=db_session,
load=[],
) as service:
yield service
23 changes: 23 additions & 0 deletions src/app/domain/people/repositories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any
from uuid import UUID # noqa: TCH003

from advanced_alchemy.repository import SQLAlchemyAsyncRepository, SQLAlchemyAsyncSlugRepository
from sqlalchemy import ColumnElement, select
from sqlalchemy.orm import joinedload, selectinload

from app.db.models import Person

if TYPE_CHECKING:
from advanced_alchemy.filters import FilterTypes

__all__ = (
"PersonRepository",
)


class PersonRepository(SQLAlchemyAsyncSlugRepository[Person]):
"""Person Repository."""

model_type = Person
Loading

0 comments on commit 5e5ddfc

Please sign in to comment.