jobhunt-platform/apps/api/app/db/repo_app.py
hermes 3035e4eac9 W2: api v1 features (CV import, AF fetch, batch scoring, today/nudges, interview prep, SMTP+clipboard transports, scheduler, 90 tests)
Implements docs/api-contract-v2.md. Migration 002 adds follow-up fields.
EchoTransport replaced by SMTP->Clipboard selection behind unchanged approval gate.
Scheduler (APScheduler) daily 07:00 fetch+score, env-gated, default off.
Test image installs workspace packages; build context moved to repo root.
Recovered and committed by integration lead after W2 worker hit iteration limit.
2026-07-30 18:29:17 +00:00

485 lines
No EOL
14 KiB
Python

"""Repository functions for job_posting, application, artifact, approval, outbox, task_run."""
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from typing import Any
from app.db import execute, fetch_all, fetch_one
# --- Job Posting ---
def create_job_posting(
source: str,
url: str,
company: str,
title: str,
location: str | None = None,
description: str = "",
external_id: str | None = None,
raw: dict[str, Any] | None = None,
) -> dict[str, Any]:
row = execute(
"""
INSERT INTO job_posting (source, external_id, url, company, title, location, description, raw)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (source, url) DO UPDATE SET
company = EXCLUDED.company,
title = EXCLUDED.title,
fetched_at = now()
RETURNING *
""",
(
source,
external_id,
url,
company,
title,
location,
description,
json.dumps(raw or {}),
),
)
if row is None:
raise RuntimeError("insert job_posting failed")
return _normalize_posting(row)
def get_job_posting(posting_id: str) -> dict[str, Any] | None:
row = fetch_one("SELECT * FROM job_posting WHERE id = %s", (posting_id,))
if row is None:
return None
return _normalize_posting(row)
def list_postings() -> list[dict[str, Any]]:
rows = fetch_all("SELECT * FROM job_posting ORDER BY fetched_at DESC")
return [_normalize_posting(r) for r in rows]
def _normalize_posting(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"source": row["source"],
"external_id": row.get("external_id"),
"url": row["url"],
"company": row["company"],
"title": row["title"],
"location": row.get("location"),
"description": row.get("description", ""),
"fetched_at": row["fetched_at"].isoformat() if row.get("fetched_at") else None,
}
# --- Application ---
def create_application(job_posting_id: str) -> dict[str, Any]:
row = execute(
"""
INSERT INTO application (job_posting_id, state)
VALUES (%s, 'discovered')
RETURNING *
""",
(job_posting_id,),
)
if row is None:
raise RuntimeError("insert application failed")
return _normalize_application(row)
def get_application(app_id: str) -> dict[str, Any] | None:
row = fetch_one(
"""
SELECT a.*, j.company, j.title, j.location
FROM application a
JOIN job_posting j ON a.job_posting_id = j.id
WHERE a.id = %s
""",
(app_id,),
)
if row is None:
return None
return _normalize_application(row)
def list_applications() -> list[dict[str, Any]]:
rows = fetch_all(
"""
SELECT a.*, j.company, j.title, j.location
FROM application a
JOIN job_posting j ON a.job_posting_id = j.id
ORDER BY a.created_at DESC
"""
)
return [_normalize_application(r) for r in rows]
def update_application_state(
app_id: str,
new_state: str,
) -> dict[str, Any] | None:
row = execute(
"""
UPDATE application
SET state = %s, state_changed_at = now(), last_activity_at = now()
WHERE id = %s
RETURNING *
""",
(new_state, app_id),
)
if row is None:
return None
return _normalize_application(row)
def update_application_score(
app_id: str,
score: float,
rationale: dict[str, Any],
) -> dict[str, Any] | None:
row = execute(
"""
UPDATE application
SET score = %s, score_rationale = %s, state = 'scored', state_changed_at = now()
WHERE id = %s
RETURNING *
""",
(score, json.dumps(rationale), app_id),
)
if row is None:
return None
return _normalize_application(row)
def _normalize_application(row: dict[str, Any]) -> dict[str, Any]:
rationale = row.get("score_rationale")
if rationale is not None and not isinstance(rationale, dict):
rationale = json.loads(rationale) if isinstance(rationale, str) else rationale
return {
"id": str(row["id"]),
"job_posting_id": str(row["job_posting_id"]),
"state": row["state"],
"score": float(row["score"]) if row.get("score") is not None else None,
"score_rationale": rationale,
"notes": row.get("notes"),
"state_changed_at": row["state_changed_at"].isoformat() if row.get("state_changed_at") else None,
"created_at": row["created_at"].isoformat() if row.get("created_at") else None,
"company": row.get("company"),
"title": row.get("title"),
"location": row.get("location"),
"follow_up_after_days": row.get("follow_up_after_days", 7),
"last_activity_at": row["last_activity_at"].isoformat() if row.get("last_activity_at") is not None else None,
"follow_up_snoozed_until": row.get("follow_up_snoozed_until").isoformat() if row.get("follow_up_snoozed_until") else None,
"interview_prep_artifact_id": str(row["interview_prep_artifact_id"]) if row.get("interview_prep_artifact_id") else None,
}
# --- Artifact ---
def create_artifact(
application_id: str,
kind: str,
filename: str,
content_bytes: bytes,
storage_path: str,
origin: str = "ai_reviewed",
version: int = 1,
) -> dict[str, Any]:
content_hash = hashlib.sha256(content_bytes).hexdigest()
row = execute(
"""
INSERT INTO artifact (application_id, kind, filename, content_hash, storage_path, version, origin)
VALUES (%s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(application_id, kind, filename, content_hash, storage_path, version, origin),
)
if row is None:
raise RuntimeError("insert artifact failed")
return _normalize_artifact(row)
def list_artifacts(application_id: str) -> list[dict[str, Any]]:
rows = fetch_all(
"SELECT * FROM artifact WHERE application_id = %s ORDER BY created_at DESC",
(application_id,),
)
return [_normalize_artifact(r) for r in rows]
def get_artifact(artifact_id: str) -> dict[str, Any] | None:
row = fetch_one("SELECT * FROM artifact WHERE id = %s", (artifact_id,))
if row is None:
return None
return _normalize_artifact(row)
def _normalize_artifact(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"application_id": str(row["application_id"]),
"kind": row["kind"],
"filename": row["filename"],
"content_hash": row["content_hash"],
"storage_path": row["storage_path"],
"version": row["version"],
"origin": row["origin"],
}
# --- Approval ---
def create_approval(
application_id: str,
artifact_id: str,
artifact_hash: str,
action: str,
) -> dict[str, Any]:
row = execute(
"""
INSERT INTO approval (application_id, artifact_id, artifact_hash, action, expires_at)
VALUES (%s, %s, %s, %s, now() + interval '24 hours')
RETURNING *
""",
(application_id, artifact_id, artifact_hash, action),
)
if row is None:
raise RuntimeError("insert approval failed")
return _normalize_approval(row)
def get_approval(approval_id: str) -> dict[str, Any] | None:
row = fetch_one("SELECT * FROM approval WHERE id = %s", (approval_id,))
if row is None:
return None
return _normalize_approval(row)
def confirm_approval(approval_id: str) -> dict[str, Any] | None:
row = execute(
"""
UPDATE approval
SET confirmed_by_user = true, confirmed_at = now()
WHERE id = %s
RETURNING *
""",
(approval_id,),
)
if row is None:
return None
return _normalize_approval(row)
def reject_approval(approval_id: str) -> dict[str, Any] | None:
row = execute(
"""
UPDATE approval
SET confirmed_by_user = false, confirmed_at = now()
WHERE id = %s
RETURNING *
""",
(approval_id,),
)
if row is None:
return None
return _normalize_approval(row)
def _normalize_approval(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"application_id": str(row["application_id"]),
"artifact_id": str(row["artifact_id"]),
"artifact_hash": row["artifact_hash"],
"action": row["action"],
"confirmed_by_user": row["confirmed_by_user"],
"confirmed_at": row["confirmed_at"].isoformat() if row.get("confirmed_at") else None,
"expires_at": row["expires_at"].isoformat() if row.get("expires_at") else None,
"created_at": row["created_at"].isoformat() if row.get("created_at") else None,
}
# --- Outbox ---
def create_outbox(approval_id: str, payload: dict[str, Any]) -> dict[str, Any]:
row = execute(
"""
INSERT INTO outbox (approval_id, payload, status)
VALUES (%s, %s, 'pending')
RETURNING *
""",
(approval_id, json.dumps(payload)),
)
if row is None:
raise RuntimeError("insert outbox failed")
return _normalize_outbox(row)
def update_outbox_sent(outbox_id: str) -> dict[str, Any] | None:
row = execute(
"""
UPDATE outbox
SET status = 'sent', sent_at = now()
WHERE id = %s
RETURNING *
""",
(outbox_id,),
)
if row is None:
return None
return _normalize_outbox(row)
def _normalize_outbox(row: dict[str, Any]) -> dict[str, Any]:
payload = row.get("payload")
if payload is not None and not isinstance(payload, dict):
payload = json.loads(payload) if isinstance(payload, str) else payload
return {
"id": str(row["id"]),
"approval_id": str(row["approval_id"]),
"kind": row.get("kind", "email"),
"payload": payload or {},
"status": row["status"],
"sent_at": row["sent_at"].isoformat() if row.get("sent_at") else None,
"error": row.get("error"),
}
# --- Task Run (telemetry) ---
def create_task_run(data: dict[str, Any]) -> dict[str, Any]:
row = execute(
"""
INSERT INTO task_run (task, model, provider, input_tokens, output_tokens, cost_usd, duration_ms, application_id)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
data["task"],
data["model"],
data["provider"],
data["input_tokens"],
data["output_tokens"],
data.get("cost_usd"),
data["duration_ms"],
data.get("application_id"),
),
)
if row is None:
raise RuntimeError("insert task_run failed")
return _normalize_task_run(row)
def list_task_runs() -> list[dict[str, Any]]:
rows = fetch_all("SELECT * FROM task_run ORDER BY created_at DESC LIMIT 100")
return [_normalize_task_run(r) for r in rows]
def _normalize_task_run(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"task": row["task"],
"model": row["model"],
"provider": row["provider"],
"input_tokens": row["input_tokens"],
"output_tokens": row["output_tokens"],
"cost_usd": float(row["cost_usd"]) if row.get("cost_usd") is not None else None,
"duration_ms": row["duration_ms"],
"application_id": str(row["application_id"]) if row.get("application_id") else None,
"created_at": row["created_at"].isoformat() if row.get("created_at") else None,
}
# --- Follow-up nudges ---
def get_nudge_applications() -> list[dict[str, Any]]:
"""Return applications in 'sent' state past follow_up_after_days and not snoozed."""
rows = fetch_all(
"""
SELECT a.*, j.company, j.title, j.location,
(now() - a.last_activity_at) AS elapsed
FROM application a
JOIN job_posting j ON a.job_posting_id = j.id
WHERE a.state = 'sent'
AND EXTRACT(day FROM now() - a.last_activity_at) > a.follow_up_after_days
AND (a.follow_up_snoozed_until IS NULL OR a.follow_up_snoozed_until < CURRENT_DATE)
ORDER BY a.last_activity_at ASC
"""
)
results: list[dict[str, Any]] = []
for row in rows:
app = _normalize_application(row)
elapsed = row.get("elapsed")
days = None
if elapsed is not None:
days = abs(int(elapsed.days))
app["days_since_sent"] = days
results.append(app)
return results
def snooze_follow_up(app_id: str, until_date: Any) -> dict[str, Any] | None:
"""Snooze follow-up nudge for an application until a given date."""
row = execute(
"UPDATE application SET follow_up_snoozed_until = %s WHERE id = %s RETURNING *",
(until_date, app_id),
)
if row is None:
return None
return _normalize_application(row)
# --- Interview prep ---
def set_interview_prep_artifact(app_id: str, artifact_id: str) -> dict[str, Any] | None:
"""Link an interview prep artifact to the application."""
row = execute(
"""
UPDATE application
SET interview_prep_artifact_id = %s, last_activity_at = now()
WHERE id = %s
RETURNING *
""",
(artifact_id, app_id),
)
if row is None:
return None
return _normalize_application(row)
# --- Pending approvals count ---
def count_pending_approvals() -> int:
"""Count approvals that are not yet confirmed and not expired."""
row = fetch_one(
"""
SELECT count(*) AS cnt
FROM approval
WHERE confirmed_by_user = false
AND expires_at > now()
"""
)
if row is None:
return 0
return int(row["cnt"])
# --- Digest (scored applications, top by score) ---
def get_digest(limit: int = 20) -> list[dict[str, Any]]:
"""Return scored applications ordered by score descending, with posting info."""
rows = fetch_all(
"""
SELECT a.*, j.company, j.title, j.location
FROM application a
JOIN job_posting j ON a.job_posting_id = j.id
WHERE a.score IS NOT NULL
ORDER BY a.score DESC NULLS LAST
LIMIT %s
""",
(limit,),
)
return [_normalize_application(r) for r in rows]