diff --git a/sonarr/models.py b/sonarr/models.py index 9e6047cc..103c50d2 100644 --- a/sonarr/models.py +++ b/sonarr/models.py @@ -7,6 +7,29 @@ from .exceptions import SonarrError +def dt_str_to_dt(dt_str: str) -> datetime: + """Convert ISO-8601 datetime string to datetime object.""" + utc = False + + if "Z" in dt_str: + utc = True + dt_str = dt_str[:-1] + + if "." in dt_str: + # Python doesn't support long microsecond values + ts_bits = dt_str.split(".", 1) + dt_str = "{}.{}".format(ts_bits[0], ts_bits[1][:2]) + fmt = "%Y-%m-%dT%H:%M:%S.%f" + else: + fmt = "%Y-%m-%dT%H:%M:%S" + + if utc: + dt_str += "Z" + fmt += "%z" + + return datetime.strptime(dt_str, fmt) + + @dataclass(frozen=True) class Disk: """Object holding disk information from Sonarr.""" @@ -85,15 +108,15 @@ def from_dict(data: dict): """Return Series object from Sonarr API response.""" premiere = data.get("firstAired", None) if premiere is not None: - premiere = datetime.strptime(premiere, "%Y-%m-%dT%H:%M:%S%z") + premiere = dt_str_to_dt(premiere) added = data.get("added", None) if added is not None: - added = datetime.strptime(added, "%Y-%m-%dT%H:%M:%S.%f%z") + added = dt_str_to_dt(added) synced = data.get("lastInfoSync", None) if synced is not None: - synced = datetime.strptime(synced, "%Y-%m-%dT%H:%M:%S.%f%z") + synced = dt_str_to_dt(synced) poster = None for image in data.get("images", []): @@ -151,7 +174,7 @@ def from_dict(data: dict): """Return Episode object from Sonarr API response.""" airs = data.get("airDateUtc", None) if airs is not None: - airs = datetime.strptime(airs, "%Y-%m-%dT%H:%M:%S%z") + airs = dt_str_to_dt(airs) episode_number = data.get("episodeNumber", 0) season_number = data.get("seasonNumber", 0) @@ -215,14 +238,14 @@ def from_dict(data: dict): queued = started if started is not None: - started = datetime.strptime(started, "%Y-%m-%dT%H:%M:%S.%f%z") + started = dt_str_to_dt(started) if queued is not None: - queued = datetime.strptime(queued, "%Y-%m-%dT%H:%M:%S.%f%z") + queued = dt_str_to_dt(queued) changed = data.get("stateChangeTime", None) if changed is not None: - changed = datetime.strptime(changed, "%Y-%m-%dT%H:%M:%S.%f%z") + changed = dt_str_to_dt(changed) return CommandItem( command_id=data.get("id", 0), @@ -264,7 +287,7 @@ def from_dict(data: dict): eta = data.get("estimatedCompletionTime", None) if eta is not None: - eta = datetime.strptime(eta, "%Y-%m-%dT%H:%M:%S.%f%z") + eta = dt_str_to_dt(eta) return QueueItem( queue_id=data.get("id", 0), diff --git a/tests/test_models.py b/tests/test_models.py index 94bfdb24..373907b0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,6 +40,18 @@ def test_application_no_data() -> None: models.Application({}) +def test_dt_str_to_dt() -> None: + """Test the dt_str_to_dt method.""" + dt = models.dt_str_to_dt("2018-05-14T19:02:13.101496Z") + assert dt == datetime(2018, 5, 14, 19, 2, 13, 100000, tzinfo=timezone.utc) + + +def test_dt_str_to_dt_long_microseconds() -> None: + """Test the dt_str_to_dt method with long microseconds.""" + dt = models.dt_str_to_dt("2018-05-14T19:02:13.1014986Z") + assert dt == datetime(2018, 5, 14, 19, 2, 13, 100000, tzinfo=timezone.utc) + + def test_info() -> None: """Test the Info model.""" info = models.Info.from_dict(INFO) @@ -59,9 +71,9 @@ def test_command_item() -> None: assert item.state == "started" assert item.priority == "normal" assert item.trigger == "manual" - assert item.started == datetime(2020, 4, 6, 16, 54, 6, 421322, tzinfo=timezone.utc) - assert item.queued == datetime(2020, 4, 6, 16, 54, 6, 419450, tzinfo=timezone.utc) - assert item.changed == datetime(2020, 4, 6, 16, 54, 6, 421322, tzinfo=timezone.utc) + assert item.started == datetime(2020, 4, 6, 16, 54, 6, 420000, tzinfo=timezone.utc) + assert item.queued == datetime(2020, 4, 6, 16, 54, 6, 410000, tzinfo=timezone.utc) + assert item.changed == datetime(2020, 4, 6, 16, 54, 6, 420000, tzinfo=timezone.utc) item = models.CommandItem.from_dict(COMMAND[1]) @@ -71,9 +83,9 @@ def test_command_item() -> None: assert item.state == "started" assert item.priority == "unknown" assert item.trigger == "unknown" - assert item.started == datetime(2020, 4, 6, 16, 57, 51, 406504, tzinfo=timezone.utc) - assert item.queued == datetime(2020, 4, 6, 16, 57, 51, 406504, tzinfo=timezone.utc) - assert item.changed == datetime(2020, 4, 6, 16, 57, 51, 417931, tzinfo=timezone.utc) + assert item.started == datetime(2020, 4, 6, 16, 57, 51, 400000, tzinfo=timezone.utc) + assert item.queued == datetime(2020, 4, 6, 16, 57, 51, 400000, tzinfo=timezone.utc) + assert item.changed == datetime(2020, 4, 6, 16, 57, 51, 410000, tzinfo=timezone.utc) def test_episode() -> None: @@ -125,7 +137,7 @@ def test_queue_item() -> None: assert item.protocol == "usenet" assert item.size == 4472186820 assert item.size_remaining == 0 - assert item.eta == datetime(2016, 2, 5, 22, 46, 52, 440104, tzinfo=timezone.utc) + assert item.eta == datetime(2016, 2, 5, 22, 46, 52, 440000, tzinfo=timezone.utc) assert item.time_remaining == "00:00:00" assert item.episode @@ -177,10 +189,10 @@ def test_series() -> None: assert series.certification == "TV-14" assert series.genres == ["Animation", "Comedy"] assert series.added == datetime( - 2011, 1, 26, 19, 25, 55, 455594, tzinfo=timezone.utc + 2011, 1, 26, 19, 25, 55, 450000, tzinfo=timezone.utc ) assert series.synced == datetime( - 2014, 1, 26, 19, 25, 55, 455594, tzinfo=timezone.utc + 2014, 1, 26, 19, 25, 55, 450000, tzinfo=timezone.utc ) @@ -228,10 +240,10 @@ def test_series_item() -> None: assert item.series.certification == "TV-G" assert item.series.genres == ["Comedy"] assert item.series.added == datetime( - 2020, 4, 5, 20, 40, 20, 50044, tzinfo=timezone.utc + 2020, 4, 5, 20, 40, 20, 50000, tzinfo=timezone.utc ) assert item.series.synced == datetime( - 2020, 4, 5, 20, 40, 21, 545669, tzinfo=timezone.utc + 2020, 4, 5, 20, 40, 21, 540000, tzinfo=timezone.utc ) assert item.seasons