Skip to content

Commit df0180d

Browse files
fix(services/util): Mv normalize_datetime_to_utc() to services/util & used it in well_inventory
1 parent c3c7648 commit df0180d

3 files changed

Lines changed: 33 additions & 30 deletions

File tree

schemas/water_level_csv.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# ===============================================================================
1616
from __future__ import annotations
1717

18-
from datetime import datetime, timezone
18+
from datetime import datetime
1919
from typing import Annotated
2020

2121
from core.enums import DataQuality, GroundwaterLevelReason, SampleMethod
@@ -29,7 +29,7 @@
2929
)
3030
from pydantic.functional_validators import BeforeValidator
3131

32-
from services.util import convert_dt_tz_naive_to_tz_aware
32+
from services.util import normalize_datetime_to_utc
3333

3434
WATER_LEVEL_REQUIRED_FIELDS = [
3535
"well_name_point_id",
@@ -84,18 +84,6 @@ def empty_str_to_none(value):
8484
OptionalFloat = Annotated[float | None, BeforeValidator(empty_str_to_none)]
8585

8686

87-
def _normalize_datetime_to_utc(value: datetime | str) -> datetime:
88-
if isinstance(value, str):
89-
value = datetime.fromisoformat(value)
90-
elif not isinstance(value, datetime):
91-
raise ValueError("value must be a datetime or ISO format string")
92-
93-
if value.tzinfo is None:
94-
value = convert_dt_tz_naive_to_tz_aware(value, "America/Denver")
95-
96-
return value.astimezone(timezone.utc)
97-
98-
9987
def _canonicalize_enum_value(
10088
value: str | None, enum_cls, field_name: str
10189
) -> str | None:
@@ -182,7 +170,7 @@ def normalize_sample_method(cls, value: str) -> str:
182170
)
183171
@classmethod
184172
def normalize_datetime_field(cls, value: datetime | str) -> datetime:
185-
return _normalize_datetime_to_utc(value)
173+
return normalize_datetime_to_utc(value)
186174

187175
@field_validator("depth_to_water_ft")
188176
@classmethod

schemas/well_inventory.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@
4848
AliasChoices,
4949
)
5050
from schemas import past_or_today_validator, PastOrTodayDatetime
51-
from services.util import convert_dt_tz_naive_to_tz_aware
52-
51+
from services.util import normalize_datetime_to_utc
5352

5453
def empty_str_to_none(v):
5554
if isinstance(v, str) and v.strip() == "":
@@ -362,19 +361,16 @@ def normalize_complete_monitoring_frequency(cls, data):
362361
return data
363362

364363
@field_validator("date_time", mode="before")
364+
@classmethod
365365
def make_date_time_tz_aware(cls, v):
366-
if isinstance(v, str):
367-
dt = datetime.fromisoformat(v)
368-
elif isinstance(v, datetime):
369-
dt = v
370-
else:
371-
raise ValueError("date_time must be a datetime or ISO format string")
372-
373-
if dt.tzinfo is None:
374-
aware_dt = convert_dt_tz_naive_to_tz_aware(dt, "America/Denver")
375-
return aware_dt
376-
else:
377-
raise ValueError("date_time must be a timezone-naive datetime")
366+
normalize_datetime_to_utc(v)
367+
368+
@field_validator("measurement_date_time", mode="before")
369+
@classmethod
370+
def normalize_measurement_date_time(cls, v):
371+
if v is None or (isinstance(v, str) and v.strip() == ""):
372+
return None
373+
return normalize_datetime_to_utc(v)
378374

379375
@model_validator(mode="after")
380376
def validate_model(self):

services/util.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ def transform_srid(geometry, source_srid, target_srid):
6464
return transform(transformer.transform, geometry)
6565

6666

67+
def normalize_datetime_to_utc(value: datetime | str) -> datetime:
68+
dt: datetime
69+
70+
if isinstance(value, str):
71+
dt = datetime.fromisoformat(value)
72+
elif isinstance(value, datetime):
73+
dt = value
74+
else:
75+
raise ValueError("value must be a datetime or ISO format string")
76+
77+
# Treat the datetime as "naive" if it has no tzinfo OR its tzinfo does not
78+
# provide a valid UTC offset (utcoffset() returns None). Some tzinfo
79+
# implementations can be attached but still behave like naive datetimes,
80+
# so we handle both cases before assigning a default timezone.
81+
if dt.tzinfo is None or dt.utcoffset() is None:
82+
dt = convert_dt_tz_naive_to_tz_aware(dt, "America/Denver")
83+
84+
return dt.astimezone(timezone.utc)
85+
6786
def convert_dt_tz_naive_to_tz_aware(
6887
dt_naive: datetime,
6988
iana_timezone: str = "America/Denver",
@@ -156,7 +175,7 @@ def get_county_from_point(lon: float, lat: float) -> str | None:
156175
return attrs["BASENAME"]
157176

158177

159-
def get_quad_name_from_point(lon: float, lat: float) -> str:
178+
def get_quad_name_from_point(lon: float, lat: float) -> str | None:
160179
url = "https://carto.nationalmap.gov/arcgis/rest/services/map_indices/MapServer/10/query"
161180
params = {
162181
"f": "json",

0 commit comments

Comments
 (0)