From 1b4e901afef62fafcb90fb5eca44868d89b2c3b8 Mon Sep 17 00:00:00 2001 From: Antoine Date: Thu, 28 May 2026 13:16:21 +0200 Subject: [PATCH] feat: add canonical candidate profile output model --- src/job_research/models.py | 37 +++++++++++++++++++++++++++++++++++++ src/job_research/storage.py | 18 ++++++++++++++++++ tests/test_storage.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/job_research/models.py create mode 100644 src/job_research/storage.py create mode 100644 tests/test_storage.py diff --git a/src/job_research/models.py b/src/job_research/models.py new file mode 100644 index 0000000..88cc08a --- /dev/null +++ b/src/job_research/models.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, Field + + +class ExperienceEntry(BaseModel): + company: str + title: str + start: str | None = None + end: str | None = None + highlights: list[str] = Field(default_factory=list) + + +class EducationEntry(BaseModel): + institution: str + credential: str + start: str | None = None + end: str | None = None + + +class WarningItem(BaseModel): + field: str + message: str + + +class CandidateProfileOutput(BaseModel): + name: str | None = None + summary: str | None = None + target_roles: list[str] = Field(default_factory=list) + strengths: list[str] = Field(default_factory=list) + skills_to_emphasize: list[str] = Field(default_factory=list) + constraints: list[str] = Field(default_factory=list) + notes: list[str] = Field(default_factory=list) + location: str | None = None + languages: list[str] = Field(default_factory=list) + skills: list[str] = Field(default_factory=list) + experience_entries: list[ExperienceEntry] = Field(default_factory=list) + education_entries: list[EducationEntry] = Field(default_factory=list) + warnings: list[WarningItem] = Field(default_factory=list) diff --git a/src/job_research/storage.py b/src/job_research/storage.py new file mode 100644 index 0000000..5f33c7a --- /dev/null +++ b/src/job_research/storage.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + +from job_research.models import CandidateProfileOutput + + +def save_candidate_profile_yaml(path: Path, profile: CandidateProfileOutput) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + payload = profile.model_dump(mode="json") + path.write_text(yaml.safe_dump(payload, sort_keys=False, allow_unicode=True), encoding="utf-8") + + +def load_yaml(path: Path) -> dict[str, Any]: + return yaml.safe_load(path.read_text(encoding="utf-8")) or {} diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..e1c49c0 --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,29 @@ +from job_research.models import CandidateProfileOutput, ExperienceEntry, WarningItem +from job_research.storage import save_candidate_profile_yaml, load_yaml + + +def test_save_candidate_profile_yaml_round_trips_readable_output(tmp_path) -> None: + profile = CandidateProfileOutput( + name="Tonio", + summary="Junior data engineer focused on Python and GCP.", + target_roles=["Data Engineer"], + strengths=["Python", "SQL"], + skills_to_emphasize=["GCP", "BigQuery"], + constraints=["CDI only", "France only"], + notes=["Slight preference for French listings."], + location="France", + languages=["French", "English"], + skills=["Python", "SQL", "Terraform", "GCP", "BigQuery"], + experience_entries=[ExperienceEntry(company="A", title="Data Engineer")], + education_entries=[], + warnings=[WarningItem(field="years_of_experience", message="CV and profile disagree.")], + ) + + out = tmp_path / "candidate-profile.yaml" + save_candidate_profile_yaml(out, profile) + + payload = load_yaml(out) + + assert payload["name"] == "Tonio" + assert payload["constraints"] == ["CDI only", "France only"] + assert payload["warnings"][0]["field"] == "years_of_experience"