Files
srbhr--resume-matcher/apps/backend/app/models.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

138 lines
5.8 KiB
Python

"""SQLAlchemy ORM models for Resume Matcher.
A single declarative ``Base`` backs all tables (doc tables migrated from
TinyDB plus the new ``applications`` and ``api_keys`` tables). The facade in
``app/database.py`` converts ORM rows to plain dicts so the rest of the app
never sees ORM objects — preserving the TinyDB-era contracts.
"""
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import JSON, Boolean, Index, Integer, String, Text, UniqueConstraint, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def _utcnow_iso() -> str:
"""Return the current UTC time as an ISO-8601 string.
Timestamps are stored as strings (not native datetimes) to preserve the
TinyDB-era behavior: code compares them lexically and returns them to
clients verbatim.
"""
return datetime.now(timezone.utc).isoformat()
class Base(DeclarativeBase):
"""Declarative base shared by every table."""
class Resume(Base):
"""A resume document (master or tailored)."""
__tablename__ = "resumes"
resume_id: Mapped[str] = mapped_column(String, primary_key=True)
content: Mapped[str] = mapped_column(Text)
content_type: Mapped[str] = mapped_column(String, default="md")
filename: Mapped[str | None] = mapped_column(String, nullable=True)
is_master: Mapped[bool] = mapped_column(Boolean, default=False)
parent_id: Mapped[str | None] = mapped_column(String, nullable=True)
processed_data: Mapped[dict | None] = mapped_column(JSON, nullable=True)
processing_status: Mapped[str] = mapped_column(String, default="pending")
cover_letter: Mapped[str | None] = mapped_column(Text, nullable=True)
outreach_message: Mapped[str | None] = mapped_column(Text, nullable=True)
interview_prep: Mapped[str | None] = mapped_column(Text, nullable=True)
title: Mapped[str | None] = mapped_column(String, nullable=True)
# original_markdown has *absence* semantics in the TinyDB era: the key was
# omitted entirely when None. The facade reproduces that by only emitting
# the key when this column is non-null.
original_markdown: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
updated_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
__table_args__ = (
# At most one master resume. Partial unique index enforces the invariant
# at the storage layer; ``_master_resume_lock`` remains the primary
# (race-free) mechanism in the facade.
Index(
"ux_resumes_single_master",
"is_master",
unique=True,
sqlite_where=text("is_master = 1"),
),
)
class Job(Base):
"""A job description.
Only the stable columns are first-class; everything the pipeline attaches
dynamically (``job_keywords``, ``job_keywords_hash``, ``preview_hash``,
``preview_hashes``, ``preview_prompt_id``, ``company``, ``role``) lives in
``metadata_json``. The facade flattens that map to top-level keys on read
and merges non-core keys into it on update, reproducing TinyDB semantics.
"""
__tablename__ = "jobs"
job_id: Mapped[str] = mapped_column(String, primary_key=True)
content: Mapped[str] = mapped_column(Text)
resume_id: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
class Improvement(Base):
"""A tailoring result linking an original resume, a tailored resume, and a job."""
__tablename__ = "improvements"
request_id: Mapped[str] = mapped_column(String, primary_key=True)
original_resume_id: Mapped[str] = mapped_column(String)
tailored_resume_id: Mapped[str] = mapped_column(String, index=True)
job_id: Mapped[str] = mapped_column(String)
improvements: Mapped[list] = mapped_column(JSON, default=list)
created_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
class Application(Base):
"""A Kanban application-tracker card."""
__tablename__ = "applications"
__table_args__ = (
# Concurrency-safe dedupe: a card is unique per (job, applied resume).
# The app-level select-then-insert relies on this to collapse races.
UniqueConstraint("job_id", "resume_id", name="uq_application_job_resume"),
)
application_id: Mapped[str] = mapped_column(String, primary_key=True)
job_id: Mapped[str] = mapped_column(String, index=True)
# The applied/tailored resume shown in the modal and opened by "Edit".
resume_id: Mapped[str] = mapped_column(String, index=True)
# Optional base resume the tailored one descends from (powers "stack" grouping).
master_resume_id: Mapped[str | None] = mapped_column(String, nullable=True)
status: Mapped[str] = mapped_column(String, default="applied", index=True)
company: Mapped[str | None] = mapped_column(String, nullable=True)
role: Mapped[str | None] = mapped_column(String, nullable=True)
applied_at: Mapped[str | None] = mapped_column(String, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
position: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
updated_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)
class ApiKey(Base):
"""An encrypted LLM provider API key.
``provider`` is the *key-store* provider name (e.g. ``google`` for the
``gemini`` LLM provider, via ``_PROVIDER_KEY_MAP``). Only ciphertext is
stored; plaintext exists in memory only at call time.
"""
__tablename__ = "api_keys"
provider: Mapped[str] = mapped_column(String, primary_key=True)
ciphertext: Mapped[str] = mapped_column(Text)
updated_at: Mapped[str] = mapped_column(String, default=_utcnow_iso)