"""Test NormalizedDatTime.""" from datetime import date, datetime, timezone import pytest from sqlalchemy.engine import default from sqlalchemy.types import DateTime as SADateTime import abacus_common_logic.models.normalized_date_time as ndt_mod from abacus_common_logic.models.normalized_date_time import ( NormalizedDateTime, to_datetime, ) @pytest.fixture(scope='module') def sa_dialect(): """Return a SQLAlchemy dialect.""" return default.DefaultDialect() @pytest.fixture(autouse=True) def force_system_tz_utc(monkeypatch): """Enforce system timezone to make tests deterministic.""" monkeypatch.setattr(ndt_mod, 'SYSTEM_TIMEZONE', timezone.utc) yield # to_datetime def test_to_datetime_none_and_empty(): """Test to_datetime with none or empty.""" assert to_datetime(None) is None assert to_datetime('') is None assert to_datetime(' ') is None def test_to_datetime_datetime_naive_pass_through(): """Test to_datetime with naive datetime.""" dt = datetime(2025, 9, 29, 15, 30, 45) assert to_datetime(dt) == dt def test_to_datetime_datetime_aware_converts_to_utc_and_strips_tz(): """Test to_datetime with UTC datetime.""" aware = datetime(2025, 9, 29, 15, 30, 45, tzinfo=timezone.utc) assert to_datetime(aware) == datetime(2025, 9, 29, 15, 30, 45) def test_to_datetime_date_to_midnight(): """Test to_datetime with a date.""" d = date(2022, 12, 31) assert to_datetime(d) == datetime(2022, 12, 31, 0, 0, 0) def test_to_datetime_epoch_seconds(): """Test to_datetime with seconds.""" for epoch in [0, 1_600_000_000, 1_600_000_000.0]: expected = datetime.fromtimestamp(epoch) assert to_datetime(epoch) == expected def test_to_datetime_bytes_utf8(): """Test to_datetime with bytes.""" assert to_datetime(b'2024-01-02T03:04:05') == datetime(2024, 1, 2, 3, 4, 5) assert to_datetime(bytearray(b'2025-01-02')) == datetime(2025, 1, 2, 0, 0, 0) def test_to_datetime_bytes_invalid_utf8_raises(): """Test to_datetime with invalid bytes.""" with pytest.raises(UnicodeDecodeError): to_datetime(b'\xff\xfe\xfa') def test_to_datetime_iso_datetime_no_tz(): """Test to_datetime with an iso datetime.""" assert to_datetime('2020-05-17T23:59:59') == datetime(2020, 5, 17, 23, 59, 59) def test_to_datetime_iso_datetime_with_z_suffix(): """Test to_datetime with an iso datetime and Z suffix.""" assert to_datetime('2020-05-17T23:59:59Z') == datetime(2020, 5, 17, 23, 59, 59) def test_to_datetime_iso_datetime_with_offset_to_utc_naive(): """Test to_datetime with timezoned datetime.""" assert to_datetime('2020-05-18T00:30:00+02:00') == datetime(2020, 5, 17, 22, 30, 0) def test_to_datetime_iso_date_only(): """Test to_datetime with an iso date.""" assert to_datetime('2020-05-17') == datetime(2020, 5, 17, 0, 0, 0) def test_to_datetime_fallback_formats_date_only_and_datetime(): """Test to_datetime fallback formats.""" # Date only assert to_datetime('2025/03/05') == datetime(2025, 3, 5, 0, 0, 0) # %Y/%m/%d assert to_datetime('03/05/2025') == datetime(2025, 3, 5, 0, 0, 0) # %m/%d/%Y assert to_datetime('05-03-2025') == datetime(2025, 3, 5, 0, 0, 0) # %d-%m-%Y # Datetime assert to_datetime('2025-03-05 01:02') == datetime( 2025, 3, 5, 1, 2, 0 ) # %Y-%m-%d %H:%M assert to_datetime('2025-03-05 01:02:03') == datetime( 2025, 3, 5, 1, 2, 3 ) # %Y-%m-%d %H:%M:%S assert to_datetime('2025-03-05 01:02:03.123456') == datetime( 2025, 3, 5, 1, 2, 3, 123456 ) # %Y-%m-%d %H:%M:%S.%f def test_to_datetime_invalid_inputs_raise_typeerror(): """Test to_datetime with bad inputs.""" for bad_value in ['not-a-datetime', '2024-13-40', '05/35/2024 99:99:99', object()]: with pytest.raises(TypeError): to_datetime(bad_value) # NormalizedDateTime def test_normalized_datetime_metadata(): """Test NormalizedDateTime metadata.""" # impl is an instance of SADateTime with timezone=True assert isinstance(NormalizedDateTime.impl, SADateTime) assert NormalizedDateTime.impl.timezone is True def test_normalized_datetime_process_bind_param(sa_dialect): """Test NormalizedDateTime param normalization.""" nd = NormalizedDateTime() # Valid coercions assert nd.process_bind_param(date(2021, 1, 2), dialect=sa_dialect) == datetime( 2021, 1, 2 ) assert nd.process_bind_param( datetime(2021, 1, 2, 12, 0, 0), dialect=sa_dialect ) == datetime(2021, 1, 2, 12, 0, 0) assert nd.process_bind_param('2021-01-02', dialect=sa_dialect) == datetime( 2021, 1, 2 ) assert nd.process_bind_param( '2021-01-02T00:00:00Z', dialect=sa_dialect ) == datetime(2021, 1, 2, 0, 0, 0) assert nd.process_bind_param( '2021-01-02T01:00:00+01:00', dialect=sa_dialect ) == datetime(2021, 1, 2, 0, 0, 0) # UTC assert nd.process_bind_param( '2025-03-05 01:02:03.123456', dialect=sa_dialect ) == datetime(2025, 3, 5, 1, 2, 3, 123456) # None / empty assert nd.process_bind_param(None, dialect=sa_dialect) is None assert nd.process_bind_param('', dialect=sa_dialect) is None # Epochs expected_epoch = datetime.fromtimestamp(1_600_000_000) assert nd.process_bind_param(1_600_000_000, dialect=sa_dialect) == expected_epoch assert nd.process_bind_param(1_600_000_000.0, dialect=sa_dialect) == expected_epoch # Bytes assert nd.process_bind_param( b'2022-02-03T04:05:06', dialect=sa_dialect ) == datetime(2022, 2, 3, 4, 5, 6) def test_normalized_datetime_process_bind_param_invalid_raises(sa_dialect): """Test NormalizedDateTime invalid params.""" nd = NormalizedDateTime() with pytest.raises(TypeError): nd.process_bind_param('not-a-datetime', dialect=sa_dialect) def test_normalized_datetime_process_result_value(sa_dialect): """Test NormalizedDateTime result normalization.""" nd = NormalizedDateTime() assert nd.process_result_value(date(2021, 1, 2), dialect=sa_dialect) == datetime( 2021, 1, 2 ) assert nd.process_result_value( datetime(2021, 1, 2, 12, 0, 0), dialect=sa_dialect ) == datetime(2021, 1, 2, 12, 0, 0) assert nd.process_result_value('2021-01-02', dialect=sa_dialect) == datetime( 2021, 1, 2 ) assert nd.process_result_value( '2021-01-02T12:30:00+00:00', dialect=sa_dialect ) == datetime(2021, 1, 2, 12, 30, 0) assert nd.process_result_value(None, dialect=sa_dialect) is None