280 lines
6.8 KiB
Python
280 lines
6.8 KiB
Python
"""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]
|