from datetime import date, datetime from typing import Optional from sqlalchemy import select from sqlalchemy.dialects.sqlite import insert as sqlite_insert from sqlalchemy.orm import Session from gimme_job.constants import CONSECUTIVE_FAILURES_THRESHOLD from gimme_job.models.db import ( JobPosting, NotificationLog, SiteConfig, SiteManifestVersion, SiteRun, Summary, ) from gimme_job.models.dto import JobPostingCandidate from gimme_job.models.runtime import SiteRunResult class JobPostingRepo: def upsert_candidates( self, session: Session, candidates: list[JobPostingCandidate], run_date: date, ) -> tuple[int, int]: """Upsert a list of candidates. Returns (total_processed, new_count).""" if not candidates: return 0, 0 now = datetime.utcnow() new_count = 0 for c in candidates: if not c.fingerprint: continue existing = session.execute( select(JobPosting).where(JobPosting.fingerprint == c.fingerprint) ).scalar_one_or_none() if existing: existing.last_seen_at = now existing.is_active = True existing.is_new = False else: posting = JobPosting( site_id=c.site_id, external_job_id=c.external_job_id, title=c.title, company=c.company, location=c.location, posted_text=c.posted_text, posted_at_normalized=c.posted_at_normalized, job_url=c.job_url, salary_text=c.salary_text, employment_type=c.employment_type, raw_text=c.raw_text, fingerprint=c.fingerprint, run_date=run_date, first_seen_at=now, last_seen_at=now, is_new=True, is_active=True, ) session.add(posting) new_count += 1 session.flush() return len(candidates), new_count def get_today_new(self, session: Session, run_date: date) -> list[JobPosting]: return list( session.execute( select(JobPosting) .where(JobPosting.run_date == run_date, JobPosting.is_new == True) .order_by(JobPosting.site_id, JobPosting.first_seen_at) ).scalars().all() ) def get_by_site(self, session: Session, site_id: str) -> list[JobPosting]: return list( session.execute( select(JobPosting) .where(JobPosting.site_id == site_id) .order_by(JobPosting.first_seen_at.desc()) ).scalars().all() ) class SiteRunRepo: def record_run(self, session: Session, result: SiteRunResult) -> SiteRun: run = SiteRun( site_id=result.site_id, started_at=result.started_at, ended_at=result.ended_at or datetime.utcnow(), status=result.status.value, items_found=result.items_found, new_items=result.new_items, error_summary=result.error_summary, failure_classification=( result.failure_classification.value if result.failure_classification else None ), trace_path=str(result.trace_path) if result.trace_path else None, screenshot_path=str(result.screenshot_path) if result.screenshot_path else None, dom_snapshot_path=( str(result.dom_snapshot_path) if result.dom_snapshot_path else None ), ) session.add(run) session.flush() return run def get_recent_runs( self, session: Session, site_id: str, limit: int = 10 ) -> list[SiteRun]: return list( session.execute( select(SiteRun) .where(SiteRun.site_id == site_id) .order_by(SiteRun.started_at.desc()) .limit(limit) ).scalars().all() ) def get_consecutive_failures(self, session: Session, site_id: str) -> int: config = session.get(SiteConfig, site_id) return config.consecutive_failures if config else 0 class SiteConfigRepo: def _get_or_create(self, session: Session, site_id: str) -> SiteConfig: config = session.get(SiteConfig, site_id) if config is None: config = SiteConfig(site_id=site_id) session.add(config) session.flush() return config def get_all_enabled(self, session: Session) -> list[SiteConfig]: return list( session.execute( select(SiteConfig).where(SiteConfig.enabled == True) ).scalars().all() ) def set_repair_needed(self, session: Session, site_id: str, value: bool) -> None: config = self._get_or_create(session, site_id) config.repair_needed = value session.flush() def increment_failure(self, session: Session, site_id: str) -> int: config = self._get_or_create(session, site_id) config.consecutive_failures += 1 config.last_failure_at = datetime.utcnow() if config.consecutive_failures >= CONSECUTIVE_FAILURES_THRESHOLD: config.repair_needed = True session.flush() return config.consecutive_failures def reset_failures(self, session: Session, site_id: str) -> None: config = self._get_or_create(session, site_id) config.consecutive_failures = 0 config.repair_needed = False config.last_success_at = datetime.utcnow() session.flush() def record_success(self, session: Session, site_id: str) -> None: config = self._get_or_create(session, site_id) config.consecutive_failures = 0 config.last_success_at = datetime.utcnow() session.flush() class NotificationLogRepo: def log_notification( self, session: Session, run_date: date, provider: str, status: str, error_message: Optional[str] = None, ) -> NotificationLog: log = NotificationLog( run_date=run_date, provider=provider, status=status, error_message=error_message, sent_at=datetime.utcnow(), ) session.add(log) session.flush() return log class SummaryRepo: def save_summary( self, session: Session, run_date: date, content: str, model_used: str, ) -> Summary: summary = Summary( run_date=run_date, content=content, model_used=model_used, created_at=datetime.utcnow(), ) session.add(summary) session.flush() return summary def get_latest(self, session: Session, run_date: date) -> Optional[Summary]: return session.execute( select(Summary) .where(Summary.run_date == run_date) .order_by(Summary.created_at.desc()) .limit(1) ).scalar_one_or_none()