Jerlshin's picture
refined progress and defect fix
761d149
Raw
History Blame Contribute Delete
5.82 kB
from __future__ import annotations
from collections.abc import Mapping
from datetime import date
from types import MappingProxyType
from typing import Any, final
from pydantic import (
BaseModel,
ConfigDict,
Field,
ValidationError,
field_serializer,
field_validator,
)
from redstack.domain.enums import (
CompanySize,
InstitutionTier,
LanguageProficiency,
Proficiency,
WorkMode,
)
from redstack.domain.errors import SchemaError
from redstack.domain.ids import CandidateId, LpaAmount, Months, SkillName
_STRICT = ConfigDict(
frozen=True, extra="forbid", str_strip_whitespace=True, validate_default=True
)
@final
class RawProfile(BaseModel):
"""Top-level profile facts."""
model_config = _STRICT
anonymized_name: str = Field(min_length=1)
headline: str
summary: str
location: str
country: str
years_of_experience: float = Field(ge=0.0, le=50.0, allow_inf_nan=False)
current_title: str
current_company: str
current_company_size: CompanySize
current_industry: str
@final
class RawPosition(BaseModel):
"""One career-history position, mirrored verbatim."""
model_config = _STRICT
company: str
title: str
start_date: date
end_date: date | None
duration_months: Months = Field(ge=0)
is_current: bool
industry: str
company_size: CompanySize
description: str
@final
class RawEducation(BaseModel):
"""One education record."""
model_config = _STRICT
institution: str
degree: str
field_of_study: str
start_year: int
end_year: int
grade: str | None
tier: InstitutionTier
@final
class RawSkill(BaseModel):
"""One claimed skill."""
model_config = _STRICT
name: SkillName
proficiency: Proficiency
endorsements: int = Field(ge=0)
duration_months: Months | None = Field(default=None, ge=0)
@final
class RawCertification(BaseModel):
"""One certification."""
model_config = _STRICT
name: str
issuer: str
year: int
@final
class RawLanguage(BaseModel):
"""One spoken language."""
model_config = _STRICT
language: str
proficiency: LanguageProficiency
@final
class RawSalaryRange(BaseModel):
"""Expected salary range (INR lpa); inversion preserved, never corrected.
Field names mirror the source JSON verbatim (``{"min": ..., "max": ...}``),
not the ``*_lpa``-suffixed names used by downstream domain models.
"""
model_config = _STRICT
min: LpaAmount = Field(ge=0.0, allow_inf_nan=False)
max: LpaAmount = Field(ge=0.0, allow_inf_nan=False)
@final
class RawSignals(BaseModel):
"""The 23 ``redrob_signals``, typed exactly; sentinels preserved as-is."""
model_config = _STRICT
profile_completeness_score: float = Field(ge=0.0, le=100.0, allow_inf_nan=False)
signup_date: date
last_active_date: date
open_to_work_flag: bool
profile_views_received_30d: int = Field(ge=0)
applications_submitted_30d: int = Field(ge=0)
recruiter_response_rate: float = Field(ge=0.0, le=1.0, allow_inf_nan=False)
avg_response_time_hours: float = Field(ge=0.0, allow_inf_nan=False)
skill_assessment_scores: Mapping[str, float]
connection_count: int = Field(ge=0)
endorsements_received: int = Field(ge=0)
notice_period_days: int = Field(ge=0, le=180)
expected_salary_range_inr_lpa: RawSalaryRange
preferred_work_mode: WorkMode
willing_to_relocate: bool
github_activity_score: float = Field(ge=-1.0, le=100.0, allow_inf_nan=False)
search_appearance_30d: int = Field(ge=0)
saved_by_recruiters_30d: int = Field(ge=0)
interview_completion_rate: float = Field(ge=0.0, le=1.0, allow_inf_nan=False)
offer_acceptance_rate: float = Field(ge=-1.0, le=1.0, allow_inf_nan=False)
verified_email: bool
verified_phone: bool
linkedin_connected: bool
@field_validator("skill_assessment_scores", mode="after")
@classmethod
def _freeze_scores(cls, value: Mapping[str, float]) -> Mapping[str, float]:
for score in value.values():
if not (0.0 <= score <= 100.0):
raise ValueError("skill_assessment_scores out of range [0, 100]")
return MappingProxyType(dict(value))
@field_serializer("skill_assessment_scores")
def _dump_scores(self, value: Mapping[str, float]) -> dict[str, float]:
# ``MappingProxyType`` is not natively serializable; project the
# read-only view back to a plain ``dict`` so ``RawCandidate`` round-trips
# to JSON losslessly (provenance depends on this, §P).
return dict(value)
@final
class RawCandidate(BaseModel):
"""Aggregate of raw facts — the canonical evidence source."""
model_config = _STRICT
candidate_id: CandidateId = Field(pattern=r"^CAND_[0-9]{7}$")
profile: RawProfile
career_history: tuple[RawPosition, ...] = Field(min_length=1, max_length=10)
education: tuple[RawEducation, ...] = Field(max_length=5)
skills: tuple[RawSkill, ...]
certifications: tuple[RawCertification, ...]
languages: tuple[RawLanguage, ...]
redrob_signals: RawSignals
@classmethod
def from_mapping(cls, data: Mapping[str, Any]) -> RawCandidate:
"""Validate and narrow a raw mapping — the sole ``Any`` boundary.
Type/shape violations are re-raised as ``SchemaError``; semantic
contradictions are intentionally preserved for downstream detection.
"""
try:
return cls.model_validate(data)
except ValidationError as exc:
raise SchemaError(str(exc)) from exc
__all__: tuple[str, ...] = (
"RawCandidate",
"RawCertification",
"RawEducation",
"RawLanguage",
"RawPosition",
"RawProfile",
"RawSalaryRange",
"RawSignals",
"RawSkill",
)