From 43ef6118e12ef82ac20eb36ea27c2638f919c1d1 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 16 Sep 2024 10:10:18 -0400 Subject: [PATCH 01/10] computing addon infra -- authorized_account --- .../authorized_account/computing/__init__.py | 0 .../authorized_account/computing/models.py | 35 +++++++++ .../computing/serializers.py | 76 +++++++++++++++++++ .../authorized_account/computing/views.py | 9 +++ .../polymorphic_serializers.py | 4 + addon_service/authorized_account/utils.py | 3 + 6 files changed, 127 insertions(+) create mode 100644 addon_service/authorized_account/computing/__init__.py create mode 100644 addon_service/authorized_account/computing/models.py create mode 100644 addon_service/authorized_account/computing/serializers.py create mode 100644 addon_service/authorized_account/computing/views.py diff --git a/addon_service/authorized_account/computing/__init__.py b/addon_service/authorized_account/computing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_service/authorized_account/computing/models.py b/addon_service/authorized_account/computing/models.py new file mode 100644 index 00000000..553c6a93 --- /dev/null +++ b/addon_service/authorized_account/computing/models.py @@ -0,0 +1,35 @@ +from addon_service.addon_imp.instantiation import get_computing_addon_instance +from addon_service.authorized_account.models import AuthorizedAccount +from addon_toolkit.interfaces.computing import ComputingConfig + + +class AuthorizedComputingAccount(AuthorizedAccount): + """Model for describing a user's account on an ExternalComputingService. + + This model collects all of the information required to actually perform remote + operations against the service and to aggregate accounts under a known user. + """ + + class Meta: + verbose_name = "Authorized Computing Account" + verbose_name_plural = "Authorized Computing Accounts" + app_label = "addon_service" + + class JSONAPIMeta: + resource_name = "authorized-computing-accounts" + + async def execute_post_auth_hook(self, auth_extras: dict | None = None): + imp = await get_computing_addon_instance( + self.imp_cls, + self, + self.computing_imp_config, + ) + self.external_account_id = await imp.get_external_account_id(auth_extras or {}) + await self.asave() + + @property + def config(self) -> ComputingConfig: + return ComputingConfig( + external_api_url=self.api_base_url, + external_account_id=self.external_account_id, + ) diff --git a/addon_service/authorized_account/computing/serializers.py b/addon_service/authorized_account/computing/serializers.py new file mode 100644 index 00000000..5480c3dc --- /dev/null +++ b/addon_service/authorized_account/computing/serializers.py @@ -0,0 +1,76 @@ +from rest_framework_json_api import serializers +from rest_framework_json_api.relations import ( + HyperlinkedRelatedField, + ResourceRelatedField, +) +from rest_framework_json_api.utils import get_resource_type_from_model + +from addon_service.addon_operation.models import AddonOperationModel +from addon_service.authorized_account.serializers import AuthorizedAccountSerializer +from addon_service.common import view_names +from addon_service.common.serializer_fields import ( + DataclassRelatedLinkField, + ReadOnlyResourceRelatedField, +) +from addon_service.models import ( + AuthorizedComputingAccount, + ConfiguredComputingAddon, + ExternalComputingService, + UserReference, +) + + +RESOURCE_TYPE = get_resource_type_from_model(AuthorizedComputingAccount) + + +class AuthorizedComputingAccountSerializer(AuthorizedAccountSerializer): + external_computing_service = ResourceRelatedField( + queryset=ExternalComputingService.objects.all(), + many=False, + source="external_service.externalcomputingservice", + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + configured_computing_addons = HyperlinkedRelatedField( + many=True, + queryset=ConfiguredComputingAddon.objects.active(), + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + required=False, + ) + url = serializers.HyperlinkedIdentityField( + view_name=view_names.detail_view(RESOURCE_TYPE), required=False + ) + account_owner = ReadOnlyResourceRelatedField( + many=False, + queryset=UserReference.objects.all(), + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + authorized_operations = DataclassRelatedLinkField( + dataclass_model=AddonOperationModel, + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + + included_serializers = { + "account_owner": "addon_service.serializers.UserReferenceSerializer", + "external_computing_service": "addon_service.serializers.ExternalComputingServiceSerializer", + "configured_computing_addons": "addon_service.serializers.ConfiguredComputingAddonSerializer", + "authorized_operations": "addon_service.serializers.AddonOperationSerializer", + } + + class Meta: + model = AuthorizedComputingAccount + fields = [ + "id", + "url", + "display_name", + "account_owner", + "api_base_url", + "auth_url", + "authorized_capabilities", + "authorized_operations", + "authorized_operation_names", + "configured_computing_addons", + "credentials", + "external_computing_service", + "initiate_oauth", + "credentials_available", + ] diff --git a/addon_service/authorized_account/computing/views.py b/addon_service/authorized_account/computing/views.py new file mode 100644 index 00000000..51f82168 --- /dev/null +++ b/addon_service/authorized_account/computing/views.py @@ -0,0 +1,9 @@ +from addon_service.authorized_account.views import AuthorizedAccountViewSet + +from .models import AuthorizedComputingAccount +from .serializers import AuthorizedComputingAccountSerializer + + +class AuthorizedComputingAccountViewSet(AuthorizedAccountViewSet): + queryset = AuthorizedComputingAccount.objects.all() + serializer_class = AuthorizedComputingAccountSerializer diff --git a/addon_service/authorized_account/polymorphic_serializers.py b/addon_service/authorized_account/polymorphic_serializers.py index a9988ade..773c63d0 100644 --- a/addon_service/authorized_account/polymorphic_serializers.py +++ b/addon_service/authorized_account/polymorphic_serializers.py @@ -3,6 +3,9 @@ from addon_service.authorized_account.citation.serializers import ( AuthorizedCitationAccountSerializer, ) +from addon_service.authorized_account.computing.serializers import ( + AuthorizedComputingAccountSerializer, +) from addon_service.authorized_account.models import AuthorizedAccount from addon_service.authorized_account.storage.serializers import ( AuthorizedStorageAccountSerializer, @@ -12,6 +15,7 @@ class AuthorizedAccountPolymorphicSerializer(serializers.PolymorphicModelSerializer): polymorphic_serializers = [ AuthorizedCitationAccountSerializer, + AuthorizedComputingAccountSerializer, AuthorizedStorageAccountSerializer, ] diff --git a/addon_service/authorized_account/utils.py b/addon_service/authorized_account/utils.py index fa9a7320..5dfd4c60 100644 --- a/addon_service/authorized_account/utils.py +++ b/addon_service/authorized_account/utils.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from addon_toolkit.interfaces.citation import CitationAddonImp +from addon_toolkit.interfaces.computing import ComputingAddonImp from addon_toolkit.interfaces.storage import StorageAddonImp @@ -15,5 +16,7 @@ def get_config_for_account(account: AuthorizedAccount): return account.authorizedstorageaccount.config elif issubclass(account.imp_cls, CitationAddonImp): return account.authorizedcitationaccount.config + elif issubclass(account.imp_cls, ComputingAddonImp): + return account.authorizedcomputingaccount.config raise ValueError(f"this function implementation does not support {account.imp_cls}") From 9f6ec2aa2a551e96ac4adfeb282e9c9d946dc399 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 16 Sep 2024 08:10:22 -0400 Subject: [PATCH 02/10] computing addon infra -- configured_addon --- .../configured_addon/computing/__init__.py | 0 .../configured_addon/computing/models.py | 23 ++++++ .../configured_addon/computing/serializers.py | 73 +++++++++++++++++++ .../configured_addon/computing/views.py | 26 +++++++ .../polymorphic_serializers.py | 4 + addon_service/configured_addon/utils.py | 3 + 6 files changed, 129 insertions(+) create mode 100644 addon_service/configured_addon/computing/__init__.py create mode 100644 addon_service/configured_addon/computing/models.py create mode 100644 addon_service/configured_addon/computing/serializers.py create mode 100644 addon_service/configured_addon/computing/views.py diff --git a/addon_service/configured_addon/computing/__init__.py b/addon_service/configured_addon/computing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_service/configured_addon/computing/models.py b/addon_service/configured_addon/computing/models.py new file mode 100644 index 00000000..25c96e93 --- /dev/null +++ b/addon_service/configured_addon/computing/models.py @@ -0,0 +1,23 @@ +from addon_service.common.known_imps import AddonImpNumbers +from addon_service.configured_addon.models import ConfiguredAddon +from addon_toolkit.interfaces.computing import ComputingConfig + + +class ConfiguredComputingAddon(ConfiguredAddon): + + class Meta: + verbose_name = "Configured Computing Addon" + verbose_name_plural = "Configured Computing Addons" + app_label = "addon_service" + + class JSONAPIMeta: + resource_name = "configured-computing-addons" + + @property + def config(self) -> ComputingConfig: + return self.base_account.authorizedcomputingaccount.config + + @property + def external_service_name(self): + number = self.base_account.external_service.int_addon_imp + return AddonImpNumbers(number).name.lower() diff --git a/addon_service/configured_addon/computing/serializers.py b/addon_service/configured_addon/computing/serializers.py new file mode 100644 index 00000000..ca5d998a --- /dev/null +++ b/addon_service/configured_addon/computing/serializers.py @@ -0,0 +1,73 @@ +from rest_framework_json_api import serializers +from rest_framework_json_api.relations import ResourceRelatedField +from rest_framework_json_api.utils import get_resource_type_from_model + +from addon_service.addon_operation.models import AddonOperationModel +from addon_service.authorized_account.computing.models import AuthorizedComputingAccount +from addon_service.common import view_names +from addon_service.common.serializer_fields import DataclassRelatedLinkField +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon +from addon_service.configured_addon.serializers import ConfiguredAddonSerializer +from addon_service.external_service.computing.models import ExternalComputingService + + +RESOURCE_TYPE = get_resource_type_from_model(ConfiguredComputingAddon) + + +class ConfiguredComputingAddonSerializer(ConfiguredAddonSerializer): + """api serializer for the `ConfiguredComputingAddon` model""" + + external_service_name = serializers.CharField(read_only=True) + url = serializers.HyperlinkedIdentityField( + view_name=view_names.detail_view(RESOURCE_TYPE) + ) + connected_operations = DataclassRelatedLinkField( + dataclass_model=AddonOperationModel, + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + read_only=True, + ) + base_account = ResourceRelatedField( + queryset=AuthorizedComputingAccount.objects.all(), + many=False, + source="base_account.authorizedcomputingaccount", + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + external_computing_service = ResourceRelatedField( + many=False, + read_only=True, + model=ExternalComputingService, + source="base_account.external_service.externalcomputingservice", + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + authorized_resource = ResourceRelatedField( + many=False, + read_only=True, + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + + included_serializers = { + "base_account": ( + "addon_service.serializers.AuthorizedComputingAccountSerializer" + ), + "external_computing_service": ( + "addon_service.serializers.ExternalComputingServiceSerializer" + ), + "authorized_resource": "addon_service.serializers.ResourceReferenceSerializer", + "connected_operations": "addon_service.serializers.AddonOperationSerializer", + } + + class Meta: + model = ConfiguredComputingAddon + fields = [ + "id", + "url", + "display_name", + "base_account", + "authorized_resource", + "authorized_resource_uri", + "connected_capabilities", + "connected_operations", + "connected_operation_names", + "external_service_name", + "external_computing_service", + ] diff --git a/addon_service/configured_addon/computing/views.py b/addon_service/configured_addon/computing/views.py new file mode 100644 index 00000000..a267b932 --- /dev/null +++ b/addon_service/configured_addon/computing/views.py @@ -0,0 +1,26 @@ +from http import HTTPMethod + +from rest_framework.decorators import action +from rest_framework.response import Response + +from addon_service.common.waterbutler_compat import WaterButlerConfigSerializer +from addon_service.configured_addon.views import ConfiguredAddonViewSet + +from .models import ConfiguredComputingAddon +from .serializers import ConfiguredComputingAddonSerializer + + +class ConfiguredComputingAddonViewSet(ConfiguredAddonViewSet): + queryset = ConfiguredComputingAddon.objects.active() + serializer_class = ConfiguredComputingAddonSerializer + + @action( + detail=True, + methods=[HTTPMethod.GET], + url_name="waterbutler-credentials", + url_path="waterbutler-credentials", + ) + def get_wb_credentials(self, request, pk=None): + addon: ConfiguredComputingAddon = self.get_object() + self.resource_name = "waterbutler-credentials" # for the jsonapi resource type + return Response(WaterButlerConfigSerializer(addon).data) diff --git a/addon_service/configured_addon/polymorphic_serializers.py b/addon_service/configured_addon/polymorphic_serializers.py index 4c60242f..28b58d13 100644 --- a/addon_service/configured_addon/polymorphic_serializers.py +++ b/addon_service/configured_addon/polymorphic_serializers.py @@ -3,6 +3,9 @@ from addon_service.configured_addon.citation.serializers import ( ConfiguredCitationAddonSerializer, ) +from addon_service.configured_addon.computing.serializers import ( + ConfiguredComputingAddonSerializer, +) from addon_service.configured_addon.models import ConfiguredAddon from addon_service.configured_addon.storage.serializers import ( ConfiguredStorageAddonSerializer, @@ -12,6 +15,7 @@ class ConfiguredAddonPolymorphicSerializer(serializers.PolymorphicModelSerializer): polymorphic_serializers = [ ConfiguredCitationAddonSerializer, + ConfiguredComputingAddonSerializer, ConfiguredStorageAddonSerializer, ] diff --git a/addon_service/configured_addon/utils.py b/addon_service/configured_addon/utils.py index 2f036150..0daad9df 100644 --- a/addon_service/configured_addon/utils.py +++ b/addon_service/configured_addon/utils.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from addon_toolkit.interfaces.citation import CitationAddonImp +from addon_toolkit.interfaces.computing import ComputingAddonImp from addon_toolkit.interfaces.storage import StorageAddonImp @@ -15,5 +16,7 @@ def get_config_for_addon(addon: ConfiguredAddon): return addon.configuredstorageaddon.config elif issubclass(addon.imp_cls, CitationAddonImp): return addon.configuredcitationaddon.config + elif issubclass(addon.imp_cls, ComputingAddonImp): + return addon.configuredcomputingaddon.config raise ValueError(f"this function implementation does not support {addon.imp_cls}") From d48a76a10df2db7695f3806d64e337dcfbb3bbca Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 16 Sep 2024 08:13:19 -0400 Subject: [PATCH 03/10] computing addon infra -- external_service --- addon_service/external_service/__init__.py | 2 + .../external_service/computing/__init__.py | 2 + .../external_service/computing/models.py | 50 +++++++++++++++++++ .../external_service/computing/serializers.py | 35 +++++++++++++ .../external_service/computing/views.py | 9 ++++ 5 files changed, 98 insertions(+) create mode 100644 addon_service/external_service/computing/__init__.py create mode 100644 addon_service/external_service/computing/models.py create mode 100644 addon_service/external_service/computing/serializers.py create mode 100644 addon_service/external_service/computing/views.py diff --git a/addon_service/external_service/__init__.py b/addon_service/external_service/__init__.py index 0b2f4009..54629a45 100644 --- a/addon_service/external_service/__init__.py +++ b/addon_service/external_service/__init__.py @@ -1,2 +1,4 @@ """addon_service.external_storage_service: represents a third-party service """ + +# TODO: fix docstring for correct module name diff --git a/addon_service/external_service/computing/__init__.py b/addon_service/external_service/computing/__init__.py new file mode 100644 index 00000000..18e269ee --- /dev/null +++ b/addon_service/external_service/computing/__init__.py @@ -0,0 +1,2 @@ +"""addon_service.external_service.computing: represents a third-party computing service +""" diff --git a/addon_service/external_service/computing/models.py b/addon_service/external_service/computing/models.py new file mode 100644 index 00000000..078d46b5 --- /dev/null +++ b/addon_service/external_service/computing/models.py @@ -0,0 +1,50 @@ +from enum import ( + Flag, + auto, +) + +from django.db import models + +from addon_service.common.validators import ( + _validate_enum_value, + validate_computing_imp_number, +) +from addon_service.external_service.models import ExternalService + + +class ComputingSupportedFeatures(Flag): + LOGS = auto() + + +def validate_supported_features(value): + _validate_enum_value(ComputingSupportedFeatures, value) + + +class ExternalComputingService(ExternalService): + int_supported_features = models.IntegerField( + validators=[validate_supported_features], + null=True, + ) + + @property + def supported_features(self) -> list[ComputingSupportedFeatures]: + """get the enum representation of int_supported_features""" + return ComputingSupportedFeatures(self.int_supported_features) + + @supported_features.setter + def supported_features(self, new_supported_features: ComputingSupportedFeatures): + """set int_authorized_capabilities without caring its int""" + validate_computing_imp_number(self.int_addon_imp) + self.int_supported_features = new_supported_features.value + + def clean(self): + super().clean() + validate_computing_imp_number(self.int_addon_imp) + + class Meta: + verbose_name = "External Computing Service" + verbose_name_plural = "External Computing Services" + app_label = "addon_service" + + class JSONAPIMeta: + resource_name = "external-computing-services" diff --git a/addon_service/external_service/computing/serializers.py b/addon_service/external_service/computing/serializers.py new file mode 100644 index 00000000..fa9cd8cd --- /dev/null +++ b/addon_service/external_service/computing/serializers.py @@ -0,0 +1,35 @@ +from rest_framework_json_api import serializers +from rest_framework_json_api.utils import get_resource_type_from_model + +from addon_service.addon_imp.models import AddonImpModel +from addon_service.common import view_names +from addon_service.common.serializer_fields import DataclassRelatedDataField +from addon_service.external_service.serializers import ExternalServiceSerializer +from addon_service.models import ExternalComputingService + + +RESOURCE_TYPE = get_resource_type_from_model(ExternalComputingService) + + +class ExternalComputingServiceSerializer(ExternalServiceSerializer): + """api serializer for the `ExternalComputing` model""" + + url = serializers.HyperlinkedIdentityField( + view_name=view_names.detail_view(RESOURCE_TYPE) + ) + addon_imp = DataclassRelatedDataField( + dataclass_model=AddonImpModel, + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + + class Meta: + model = ExternalComputingService + fields = [ + "id", + "addon_imp", + "auth_uri", + "credentials_format", + "display_name", + "url", + "configurable_api_root", + ] diff --git a/addon_service/external_service/computing/views.py b/addon_service/external_service/computing/views.py new file mode 100644 index 00000000..146b5adb --- /dev/null +++ b/addon_service/external_service/computing/views.py @@ -0,0 +1,9 @@ +from rest_framework_json_api.views import ReadOnlyModelViewSet + +from .models import ExternalComputingService +from .serializers import ExternalComputingServiceSerializer + + +class ExternalComputingServiceViewSet(ReadOnlyModelViewSet): + queryset = ExternalComputingService.objects.all() + serializer_class = ExternalComputingServiceSerializer From 75f6925051d83ba3963abb6d5060d60ebdbe9560 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 16 Sep 2024 09:44:01 -0400 Subject: [PATCH 04/10] computing addon infra -- addon_operation_invocation --- .../addon_operation_invocation/models.py | 3 ++- .../addon_operation_invocation/views.py | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/addon_service/addon_operation_invocation/models.py b/addon_service/addon_operation_invocation/models.py index ca4d50ba..d1ece760 100644 --- a/addon_service/addon_operation_invocation/models.py +++ b/addon_service/addon_operation_invocation/models.py @@ -12,6 +12,7 @@ from addon_service.models import AddonOperationModel from addon_toolkit import AddonImp from addon_toolkit.interfaces.citation import CitationConfig +from addon_toolkit.interfaces.computing import ComputingConfig from addon_toolkit.interfaces.storage import StorageConfig @@ -70,7 +71,7 @@ def imp_cls(self) -> type[AddonImp]: return self.thru_account.imp_cls @property - def config(self) -> StorageConfig | CitationConfig: + def config(self) -> StorageConfig | CitationConfig | ComputingConfig: if self.thru_addon: return get_config_for_addon(self.thru_addon) return get_config_for_account(self.thru_account) diff --git a/addon_service/addon_operation_invocation/views.py b/addon_service/addon_operation_invocation/views.py index 42fe47da..c58d6719 100644 --- a/addon_service/addon_operation_invocation/views.py +++ b/addon_service/addon_operation_invocation/views.py @@ -16,9 +16,13 @@ from ..authorized_account.citation.serializers import ( AuthorizedCitationAccountSerializer, ) +from ..authorized_account.computing.serializers import ( + AuthorizedComputingAccountSerializer, +) from ..authorized_account.models import AuthorizedAccount from ..authorized_account.storage.serializers import AuthorizedStorageAccountSerializer from ..configured_addon.citation.serializers import ConfiguredCitationAddonSerializer +from ..configured_addon.computing.serializers import ConfiguredComputingAddonSerializer from ..configured_addon.models import ConfiguredAddon from ..configured_addon.storage.serializers import ConfiguredStorageAddonSerializer from .models import AddonOperationInvocation @@ -51,19 +55,31 @@ def retrieve_related(self, request, *args, **kwargs): serializer = AuthorizedStorageAccountSerializer( instance, context={"request": request} ) - else: + elif hasattr(instance, "authorizedcitationaccount"): serializer = AuthorizedCitationAccountSerializer( instance, context={"request": request} ) + elif hasattr(instance, "authorizedcomputingaccount"): + serializer = AuthorizedComputingAccountSerializer( + instance, context={"request": request} + ) + else: + raise ValueError("unknown authorized account type") elif isinstance(instance, ConfiguredAddon): if hasattr(instance, "configuredstorageaddon"): serializer = ConfiguredStorageAddonSerializer( instance, context={"request": request} ) - else: + elif hasattr(instance, "configuredcitationaddon"): serializer = ConfiguredCitationAddonSerializer( instance, context={"request": request} ) + elif hasattr(instance, "configuredcomputingaddon"): + serializer = ConfiguredComputingAddonSerializer( + instance, context={"request": request} + ) + else: + raise ValueError("unknown configured addon type") else: serializer = self.get_related_serializer(instance) return Response(serializer.data) From 9064504f1b53ce38c3a45333da390e898ebe96b2 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Wed, 27 Nov 2024 09:19:05 -0500 Subject: [PATCH 05/10] computing addon infra -- resource_reference --- addon_service/resource_reference/models.py | 5 +++++ addon_service/resource_reference/serializers.py | 10 ++++++++++ .../tests/test_by_type/test_resource_reference.py | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/addon_service/resource_reference/models.py b/addon_service/resource_reference/models.py index e35ed6f1..d87b9ed7 100644 --- a/addon_service/resource_reference/models.py +++ b/addon_service/resource_reference/models.py @@ -2,6 +2,7 @@ from addon_service.common.base_model import AddonsServiceBaseModel from addon_service.configured_addon.citation.models import ConfiguredCitationAddon +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon from addon_service.configured_addon.storage.models import ConfiguredStorageAddon @@ -16,6 +17,10 @@ def configured_storage_addons(self): def configured_citation_addons(self): return ConfiguredCitationAddon.objects.filter(authorized_resource=self) + @property + def configured_computing_addons(self): + return ConfiguredComputingAddon.objects.filter(authorized_resource=self) + class Meta: verbose_name = "Resource Reference" verbose_name_plural = "Resource References" diff --git a/addon_service/resource_reference/serializers.py b/addon_service/resource_reference/serializers.py index 00c79e70..dccf363a 100644 --- a/addon_service/resource_reference/serializers.py +++ b/addon_service/resource_reference/serializers.py @@ -4,6 +4,7 @@ from addon_service.common import view_names from addon_service.configured_addon.citation.models import ConfiguredCitationAddon +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon from addon_service.models import ( ConfiguredStorageAddon, ResourceReference, @@ -29,6 +30,11 @@ class ResourceReferenceSerializer(serializers.HyperlinkedModelSerializer): queryset=ConfiguredCitationAddon.objects.active(), related_link_view_name=view_names.related_view(RESOURCE_TYPE), ) + configured_computing_addons = HyperlinkedRelatedField( + many=True, + queryset=ConfiguredComputingAddon.objects.active(), + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) included_serializers = { "configured_storage_addons": ( @@ -37,6 +43,9 @@ class ResourceReferenceSerializer(serializers.HyperlinkedModelSerializer): "configured_citation_addons": ( "addon_service.serializers.ConfiguredCitationAddonSerializer" ), + "configured_computing_addons": ( + "addon_service.serializers.ConfiguredComputingAddonSerializer" + ), } class Meta: @@ -47,4 +56,5 @@ class Meta: "resource_uri", "configured_storage_addons", "configured_citation_addons", + "configured_computing_addons", ] diff --git a/addon_service/tests/test_by_type/test_resource_reference.py b/addon_service/tests/test_by_type/test_resource_reference.py index 66e875a8..a9af0a4f 100644 --- a/addon_service/tests/test_by_type/test_resource_reference.py +++ b/addon_service/tests/test_by_type/test_resource_reference.py @@ -161,7 +161,11 @@ def test_get(self): with self.subTest("Confirm expected relationships"): self.assertEqual( json.loads(_resp.rendered_content)["data"]["relationships"].keys(), - {"configured_storage_addons", "configured_citation_addons"}, + { + "configured_storage_addons", + "configured_citation_addons", + "configured_computing_addons", + }, ) def test_unauthorized__private_resource(self): From 7933ca2f5259b45295b719d4ff8c1a76dfd096ce Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Wed, 11 Dec 2024 07:54:39 -0500 Subject: [PATCH 06/10] computing addon infra -- user_reference --- .../tests/test_by_type/test_user_reference.py | 2 ++ addon_service/user_reference/models.py | 12 ++++++++++++ addon_service/user_reference/serializers.py | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/addon_service/tests/test_by_type/test_user_reference.py b/addon_service/tests/test_by_type/test_user_reference.py index 4f026d6b..de0eeb56 100644 --- a/addon_service/tests/test_by_type/test_user_reference.py +++ b/addon_service/tests/test_by_type/test_user_reference.py @@ -66,6 +66,7 @@ def test_get(self): { "authorized_storage_accounts", "authorized_citation_accounts", + "authorized_computing_accounts", "configured_resources", }, ) @@ -200,6 +201,7 @@ def test_get(self): { "authorized_storage_accounts", "authorized_citation_accounts", + "authorized_computing_accounts", "configured_resources", }, ) diff --git a/addon_service/user_reference/models.py b/addon_service/user_reference/models.py index 8c4a2dbf..05fc108e 100644 --- a/addon_service/user_reference/models.py +++ b/addon_service/user_reference/models.py @@ -2,8 +2,10 @@ from django.utils import timezone from addon_service.authorized_account.citation.models import AuthorizedCitationAccount +from addon_service.authorized_account.computing.models import AuthorizedComputingAccount from addon_service.authorized_account.storage.models import AuthorizedStorageAccount from addon_service.common.base_model import AddonsServiceBaseModel +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon from addon_service.configured_addon.storage.models import ConfiguredStorageAddon from addon_service.resource_reference.models import ResourceReference @@ -18,6 +20,12 @@ def configured_storage_addons(self): base_account__account_owner=self, ) + @property + def configured_computing_addons(self): + return ConfiguredComputingAddon.objects.filter( + base_account__account_owner=self, + ) + @property def configured_resources(self): return ResourceReference.objects.annotate( @@ -37,6 +45,10 @@ def authorized_storage_accounts(self): def authorized_citation_accounts(self): return AuthorizedCitationAccount.objects.filter(account_owner=self) + @property + def authorized_computing_accounts(self): + return AuthorizedComputingAccount.objects.filter(account_owner=self) + class Meta: verbose_name = "User Reference" verbose_name_plural = "User References" diff --git a/addon_service/user_reference/serializers.py b/addon_service/user_reference/serializers.py index c9656d3a..b45204e5 100644 --- a/addon_service/user_reference/serializers.py +++ b/addon_service/user_reference/serializers.py @@ -5,6 +5,7 @@ from addon_service.common import view_names from addon_service.models import ( AuthorizedCitationAccount, + AuthorizedComputingAccount, AuthorizedStorageAccount, ResourceReference, UserReference, @@ -33,6 +34,12 @@ class UserReferenceSerializer(serializers.HyperlinkedModelSerializer): related_link_view_name=view_names.related_view(RESOURCE_TYPE), ) + authorized_computing_accounts = HyperlinkedRelatedField( + many=True, + queryset=AuthorizedComputingAccount.objects.all(), + related_link_view_name=view_names.related_view(RESOURCE_TYPE), + ) + configured_resources = HyperlinkedRelatedField( many=True, queryset=ResourceReference.objects.all(), @@ -46,6 +53,9 @@ class UserReferenceSerializer(serializers.HyperlinkedModelSerializer): "authorized_citation_accounts": ( "addon_service.serializers.AuthorizedCitationAccountSerializer" ), + "authorized_computing_accounts": ( + "addon_service.serializers.AuthorizedComputingAccountSerializer" + ), "configured_resources": ( "addon_service.serializers.ResourceReferenceSerializer" ), @@ -59,5 +69,6 @@ class Meta: "user_uri", "authorized_storage_accounts", "authorized_citation_accounts", + "authorized_computing_accounts", "configured_resources", ] From 7062b459b9c4cf5a02c48e5962a7a20a507de7f1 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 16 Sep 2024 10:09:30 -0400 Subject: [PATCH 07/10] computing addon infra -- other modules --- addon_service/addon_imp/instantiation.py | 30 ++++++++++++++++++- addon_service/admin/__init__.py | 20 +++++++++++++ addon_service/common/serializer_fields.py | 4 +++ addon_service/common/validators.py | 5 ++++ addon_service/credentials/models.py | 7 +++-- .../commands/migrate_authorized_account.py | 10 +++++++ addon_service/models.py | 6 ++++ addon_service/oauth1/views.py | 3 ++ addon_service/serializers.py | 12 ++++++++ addon_service/urls.py | 3 ++ addon_service/views.py | 12 ++++++++ 11 files changed, 108 insertions(+), 4 deletions(-) diff --git a/addon_service/addon_imp/instantiation.py b/addon_service/addon_imp/instantiation.py index 83427498..0585ab05 100644 --- a/addon_service/addon_imp/instantiation.py +++ b/addon_service/addon_imp/instantiation.py @@ -11,6 +11,10 @@ CitationAddonImp, CitationConfig, ) +from addon_toolkit.interfaces.computing import ( + ComputingAddonImp, + ComputingConfig, +) from addon_toolkit.interfaces.storage import ( StorageAddonClientRequestorImp, StorageAddonHttpRequestorImp, @@ -23,6 +27,7 @@ from addon_service.authorized_account.models import AuthorizedAccount from addon_service.models import ( AuthorizedCitationAccount, + AuthorizedComputingAccount, AuthorizedStorageAccount, ) @@ -30,12 +35,14 @@ async def get_addon_instance( imp_cls: type[AddonImp], account: AuthorizedAccount, - config: StorageConfig | CitationConfig, + config: StorageConfig | CitationConfig | ComputingConfig, ) -> AddonImp: if issubclass(imp_cls, StorageAddonImp): return await get_storage_addon_instance(imp_cls, account, config) elif issubclass(imp_cls, CitationAddonImp): return await get_citation_addon_instance(imp_cls, account, config) + elif issubclass(imp_cls, ComputingAddonImp): + return await get_computing_addon_instance(imp_cls, account, config) raise ValueError(f"unknown addon type {imp_cls}") @@ -96,3 +103,24 @@ async def get_citation_addon_instance( get_citation_addon_instance__blocking = async_to_sync(get_citation_addon_instance) + + +async def get_computing_addon_instance( + imp_cls: type[ComputingAddonImp], + account: AuthorizedComputingAccount, + config: ComputingConfig, +) -> ComputingAddonImp: + """create an instance of a `ComputingAddonImp`""" + + assert issubclass(imp_cls, ComputingAddonImp) + return imp_cls( + config=config, + network=GravyvaletHttpRequestor( + client_session=await get_singleton_client_session(), + prefix_url=config.external_api_url, + account=account, + ), + ) + + +get_computing_addon_instance__blocking = async_to_sync(get_computing_addon_instance) diff --git a/addon_service/admin/__init__.py b/addon_service/admin/__init__.py index 847cebdf..45eb02b6 100644 --- a/addon_service/admin/__init__.py +++ b/addon_service/admin/__init__.py @@ -4,6 +4,7 @@ from addon_service.common import known_imps from addon_service.common.credentials_formats import CredentialsFormats from addon_service.common.service_types import ServiceTypes +from addon_service.external_service.computing.models import ComputingSupportedFeatures from addon_service.external_service.storage.models import StorageSupportedFeatures from ..external_service.citation.models import CitationSupportedFeatures @@ -49,6 +50,25 @@ class ExternalCitationServiceAdmin(GravyvaletModelAdmin): } +@admin.register(models.ExternalComputingService) +class ExternalComputingServiceAdmin(GravyvaletModelAdmin): + list_display = ("display_name", "created", "modified") + readonly_fields = ( + "id", + "created", + "modified", + ) + raw_id_fields = ("oauth2_client_config",) + enum_choice_fields = { + "int_addon_imp": known_imps.AddonImpNumbers, + "int_credentials_format": CredentialsFormats, + "int_service_type": ServiceTypes, + } + enum_multiple_choice_fields = { + "int_supported_features": ComputingSupportedFeatures, + } + + @admin.register(models.OAuth2ClientConfig) @linked_many_field("external_storage_services") @linked_many_field("external_citation_services") diff --git a/addon_service/common/serializer_fields.py b/addon_service/common/serializer_fields.py index 3af7bc57..a818a434 100644 --- a/addon_service/common/serializer_fields.py +++ b/addon_service/common/serializer_fields.py @@ -56,10 +56,14 @@ def to_representation(self, value): data = super().to_representation(value) if hasattr(value, "authorizedcitationaccount"): data["type"] = "authorized-citation-accounts" + elif hasattr(value, "authorizedcomputingaccount"): + data["type"] = "authorized-computing-accounts" elif hasattr(value, "authorizedstorageaccount"): data["type"] = "authorized-storage-accounts" elif hasattr(value, "configuredcitationaddon"): data["type"] = "configured-citation-addons" + elif hasattr(value, "configuredcomputingaddon"): + data["type"] = "configured-computing-addons" elif hasattr(value, "configuredstorageaccount"): data["type"] = "configured-storage-addons" return data diff --git a/addon_service/common/validators.py b/addon_service/common/validators.py index c7a55877..550ee414 100644 --- a/addon_service/common/validators.py +++ b/addon_service/common/validators.py @@ -9,6 +9,7 @@ from addon_toolkit import AddonCapabilities from addon_toolkit.interfaces.citation import CitationAddonImp +from addon_toolkit.interfaces.computing import ComputingAddonImp from addon_toolkit.interfaces.storage import StorageAddonImp from . import known_imps @@ -62,6 +63,10 @@ def validate_citation_imp_number(value): _validate_imp_number(value, CitationAddonImp) +def validate_computing_imp_number(value): + _validate_imp_number(value, ComputingAddonImp) + + ### # module-private helpers diff --git a/addon_service/credentials/models.py b/addon_service/credentials/models.py index 3a7c88cb..a0ebe128 100644 --- a/addon_service/credentials/models.py +++ b/addon_service/credentials/models.py @@ -18,6 +18,7 @@ class ExternalCredentials(AddonsServiceBaseModel): # Attributes inherited from back-references: # storage (AuthorizedStorageAccount._credentials, One2One) + # FIXME: Where is citation and computing? class Meta: verbose_name = "External Credentials" @@ -90,8 +91,8 @@ def _key_parameters(self, value: encryption.KeyParameters) -> None: def authorized_accounts(self): """Returns the list of all accounts that point to this set of credentials. - For now, this will just be a single AuthorizedStorageAccount, but in the future - other types of accounts for the same user could point to the same set of credentials + For now, this will just be a single AuthorizedAccount, but in the future other + types of accounts for the same user could point to the same set of credentials """ try: return [ @@ -103,7 +104,7 @@ def authorized_accounts(self): ], ) ] - except ExternalCredentials.authorized_storage_account.RelatedObjectDoesNotExist: + except ExternalCredentials.authorized_account.RelatedObjectDoesNotExist: return None @property diff --git a/addon_service/management/commands/migrate_authorized_account.py b/addon_service/management/commands/migrate_authorized_account.py index 2a2b4163..37fcbff8 100644 --- a/addon_service/management/commands/migrate_authorized_account.py +++ b/addon_service/management/commands/migrate_authorized_account.py @@ -4,15 +4,19 @@ from django.db import transaction from addon_service.authorized_account.citation.models import AuthorizedCitationAccount +from addon_service.authorized_account.computing.models import AuthorizedComputingAccount from addon_service.authorized_account.storage.models import AuthorizedStorageAccount from addon_service.common.credentials_formats import CredentialsFormats from addon_service.configured_addon.citation.models import ConfiguredCitationAddon +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon from addon_service.configured_addon.storage.models import ConfiguredStorageAddon from addon_service.external_service.models import ExternalService from addon_service.oauth2 import OAuth2TokenMetadata from addon_service.osf_models.models import ( BitbucketNodeSettings, BitbucketUserSettings, + BoaNodeSettings, + BoaUserSettings, BoxNodeSettings, BoxUserSettings, DataverseNodeSettings, @@ -78,6 +82,7 @@ def fetch_external_accounts(user_id: int, provider: str): ["storage", "s3", S3UserSettings, S3NodeSettings], ["citations", "mendeley", MendeleyUserSettings, MendeleyNodeSettings], ["citations", "zotero", ZoteroUserSettings, ZoteroNodeSettings], + ["computing", "boa", BoaUserSettings, BoaNodeSettings], ] @@ -112,6 +117,8 @@ def get_root_folder_for_provider(node_settings, service_name): return f"{node_settings.library_id}/{node_settings.list_id}" case "mendeley": return node_settings.list_id + case "boa": + return None class Command(BaseCommand): @@ -144,6 +151,9 @@ def migrate_for_user( elif integration_type == "citations": AuthorizedAccount = AuthorizedCitationAccount ConfiguredAddon = ConfiguredCitationAddon + elif integration_type == "computing": + AuthorizedAccount = AuthorizedComputingAccount + ConfiguredAddon = ConfiguredComputingAddon else: raise diff --git a/addon_service/models.py b/addon_service/models.py index 3553b95b..99e0395e 100644 --- a/addon_service/models.py +++ b/addon_service/models.py @@ -4,11 +4,14 @@ from addon_service.addon_operation.models import AddonOperationModel from addon_service.addon_operation_invocation.models import AddonOperationInvocation from addon_service.authorized_account.citation.models import AuthorizedCitationAccount +from addon_service.authorized_account.computing.models import AuthorizedComputingAccount from addon_service.authorized_account.storage.models import AuthorizedStorageAccount from addon_service.configured_addon.citation.models import ConfiguredCitationAddon +from addon_service.configured_addon.computing.models import ConfiguredComputingAddon from addon_service.configured_addon.storage.models import ConfiguredStorageAddon from addon_service.credentials.models import ExternalCredentials from addon_service.external_service.citation.models import ExternalCitationService +from addon_service.external_service.computing.models import ExternalComputingService from addon_service.external_service.storage.models import ExternalStorageService from addon_service.oauth1.models import OAuth1ClientConfig from addon_service.oauth2.models import ( @@ -35,4 +38,7 @@ "AuthorizedCitationAccount", "ConfiguredCitationAddon", "ExternalCitationService", + "AuthorizedComputingAccount", + "ConfiguredComputingAddon", + "ExternalComputingService", ) diff --git a/addon_service/oauth1/views.py b/addon_service/oauth1/views.py index ff3ffcef..9a72e367 100644 --- a/addon_service/oauth1/views.py +++ b/addon_service/oauth1/views.py @@ -4,6 +4,7 @@ from django.http import HttpResponse from addon_service.authorized_account.citation.models import AuthorizedCitationAccount +from addon_service.authorized_account.computing.models import AuthorizedComputingAccount from addon_service.authorized_account.storage.models import AuthorizedStorageAccount from addon_service.oauth1.utils import get_access_token from addon_service.osf_models.fields import decrypt_string @@ -20,6 +21,8 @@ def oauth1_callback_view(request): account = AuthorizedStorageAccount.objects.get(pk=pk) case "AuthorizedCitationAccount": account = AuthorizedCitationAccount.objects.get(pk=pk) + case "AuthorizedComputingAccount": + account = AuthorizedComputingAccount.objects.get(pk=pk) oauth1_client_config = account.external_service.oauth1_client_config final_credentials, other_info = async_to_sync(get_access_token)( diff --git a/addon_service/serializers.py b/addon_service/serializers.py index 41f2165e..e3f15b46 100644 --- a/addon_service/serializers.py +++ b/addon_service/serializers.py @@ -8,6 +8,9 @@ from addon_service.authorized_account.citation.serializers import ( AuthorizedCitationAccountSerializer, ) +from addon_service.authorized_account.computing.serializers import ( + AuthorizedComputingAccountSerializer, +) from addon_service.authorized_account.polymorphic_serializers import ( AuthorizedAccountPolymorphicSerializer, ) @@ -18,6 +21,9 @@ from addon_service.configured_addon.citation.serializers import ( ConfiguredCitationAddonSerializer, ) +from addon_service.configured_addon.computing.serializers import ( + ConfiguredComputingAddonSerializer, +) from addon_service.configured_addon.polymorphic_serializers import ( ConfiguredAddonPolymorphicSerializer, ) @@ -28,6 +34,9 @@ from addon_service.external_service.citation.serializers import ( ExternalCitationServiceSerializer, ) +from addon_service.external_service.computing.serializers import ( + ExternalComputingServiceSerializer, +) from addon_service.external_service.serializers import ExternalServiceSerializer from addon_service.external_service.storage.serializers import ( ExternalStorageServiceSerializer, @@ -45,6 +54,9 @@ "ConfiguredCitationAddonSerializer", "ExternalCitationServiceSerializer", "AuthorizedCitationAccountSerializer", + "ConfiguredComputingAddonSerializer", + "ExternalComputingServiceSerializer", + "AuthorizedComputingAccountSerializer", "ResourceReferenceSerializer", "AddonImpSerializer", "AddonOperationInvocationSerializer", diff --git a/addon_service/urls.py b/addon_service/urls.py index 62df6370..d6b404bf 100644 --- a/addon_service/urls.py +++ b/addon_service/urls.py @@ -57,6 +57,9 @@ def _register_viewset(viewset): _register_viewset(views.AuthorizedCitationAccountViewSet) _register_viewset(views.ConfiguredCitationAddonViewSet) _register_viewset(views.ExternalCitationServiceViewSet) +_register_viewset(views.AuthorizedComputingAccountViewSet) +_register_viewset(views.ConfiguredComputingAddonViewSet) +_register_viewset(views.ExternalComputingServiceViewSet) _register_viewset(views.ResourceReferenceViewSet) _register_viewset(views.AddonOperationInvocationViewSet) _register_viewset(views.AddonOperationViewSet) diff --git a/addon_service/views.py b/addon_service/views.py index 86e25731..4f7078a8 100644 --- a/addon_service/views.py +++ b/addon_service/views.py @@ -13,12 +13,21 @@ from addon_service.authorized_account.citation.views import ( AuthorizedCitationAccountViewSet, ) +from addon_service.authorized_account.computing.views import ( + AuthorizedComputingAccountViewSet, +) from addon_service.authorized_account.storage.views import ( AuthorizedStorageAccountViewSet, ) from addon_service.configured_addon.citation.views import ConfiguredCitationAddonViewSet +from addon_service.configured_addon.computing.views import ( + ConfiguredComputingAddonViewSet, +) from addon_service.configured_addon.storage.views import ConfiguredStorageAddonViewSet from addon_service.external_service.citation.views import ExternalCitationServiceViewSet +from addon_service.external_service.computing.views import ( + ExternalComputingServiceViewSet, +) from addon_service.external_service.storage.views import ExternalStorageServiceViewSet from addon_service.oauth1.views import oauth1_callback_view from addon_service.oauth2.views import oauth2_callback_view @@ -49,6 +58,9 @@ async def status(request): "AuthorizedCitationAccountViewSet", "ConfiguredCitationAddonViewSet", "ExternalCitationServiceViewSet", + "AuthorizedComputingAccountViewSet", + "ConfiguredComputingAddonViewSet", + "ExternalComputingServiceViewSet", "AuthorizedStorageAccountViewSet", "ConfiguredStorageAddonViewSet", "ExternalStorageServiceViewSet", From ca92e6e377f41e042ddcec0b89a6ea5337799709 Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Mon, 28 Oct 2024 23:19:10 -0400 Subject: [PATCH 08/10] add boa-api as dep --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index f9b4f533..d195bc8f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -210,6 +210,17 @@ files = [ {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, ] +[[package]] +name = "boa-api" +version = "0.1.14" +description = "Client API for using the Boa infrastructure" +optional = false +python-versions = ">=3.5" +files = [ + {file = "boa-api-0.1.14.tar.gz", hash = "sha256:1b7e69b30b43d1966cc321271a4925657c654220eecb85f6e2eadd6e9c1f751c"}, + {file = "boa_api-0.1.14-py3-none-any.whl", hash = "sha256:b131dbfbc6ed04045aaff56f0520bde7f199b4940b99c136427cad3c22864d67"}, +] + [[package]] name = "boto3" version = "1.34.0" @@ -2198,4 +2209,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "3fc10ab916c08fb7ce133e1a4cba9648e2ee3858ed2b11cac2a8e0291ea6140c" +content-hash = "f2198ba94898f4260705bc395c05b70dff5b019786c9c0011da0ba92852f92b4" diff --git a/pyproject.toml b/pyproject.toml index 066c8f87..84cbfc1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ aiohttp = "3.9.3" pyjwe = "1.0.0" cryptography = "42.0.7" boto3 = "1.34" +boa-api = "^0.1.14" [tool.poetry.group.dev.dependencies] From 2a75476dc51daa1977d778b570cd91fc8eb02e5e Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Tue, 10 Sep 2024 00:20:56 -0400 Subject: [PATCH 09/10] boa-computing: impl, interfaces, tests --- addon_imps/computing/__init__.py | 2 + addon_imps/computing/boa.py | 34 +++++++++++++ addon_imps/tests/computing/__init__.py | 0 addon_imps/tests/computing/test_boa.py | 58 ++++++++++++++++++++++ addon_service/common/known_imps.py | 6 +++ addon_toolkit/interfaces/__init__.py | 3 ++ addon_toolkit/interfaces/computing.py | 67 ++++++++++++++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 addon_imps/computing/__init__.py create mode 100644 addon_imps/computing/boa.py create mode 100644 addon_imps/tests/computing/__init__.py create mode 100644 addon_imps/tests/computing/test_boa.py create mode 100644 addon_toolkit/interfaces/computing.py diff --git a/addon_imps/computing/__init__.py b/addon_imps/computing/__init__.py new file mode 100644 index 00000000..1d74e8f0 --- /dev/null +++ b/addon_imps/computing/__init__.py @@ -0,0 +1,2 @@ +"""addon_imps.computing: imps that implement a computing-like interface +""" diff --git a/addon_imps/computing/boa.py b/addon_imps/computing/boa.py new file mode 100644 index 00000000..4d7a76ab --- /dev/null +++ b/addon_imps/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 computing + + +logger = logging.getLogger(__name__) + + +class BoaComputingImp(computing.ComputingAddonClientRequestorImp): + """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/computing/__init__.py b/addon_imps/tests/computing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_imps/tests/computing/test_boa.py b/addon_imps/tests/computing/test_boa.py new file mode 100644 index 00000000..b855e51b --- /dev/null +++ b/addon_imps/tests/computing/test_boa.py @@ -0,0 +1,58 @@ +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.computing.boa import BoaComputingImp +from addon_toolkit.credentials import UsernamePasswordCredentials +from addon_toolkit.interfaces.computing import ComputingConfig + + +logger = logging.getLogger(__name__) + + +class TestBoaComputingImp(unittest.IsolatedAsyncioTestCase): + + @patch.object(BoaComputingImp, "create_client") + def setUp(self, create_client_mock): + self.base_url = BOA_API_ENDPOINT + self.config = ComputingConfig(external_api_url=self.base_url) + self.client = MagicMock() + self.credentials = UsernamePasswordCredentials(username="dog", password="woof") + self.imp = BoaComputingImp(config=self.config, credentials=self.credentials) + self.imp.client = self.client + + @patch.object(BoaComputingImp, "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(BoaComputingImp, "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"{BoaComputingImp.__module__}.BoaClient") + def test_create_client(self, mock_cls): + mock_obj = MagicMock() + mock_cls.return_value = mock_obj + creds = UsernamePasswordCredentials(username="dog", password="woof") + BoaComputingImp.create_client(creds) + + mock_cls.assert_called_once_with(endpoint=BOA_API_ENDPOINT) + mock_obj.login.assert_called_once_with("dog", "woof") diff --git a/addon_service/common/known_imps.py b/addon_service/common/known_imps.py index 565cb6d6..30a8afc3 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.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.BoaComputingImp + 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..2b878dd7 100644 --- a/addon_toolkit/interfaces/__init__.py +++ b/addon_toolkit/interfaces/__init__.py @@ -2,6 +2,7 @@ from . import ( citation, + computing, storage, ) from ._base import BaseAddonInterface @@ -12,9 +13,11 @@ "BaseAddonInterface", "storage", "citation", + "computing", ) class AllAddonInterfaces(enum.Enum): STORAGE = storage.StorageAddonInterface CITATION = citation.CitationServiceInterface + COMPUTING = computing.ComputingAddonInterface diff --git a/addon_toolkit/interfaces/computing.py b/addon_toolkit/interfaces/computing.py new file mode 100644 index 00000000..1bd91f90 --- /dev/null +++ b/addon_toolkit/interfaces/computing.py @@ -0,0 +1,67 @@ +"""a static (and still in progress) definition of what composes a 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__ = ( + "ComputingAddonInterface", + "ComputingAddonImp", + "ComputingConfig", +) + + +### +# dataclasses used for operation args and return values + + +@dataclasses.dataclass(frozen=True) +class ComputingConfig: + external_api_url: str + external_account_id: str | None = None + + +### +# declaration of all computing addon operations + + +class ComputingAddonInterface(BaseAddonInterface, typing.Protocol): + + pass + + +@dataclasses.dataclass +class ComputingAddonImp(AddonImp): + """base class for computing addon implementations""" + + ADDON_INTERFACE = ComputingAddonInterface + + config: ComputingConfig + + +@dataclasses.dataclass +class ComputingAddonHttpRequestorImp(ComputingAddonImp): + """base class for computing addon implementations using GV network""" + + network: HttpRequestor + + +@dataclasses.dataclass +class ComputingAddonClientRequestorImp[T](ComputingAddonImp): + """base class for 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 From 0f0657da76224c9382c730c150329c3653891cba Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Wed, 18 Dec 2024 03:15:36 -0500 Subject: [PATCH 10/10] generate computing migrations --- ...count_configuredcomputingaddon_and_more.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 addon_service/migrations/0008_authorizedcomputingaccount_configuredcomputingaddon_and_more.py diff --git a/addon_service/migrations/0008_authorizedcomputingaccount_configuredcomputingaddon_and_more.py b/addon_service/migrations/0008_authorizedcomputingaccount_configuredcomputingaddon_and_more.py new file mode 100644 index 00000000..6676243e --- /dev/null +++ b/addon_service/migrations/0008_authorizedcomputingaccount_configuredcomputingaddon_and_more.py @@ -0,0 +1,91 @@ +# Generated by Django 4.2.7 on 2024-12-18 08:12 + +import django.db.models.deletion +from django.db import ( + migrations, + models, +) + +import addon_service.external_service.computing.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("addon_service", "0007_oauth2clientconfig_quirks"), + ] + + operations = [ + migrations.CreateModel( + name="AuthorizedComputingAccount", + fields=[ + ( + "authorizedaccount_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="addon_service.authorizedaccount", + ), + ), + ], + options={ + "verbose_name": "Authorized Computing Account", + "verbose_name_plural": "Authorized Computing Accounts", + }, + bases=("addon_service.authorizedaccount",), + ), + migrations.CreateModel( + name="ConfiguredComputingAddon", + fields=[ + ( + "configuredaddon_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="addon_service.configuredaddon", + ), + ), + ], + options={ + "verbose_name": "Configured Computing Addon", + "verbose_name_plural": "Configured Computing Addons", + }, + bases=("addon_service.configuredaddon",), + ), + migrations.CreateModel( + name="ExternalComputingService", + fields=[ + ( + "externalservice_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="addon_service.externalservice", + ), + ), + ( + "int_supported_features", + models.IntegerField( + null=True, + validators=[ + addon_service.external_service.computing.models.validate_supported_features + ], + ), + ), + ], + options={ + "verbose_name": "External Computing Service", + "verbose_name_plural": "External Computing Services", + }, + bases=("addon_service.externalservice",), + ), + ]