- OpportunityState model + repo: persist applied / dismissed / restored status and the generated resume-builder doc ids (never re-pay to re-tailor). annotate_and_filter drops dismissed + annotates applied/docs on read; handlers for dismiss/restore/mark_applied/ save_apply_docs. - Unique decks per run (spend only for NEW jobs, no cache, no re-fetch+dedup band-aid): per-(user,query) search cursor → LinkedIn page++ and Naukri incremental+stateKey (async run path in apify_client, since the actor's crawl exceeds the run-sync window); seen-net excludes already-shown ids (covers Foundit, which can't paginate). - normalize: every board offsite-first (apply_url prefers the employer/ATS redirect over the board listing) + offsite_apply flag; real company logos (_logo_url across Naukri logoPath / LinkedIn company.logo / etc.).
50 lines
2.7 KiB
Python
50 lines
2.7 KiB
Python
"""ORM models. One table for now: the per-user cached feed (the last completed search)."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import JSON, Boolean, DateTime, Integer, String, func
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class UserFeed(Base):
|
|
"""The user's last completed search — replayed on load so matches survive navigation.
|
|
Upserted on each `search_complete`; read by `get_feed`. One row per user (PK = user_id)."""
|
|
__tablename__ = "user_feed"
|
|
|
|
user_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
prefs: Mapped[dict] = mapped_column(JSON, default=dict) # the ScoutPrefs that produced it
|
|
opportunities: Mapped[list] = mapped_column(JSON, default=list) # the curated deck (with match blocks)
|
|
sources: Mapped[dict] = mapped_column(JSON, default=dict) # per-board counts
|
|
engine: Mapped[str] = mapped_column(String, default="") # "opus" | "fallback:…"
|
|
scanned: Mapped[int] = mapped_column(Integer, default=0)
|
|
# Search cursor — advances per re-run of the SAME query so each run fetches NEW jobs (LinkedIn
|
|
# page++, Naukri incremental stateKey). Resets when the query signature changes.
|
|
query_sig: Mapped[str] = mapped_column(String, default="")
|
|
cursor_page: Mapped[int] = mapped_column(Integer, default=1)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class OpportunityState(Base):
|
|
"""Per-(user, opportunity) state that SURVIVES deck refreshes. Persists three things:
|
|
`applied`/`dismissed` status, and the resume-builder document ids already generated for the
|
|
role (so we never re-tailor / re-generate a paid document twice). Keyed by (user, opportunity)."""
|
|
__tablename__ = "opportunity_state"
|
|
|
|
user_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
opportunity_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
status: Mapped[str | None] = mapped_column(String, nullable=True) # 'applied' | 'dismissed' | None
|
|
tailored_resume_id: Mapped[str | None] = mapped_column(String, nullable=True) # resume-builder resume id
|
|
tailored_version_id: Mapped[str | None] = mapped_column(String, nullable=True) # the tailored version
|
|
cover_letter_id: Mapped[str | None] = mapped_column(String, nullable=True) # the generated cover letter
|
|
seen: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # shown in a deck → exclude from new runs
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|