jobhunt-platform/apps/api/app/db/repo_profile.py
hermes 17759ef564 T1: FastAPI backend core (models, state machine, approval gate) + compose test runner
- apps/api: psycopg v3 repositories, schema.sql + migration runner, state machine
  implementing the data-model transition table, approval gate with sha256 hash
  match + 24h expiry enforced at send time, EchoTransport pluggable sender,
  LLM calls routed through packages.llm-gateway with mock mode, 47 pytest tests green
- apps/api/Dockerfile.test: python 3.13-slim test image (DinD-safe: migrations
  copied as directory)
- docker-compose.yml: add api-test service on compose network (host port
  publishing is broken in this sandbox; container-to-container networking used)
- Fix: replace masked placeholder password in config.py/conftest.py defaults
2026-07-30 18:10:59 +00:00

157 lines
No EOL
5.3 KiB
Python

"""Repository functions for profile and cv_section tables."""
from __future__ import annotations
import json
from typing import Any
from app.db import execute, fetch_all, fetch_one
def get_or_create_profile() -> dict[str, Any] | None:
"""Get the single profile row, or create a default one if none exists."""
row = fetch_one("SELECT * FROM profile LIMIT 1")
if row is not None:
return _normalize_profile(row)
# Create default profile
row = execute(
"""
INSERT INTO profile (full_name, email)
VALUES ('', '')
RETURNING *
"""
)
if row is None:
return None
return _normalize_profile(row)
def update_profile(data: dict[str, Any]) -> dict[str, Any] | None:
"""Update the single profile row with provided fields."""
profile = get_or_create_profile()
if profile is None:
return None
pid = profile["id"]
fields = []
values: list[Any] = []
for key in ("full_name", "email", "phone", "location", "headline", "summary"):
if key in data and data[key] is not None:
fields.append(f"{key} = %s")
values.append(data[key])
if "languages" in data and data["languages"] is not None:
fields.append("languages = %s")
values.append(json.dumps(data["languages"]))
if "hard_rules" in data and data["hard_rules"] is not None:
fields.append("hard_rules = %s")
values.append(json.dumps(data["hard_rules"]))
if not fields:
return profile
fields.append("updated_at = now()")
values.append(pid)
sql = f"UPDATE profile SET {', '.join(fields)} WHERE id = %s RETURNING *"
row = execute(sql, tuple(values))
if row is None:
return None
return _normalize_profile(row)
def _normalize_profile(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"full_name": row["full_name"],
"email": row["email"],
"phone": row.get("phone"),
"location": row.get("location"),
"headline": row.get("headline"),
"summary": row.get("summary"),
"languages": row.get("languages", []) if isinstance(row.get("languages"), list) else json.loads(row.get("languages", "[]")),
"hard_rules": row.get("hard_rules", {}) if isinstance(row.get("hard_rules"), dict) else json.loads(row.get("hard_rules", "{}")),
}
# --- CV Sections ---
def list_sections() -> list[dict[str, Any]]:
rows = fetch_all(
"SELECT * FROM cv_section ORDER BY kind, sort_order"
)
return [_normalize_section(r) for r in rows]
def create_section(profile_id: str, data: dict[str, Any]) -> dict[str, Any]:
row = execute(
"""
INSERT INTO cv_section
(profile_id, kind, title, org, location, start_date, end_date, bullets, tags, sort_order)
VALUES
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
""",
(
profile_id,
data["kind"],
data["title"],
data.get("org"),
data.get("location"),
data.get("start_date"),
data.get("end_date"),
json.dumps(data.get("bullets", [])),
data.get("tags", []),
data.get("sort_order", 0),
),
)
if row is None:
raise RuntimeError("insert failed")
return _normalize_section(row)
def update_section(section_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
fields = []
values: list[Any] = []
for key in ("kind", "title", "org", "location", "start_date", "end_date", "sort_order"):
if key in data and data[key] is not None:
fields.append(f"{key} = %s")
values.append(data[key])
if "bullets" in data and data["bullets"] is not None:
fields.append("bullets = %s")
values.append(json.dumps(data["bullets"]))
if "tags" in data and data["tags"] is not None:
fields.append("tags = %s")
values.append(data["tags"])
if not fields:
return get_section(section_id)
fields.append("updated_at = now()")
values.append(section_id)
sql = f"UPDATE cv_section SET {', '.join(fields)} WHERE id = %s RETURNING *"
row = execute(sql, tuple(values))
if row is None:
return None
return _normalize_section(row)
def get_section(section_id: str) -> dict[str, Any] | None:
row = fetch_one("SELECT * FROM cv_section WHERE id = %s", (section_id,))
if row is None:
return None
return _normalize_section(row)
def delete_section(section_id: str) -> bool:
row = execute("DELETE FROM cv_section WHERE id = %s RETURNING id", (section_id,))
return row is not None
def _normalize_section(row: dict[str, Any]) -> dict[str, Any]:
return {
"id": str(row["id"]),
"profile_id": str(row["profile_id"]),
"kind": row["kind"],
"title": row["title"],
"org": row.get("org"),
"location": row.get("location"),
"start_date": row.get("start_date").isoformat() if row.get("start_date") else None,
"end_date": row.get("end_date").isoformat() if row.get("end_date") else None,
"bullets": row.get("bullets", []) if isinstance(row.get("bullets"), list) else json.loads(row.get("bullets", "[]")),
"tags": list(row.get("tags", [])) if row.get("tags") else [],
"sort_order": row.get("sort_order", 0),
}