jobhunt-platform/apps/api/app/schemas.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

322 lines
No EOL
6.3 KiB
Python

"""Pydantic schemas for all API contract payloads."""
from __future__ import annotations
from datetime import date
from typing import Any
from pydantic import BaseModel, Field
# --- Profile ---
class ProfileBase(BaseModel):
full_name: str = ""
email: str = ""
phone: str | None = None
location: str | None = None
headline: str | None = None
summary: str | None = None
languages: list[dict[str, Any]] = Field(default_factory=list)
hard_rules: dict[str, Any] = Field(default_factory=dict)
class ProfileUpdate(BaseModel):
full_name: str | None = None
email: str | None = None
phone: str | None = None
location: str | None = None
headline: str | None = None
summary: str | None = None
languages: list[dict[str, Any]] | None = None
hard_rules: dict[str, Any] | None = None
class ProfileOut(BaseModel):
id: str
full_name: str
email: str
phone: str | None = None
location: str | None = None
headline: str | None = None
summary: str | None = None
languages: list[dict[str, Any]] = []
hard_rules: dict[str, Any] = {}
# --- CV Section ---
class CvSectionCreate(BaseModel):
kind: str
title: str
org: str | None = None
location: str | None = None
start_date: date | None = None
end_date: date | None = None
bullets: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
sort_order: int = 0
class CvSectionUpdate(BaseModel):
kind: str | None = None
title: str | None = None
org: str | None = None
location: str | None = None
start_date: date | None = None
end_date: date | None = None
bullets: list[str] | None = None
tags: list[str] | None = None
sort_order: int | None = None
class CvSectionOut(BaseModel):
id: str
profile_id: str
kind: str
title: str
org: str | None = None
location: str | None = None
start_date: date | None = None
end_date: date | None = None
bullets: list[str] = []
tags: list[str] = []
sort_order: int = 0
class AiAssistRequest(BaseModel):
instruction: str
class AiAssistResponse(BaseModel):
suggestions: list[str]
# --- Job Posting ---
class JobPostingCreate(BaseModel):
url: str
class JobPostingOut(BaseModel):
id: str
source: str
external_id: str | None = None
url: str
company: str
title: str
location: str | None = None
description: str = ""
fetched_at: str | None = None
class ScoreResponse(BaseModel):
score: float
rationale: dict[str, Any]
# --- Application ---
class ApplicationOut(BaseModel):
id: str
job_posting_id: str
state: str
score: float | None = None
score_rationale: dict[str, Any] | None = None
notes: str | None = None
state_changed_at: str | None = None
created_at: str | None = None
# joined posting info
company: str | None = None
title: str | None = None
location: str | None = None
# v1 follow-up fields
follow_up_after_days: int = 7
last_activity_at: str | None = None
follow_up_snoozed_until: str | None = None
interview_prep_artifact_id: str | None = None
class TransitionRequest(BaseModel):
to: str
# --- Artifact ---
class ArtifactCreate(BaseModel):
kind: str
content: str
class ArtifactOut(BaseModel):
id: str
application_id: str
kind: str
filename: str
content_hash: str
storage_path: str
version: int
origin: str
class CoverLetterRequest(BaseModel):
letter_text: str
class CoverLetterResponse(BaseModel):
artifact: ArtifactOut
critique: list[dict[str, Any]]
# --- Approval ---
class ApprovalCreate(BaseModel):
action: str
artifact_id: str
class ApprovalOut(BaseModel):
id: str
application_id: str
artifact_id: str
artifact_hash: str
action: str
confirmed_by_user: bool
confirmed_at: str | None = None
expires_at: str
created_at: str | None = None
# --- Outbox ---
class OutboxSendRequest(BaseModel):
approval_id: str
payload: dict[str, Any]
class OutboxOut(BaseModel):
id: str
approval_id: str
kind: str
payload: dict[str, Any]
status: str
sent_at: str | None = None
error: str | None = None
# --- Telemetry ---
class TaskRunOut(BaseModel):
id: str
task: str
model: str
provider: str
input_tokens: int
output_tokens: int
cost_usd: float | None = None
duration_ms: int
application_id: str | None = None
created_at: str | None = None
# --- Errors ---
class ErrorOut(BaseModel):
error: dict[str, str]
# --- v1: CV Import ---
class CvImportRequest(BaseModel):
filename: str
content_base64: str
class CvDraft(BaseModel):
kind: str = "experience"
title: str = ""
org: str | None = None
location: str | None = None
start_date: str | None = None
end_date: str | None = None
bullets: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
class CvImportResponse(BaseModel):
drafts: list[dict[str, Any]]
class CvImportConfirmRequest(BaseModel):
drafts: list[CvDraft]
class CvImportConfirmResponse(BaseModel):
created: int
sections: list[CvSectionOut]
# --- v1: Postings Fetch ---
class PostingsFetchRequest(BaseModel):
query: str
region: str | None = None
class PostingsFetchResponse(BaseModel):
new: int
dupes: int
# --- v1: Batch Scoring ---
class BatchScoreRequest(BaseModel):
application_ids: list[str]
class BatchScoreResult(BaseModel):
application_id: str
score: float
rationale: dict[str, Any]
red_flags: list[str] = Field(default_factory=list)
class BatchScoreResponse(BaseModel):
results: list[BatchScoreResult]
# --- v1: Today ---
class DigestItem(BaseModel):
application_id: str
title: str
company: str
score: float | None = None
class NudgeItem(BaseModel):
application_id: str
days_since_sent: int
suggestion: str
class TodayResponse(BaseModel):
digest: list[DigestItem]
nudges: list[NudgeItem]
pending_approvals: int
# --- v1: Interview Prep ---
class InterviewPrepResponse(BaseModel):
artifact_id: str
content: str
# --- v1: Concierge Seed ---
class SeedDemoResponse(BaseModel):
profile: str
postings: int
applications: int
sections: int