Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] addon toolkit #8

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added addon_interfaces/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions addon_interfaces/_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from http import HTTPMethod

from addon_toolkit.storage import StorageInterface


# TODO: actual implementations
class _ExampleStorageImplementation(StorageInterface):
# implement method from StorageInterface
def item_download_url(self, item_id: str) -> str:
return self._waterbutler_download_url(item_id)

# implement method from StorageInterface
async def get_item_description(self, item_id: str):
yield ("http://purl.org/dc/terms/identifier", item_id)

# implement method from StorageInterface
def item_upload_url(self, item_id: str) -> str:
return self._waterbutler_upload_url(item_id)

# implement method from StorageInterface
async def pls_delete_item(self, item_id: str):
await self.external_request(
HTTPMethod.DELETE,
self._external_url(item_id),
)

###
# private, implementation-specific methods

def _waterbutler_download_url(self, item_id):
raise NotImplementedError

def _waterbutler_upload_url(self, item_id):
raise NotImplementedError
2 changes: 1 addition & 1 deletion addon_service/authorized_storage_account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


class AuthorizedStorageAccount(AddonsServiceBaseModel):
# TODO: capabilities = ArrayField(...)
# TODO: authorized_capabilities = ArrayField(...)
default_root_folder = models.CharField(blank=True)

external_storage_service = models.ForeignKey(
Expand Down
47 changes: 42 additions & 5 deletions addon_service/authorized_storage_account/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from addon_service.models import (
AuthorizedStorageAccount,
ConfiguredStorageAddon,
ExternalStorageService,
InternalUser,
)
Expand All @@ -16,21 +15,22 @@
RESOURCE_NAME = get_resource_type_from_model(AuthorizedStorageAccount)


class AuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer):
class ReadAuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")
account_owner = HyperlinkedRelatedField(
many=False,
queryset=InternalUser.objects.all(),
read_only=True,
related_link_view_name=f"{RESOURCE_NAME}-related",
)
external_storage_service = ResourceRelatedField(
queryset=ExternalStorageService.objects.all(),
many=False,
read_only=True,
queryset=ExternalStorageService.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
)
configured_storage_addons = HyperlinkedRelatedField(
many=True,
queryset=ConfiguredStorageAddon.objects.all(),
read_only=True,
related_link_view_name=f"{RESOURCE_NAME}-related",
)

Expand All @@ -53,3 +53,40 @@ class Meta:
"default_root_folder",
"external_storage_service",
]


class WriteAuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer):
account_owner = HyperlinkedRelatedField(
many=False,
queryset=InternalUser.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
)
external_storage_service = ResourceRelatedField(
many=False,
queryset=ExternalStorageService.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
)
# credentials, in-line as fields

included_serializers = {
"account_owner": "addon_service.serializers.InternalUserSerializer",
"external_storage_service": (
"addon_service.serializers.ExternalStorageServiceSerializer"
),
}

class Meta:
model = AuthorizedStorageAccount
fields = [
"url",
"account_owner",
"configured_storage_addons",
"default_root_folder",
"external_storage_service",
]

def create(self, validated_data):
# implicitly create ExternalAccount
# depending on
_resp = super().create(validated_data)
return _resp
8 changes: 8 additions & 0 deletions addon_service/common/serializer_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from rest_framework_json_api.relations import HyperlinkedRelatedField


class WritableHyperlinkedRelatedField(HyperlinkedRelatedField):
def to_internal_value(self, external_value):
breakpoint()
_result = super().to_internal_value(external_value)
return _result
4 changes: 2 additions & 2 deletions addon_service/configured_storage_addon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
class ConfiguredStorageAddon(AddonsServiceBaseModel):
root_folder = models.CharField()

authorized_storage_account = models.ForeignKey(
base_account = models.ForeignKey(
"addon_service.AuthorizedStorageAccount",
on_delete=models.CASCADE,
related_name="configured_storage_addons",
)
internal_resource = models.ForeignKey(
authorized_resource = models.ForeignKey(
"addon_service.InternalResource",
on_delete=models.CASCADE,
related_name="configured_storage_addons",
Expand Down
15 changes: 8 additions & 7 deletions addon_service/configured_storage_addon/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework_json_api.utils import get_resource_type_from_model

from addon_service.models import (
AuthorizedStorageAccount,
ConfiguredStorageAddon,
InternalResource,
)
Expand All @@ -13,29 +14,29 @@

class ConfiguredStorageAddonSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")
authorized_storage_account = ResourceRelatedField(
queryset=ConfiguredStorageAddon.objects.all(),
base_account = ResourceRelatedField(
queryset=AuthorizedStorageAccount.objects.all(),
many=False,
related_link_view_name=f"{RESOURCE_NAME}-related",
)
internal_resource = ResourceRelatedField(
authorized_resource = ResourceRelatedField(
queryset=InternalResource.objects.all(),
many=False,
related_link_view_name=f"{RESOURCE_NAME}-related",
)

included_serializers = {
"authorized_storage_account": (
"base_account": (
"addon_service.serializers.AuthorizedStorageAccountSerializer"
),
"internal_resource": "addon_service.serializers.InternalResourceSerializer",
"authorized_resource": "addon_service.serializers.InternalResourceSerializer",
}

class Meta:
model = ConfiguredStorageAddon
fields = [
"url",
"root_folder",
"authorized_storage_account",
"internal_resource",
"base_account",
"authorized_resource",
]
1 change: 1 addition & 0 deletions addon_service/external_storage_service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ExternalStorageService(AddonsServiceBaseModel):

auth_uri = models.URLField(null=False)

storage_interface_key = models.TextField(null=False)
credentials_issuer = models.ForeignKey(
"addon_service.CredentialsIssuer",
on_delete=models.CASCADE,
Expand Down
4 changes: 2 additions & 2 deletions addon_service/external_storage_service/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework_json_api import serializers
from rest_framework_json_api.relations import HyperlinkedRelatedField
from rest_framework_json_api.utils import get_resource_type_from_model

from addon_service.common.serializer_fields import WritableHyperlinkedRelatedField
from addon_service.models import (
AuthorizedStorageAccount,
ExternalStorageService,
Expand All @@ -14,7 +14,7 @@
class ExternalStorageServiceSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")

authorized_storage_accounts = HyperlinkedRelatedField(
authorized_storage_accounts = WritableHyperlinkedRelatedField(
many=True,
queryset=AuthorizedStorageAccount.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
Expand Down
4 changes: 2 additions & 2 deletions addon_service/internal_resource/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework_json_api import serializers
from rest_framework_json_api.relations import HyperlinkedRelatedField
from rest_framework_json_api.utils import get_resource_type_from_model

from addon_service.common.serializer_fields import WritableHyperlinkedRelatedField
from addon_service.models import (
ConfiguredStorageAddon,
InternalResource,
Expand All @@ -13,7 +13,7 @@

class InternalResourceSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")
configured_storage_addons = HyperlinkedRelatedField(
configured_storage_addons = WritableHyperlinkedRelatedField(
many=True,
queryset=ConfiguredStorageAddon.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
Expand Down
4 changes: 2 additions & 2 deletions addon_service/internal_user/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework_json_api import serializers
from rest_framework_json_api.relations import HyperlinkedRelatedField
from rest_framework_json_api.utils import get_resource_type_from_model

from addon_service.common.serializer_fields import WritableHyperlinkedRelatedField
from addon_service.models import (
AuthorizedStorageAccount,
InternalUser,
Expand All @@ -14,7 +14,7 @@
class InternalUserSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")

authorized_storage_accounts = HyperlinkedRelatedField(
authorized_storage_accounts = WritableHyperlinkedRelatedField(
many=True,
queryset=AuthorizedStorageAccount.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
Expand Down
4 changes: 2 additions & 2 deletions addon_service/management/commands/fill_garbage.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def handle_label(self, label, **options):
resource_uri=f"http://osf.example/r{label}{_j}",
)
_csa = db.ConfiguredStorageAddon.objects.create(
authorized_storage_account=_asa,
internal_resource=_ir,
base_account=_asa,
authorized_resource=_ir,
)
return str(_csa)
4 changes: 2 additions & 2 deletions addon_service/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,15 @@ class Migration(migrations.Migration):
("modified", models.DateTimeField()),
("root_folder", models.CharField()),
(
"authorized_storage_account",
"base_account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="configured_storage_addons",
to="addon_service.authorizedstorageaccount",
),
),
(
"internal_resource",
"authorized_resource",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="configured_storage_addons",
Expand Down
4 changes: 2 additions & 2 deletions addon_service/tests/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@ class Meta:
model = db.ConfiguredStorageAddon

root_folder = "/"
authorized_storage_account = factory.SubFactory(AuthorizedStorageAccountFactory)
internal_resource = factory.SubFactory(InternalResourceFactory)
base_account = factory.SubFactory(AuthorizedStorageAccountFactory)
authorized_resource = factory.SubFactory(InternalResourceFactory)
83 changes: 83 additions & 0 deletions addon_service/tests/test_api_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from django.urls import reverse
from rest_framework.test import APITestCase

from addon_service import models as db
from addon_service.tests import _factories


class TestCrudFlow(APITestCase):
@classmethod
def setUpTestData(cls):
# assumed to be set up thru admin interface, read-only in the api
cls._external_service = _factories.ExternalStorageServiceFactory()
# (TODO: user/resource created implicitly)
cls._internal_user = _factories.InternalUserFactory()
cls._internal_resource = _factories.InternalResourceFactory()

def test_create_read_update_delete(self):
_account = self._create_authorized_account()
self.assertNotNone(_account)
# _addon = self._create_configured_addon()

def _create_authorized_account(self):
_default_root_folder = "/hello/hello/"
_account_post_body = {
"data": {
"type": _resource_type(db.AuthorizedStorageAccount),
"attributes": {
"default_root_folder": _default_root_folder,
},
# TODO: attributes
"relationships": {
"account_owner": {
"data": _resource_ref(self._internal_user),
},
"external_storage_service": {
"data": _resource_ref(self._external_service),
},
},
},
}
_create_account_resp = self.client.post(
_path__create(db.AuthorizedStorageAccount),
_account_post_body,
)
_create_resp_json = _create_account_resp.json()
_account_pk = _create_resp_json["data"]["id"]
_db_account = db.AuthorizedStorageAccount.objects.get(pk=_account_pk)
self.assertEqual(_default_root_folder, _db_account.default_root_folder)
self.assertEqual(
_default_root_folder,
_create_resp_json["data"]["attributes"]["default_root_folder"],
)
return _db_account


##
# helpers for the tests above


def _resource_type(something):
try:
return something.JSONAPIMeta.resource_name
except AttributeError:
raise ValueError(f"unsure how to type {something}")


def _path__create(model_cls):
return reverse(f"{_resource_type(model_cls)}-list")


def _path__by_pk(model_cls, pk):
return reverse(
f"{_resource_type(model_cls)}-detail",
kwargs={"pk": pk},
)


def _resource_ref(model_instance):
# jsonapi resource reference
return {
"type": model_instance.JSONAPIMeta.resource_name,
"id": model_instance.pk,
}
15 changes: 0 additions & 15 deletions addon_service/tests/test_base.py

This file was deleted.

Empty file.
Loading
Loading