added Contact api and data model

This commit is contained in:
2026-01-11 18:37:40 -05:00
parent 8bbcd37933
commit 8afa4fce6c
8 changed files with 838 additions and 70 deletions

View File

@@ -3,6 +3,20 @@
from __future__ import annotations
from python.orm.base import RichieBase, TableBase
from python.orm.temp import Temp
from python.orm.contact import (
Contact,
ContactNeed,
ContactRelationship,
Need,
RelationshipType,
)
__all__ = ["RichieBase", "TableBase", "Temp"]
__all__ = [
"Contact",
"ContactNeed",
"ContactRelationship",
"Need",
"RelationshipType",
"RichieBase",
"TableBase",
]

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import datetime
from os import getenv
from typing import cast
from sqlalchemy import DateTime, MetaData, create_engine, func
from sqlalchemy.engine import URL, Engine
@@ -34,11 +35,11 @@ class TableBase(AbstractConcreteBase, RichieBase):
__abstract__ = True
id: Mapped[int] = mapped_column(primary_key=True)
created_at: Mapped[datetime] = mapped_column(
created: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
)
updated_at: Mapped[datetime] = mapped_column(
updated: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
@@ -63,7 +64,7 @@ def get_connection_info() -> tuple[str, str, str, str, str | None]:
f"password{'***' if password else None}\n"
)
raise ValueError(error)
return database, host, port, username, password
return cast("tuple[str, str, str, str, str | None]", (database, host, port, username, password))
def get_postgres_engine(*, pool_pre_ping: bool = True) -> Engine:

168
python/orm/contact.py Normal file
View File

@@ -0,0 +1,168 @@
"""Contact database models."""
from __future__ import annotations
from enum import Enum
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from python.orm.base import RichieBase, TableBase
class RelationshipType(str, Enum):
"""Relationship types with default closeness weights.
Default weight is an integer 1-10 where 10 = closest relationship.
Users can override this per-relationship in the UI.
"""
SPOUSE = "spouse"
PARTNER = "partner"
PARENT = "parent"
CHILD = "child"
SIBLING = "sibling"
BEST_FRIEND = "best_friend"
GRANDPARENT = "grandparent"
GRANDCHILD = "grandchild"
AUNT_UNCLE = "aunt_uncle"
NIECE_NEPHEW = "niece_nephew"
COUSIN = "cousin"
IN_LAW = "in_law"
CLOSE_FRIEND = "close_friend"
FRIEND = "friend"
MENTOR = "mentor"
MENTEE = "mentee"
BUSINESS_PARTNER = "business_partner"
COLLEAGUE = "colleague"
MANAGER = "manager"
DIRECT_REPORT = "direct_report"
CLIENT = "client"
ACQUAINTANCE = "acquaintance"
NEIGHBOR = "neighbor"
EX = "ex"
OTHER = "other"
@property
def default_weight(self) -> int:
"""Return the default closeness weight (1-10) for this relationship type."""
weights = {
RelationshipType.SPOUSE: 10,
RelationshipType.PARTNER: 10,
RelationshipType.PARENT: 9,
RelationshipType.CHILD: 9,
RelationshipType.SIBLING: 9,
RelationshipType.BEST_FRIEND: 8,
RelationshipType.GRANDPARENT: 7,
RelationshipType.GRANDCHILD: 7,
RelationshipType.AUNT_UNCLE: 7,
RelationshipType.NIECE_NEPHEW: 7,
RelationshipType.COUSIN: 7,
RelationshipType.IN_LAW: 7,
RelationshipType.CLOSE_FRIEND: 6,
RelationshipType.FRIEND: 6,
RelationshipType.MENTOR: 5,
RelationshipType.MENTEE: 5,
RelationshipType.BUSINESS_PARTNER: 5,
RelationshipType.COLLEAGUE: 4,
RelationshipType.MANAGER: 4,
RelationshipType.DIRECT_REPORT: 4,
RelationshipType.CLIENT: 4,
RelationshipType.ACQUAINTANCE: 3,
RelationshipType.NEIGHBOR: 3,
RelationshipType.EX: 2,
RelationshipType.OTHER: 2,
}
return weights.get(self, 5)
@property
def display_name(self) -> str:
"""Return a human-readable display name."""
return self.value.replace("_", " ").title()
class ContactNeed(RichieBase):
"""Association table: Contact <-> Need."""
__tablename__ = "contact_need"
contact_id: Mapped[int] = mapped_column(
ForeignKey("main.contact.id", ondelete="CASCADE"),
primary_key=True,
)
need_id: Mapped[int] = mapped_column(
ForeignKey("main.need.id", ondelete="CASCADE"),
primary_key=True,
)
class ContactRelationship(RichieBase):
"""Association table: Contact <-> Contact with relationship type and weight."""
__tablename__ = "contact_relationship"
contact_id: Mapped[int] = mapped_column(
ForeignKey("main.contact.id", ondelete="CASCADE"),
primary_key=True,
)
related_contact_id: Mapped[int] = mapped_column(
ForeignKey("main.contact.id", ondelete="CASCADE"),
primary_key=True,
)
relationship_type: Mapped[str] = mapped_column(String(100))
closeness_weight: Mapped[int] = mapped_column(default=5)
class Contact(TableBase):
"""Contact model."""
__tablename__ = "contact"
name: Mapped[str]
age: Mapped[int | None]
bio: Mapped[str | None]
current_job: Mapped[str | None]
gender: Mapped[str | None]
goals: Mapped[str | None]
legal_name: Mapped[str | None]
profile_pic: Mapped[str | None]
safe_conversation_starters: Mapped[str | None]
self_sufficiency_score: Mapped[int | None]
social_structure_style: Mapped[str | None]
ssn: Mapped[str | None]
suffix: Mapped[str | None]
timezone: Mapped[str | None]
topics_to_avoid: Mapped[str | None]
needs: Mapped[list[Need]] = relationship(
"Need",
secondary=ContactNeed.__table__,
back_populates="contacts",
)
related_to: Mapped[list[ContactRelationship]] = relationship(
"ContactRelationship",
foreign_keys=[ContactRelationship.contact_id],
cascade="all, delete-orphan",
)
related_from: Mapped[list[ContactRelationship]] = relationship(
"ContactRelationship",
foreign_keys=[ContactRelationship.related_contact_id],
cascade="all, delete-orphan",
)
class Need(TableBase):
"""Need/accommodation model (e.g., light sensitive, ADHD)."""
__tablename__ = "need"
name: Mapped[str]
description: Mapped[str | None]
contacts: Mapped[list[Contact]] = relationship(
"Contact",
secondary=ContactNeed.__table__,
back_populates="needs",
)

View File

@@ -1,16 +0,0 @@
"""Temporary ORM model."""
from __future__ import annotations
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from python.orm.base import TableBase
class Temp(TableBase):
"""Temporary table for initial testing."""
__tablename__ = "temp"
name: Mapped[str] = mapped_column(String(255), nullable=False)