"""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() 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"), } # --- 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, }