diff --git a/addon_service/authorized_storage_account/models.py b/addon_service/authorized_storage_account/models.py index 25011897..40c282eb 100644 --- a/addon_service/authorized_storage_account/models.py +++ b/addon_service/authorized_storage_account/models.py @@ -1,8 +1,8 @@ from django.contrib.postgres.fields import ArrayField from django.db import models +from addon_service.capability.models import IntStorageCapability from addon_service.common.base_model import AddonsServiceBaseModel -from addon_service.common.int_capability import IntStorageCapability class AuthorizedStorageAccount(AddonsServiceBaseModel): diff --git a/addon_service/authorized_storage_account/serializers.py b/addon_service/authorized_storage_account/serializers.py index cd74a757..c403ae2b 100644 --- a/addon_service/authorized_storage_account/serializers.py +++ b/addon_service/authorized_storage_account/serializers.py @@ -1,19 +1,21 @@ from rest_framework_json_api import serializers from rest_framework_json_api.relations import ( - ResourceRelatedField, HyperlinkedRelatedField, + ResourceRelatedField, ) from rest_framework_json_api.utils import get_resource_type_from_model +from addon_service.capability.serializers import StorageCapabilityField from addon_service.models import ( AuthorizedStorageAccount, ConfiguredStorageAddon, - ExternalStorageService, - ExternalCredentials, ExternalAccount, + ExternalCredentials, + ExternalStorageService, InternalUser, ) + RESOURCE_NAME = get_resource_type_from_model(AuthorizedStorageAccount) @@ -32,7 +34,6 @@ def to_internal_value(self, data): class AuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -41,9 +42,9 @@ def __init__(self, *args, **kwargs): self.fields.pop("configured_storage_addons", None) url = serializers.HyperlinkedIdentityField( - view_name=f"{RESOURCE_NAME}-detail", - required=False + view_name=f"{RESOURCE_NAME}-detail", required=False ) + authorized_capabilities = StorageCapabilityField() account_owner = AccountOwnerField( many=False, queryset=InternalUser.objects.all(), @@ -103,4 +104,5 @@ class Meta: "external_storage_service", "username", "password", + "authorized_capabilities", ] diff --git a/addon_service/capability/__init__.py b/addon_service/capability/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_service/common/int_capability.py b/addon_service/capability/models.py similarity index 96% rename from addon_service/common/int_capability.py rename to addon_service/capability/models.py index 8678000b..bf3496fc 100644 --- a/addon_service/common/int_capability.py +++ b/addon_service/capability/models.py @@ -13,6 +13,7 @@ class _IntEnumForEnum(enum.IntEnum): def __init_subclass__(cls, /, base_enum: type[enum.Enum], **kwargs): super().__init_subclass__(**kwargs) cls.__base_enum = base_enum + # ensure enums have same names _base_names = {_item.name for _item in base_enum} _int_names = {_item.name for _item in cls} assert _base_names == _int_names diff --git a/addon_service/capability/serializers.py b/addon_service/capability/serializers.py new file mode 100644 index 00000000..51d6b679 --- /dev/null +++ b/addon_service/capability/serializers.py @@ -0,0 +1,44 @@ +import enum + +from rest_framework_json_api import serializers + +from addon_service.capability.models import ( + IntStorageCapability, + StorageCapability, +) + + +class EnumsMultipleChoiceField(serializers.MultipleChoiceField): + __internal_enum: type[enum.Enum] + __external_enum: type[enum.Enum] + + def __init__(self, /, internal_enum, external_enum, **kwargs): + _choices = {_external_member.value for _external_member in external_enum} + super().__init__(**kwargs, choices=_choices) + self.__internal_enum = internal_enum + self.__external_enum = external_enum + + def to_internal_value(self, data): + _names = super().to_internal_value(data) + return {self._to_internal_enum_member(_name) for _name in _names} + + def to_representation(self, value): + _member_list = super().to_representation(value) + return {self._to_external_enum_value(_member) for _member in _member_list} + + def _to_internal_enum_member(self, external_value): + _external_member = self.__external_enum(external_value) + return self.__internal_enum[_external_member.name] + + def _to_external_enum_value(self, internal_value): + _internal_member = self.__internal_enum(internal_value) + _external_member = self.__external_enum[_internal_member.name] + return _external_member.value + + +def StorageCapabilityField(**kwargs): + return EnumsMultipleChoiceField( + external_enum=StorageCapability, + internal_enum=IntStorageCapability, + **kwargs, + ) diff --git a/addon_service/configured_storage_addon/models.py b/addon_service/configured_storage_addon/models.py index a7c18a74..bd6257d3 100644 --- a/addon_service/configured_storage_addon/models.py +++ b/addon_service/configured_storage_addon/models.py @@ -1,8 +1,8 @@ from django.contrib.postgres.fields import ArrayField from django.db import models +from addon_service.capability.models import IntStorageCapability from addon_service.common.base_model import AddonsServiceBaseModel -from addon_service.common.int_capability import IntStorageCapability class ConfiguredStorageAddon(AddonsServiceBaseModel): diff --git a/addon_service/configured_storage_addon/serializers.py b/addon_service/configured_storage_addon/serializers.py index 7bf2d0ee..bd45cef7 100644 --- a/addon_service/configured_storage_addon/serializers.py +++ b/addon_service/configured_storage_addon/serializers.py @@ -2,6 +2,7 @@ from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import get_resource_type_from_model +from addon_service.capability.serializers import StorageCapabilityField from addon_service.models import ( AuthorizedStorageAccount, ConfiguredStorageAddon, @@ -22,6 +23,7 @@ def to_internal_value(self, data): class ConfiguredStorageAddonSerializer(serializers.HyperlinkedModelSerializer): root_folder = serializers.CharField(required=False) url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail") + connected_capabilities = StorageCapabilityField() base_account = ResourceRelatedField( queryset=AuthorizedStorageAccount.objects.all(), many=False, @@ -47,4 +49,5 @@ class Meta: "root_folder", "base_account", "authorized_resource", + "connected_capabilities", ] diff --git a/addon_service/migrations/0001_initial.py b/addon_service/migrations/0001_initial.py index 6d5f8334..de96a12e 100644 --- a/addon_service/migrations/0001_initial.py +++ b/addon_service/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 4.2.7 on 2023-12-11 20:02 +# Generated by Django 4.2.7 on 2024-02-07 20:42 +import django.contrib.postgres.fields import django.db.models.deletion from django.db import ( migrations, @@ -27,6 +28,20 @@ class Migration(migrations.Migration): ), ("created", models.DateTimeField(editable=False)), ("modified", models.DateTimeField()), + ( + "authorized_capabilities", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField( + choices=[ + (1, "ACCESS"), + (2, "BROWSE"), + (3, "UPDATE"), + (4, "COMMIT"), + ] + ), + size=None, + ), + ), ("default_root_folder", models.CharField(blank=True)), ], options={ @@ -215,19 +230,33 @@ class Migration(migrations.Migration): ("modified", models.DateTimeField()), ("root_folder", models.CharField()), ( - "base_account", + "connected_capabilities", + django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField( + choices=[ + (1, "ACCESS"), + (2, "BROWSE"), + (3, "UPDATE"), + (4, "COMMIT"), + ] + ), + size=None, + ), + ), + ( + "authorized_resource", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="configured_storage_addons", - to="addon_service.authorizedstorageaccount", + to="addon_service.internalresource", ), ), ( - "authorized_resource", + "base_account", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="configured_storage_addons", - to="addon_service.internalresource", + to="addon_service.authorizedstorageaccount", ), ), ], diff --git a/addon_service/tests/_factories.py b/addon_service/tests/_factories.py index 5b0b5610..bc7d3832 100644 --- a/addon_service/tests/_factories.py +++ b/addon_service/tests/_factories.py @@ -2,6 +2,7 @@ from factory.django import DjangoModelFactory from addon_service import models as db +from addon_service.capability.models import IntStorageCapability class InternalUserFactory(DjangoModelFactory): @@ -59,6 +60,7 @@ class Meta: model = db.AuthorizedStorageAccount default_root_folder = "/" + authorized_capabilities = factory.List([IntStorageCapability.ACCESS]) external_storage_service = factory.SubFactory(ExternalStorageServiceFactory) external_account = factory.SubFactory(ExternalAccountFactory) # TODO: external_account.credentials_issuer same as @@ -70,5 +72,6 @@ class Meta: model = db.ConfiguredStorageAddon root_folder = "/" + connected_capabilities = factory.List([IntStorageCapability.ACCESS]) base_account = factory.SubFactory(AuthorizedStorageAccountFactory) authorized_resource = factory.SubFactory(InternalResourceFactory) diff --git a/addon_service/tests/test_by_type/test_authorized_storage_account.py b/addon_service/tests/test_by_type/test_authorized_storage_account.py index abcdc9d6..12c614c6 100644 --- a/addon_service/tests/test_by_type/test_authorized_storage_account.py +++ b/addon_service/tests/test_by_type/test_authorized_storage_account.py @@ -110,8 +110,13 @@ def test_get(self): set(_content["data"]["attributes"].keys()), { "default_root_folder", + "authorized_capabilities", }, ) + self.assertEqual( + _content["data"]["attributes"]["authorized_capabilities"], + ["access"], + ) self.assertEqual( set(_content["data"]["relationships"].keys()), { diff --git a/addon_service/tests/test_by_type/test_configured_storage_addon.py b/addon_service/tests/test_by_type/test_configured_storage_addon.py index be95bc21..2cc16e36 100644 --- a/addon_service/tests/test_by_type/test_configured_storage_addon.py +++ b/addon_service/tests/test_by_type/test_configured_storage_addon.py @@ -97,8 +97,13 @@ def test_get(self): set(_content["data"]["attributes"].keys()), { "root_folder", + "connected_capabilities", }, ) + self.assertEqual( + _content["data"]["attributes"]["connected_capabilities"], + ["access"], + ) self.assertEqual( set(_content["data"]["relationships"].keys()), { diff --git a/addon_toolkit/operation.py b/addon_toolkit/operation.py index e5d0e278..3b8073af 100644 --- a/addon_toolkit/operation.py +++ b/addon_toolkit/operation.py @@ -137,7 +137,7 @@ def __set_name__(self, cls, name): AddonOperation.register( AddonOperation( operation_type=self.operation_type, - capability_id=self.capability_id, + capability_id=str(self.capability_id), declaration_cls=cls, method_name=name, ), diff --git a/addon_toolkit/tests/__init__.py b/addon_toolkit/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/addon_toolkit/tests/test_addon_category.py b/addon_toolkit/tests/test_addon_category.py index 8b2437f3..86fe86fb 100644 --- a/addon_toolkit/tests/test_addon_category.py +++ b/addon_toolkit/tests/test_addon_category.py @@ -14,8 +14,8 @@ class TestAddonCategory(unittest.TestCase): def setUp(self): class _MyChecksumArchiveCapability(enum.StrEnum): - GET_IT = enum.auto() - PUT_IT = enum.auto() + GET_IT = "get-it" + PUT_IT = "put-it" class _MyChecksumArchiveInterface(AddonInterface): """this is a docstring for _MyChecksumArchiveInterface @@ -26,23 +26,17 @@ class _MyChecksumArchiveInterface(AddonInterface): @redirect_operation(capability=_MyChecksumArchiveCapability.GET_IT) def url_for_get(self, checksum_iri) -> str: """this url_for_get docstring should find its way to docs""" - return f"https://myarchive.example///{checksum_iri}" + raise NotImplementedError @proxy_operation(capability=_MyChecksumArchiveCapability.GET_IT) async def query_relations(self, checksum_iri, query=None): """this query_relations docstring should find its way to docs""" - # yields rdf triples (or twoples with implicit subject) - yield ("http://purl.org/dc/terms/references", "checksum:foo:bar") + raise NotImplementedError @redirect_operation(capability=_MyChecksumArchiveCapability.PUT_IT) def url_for_put(self, checksum_iri): """this url_for_put docstring should find its way to docs""" - # TODO: how to represent "send a PUT request here"? - # return RedirectLadle( - # HTTPMethod.PUT, - # f'https://myarchive.example///{checksum_iri}', - # )? - return f"https://myarchive.example///{checksum_iri}" + raise NotImplementedError self.my_addon_category = AddonCategory( capabilities=_MyChecksumArchiveCapability, @@ -54,7 +48,7 @@ def url_for_get(self, checksum_iri) -> str: return f"https://myarchive.example///{checksum_iri}" async def query_relations(self, checksum_iri, query=None): - # yields rdf triples (or twoples with implicit subject) + # maybe yield rdf triples (or twoples with implicit subject) yield ("http://purl.org/dc/terms/references", "checksum:foo:bar") def url_for_put(self, checksum_iri): @@ -70,19 +64,19 @@ def url_for_put(self, checksum_iri): def test_operation_list(self): _get_operation = AddonOperation( operation_type=AddonOperationType.REDIRECT, - capability=self.my_addon_category.capabilities.GET, + capability_id=self.my_addon_category.capabilities.GET_IT, declaration_cls=self.my_addon_category.base_interface, method_name="url_for_get", ) _put_operation = AddonOperation( operation_type=AddonOperationType.REDIRECT, - capability=self.my_addon_category.capabilities.PUT, + capability_id=self.my_addon_category.capabilities.PUT_IT, declaration_cls=self.my_addon_category.base_interface, method_name="url_for_put", ) _query_operation = AddonOperation( operation_type=AddonOperationType.PROXY, - capability=self.my_addon_category.capabilities.GET, + capability_id=self.my_addon_category.capabilities.GET_IT, declaration_cls=self.my_addon_category.base_interface, method_name="query_relations", ) @@ -91,26 +85,14 @@ def test_operation_list(self): {_get_operation, _put_operation, _query_operation}, ) self.assertEqual( - set( - self.my_addon_category.operations_declared( - capability_iri=self.namespace.get_thing, - ) - ), + set(self.my_addon_category.operations_declared(capability_id="get-it")), {_get_operation, _query_operation}, ) self.assertEqual( - set( - self.my_addon_category.operations_declared( - capability_iri=self.namespace.put_thing, - ) - ), + set(self.my_addon_category.operations_declared(capability_id="put-it")), {_put_operation}, ) self.assertEqual( - set( - self.my_addon_category.operations_declared( - capability_iri="http://nothing.example/", - ) - ), + set(self.my_addon_category.operations_declared(capability_id="nothing")), set(), )