Skip to content
Closed
Changes from 1 commit
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
159 changes: 159 additions & 0 deletions unit_tests/utils/test_datetime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import freezegun
import pytest
from dateutil import parser
from whenever import Instant

from airbyte_cdk.utils.datetime_helpers import (
AirbyteDateTime,
Expand Down Expand Up @@ -262,3 +264,160 @@ def test_epoch_millis():
# Test roundtrip conversion
dt3 = AirbyteDateTime.from_epoch_millis(dt.to_epoch_millis())
assert dt3 == dt


@pytest.mark.parametrize(
"input_value,expected_parser",
[
# Formats that use the whenever parser
("2023-03-14", "whenever"), # Date-only format
(1678806566, "whenever"), # Unix timestamp
# Formats that use the dateutil parser
("2023-03-14T15:09:26Z", "dateutil"), # ISO format with T delimiter
("2023-03-14T15:09:26+00:00", "dateutil"), # ISO format with timezone
("2023-03-14 15:09:26", "dateutil"), # Missing T delimiter
("14/03/2023 15:09:26", "dateutil"), # Different date format
],
)
def test_datetime_parser_selection(input_value, expected_parser, monkeypatch):
"""Test that the correct parser is used based on the input format."""
# Create tracking variables
whenever_called = False
dateutil_called = False

# Store original functions
original_instant_module = __import__("whenever").Instant
original_parser_parse = parser.parse

# Create a spy for Instant.from_timestamp
def spy_from_timestamp(*args, **kwargs):
nonlocal whenever_called
whenever_called = True
# Call the original function
return original_instant_module.from_timestamp(*args, **kwargs)

# Create a spy for Instant.from_utc
def spy_from_utc(*args, **kwargs):
nonlocal whenever_called
whenever_called = True
# Call the original function
return original_instant_module.from_utc(*args, **kwargs)

# Create a spy for parser.parse
def spy_parser_parse(*args, **kwargs):
nonlocal dateutil_called
dateutil_called = True
# Call the original function
return original_parser_parse(*args, **kwargs)

# Create a mock Instant class with our spy methods
class MockInstant:
@staticmethod
def from_timestamp(*args, **kwargs):
return spy_from_timestamp(*args, **kwargs)

@staticmethod
def from_utc(*args, **kwargs):
return spy_from_utc(*args, **kwargs)

# Add any other methods that might be called
@staticmethod
def py_datetime():
return original_instant_module.py_datetime()

# Apply the mocks at the module level
monkeypatch.setattr("airbyte_cdk.utils.datetime_helpers.Instant", MockInstant)
monkeypatch.setattr("airbyte_cdk.utils.datetime_helpers.parser.parse", spy_parser_parse)

# Skip formats that would be rejected by validation checks
if isinstance(input_value, str) and "March" in input_value:
# Skip this test case as it would be rejected by validation
return

# Parse the datetime
ab_datetime_parse(input_value)

# Check which parser was used
if expected_parser == "whenever":
assert whenever_called, f"Expected whenever parser to be used for {input_value}"
assert not dateutil_called, f"Did not expect dateutil parser to be used for {input_value}"
else:
assert dateutil_called, f"Expected dateutil parser to be used for {input_value}"
assert not whenever_called, f"Did not expect whenever parser to be used for {input_value}"


def test_whenever_parser_for_iso_formats(monkeypatch):
"""Test that the whenever parser is used for certain formats even when dateutil is unavailable."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameterized test seems to cover these cases. Can we delete this test?


# Create a mock dateutil.parser.parse that always raises an exception
def mock_parser_parse(dt_str, **kwargs):
raise ValueError("dateutil parser is unavailable")

# Apply the mock at the module level
monkeypatch.setattr("airbyte_cdk.utils.datetime_helpers.parser.parse", mock_parser_parse)

# These formats should still parse correctly using the whenever parser
whenever_formats = [
"2023-03-14", # Date-only format
1678806566, # Unix timestamp
]

for dt_str in whenever_formats:
# This should not raise an exception because the whenever parser should be used
result = ab_datetime_parse(dt_str)
assert isinstance(result, AirbyteDateTime)


def test_dateutil_fallback_for_non_iso_formats(monkeypatch):
"""Test that the dateutil parser is used as a fallback for non-ISO/RFC compliant formats."""
# Create tracking variables
whenever_called = False
dateutil_called = False

# Store original functions
original_instant_module = __import__("whenever").Instant
original_parser_parse = parser.parse

# Create a mock Instant class with methods that always raise exceptions
class MockInstant:
@staticmethod
def from_timestamp(*args, **kwargs):
nonlocal whenever_called
whenever_called = True
raise ValueError("whenever parser is unavailable")

@staticmethod
def from_utc(*args, **kwargs):
nonlocal whenever_called
whenever_called = True
raise ValueError("whenever parser is unavailable")

# Create a spy for parser.parse
def spy_parser_parse(*args, **kwargs):
nonlocal dateutil_called
dateutil_called = True
return original_parser_parse(*args, **kwargs)

# Apply the mocks at the module level
monkeypatch.setattr("airbyte_cdk.utils.datetime_helpers.Instant", MockInstant)
monkeypatch.setattr("airbyte_cdk.utils.datetime_helpers.parser.parse", spy_parser_parse)

# These non-ISO/RFC formats should use the dateutil parser
non_iso_formats = [
"2023-03-14T15:09:26Z", # ISO format with T delimiter
"2023-03-14T15:09:26+00:00", # ISO format with timezone
"2023-03-14 15:09:26", # Missing T delimiter
"14/03/2023 15:09:26", # Different date format
]

for dt_str in non_iso_formats:
# Skip formats that would be rejected by validation checks
if "March" in dt_str:
continue

# This should not raise an exception because the dateutil parser should be used
result = ab_datetime_parse(dt_str)
assert isinstance(result, AirbyteDateTime)
assert dateutil_called, f"Expected dateutil parser to be used for {dt_str}"
# Reset the flag for the next iteration
dateutil_called = False
Loading