Skip to content

Commit 8d82397

Browse files
franccescoclaude
andcommitted
refactor(models): consolidate validators and merge identical models
- Extract shared OptionalDatetime and OptionalFloat annotated types - Remove duplicate parse_optional_datetime validators from Todo, Issue, Goal - Merge HeadlineListItem into HeadlineDetails (type alias for backward compat) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 949d16f commit 8d82397

1 file changed

Lines changed: 42 additions & 73 deletions

File tree

src/bloomy/models.py

Lines changed: 42 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,38 @@
44

55
from datetime import datetime
66
from 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

1241
class 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

146162
class 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

175178
class 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

216206
class 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

244221
class 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

403372
class BulkCreateError(BloomyBaseModel):

0 commit comments

Comments
 (0)