From a5585d380d4b60db73d5c0b58540ee7d3fde5f0a Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Tue, 10 Sep 2024 00:20:56 -0400 Subject: [PATCH] boa-remote-computing: impl, interfaces, tests --- addon_imps/remote_computing/__init__.py | 2 + addon_imps/remote_computing/boa.py | 34 ++++++++++ addon_imps/tests/remote_computing/__init__.py | 0 addon_imps/tests/remote_computing/test_boa.py | 60 +++++++++++++++++ addon_service/common/known_imps.py | 6 ++ addon_toolkit/interfaces/__init__.py | 3 + addon_toolkit/interfaces/remote_computing.py | 67 +++++++++++++++++++ 7 files changed, 172 insertions(+) create mode 100644 addon_imps/remote_computing/__init__.py create mode 100644 addon_imps/remote_computing/boa.py create mode 100644 addon_imps/tests/remote_computing/__init__.py create mode 100644 addon_imps/tests/remote_computing/test_boa.py create mode 100644 addon_toolkit/interfaces/remote_computing.py diff --git a/addon_imps/remote_computing/__init__.py b/addon_imps/remote_computing/__init__.py new file mode 100644 index 00000000..93e92e5e --- /dev/null +++ b/addon_imps/remote_computing/__init__.py @@ -0,0 +1,2 @@ +"""addon_imps.remote_computing: imps that implement a "remote_computing"-like interface +""" diff --git a/addon_imps/remote_computing/boa.py b/addon_imps/remote_computing/boa.py new file mode 100644 index 00000000..8f7d57a2 --- /dev/null +++ b/addon_imps/remote_computing/boa.py @@ -0,0 +1,34 @@ +import logging + +from boaapi.boa_client import ( + BOA_API_ENDPOINT, + BoaClient, + BoaException, +) +from django.core.exceptions import ValidationError + +from addon_toolkit.interfaces import remote_computing + + +logger = logging.getLogger(__name__) + + +class BoaRemoteComputingImp(remote_computing.RemoteComputingAddonClientRequestorImp): + """sending compute jobs to Iowa State's Boa cluster.""" + + @classmethod + def confirm_credentials(cls, credentials): + try: + boa_client = cls.create_client(credentials) + boa_client.close() + except BoaException: + raise ValidationError( + "Fail to validate username and password for " + "endpoint:({BOA_API_ENDPOINT})" + ) + + @staticmethod + def create_client(credentials): + boa_client = BoaClient(endpoint=BOA_API_ENDPOINT) + boa_client.login(credentials.username, credentials.password) + return boa_client diff --git a/addon_imps/tests/remote_computing/__init__.py b/addon_imps/tests/remote_computing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_imps/tests/remote_computing/test_boa.py b/addon_imps/tests/remote_computing/test_boa.py new file mode 100644 index 00000000..be1b9288 --- /dev/null +++ b/addon_imps/tests/remote_computing/test_boa.py @@ -0,0 +1,60 @@ +import logging +import unittest +from unittest.mock import ( + MagicMock, + patch, +) + +from boaapi.boa_client import ( + BOA_API_ENDPOINT, + BoaException, +) +from django.core.exceptions import ValidationError + +from addon_imps.remote_computing.boa import BoaRemoteComputingImp +from addon_toolkit.credentials import UsernamePasswordCredentials +from addon_toolkit.interfaces.remote_computing import RemoteComputingConfig + + +logger = logging.getLogger(__name__) + + +class TestBoaRemoteComputingImp(unittest.IsolatedAsyncioTestCase): + + @patch.object(BoaRemoteComputingImp, "create_client") + def setUp(self, create_client_mock): + self.base_url = BOA_API_ENDPOINT + self.config = RemoteComputingConfig(external_api_url=self.base_url) + self.client = MagicMock() + self.credentials = UsernamePasswordCredentials(username="dog", password="woof") + self.imp = BoaRemoteComputingImp( + config=self.config, credentials=self.credentials + ) + self.imp.client = self.client + + @patch.object(BoaRemoteComputingImp, "create_client") + def test_confirm_credentials_success(self, create_client_mock): + creds = UsernamePasswordCredentials(username="dog", password="woof") + self.imp.confirm_credentials(creds) + + create_client_mock.assert_called_once_with(creds) + create_client_mock.return_value.close.assert_called_once_with() + + @patch.object( + BoaRemoteComputingImp, "create_client", side_effect=BoaException("nope") + ) + def test_confirm_credentials_fail(self, create_client_mock): + creds = UsernamePasswordCredentials(username="dog", password="woof") + create_client_mock.return_value.side_effect = BoaException("could not login") + with self.assertRaises(ValidationError): + self.imp.confirm_credentials(creds) + + create_client_mock.assert_called_once_with(creds) + + @patch(f"{BoaRemoteComputingImp.__module__}.BoaClient") + def test_create_client(self, create_mock): + mock_login = MagicMock() + create_mock.login.return_value = mock_login + + create_mock.assert_called_once_with(endpoint=BOA_API_ENDPOINT) + mock_login.assert_called_once_with(username="dog", password="woof") diff --git a/addon_service/common/known_imps.py b/addon_service/common/known_imps.py index 565cb6d6..482b708d 100644 --- a/addon_service/common/known_imps.py +++ b/addon_service/common/known_imps.py @@ -9,6 +9,7 @@ mendeley, zotero_org, ) +from addon_imps.remote_computing import boa from addon_imps.storage import ( bitbucket, box_dot_com, @@ -83,6 +84,8 @@ class KnownAddonImps(enum.Enum): GITLAB = gitlab.GitlabStorageImp DROPBOX = dropbox.DropboxStorageImp + BOA = boa.BoaRemoteComputingImp + if __debug__: BLARG = my_blarg.MyBlargStorage @@ -106,5 +109,8 @@ class AddonImpNumbers(enum.Enum): BITBUCKET = 1012 GIT_HUB = 1013 + + BOA = 1020 + if __debug__: BLARG = -7 diff --git a/addon_toolkit/interfaces/__init__.py b/addon_toolkit/interfaces/__init__.py index 73e092c2..376c8b19 100644 --- a/addon_toolkit/interfaces/__init__.py +++ b/addon_toolkit/interfaces/__init__.py @@ -2,6 +2,7 @@ from . import ( citation, + remote_computing, storage, ) from ._base import BaseAddonInterface @@ -12,9 +13,11 @@ "BaseAddonInterface", "storage", "citation", + "remote_computing", ) class AllAddonInterfaces(enum.Enum): STORAGE = storage.StorageAddonInterface CITATION = citation.CitationServiceInterface + REMOTE_COMPUTING = remote_computing.RemoteComputingAddonInterface diff --git a/addon_toolkit/interfaces/remote_computing.py b/addon_toolkit/interfaces/remote_computing.py new file mode 100644 index 00000000..8df43037 --- /dev/null +++ b/addon_toolkit/interfaces/remote_computing.py @@ -0,0 +1,67 @@ +"""a static (and still in progress) definition of what composes a remote computing addon""" + +import dataclasses +import typing + +from addon_toolkit.constrained_network.http import HttpRequestor +from addon_toolkit.credentials import Credentials +from addon_toolkit.imp import AddonImp + +from ._base import BaseAddonInterface + + +__all__ = ( + "RemoteComputingAddonInterface", + "RemoteComputingAddonImp", + "RemoteComputingConfig", +) + + +### +# dataclasses used for operation args and return values + + +@dataclasses.dataclass(frozen=True) +class RemoteComputingConfig: + external_api_url: str + external_account_id: str | None = None + + +### +# declaration of all remote computing addon operations + + +class RemoteComputingAddonInterface(BaseAddonInterface, typing.Protocol): + + pass + + +@dataclasses.dataclass +class RemoteComputingAddonImp(AddonImp): + """base class for remote computing addon implementations""" + + ADDON_INTERFACE = RemoteComputingAddonInterface + + config: RemoteComputingConfig + + +@dataclasses.dataclass +class RemoteComputingAddonHttpRequestorImp(RemoteComputingAddonImp): + """base class for remote computing addon implementations using GV network""" + + network: HttpRequestor + + +@dataclasses.dataclass +class RemoteComputingAddonClientRequestorImp[T](RemoteComputingAddonImp): + """base class for remote computing addon with custom clients""" + + client: T = dataclasses.field(init=False) + credentials: dataclasses.InitVar[Credentials] + + def __post_init__(self, credentials): + self.client = self.create_client(credentials) + + @staticmethod + def create_client(credentials) -> T: + raise NotImplementedError