diff --git a/src/imednet/utils/validators.py b/src/imednet/utils/validators.py index 902c0239..5e1ed35a 100644 --- a/src/imednet/utils/validators.py +++ b/src/imednet/utils/validators.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Callable, Dict, List, TypeVar from imednet.utils.dates import parse_iso_datetime # Centralized date parsing @@ -19,17 +19,23 @@ _SENTINEL_DATETIME = datetime(1969, 4, 20, 16, 20) -def parse_datetime(v: str | datetime) -> datetime: - """Parse an ISO datetime string or return a sentinel value. +def parse_datetime(v: str | int | float | datetime) -> datetime: + """Parse an ISO datetime string, numeric timestamp, or return a sentinel value. The SDK historically returns ``datetime(1969, 4, 20, 16, 20)`` when a timestamp field is empty. This helper mirrors that behaviour for backward compatibility. + + Args: + v: Date string, numeric timestamp (seconds since epoch), or datetime object. + Numeric values are assumed to be UTC timestamps. """ if not v: return _SENTINEL_DATETIME if isinstance(v, str): return parse_iso_datetime(v) + if isinstance(v, (int, float)): + return datetime.fromtimestamp(v, tz=timezone.utc) return v diff --git a/tests/unit/test_parse_datetime_robustness.py b/tests/unit/test_parse_datetime_robustness.py new file mode 100644 index 00000000..778b4730 --- /dev/null +++ b/tests/unit/test_parse_datetime_robustness.py @@ -0,0 +1,42 @@ +from datetime import datetime, timezone + +from imednet.utils.validators import parse_datetime + + +def test_parse_datetime_int_timestamp(): + """Test that parse_datetime correctly handles integer timestamps (seconds).""" + ts = 1609459200 # 2021-01-01 00:00:00 UTC + result = parse_datetime(ts) + assert isinstance(result, datetime) + assert result == datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + + +def test_parse_datetime_float_timestamp(): + """Test that parse_datetime correctly handles float timestamps (seconds).""" + ts = 1609459200.5 # 2021-01-01 00:00:00.5 UTC + result = parse_datetime(ts) + assert isinstance(result, datetime) + assert result == datetime(2021, 1, 1, 0, 0, 0, 500000, tzinfo=timezone.utc) + + +def test_parse_datetime_negative_timestamp(): + """Test that parse_datetime correctly handles negative timestamps (historic dates).""" + ts = -2208988800 # 1900-01-01 00:00:00 UTC + result = parse_datetime(ts) + assert isinstance(result, datetime) + # Note: Depending on platform/implementation, negative timestamps might behave differently, + # but Python's fromtimestamp usually handles them on modern systems. + # We'll check it works generally. + assert result.year == 1900 + assert result.month == 1 + assert result.day == 1 + assert result.tzinfo == timezone.utc + + +def test_parse_datetime_zero_timestamp(): + """Test that parse_datetime treats 0 (epoch) as empty/sentinel due to legacy falsy check.""" + ts = 0 + result = parse_datetime(ts) + # Current behavior: 0 is falsy -> Sentinel. + # This documents existing behavior rather than enforcing a change. + assert result == datetime(1969, 4, 20, 16, 20)