Coverage for src / rendercv / schema / models / cv / entries / bases / entry_with_complex_fields.py: 100%
59 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import re
2from datetime import date as Date
3from typing import Annotated, Literal, Self
5import pydantic
6import pydantic_core
8from rendercv.exception import RenderCVInternalError
10from .....pydantic_error_handling import CustomPydanticErrorTypes
11from ....validation_context import get_current_date
12from .entry_with_date import BaseEntryWithDate
15def validate_exact_date(date: str | int) -> str | int:
16 """Validate date conforms to strict format requirements.
18 Why:
19 start_date/end_date need strict formats for date arithmetic (calculating
20 duration). Unlike arbitrary dates, these must parse to actual Date objects
21 for comparison and duration rendering.
23 Args:
24 date: Date value to validate.
26 Returns:
27 Original date if valid.
28 """
29 try:
30 get_date_object(date)
31 except RenderCVInternalError as e:
32 raise pydantic_core.PydanticCustomError(
33 CustomPydanticErrorTypes.other.value,
34 "This is not a valid date! Please use either YYYY-MM-DD, YYYY-MM, or YYYY"
35 " format.",
36 ) from e
37 return date
40type ExactDate = Annotated[str | int, pydantic.AfterValidator(validate_exact_date)]
43def get_date_object(date: str | int, current_date: Date | None = None) -> Date:
44 """Convert date string/int to Python Date object.
46 Why:
47 Date arithmetic (start/end comparison, duration calculation) requires
48 Python Date objects. This parser handles multiple formats including
49 "present" keyword for ongoing positions.
51 Example:
52 ```py
53 date_obj = get_date_object("2023-05", Date(2025, 1, 1))
54 # Returns Date(2023, 5, 1)
56 present_obj = get_date_object("present", Date(2025, 1, 1))
57 # Returns Date(2025, 1, 1)
58 ```
60 Args:
61 date: Date in YYYY-MM-DD, YYYY-MM, YYYY format, or "present".
62 current_date: Reference date for "present" keyword.
64 Returns:
65 Python Date object.
66 """
67 if isinstance(date, int):
68 date_object = Date(date, 1, 1)
69 elif re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
70 # Then it is in YYYY-MM-DD format
71 date_object = Date.fromisoformat(date)
72 elif re.fullmatch(r"\d{4}-\d{2}", date):
73 # Then it is in YYYY-MM format
74 date_object = Date.fromisoformat(f"{date}-01")
75 elif re.fullmatch(r"\d{4}", date):
76 # Then it is in YYYY format
77 date_object = Date.fromisoformat(f"{date}-01-01")
78 elif date == "present":
79 if current_date is None:
80 raise RenderCVInternalError(
81 "current_date is None when processing 'present' date"
82 )
83 date_object = current_date
84 else:
85 raise RenderCVInternalError("This is not a valid date!")
87 return date_object
90class BaseEntryWithComplexFields(BaseEntryWithDate):
91 model_config = pydantic.ConfigDict(json_schema_extra={"description": None})
93 start_date: ExactDate | None = pydantic.Field(
94 default=None,
95 description="The start date in YYYY-MM-DD, YYYY-MM, or YYYY format.",
96 examples=["2020-09-24", "2020-09", "2020"],
97 )
98 end_date: ExactDate | Literal["present"] | None = pydantic.Field(
99 default=None,
100 description=(
101 'The end date in YYYY-MM-DD, YYYY-MM, or YYYY format. Use "present" for'
102 " ongoing events, or omit it to indicate the event is ongoing."
103 ),
104 examples=["2024-05-20", "2024-05", "2024", "present"],
105 )
106 location: str | None = pydantic.Field(
107 default=None,
108 examples=["Istanbul, Türkiye", "New York, NY", "Remote"],
109 )
110 summary: str | None = pydantic.Field(
111 default=None,
112 examples=[
113 "Led a team of 5 engineers to develop innovative solutions.",
114 (
115 "Completed advanced coursework in machine learning and artificial"
116 " intelligence."
117 ),
118 ],
119 )
120 highlights: list[str] | None = pydantic.Field(
121 default=None,
122 description=(
123 "Bullet points for key achievements, responsibilities, or contributions."
124 ),
125 examples=[
126 [
127 "Increased system performance by 40% through optimization.",
128 "Mentored 3 junior developers and conducted code reviews.",
129 "Implemented CI/CD pipeline reducing deployment time by 60%.",
130 ]
131 ],
132 )
134 @pydantic.model_validator(mode="after")
135 def check_and_adjust_dates(self, info: pydantic.ValidationInfo) -> Self:
136 date_is_provided = self.date is not None
137 start_date_is_provided = self.start_date is not None
138 end_date_is_provided = self.end_date is not None
140 if date_is_provided:
141 # If only date is provided, ignore start_date and end_date:
142 self.start_date = None
143 self.end_date = None
144 elif not start_date_is_provided and end_date_is_provided:
145 # If only end_date is provided, assume it is a one-day event and act like
146 # only the date is provided:
147 self.date = self.end_date
148 self.start_date = None
149 self.end_date = None
150 elif start_date_is_provided and not end_date_is_provided:
151 # If only start_date is provided, assume it is an ongoing event, i.e., the
152 # end_date is present:
153 self.end_date = "present"
155 if self.start_date and self.end_date:
156 # Check if the start_date is before the end_date:
157 current_date = get_current_date(info)
158 start_date_object = get_date_object(self.start_date, current_date)
159 end_date_object = get_date_object(self.end_date, current_date)
160 if start_date_object > end_date_object:
161 raise pydantic_core.PydanticCustomError(
162 CustomPydanticErrorTypes.other.value,
163 "`start_date` cannot be after `end_date`. The `start_date` is"
164 " {start_date} and the `end_date` is {end_date}.",
165 {
166 "start_date": self.start_date,
167 "end_date": self.end_date,
168 },
169 )
171 return self