Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/imednet/utils/validators.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand Down
42 changes: 42 additions & 0 deletions tests/unit/test_parse_datetime_robustness.py
Original file line number Diff line number Diff line change
@@ -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)