Skip to content

Commit

Permalink
fix: handle Z timezone in ISO8601 datetime format
Browse files Browse the repository at this point in the history
  • Loading branch information
jooola committed Jun 30, 2023
1 parent c821dbe commit 5b4965c
Show file tree
Hide file tree
Showing 19 changed files with 81 additions and 63 deletions.
11 changes: 11 additions & 0 deletions hcloud/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from datetime import datetime


def isoparse(value: str) -> datetime:
# Python <3.11 doesn't fully support parsing ISO8601 datetime strings. This
# workaround replaces the ending `Z` or `z` with `+00:00` and allows
# `datetime.fromisoformat` to parse the datetime string.
if value[-1] in "Zz":
value = value[:-1] + "+00:00"

return datetime.fromisoformat(value)
7 changes: 3 additions & 4 deletions hcloud/actions/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from .._exceptions import HCloudException
from ..core.domain import BaseDomain

Expand Down Expand Up @@ -51,8 +50,8 @@ def __init__(

self.status = status
self.progress = progress
self.started = datetime.fromisoformat(started) if started else None
self.finished = datetime.fromisoformat(finished) if finished else None
self.started = isoparse(started) if started else None
self.finished = isoparse(finished) if finished else None
self.resources = resources
self.error = error

Expand Down
13 changes: 4 additions & 9 deletions hcloud/certificates/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain, DomainIdentityMixin


Expand Down Expand Up @@ -59,13 +58,9 @@ def __init__(
self.certificate = certificate
self.domain_names = domain_names
self.fingerprint = fingerprint
self.not_valid_before = (
datetime.fromisoformat(not_valid_before) if not_valid_before else None
)
self.not_valid_after = (
datetime.fromisoformat(not_valid_after) if not_valid_after else None
)
self.created = datetime.fromisoformat(created) if created else None
self.not_valid_before = isoparse(not_valid_before) if not_valid_before else None
self.not_valid_after = isoparse(not_valid_after) if not_valid_after else None
self.created = isoparse(created) if created else None
self.labels = labels
self.status = status

Expand Down
7 changes: 3 additions & 4 deletions hcloud/deprecation/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand All @@ -25,7 +24,7 @@ def __init__(
announced=None,
unavailable_after=None,
):
self.announced = datetime.fromisoformat(announced) if announced else None
self.announced = isoparse(announced) if announced else None
self.unavailable_after = (
datetime.fromisoformat(unavailable_after) if unavailable_after else None
isoparse(unavailable_after) if unavailable_after else None
)
5 changes: 2 additions & 3 deletions hcloud/firewalls/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -30,7 +29,7 @@ def __init__(
self.rules = rules
self.applied_to = applied_to
self.labels = labels
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None


class FirewallRule:
Expand Down
5 changes: 2 additions & 3 deletions hcloud/floating_ips/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -72,7 +71,7 @@ def __init__(
self.blocked = blocked
self.protection = protection
self.labels = labels
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.name = name


Expand Down
7 changes: 3 additions & 4 deletions hcloud/images/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain, DomainIdentityMixin


Expand Down Expand Up @@ -85,11 +84,11 @@ def __init__(
self.id = id
self.name = name
self.type = type
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.description = description
self.image_size = image_size
self.disk_size = disk_size
self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None
self.deprecated = isoparse(deprecated) if deprecated else None
self.bound_to = bound_to
self.os_flavor = os_flavor
self.os_version = os_version
Expand Down
5 changes: 2 additions & 3 deletions hcloud/isos/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain, DomainIdentityMixin


Expand Down Expand Up @@ -36,4 +35,4 @@ def __init__(
self.type = type
self.architecture = architecture
self.description = description
self.deprecated = datetime.fromisoformat(deprecated) if deprecated else None
self.deprecated = isoparse(deprecated) if deprecated else None
5 changes: 2 additions & 3 deletions hcloud/load_balancers/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -76,7 +75,7 @@ def __init__(
):
self.id = id
self.name = name
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.public_net = public_net
self.private_net = private_net
self.location = location
Expand Down
5 changes: 2 additions & 3 deletions hcloud/networks/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -54,7 +53,7 @@ def __init__(
):
self.id = id
self.name = name
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.ip_range = ip_range
self.subnets = subnets
self.routes = routes
Expand Down
5 changes: 2 additions & 3 deletions hcloud/placement_groups/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -35,7 +34,7 @@ def __init__(
self.labels = labels
self.servers = servers
self.type = type
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None


class CreatePlacementGroupResponse(BaseDomain):
Expand Down
5 changes: 2 additions & 3 deletions hcloud/primary_ips/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -74,7 +73,7 @@ def __init__(
self.blocked = blocked
self.protection = protection
self.labels = labels
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.name = name
self.assignee_id = assignee_id
self.assignee_type = assignee_type
Expand Down
5 changes: 2 additions & 3 deletions hcloud/servers/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain


Expand Down Expand Up @@ -113,7 +112,7 @@ def __init__(
self.id = id
self.name = name
self.status = status
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.public_net = public_net
self.server_type = server_type
self.datacenter = datacenter
Expand Down
5 changes: 2 additions & 3 deletions hcloud/ssh_keys/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain, DomainIdentityMixin


Expand Down Expand Up @@ -36,4 +35,4 @@ def __init__(
self.fingerprint = fingerprint
self.public_key = public_key
self.labels = labels
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
5 changes: 2 additions & 3 deletions hcloud/volumes/domain.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime

from .._compat import isoparse
from ..core.domain import BaseDomain, DomainIdentityMixin


Expand Down Expand Up @@ -66,7 +65,7 @@ def __init__(
self.id = id
self.name = name
self.server = server
self.created = datetime.fromisoformat(created) if created else None
self.created = isoparse(created) if created else None
self.location = location
self.size = size
self.linux_device = linux_device
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/core/test_domain.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from datetime import datetime

import pytest

from hcloud._compat import isoparse
from hcloud.core.domain import (
BaseDomain,
DomainIdentityMixin,
Expand Down Expand Up @@ -102,7 +101,7 @@ class ActionDomain(BaseDomain, DomainIdentityMixin):
def __init__(self, id, name="name1", started=None):
self.id = id
self.name = name
self.started = datetime.fromisoformat(started) if started else None
self.started = isoparse(started) if started else None


class TestBaseDomain:
Expand All @@ -126,7 +125,7 @@ class TestBaseDomain:
{
"id": 4,
"name": "name-name3",
"started": datetime.fromisoformat("2016-01-30T23:50+00:00"),
"started": isoparse("2016-01-30T23:50+00:00"),
},
),
],
Expand Down
6 changes: 2 additions & 4 deletions tests/unit/networks/test_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from datetime import datetime
from unittest import mock

import pytest

from hcloud._compat import isoparse
from hcloud.actions.client import BoundAction
from hcloud.networks.client import BoundNetwork, NetworksClient
from hcloud.networks.domain import Network, NetworkRoute, NetworkSubnet
Expand All @@ -20,9 +20,7 @@ def test_bound_network_init(self, network_response):
)

assert bound_network.id == 1
assert bound_network.created == datetime.fromisoformat(
"2016-01-30T23:50:11+00:00"
)
assert bound_network.created == isoparse("2016-01-30T23:50:11+00:00")
assert bound_network.name == "mynet"
assert bound_network.ip_range == "10.0.0.0/16"
assert bound_network.protection["delete"] is False
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from datetime import datetime, timezone

import pytest

from hcloud._compat import isoparse


@pytest.mark.parametrize(
["value", "expected"],
[
(
"2023-06-29T15:37:22",
datetime(2023, 6, 29, 15, 37, 22),
),
(
"2023-06-29T15:37:22+00:00",
datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc),
),
(
"2023-06-29T15:37:22Z",
datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc),
),
(
"2023-06-29T15:37:22z",
datetime(2023, 6, 29, 15, 37, 22, tzinfo=timezone.utc),
),
],
)
def test_isoparse(value: str, expected: datetime):
assert isoparse(value) == expected
6 changes: 2 additions & 4 deletions tests/unit/volumes/test_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from datetime import datetime
from unittest import mock

import pytest

from hcloud._compat import isoparse
from hcloud.actions.client import BoundAction
from hcloud.locations.client import BoundLocation
from hcloud.locations.domain import Location
Expand All @@ -23,9 +23,7 @@ def test_bound_volume_init(self, volume_response):
)

assert bound_volume.id == 1
assert bound_volume.created == datetime.fromisoformat(
"2016-01-30T23:50:11+00:00"
)
assert bound_volume.created == isoparse("2016-01-30T23:50:11+00:00")
assert bound_volume.name == "database-storage"
assert isinstance(bound_volume.server, BoundServer)
assert bound_volume.server.id == 12
Expand Down

0 comments on commit 5b4965c

Please sign in to comment.