"""Sheet header aliases, FormData keys, and date/round format mappings. (str, Enum) like inbox/enums.py: members compare to and serialize as plain strings. Non-string mappings (month pairs, ordinal slot+pattern) use plain Enum. """ from enum import Enum class FormDataField(str, Enum): """Canonical FormData column keys (plus title, which stays in JSONB only).""" NAME = "name" DEGREE = "degree" EXPERIENCE = "experience" AGE = "age" FAMILY_DETAILS = "family_details" TITLE = "title" class NameAlias(str, Enum): NAME = "name" NAMES = "names" CANDIDATE_NAME = "candidate name" CANDIDATE = "candidate" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ class DegreeAlias(str, Enum): EDUCATION = "education" DEGREE = "degree" QUALIFICATION = "qualification" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ class ExperienceAlias(str, Enum): EXPERIENCE = "experience" EXP = "exp" YEARS_OF_EXPERIENCE = "years of experience" TOTAL_EXPERIENCE = "total experience" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ class AgeAlias(str, Enum): AGE = "age" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ class FamilyDetailsAlias(str, Enum): FAMILY_DETAILS = "family details" MARITAL_STATUS = "marital status" MARITAL = "marital" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ class TitleAlias(str, Enum): """No FormData column — recognised so headers are not treated as unknown noise.""" TITLE = "title" DESIGNATION = "designation" ROLE = "role" POSITION = "position" TEAM = "team" JOB_TITLE = "job title" AREA_OF_EXPERTISE = "area of expertise" DEPARTMENT = "department" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ # FormDataField → alias Enum. Order is match priority for overlapping startswith hits. FIELD_ALIAS_ENUMS = { FormDataField.NAME: NameAlias, FormDataField.DEGREE: DegreeAlias, FormDataField.EXPERIENCE: ExperienceAlias, FormDataField.AGE: AgeAlias, FormDataField.FAMILY_DETAILS: FamilyDetailsAlias, FormDataField.TITLE: TitleAlias, } class RoundRole(str, Enum): """Interview-round column roles resolved left-to-right into four slots.""" DATE = "date" BY = "by" STATUS = "status" NOTES = "notes" RESULT = "result" class ConductedByAlias(str, Enum): """Header spellings that map to RoundRole.BY.""" CONDUCTED_BY = "conducted by" INTERVIEWED_BY = "interviewed by" INTERVIEW_BY = "interview by" CONDUCTED = "conducted" BY = "by" @classmethod def has(cls, value) -> bool: return value in cls._value2member_map_ @classmethod def contained_in(cls, text: str) -> bool: return any(member.value in text for member in cls if " " in member.value) class NotesToken(str, Enum): """Substrings that classify a header as RoundRole.NOTES.""" NOTE = "note" REMARK = "remark" COMMENT = "comment" @classmethod def contained_in(cls, text: str) -> bool: return any(member.value in text for member in cls) # -- Round → FormData column names (slot 0..3 = definition order) ------------ class RoundDateColumn(str, Enum): R1 = "interview_date" R2 = "second_interview_date" R3 = "third_interview_date" R4 = "fourth_interview_date" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) class RoundByColumn(str, Enum): R1 = "interview_by" R2 = "second_interview_by" R3 = "third_interview_by" R4 = "fourth_interview_by" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) class RoundTimeColumn(str, Enum): R1 = "interview_time" R2 = "second_interview_time" R3 = "third_interview_time" R4 = "fourth_interview_time" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) class RoundStatusColumn(str, Enum): R1 = "interview_status" R2 = "second_interview_status" R3 = "third_interview_status" R4 = "fourth_interview_status" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) class RoundNotesColumn(str, Enum): R1 = "interview_notes" R2 = "second_interview_notes" R3 = "third_interview_notes" R4 = "fourth_interview_notes" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) class RoundResultColumn(str, Enum): R1 = "interview_result" R2 = "second_interview_result" R3 = "third_interview_result" R4 = "fourth_interview_result" @classmethod def ordered(cls) -> tuple[str, ...]: return tuple(member.value for member in cls) # -- Date parsing ------------------------------------------------------------ class DateFormat(str, Enum): """strptime patterns tried in definition order. DD/MM before MM/DD: 14/10/20 is ambiguous and DD/MM is the local convention. """ D_MON_Y_DASH = "%d-%b-%Y" D_MONTH_Y_DASH = "%d-%B-%Y" D_MON_Y_SPACE = "%d %b %Y" D_MONTH_Y_SPACE = "%d %B %Y" DMY_SLASH = "%d/%m/%Y" DMY_SLASH_SHORT = "%d/%m/%y" DMY_DASH = "%d-%m-%Y" DMY_DASH_SHORT = "%d-%m-%y" ISO = "%Y-%m-%d" DMY_DOT = "%d.%m.%Y" DMY_DOT_SHORT = "%d.%m.%y" MDY_SLASH = "%m/%d/%Y" MDY_SLASH_SHORT = "%m/%d/%y" MON_D_Y = "%b %d %Y" MONTH_D_Y = "%B %d %Y" D_MON_Y_SHORT = "%d-%b-%y" D_MON_Y_SPACE_SHORT = "%d %b %y" D_MON_Y_SLASH = "%d/%b/%Y" D_MON_Y_SLASH_SHORT = "%d/%b/%y" class DateTimeSeparator(str, Enum): """Separators that split a date cell into date + time tails.""" DASH = " - " EN_DASH = " – " EM_DASH = " — " SLASH_SPACE = "/ " PIPE = " | " class MonthNormalisation(Enum): """Sheet month spellings → %b-safe short form. value is (source, short).""" SEPTEMBER = ("september", "sep") SEPT = ("sept", "sep") JULY = ("july", "jul") JUNE = ("june", "jun") APRIL = ("april", "apr") MARCH = ("march", "mar") @property def source(self) -> str: return self.value[0] @property def short(self) -> str: return self.value[1] class RoundOrdinal(Enum): """Interview-round ordinal in a header → slot index 0..3. value is (slot, regex).""" FIRST = (0, r"(?:1st|first|01st)") SECOND = (1, r"(?:2nd|second|02nd)") THIRD = (2, r"(?:3rd|third|03rd)") FOURTH = (3, r"(?:4th|fourth|04th)") @property def slot(self) -> int: return self.value[0] @property def pattern(self) -> str: return self.value[1]