From b9ba0f83e36522298b9cf7964193416247aee848 Mon Sep 17 00:00:00 2001 From: SwapnilV Date: Wed, 7 Feb 2024 22:48:27 +0530 Subject: [PATCH 1/2] Devices: Added convertion of datetime strings to python datetime objects Fix issue at status_updated_at Fix: PEP 8 correction Fix: Version correction Added check for datetime string format + tests --- shellhub/__init__.py | 2 +- shellhub/models/device.py | 20 ++++++++++++++------ tests/test_devices.py | 40 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/shellhub/__init__.py b/shellhub/__init__.py index e7ca9b2..bfe43a1 100644 --- a/shellhub/__init__.py +++ b/shellhub/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.2.4" +__version__ = "0.3.0" from .models.device import ShellHubDevice, ShellHubDeviceInfo from .models.base import ShellHub diff --git a/shellhub/models/device.py b/shellhub/models/device.py index 4a5a40c..7b34f4d 100644 --- a/shellhub/models/device.py +++ b/shellhub/models/device.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Dict from typing import List from typing import Optional @@ -40,12 +41,12 @@ class ShellHubDevice: info: ShellHubDeviceInfo public_key: str tenant_id: str - last_seen: str + last_seen: datetime online: bool namespace: str status: str - status_updated_at: str - created_at: str + status_updated_at: datetime + created_at: datetime remote_addr: str tags: List[str] acceptable: bool @@ -59,16 +60,23 @@ def __init__(self, api_object: shellhub.models.base.ShellHub, device_json): # t self.info = ShellHubDeviceInfo(device_json["info"]) self.public_key = device_json["public_key"] self.tenant_id = device_json["tenant_id"] - self.last_seen = device_json["last_seen"] + self.last_seen = self._safe_isoformat_to_datetime(device_json["last_seen"]) self.online = device_json["online"] self.namespace = device_json["namespace"] self.status = device_json["status"] - self.status_updated_at = device_json["status_updated_at"] - self.created_at = device_json["created_at"] + self.status_updated_at = self._safe_isoformat_to_datetime(device_json["status_updated_at"]) + self.created_at = self._safe_isoformat_to_datetime(device_json["created_at"]) self.remote_addr = device_json["remote_addr"] self.tags = device_json["tags"] self.acceptable = device_json["acceptable"] + @staticmethod + def _safe_isoformat_to_datetime(date_string: str) -> datetime: + try: + return datetime.fromisoformat(date_string) + except ValueError as e: + raise ShellHubApiError(f"Invalid date string: {date_string} (Couldn't convert to datetime)") from e + def delete(self) -> bool: """ Delete the device from the API diff --git a/tests/test_devices.py b/tests/test_devices.py index 29dfd8d..bcbe31c 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -1,3 +1,5 @@ +from datetime import datetime + import pytest from shellhub.exceptions import ShellHubApiError @@ -118,16 +120,48 @@ def test_get_device(self, shellhub, requests_mock): == "-----BEGIN RSA PUBLIC KEY-----\nxxx\nxxx\nxxx\nxxx\nxxx\nxxx\n-----END RSA PUBLIC KEY-----\n" ) assert device.tenant_id == "1" - assert device.last_seen == "1970-01-01T00:00:00Z" + assert device.last_seen == datetime.fromisoformat("1970-01-01T00:00:00Z") assert device.online assert device.namespace == "dev" assert device.status == "accepted" - assert device.status_updated_at == "1970-01-01T00:00:00Z" - assert device.created_at == "1970-01-01T00:00:00Z" + assert device.status_updated_at == datetime.fromisoformat("1970-01-01T00:00:00Z") + assert device.created_at == datetime.fromisoformat("1970-01-01T00:00:00Z") assert device.remote_addr == "0.0.0.0" assert device.tags == [] assert not device.acceptable + def test_get_incorrect_datetime_format(self, shellhub, requests_mock): + mock_response = { + "uid": "1", + "name": "default", + "identity": {"mac": "06:04:ju:le:s7:08"}, + "info": { + "id": "ubuntu", + "pretty_name": "Ubuntu 20.04.2 LTS", + "version": "v0.14.1", + "arch": "amd64", + "platform": "docker", + }, + "public_key": "-----BEGIN RSA PUBLIC KEY-----\nxxx\nxxx\nxxx\n" + "xxx\nxxx\nxxx\n-----END RSA PUBLIC KEY-----\n", + "tenant_id": "1", + "last_seen": "-1", + "online": True, + "namespace": "dev", + "status": "accepted", + "status_updated_at": "-1", + "created_at": "-1", + "remote_addr": "0.0.0.0", + "position": {"latitude": 0, "longitude": 0}, + "tags": [], + "public_url": False, + "public_url_address": "", + "acceptable": False, + } + requests_mock.get(f"{MOCKED_DOMAIN_URL}/api/devices/1", json=mock_response) + with pytest.raises(ShellHubApiError): + shellhub.get_device("1") + class TestDeleteDevice: def test_delete_device(self, shellhub_device, requests_mock): From 530f211284c28be65a31d8b171bb3df19a579ec6 Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Fri, 9 Feb 2024 10:43:58 +0100 Subject: [PATCH 2/2] Devices: Handling previous versions of Python on datetime conversions --- shellhub/models/device.py | 19 +++++++++++++++++-- tests/test_devices.py | 8 +++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/shellhub/models/device.py b/shellhub/models/device.py index 7b34f4d..1929b3f 100644 --- a/shellhub/models/device.py +++ b/shellhub/models/device.py @@ -72,10 +72,25 @@ def __init__(self, api_object: shellhub.models.base.ShellHub, device_json): # t @staticmethod def _safe_isoformat_to_datetime(date_string: str) -> datetime: + # Replace "Z" with "+00:00" to indicate UTC in a format compatible with Python 3.7-3.10. + if date_string.endswith("Z"): + date_string = date_string[:-1] + "+00:00" try: + # Direct conversion using fromisoformat return datetime.fromisoformat(date_string) - except ValueError as e: - raise ShellHubApiError(f"Invalid date string: {date_string} (Couldn't convert to datetime)") from e + except ValueError: + try: + # For Python versions that do not handle offset-aware datetimes well in fromisoformat + # This part is more of a catch-all to ensure even non-standard or unexpected formats + # might be parsed, but primarily, the first attempt should work for ISO 8601 formats. + # Note: strptime might not be necessary if fromisoformat works after the 'Z' to '+00:00' replacement, + # but it's here as an example if further customization is needed. + return datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S%z") + except ValueError as e: + # If the first attempt fails due to the format not being exactly ISO 8601 after 'Z' replacement, + # this additional attempt can catch other variations. This might not be strictly necessary, + # depending on your input formats. + raise ShellHubApiError(f"Invalid date string: {date_string} (Couldn't convert to datetime)") from e def delete(self) -> bool: """ diff --git a/tests/test_devices.py b/tests/test_devices.py index bcbe31c..64b8524 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -1,4 +1,5 @@ from datetime import datetime +from datetime import timezone import pytest @@ -106,6 +107,7 @@ def test_get_device(self, shellhub, requests_mock): } requests_mock.get(f"{MOCKED_DOMAIN_URL}/api/devices/1", json=mock_response) device = shellhub.get_device("1") + origin_time = datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) assert device.uid == "1" assert device.name == "default" @@ -120,12 +122,12 @@ def test_get_device(self, shellhub, requests_mock): == "-----BEGIN RSA PUBLIC KEY-----\nxxx\nxxx\nxxx\nxxx\nxxx\nxxx\n-----END RSA PUBLIC KEY-----\n" ) assert device.tenant_id == "1" - assert device.last_seen == datetime.fromisoformat("1970-01-01T00:00:00Z") + assert device.last_seen == origin_time assert device.online assert device.namespace == "dev" assert device.status == "accepted" - assert device.status_updated_at == datetime.fromisoformat("1970-01-01T00:00:00Z") - assert device.created_at == datetime.fromisoformat("1970-01-01T00:00:00Z") + assert device.status_updated_at == origin_time + assert device.created_at == origin_time assert device.remote_addr == "0.0.0.0" assert device.tags == [] assert not device.acceptable