44
55from datetime import datetime
66from enum import StrEnum
7- from typing import Any
7+ from typing import Annotated , Any
88
9- from pydantic import BaseModel , ConfigDict , Field , field_validator
9+ from pydantic import BaseModel , BeforeValidator , ConfigDict , Field
10+
11+
12+ def _parse_optional_datetime (v : Any ) -> datetime | None :
13+ """Parse optional datetime fields, treating empty strings as None.
14+
15+ Returns:
16+ The datetime value or None if empty/None.
17+
18+ """
19+ if v is None or v == "" :
20+ return None
21+ return v
22+
23+
24+ def _parse_optional_float (v : Any ) -> float | None :
25+ """Parse optional float fields, treating empty strings as None.
26+
27+ Returns:
28+ The float value or None if empty/None.
29+
30+ """
31+ if v is None or v == "" :
32+ return None
33+ return float (v )
34+
35+
36+ # Reusable annotated types for optional fields that may come as empty strings
37+ OptionalDatetime = Annotated [datetime | None , BeforeValidator (_parse_optional_datetime )]
38+ OptionalFloat = Annotated [float | None , BeforeValidator (_parse_optional_float )]
1039
1140
1241class GoalStatus (StrEnum ):
@@ -122,26 +151,13 @@ class Todo(BloomyBaseModel):
122151 id : int = Field (alias = "Id" )
123152 name : str = Field (alias = "Name" )
124153 details_url : str | None = Field (alias = "DetailsUrl" , default = None )
125- due_date : datetime | None = Field (alias = "DueDate" , default = None )
126- complete_date : datetime | None = Field (alias = "CompleteTime" , default = None )
127- create_date : datetime | None = Field (alias = "CreateTime" , default = None )
154+ due_date : OptionalDatetime = Field (alias = "DueDate" , default = None )
155+ complete_date : OptionalDatetime = Field (alias = "CompleteTime" , default = None )
156+ create_date : OptionalDatetime = Field (alias = "CreateTime" , default = None )
128157 meeting_id : int | None = Field (alias = "OriginId" , default = None )
129158 meeting_name : str | None = Field (alias = "Origin" , default = None )
130159 complete : bool = Field (alias = "Complete" , default = False )
131160
132- @field_validator ("due_date" , "complete_date" , "create_date" , mode = "before" )
133- @classmethod
134- def parse_optional_datetime (cls , v : Any ) -> datetime | None :
135- """Parse optional datetime fields.
136-
137- Returns:
138- The parsed datetime or None if empty.
139-
140- """
141- if v is None or v == "" :
142- return None
143- return v
144-
145161
146162class Issue (BloomyBaseModel ):
147163 """Model for issue."""
@@ -155,21 +171,8 @@ class Issue(BloomyBaseModel):
155171 owner_name : str = Field (alias = "OwnerName" )
156172 owner_id : int = Field (alias = "OwnerId" )
157173 owner_image_url : str = Field (alias = "OwnerImageUrl" )
158- closed_date : datetime | None = Field (alias = "ClosedDate" , default = None )
159- completion_date : datetime | None = Field (alias = "CompletionDate" , default = None )
160-
161- @field_validator ("closed_date" , "completion_date" , mode = "before" )
162- @classmethod
163- def parse_optional_datetime (cls , v : Any ) -> datetime | None :
164- """Parse optional datetime fields.
165-
166- Returns:
167- The parsed datetime or None if empty.
168-
169- """
170- if v is None or v == "" :
171- return None
172- return v
174+ closed_date : OptionalDatetime = Field (alias = "ClosedDate" , default = None )
175+ completion_date : OptionalDatetime = Field (alias = "CompletionDate" , default = None )
173176
174177
175178class Headline (BloomyBaseModel ):
@@ -192,54 +195,28 @@ class Goal(BloomyBaseModel):
192195 id : int = Field (alias = "Id" )
193196 name : str = Field (alias = "Name" )
194197 due_date : datetime = Field (alias = "DueDate" )
195- complete_date : datetime | None = Field (alias = "CompleteDate" , default = None )
198+ complete_date : OptionalDatetime = Field (alias = "CompleteDate" , default = None )
196199 create_date : datetime = Field (alias = "CreateDate" )
197200 is_archived : bool = Field (alias = "IsArchived" , default = False )
198201 percent_complete : float = Field (alias = "PercentComplete" , default = 0.0 )
199202 accountable_user_id : int = Field (alias = "AccountableUserId" )
200203 accountable_user_name : str | None = Field (alias = "AccountableUserName" , default = None )
201204
202- @field_validator ("complete_date" , mode = "before" )
203- @classmethod
204- def parse_optional_datetime (cls , v : Any ) -> datetime | None :
205- """Parse optional datetime fields.
206-
207- Returns:
208- The parsed datetime or None if empty.
209-
210- """
211- if v is None or v == "" :
212- return None
213- return v
214-
215205
216206class ScorecardMetric (BloomyBaseModel ):
217207 """Model for scorecard metric."""
218208
219209 id : int = Field (alias = "Id" )
220210 title : str = Field (alias = "Title" )
221- target : float | None = Field (alias = "Target" , default = None )
211+ target : OptionalFloat = Field (alias = "Target" , default = None )
222212 unit : str | None = Field (alias = "Unit" , default = None )
223213 week_number : int = Field (alias = "WeekNumber" )
224- value : float | None = Field (alias = "Value" , default = None )
214+ value : OptionalFloat = Field (alias = "Value" , default = None )
225215 metric_type : str = Field (alias = "MetricType" )
226216 accountable_user_id : int = Field (alias = "AccountableUserId" )
227217 accountable_user_name : str | None = Field (alias = "AccountableUserName" , default = None )
228218 is_inverse : bool = Field (alias = "IsInverse" , default = False )
229219
230- @field_validator ("target" , "value" , mode = "before" )
231- @classmethod
232- def parse_optional_float (cls , v : Any ) -> float | None :
233- """Parse optional float fields.
234-
235- Returns:
236- The parsed float or None if empty.
237-
238- """
239- if v is None or v == "" :
240- return None
241- return float (v )
242-
243220
244221class CurrentWeek (BloomyBaseModel ):
245222 """Model for current week information."""
@@ -380,24 +357,16 @@ class HeadlineDetails(BloomyBaseModel):
380357
381358 id : int
382359 title : str
383- notes_url : str
360+ notes_url : str | None = None
384361 meeting_details : MeetingInfo
385362 owner_details : OwnerDetails
386363 archived : bool
387364 created_at : str
388365 closed_at : str | None = None
389366
390367
391- class HeadlineListItem (BloomyBaseModel ):
392- """Model for headline list items."""
393-
394- id : int
395- title : str
396- meeting_details : MeetingInfo
397- owner_details : OwnerDetails
398- archived : bool
399- created_at : str
400- closed_at : str | None = None
368+ # HeadlineListItem is identical to HeadlineDetails - use type alias
369+ HeadlineListItem = HeadlineDetails
401370
402371
403372class BulkCreateError (BloomyBaseModel ):
0 commit comments