from datetime import datetime from typing import Optional from pydantic import BaseModel, Field class RawJobCard(BaseModel): """Raw extracted data from a single job card on a page.""" title: str company: Optional[str] = None location: Optional[str] = None posted_text: Optional[str] = None url: Optional[str] = None salary_text: Optional[str] = None employment_type: Optional[str] = None raw_text: Optional[str] = None external_job_id: Optional[str] = None class JobPostingCandidate(BaseModel): """Normalized job posting ready for DB upsert.""" site_id: str external_job_id: Optional[str] = None title: str company: Optional[str] = None location: Optional[str] = None posted_text: Optional[str] = None posted_at_normalized: Optional[datetime] = None job_url: Optional[str] = None salary_text: Optional[str] = None employment_type: Optional[str] = None raw_text: Optional[str] = None fingerprint: str = "" def is_valid(self) -> bool: return bool(self.title and self.title.strip()) class SearchQuery(BaseModel): """Merged search parameters for a single site run.""" keywords: list[str] = Field(default_factory=lambda: ["orthodontist"]) location: str = "" remote: bool = False date_mode: str = "today_or_last_24h" sort: str = "relevance" max_items: int = 30 @property def keywords_str(self) -> str: return " ".join(self.keywords) @property def keywords_urlencoded(self) -> str: from urllib.parse import quote_plus return quote_plus(self.keywords_str) @property def location_urlencoded(self) -> str: from urllib.parse import quote_plus return quote_plus(self.location)