You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

222 lines
7.1 KiB
Python

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()