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