From 91c5485d4c1cc4d436d9ef5749983f03c744df4d Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Thu, 8 Feb 2024 18:47:00 +0100 Subject: [PATCH 1/6] Core: Fixed models import --- pyproject.toml | 5 ++++- shellhub/__init__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e02cddc..c08f224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,10 @@ classifiers=[ dependencies = ["requests>=2.31.0"] [tool.setuptools] -packages = ["shellhub"] +packages = [ + "shellhub", + "shellhub.models", +] [tool.setuptools.dynamic] version = {attr = "shellhub.__version__"} diff --git a/shellhub/__init__.py b/shellhub/__init__.py index 0222d04..6915165 100644 --- a/shellhub/__init__.py +++ b/shellhub/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.2.0" +__version__ = "0.2.1" from .models.device import ShellHubDevice, ShellHubDeviceInfo from .models.base import ShellHub From a6631514c52329157eb98eb1d00bffb6b3bb06f7 Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Fri, 9 Feb 2024 10:25:18 +0100 Subject: [PATCH 2/6] Core: Fixed accept device --- shellhub/__init__.py | 2 +- shellhub/models/base.py | 17 ++++++++++++++++- shellhub/models/device.py | 22 +++++++++++++++++++--- tests/test_devices.py | 4 ++-- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/shellhub/__init__.py b/shellhub/__init__.py index 6915165..3bd34e6 100644 --- a/shellhub/__init__.py +++ b/shellhub/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.2.1" +__version__ = "0.2.2" from .models.device import ShellHubDevice, ShellHubDeviceInfo from .models.base import ShellHub diff --git a/shellhub/models/base.py b/shellhub/models/base.py index 799c0ef..18154b0 100644 --- a/shellhub/models/base.py +++ b/shellhub/models/base.py @@ -33,6 +33,12 @@ def __init__(self, username: str, password: str, endpoint_or_url: str, use_ssl: self._login() def _format_and_validate_url(self, endpoint: str) -> Tuple[str, str]: + """ + Format and validate the URL provided by the user. If the URL doesn't start with http:// or https://, it will + :param endpoint: The URL provided by the user for the shellhub instance + :return: A tuple containing the full URL and the base endpoint + """ + # Adjust the endpoint based on the _use_ssl flag if not endpoint.startswith(("http://", "https://")): protocol = "https://" if self._use_ssl else "http://" @@ -50,7 +56,11 @@ def _format_and_validate_url(self, endpoint: str) -> Tuple[str, str]: @staticmethod def _is_valid_url(url: str) -> bool: - # Simple pattern to check if the URL is well-formed + """ + Check if the URL provided is valid + :param url: The URL to be checked + :return: True if the URL is valid, False otherwise + """ pattern = re.compile( r"^https?:\/\/" # http:// or https:// r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|" # domain... @@ -164,6 +174,11 @@ def get_all_devices( return devices def get_device(self, uid: str) -> "shellhub.models.device.ShellHubDevice": + """ + Get a device from ShellHub by its UID + :param uid: The UID of the device + :return: A ShellHubDevice object + """ response = self.make_request(endpoint=f"/api/devices/{uid}", method="GET") if response.status_code == 404: raise DeviceNotFoundError(f"Device {uid} not found.") diff --git a/shellhub/models/device.py b/shellhub/models/device.py index 04d2ef5..4a5a40c 100644 --- a/shellhub/models/device.py +++ b/shellhub/models/device.py @@ -70,6 +70,10 @@ def __init__(self, api_object: shellhub.models.base.ShellHub, device_json): # t self.acceptable = device_json["acceptable"] def delete(self) -> bool: + """ + Delete the device from the API + :return: True if the device was deleted, False otherwise + """ response = self._api.make_request(endpoint=f"/api/devices/{self.uid}", method="DELETE") if response.status_code == 200: return True @@ -106,10 +110,14 @@ def rename(self, name: Optional[str] = None) -> bool: return False def accept(self) -> bool: - if not self.acceptable: - raise ShellHubApiError(f"Device {self.uid} is not acceptable.") + """ + Accept the device if it is pending + :return: True if the device was accepted, False otherwise + """ + if self.status != "pending": + raise ShellHubApiError(f"Device {self.uid} is not pending.") - response = self._api.make_request(endpoint=f"/api/devices/{self.uid}/accept", method="POST") + response = self._api.make_request(endpoint=f"/api/devices/{self.uid}/accept", method="PATCH") if response.status_code == 200: self.refresh() return True @@ -124,6 +132,10 @@ def accept(self) -> bool: return False def refresh(self) -> None: + """ + Refresh the device information from the API + :return: None + """ response = self._api.make_request(endpoint=f"/api/devices/{self.uid}", method="GET") if response.status_code == 404: raise DeviceNotFoundError(f"Device {self.uid} not found.") @@ -137,6 +149,10 @@ def refresh(self) -> None: @property def sshid(self) -> Optional[str]: + """ + Fabricates the SSHID of the devices from the namespace, name and endpoint + :return: SSHID of the device + """ if self.acceptable: return None return f"{self.namespace}.{self.name}@{self._api._endpoint}" diff --git a/tests/test_devices.py b/tests/test_devices.py index 1328866..c7d53f6 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -220,7 +220,7 @@ def test_not_acceptable_device(self, shellhub_device): shellhub_device.accept() def test_accept_notfound_device(self, shellhub_device, requests_mock): - requests_mock.post(f"{MOCKED_DOMAIN_URL}/api/devices/1/accept", status_code=404) + requests_mock.patch(f"{MOCKED_DOMAIN_URL}/api/devices/1/accept", status_code=404) shellhub_device.acceptable = True with pytest.raises(ShellHubApiError): shellhub_device.accept() @@ -254,7 +254,7 @@ def test_accept_device(self, shellhub_device, requests_mock): "acceptable": False, } requests_mock.get(f"{MOCKED_DOMAIN_URL}/api/devices/1", json=mock_response) - requests_mock.post(f"{MOCKED_DOMAIN_URL}/api/devices/1/accept", status_code=200) + requests_mock.patch(f"{MOCKED_DOMAIN_URL}/api/devices/1/accept", status_code=200) shellhub_device.acceptable = True shellhub_device.accept() From bdb5a345adef32109bdb9b7280fda30aa9fce876 Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Fri, 9 Feb 2024 10:34:10 +0100 Subject: [PATCH 3/6] Chore: Fixed tests on device accepting --- shellhub/__init__.py | 2 +- tests/test_devices.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shellhub/__init__.py b/shellhub/__init__.py index 3bd34e6..de222d2 100644 --- a/shellhub/__init__.py +++ b/shellhub/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.2.2" +__version__ = "0.2.3" from .models.device import ShellHubDevice, ShellHubDeviceInfo from .models.base import ShellHub diff --git a/tests/test_devices.py b/tests/test_devices.py index c7d53f6..29dfd8d 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -256,7 +256,7 @@ def test_accept_device(self, shellhub_device, requests_mock): requests_mock.get(f"{MOCKED_DOMAIN_URL}/api/devices/1", json=mock_response) requests_mock.patch(f"{MOCKED_DOMAIN_URL}/api/devices/1/accept", status_code=200) - shellhub_device.acceptable = True + shellhub_device.status = "pending" shellhub_device.accept() assert not shellhub_device.acceptable From 41e0ef66b9ba4fe9617456113ef85e2b3613fe0e Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Fri, 9 Feb 2024 10:54:14 +0100 Subject: [PATCH 4/6] CI/CD: Removed failing step on lint commits workflow --- .github/workflows/lint-commits.yml | 13 ------------- shellhub/__init__.py | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/lint-commits.yml b/.github/workflows/lint-commits.yml index b830952..9f5f1d3 100644 --- a/.github/workflows/lint-commits.yml +++ b/.github/workflows/lint-commits.yml @@ -74,16 +74,3 @@ jobs: if (errors.length > 0) { core.setFailed(`One or more of the commits in this PR do not match the code submission policy:\n\n${errors.join("\n")}`); } - - - name: Comment on PR - uses: actions/github-script@v7 - if: ${{ failure() && !github.event.pull_request.draft }} - with: - github-token: ${{ secrets.GH_ACCESS_KEY }} - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: 'Seluj78', - repo: 'shellhub-python', - body: 'Hello!\n\nOne or more of the commit messages in this PR do not match the code style. Please check the `lint_commits` CI job for more details on which commits were flagged and why.\nPlease do not close this PR and open another, instead modify your commit message(s) with [git commit --amend](https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/changing-a-commit-message) and force push those changes to update this PR.' - }) \ No newline at end of file diff --git a/shellhub/__init__.py b/shellhub/__init__.py index de222d2..e7ca9b2 100644 --- a/shellhub/__init__.py +++ b/shellhub/__init__.py @@ -1,5 +1,5 @@ # Increment versions here according to SemVer -__version__ = "0.2.3" +__version__ = "0.2.4" from .models.device import ShellHubDevice, ShellHubDeviceInfo from .models.base import ShellHub From fd97e2e00ad275ea3b1a62ce055133411d5ac373 Mon Sep 17 00:00:00 2001 From: SwapnilV Date: Wed, 7 Feb 2024 22:48:27 +0530 Subject: [PATCH 5/6] 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 e9080a3010808ac87a04eef3a5ef5a62b70f0af0 Mon Sep 17 00:00:00 2001 From: Jules Lasne Date: Fri, 9 Feb 2024 10:43:58 +0100 Subject: [PATCH 6/6] 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