Skip to content

Commit

Permalink
wip: tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Mar 7, 2024
1 parent 37a4bef commit d3c0481
Show file tree
Hide file tree
Showing 18 changed files with 356 additions and 199 deletions.
2 changes: 1 addition & 1 deletion addon_service/addon_imp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def implemented_operations(self) -> list:
from addon_service.addon_operation.models import AddonOperationModel

return [
AddonOperationModel.for_operation_imp(_op_imp)
AddonOperationModel(_op_imp)
for _op_imp in AddonOperationImplementation.on_implementation_cls(
self.imp_cls
)
Expand Down
54 changes: 29 additions & 25 deletions addon_service/addon_operation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,46 @@
from addon_service.addon_imp.models import AddonImpModel
from addon_service.common.dataclass_model import BaseDataclassModel
from addon_toolkit import AddonOperationImplementation
from addon_toolkit.dataclass_json import jsonschema_for_dataclass
from addon_toolkit.json_arguments import jsonschema_for_signature_params
from addon_toolkit.operation import AddonOperationType
from addon_toolkit.storage import StorageCapability


# not a database model; just a convenience wrapper for
# AddonOperationImplementation to give the serializer
@dataclasses.dataclass
class AddonOperationModel(BaseDataclassModel):
name: str
operation_type: AddonOperationType
docstring: str
implementation_docstring: str
capability: enum.Enum
imp_cls: type
params_dataclass: type
imp: AddonOperationImplementation

@classmethod
def for_operation_imp(
cls, op_imp: AddonOperationImplementation
) -> "AddonOperationModel":
return cls(
name=op_imp.operation.name,
operation_type=op_imp.operation.operation_type,
docstring=op_imp.operation.docstring,
implementation_docstring=op_imp.implementation_docstring,
capability=StorageCapability(op_imp.operation.capability),
imp_cls=op_imp.implementation_cls,
params_dataclass=op_imp.operation.params_dataclass,
)
@property
def name(self) -> str:
return self.imp.operation.name

@property
def operation_type(self) -> AddonOperationType:
return self.imp.operation.operation_type

@property
def docstring(self) -> str:
return self.imp.operation.docstring

@property
def implementation_docstring(self) -> str:
return self.imp.implementation_docstring

@property
def capability(self) -> enum.Enum:
# TODO: any known capability?
return self.imp.operation.capability

@property
def imp_cls(self) -> type:
return self.imp.implementation_cls

@classmethod
def get_by_natural_key(cls, key: list) -> "AddonOperationModel":
_imp_name, _operation_name = key
_imp_cls = KnownAddonImp[_imp_name].value # TODO: try, 404
return cls.for_operation_imp(
return cls(
AddonOperationImplementation.by_operation_name(
_imp_cls,
_operation_name,
Expand All @@ -53,12 +57,12 @@ def natural_key(self) -> list:
return [_imp_name, self.name]

@property
def implemented_by(self):
def implemented_by(self) -> AddonImpModel:
return AddonImpModel.for_imp(KnownAddonImp(self.imp_cls))

@property
def params_jsonschema(self) -> dict:
return jsonschema_for_dataclass(self.params_dataclass)
return jsonschema_for_signature_params(self.imp.operation.signature)

class JSONAPIMeta:
resource_name = "addon-operations"
38 changes: 26 additions & 12 deletions addon_service/addon_operation_invocation/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import jsonschema
from django.core.exceptions import ValidationError
from django.db import models
from django.db import (
models,
transaction,
)

from addon_service.addon_imp.known import KnownAddonImp
from addon_service.common.base_model import AddonsServiceBaseModel
from addon_service.common.dibs import dibs
from addon_service.common.invocation import IntInvocationStatus
from addon_service.models import AddonOperationModel
from addon_toolkit import AddonOperationImplementation
from addon_toolkit.dataclass_json import json_for_dataclass
from addon_toolkit.json_arguments import json_for_dataclass


class AddonOperationInvocation(AddonsServiceBaseModel):
Expand All @@ -17,7 +20,7 @@ class AddonOperationInvocation(AddonsServiceBaseModel):
default=IntInvocationStatus.STARTING,
)
operation_identifier = models.TextField()
operation_params = models.JSONField(default=dict, blank=True)
operation_kwargs = models.JSONField(default=dict, blank=True)
thru_addon = models.ForeignKey("ConfiguredStorageAddon", on_delete=models.CASCADE)
by_user = models.ForeignKey("UserReference", on_delete=models.CASCADE)
operation_result = models.JSONField(null=True, default=None, blank=True)
Expand All @@ -32,39 +35,50 @@ class JSONAPIMeta:

@property
def operation(self) -> AddonOperationModel:
return AddonOperationModel.for_operation_imp(self.operation_imp)
return AddonOperationModel(self.operation_imp)

@property
def operation_imp(self) -> AddonOperationImplementation:
return AddonOperationImplementation.by_operation_name(
imp_cls=self.known_addon_imp.value,
imp_cls=self.imp_cls,
op_name=self.operation_identifier,
)

@property
def known_addon_imp(self) -> KnownAddonImp:
return self.thru_addon.base_account.external_storage_service.known_addon_imp

@property
def imp_cls(self) -> type:
return self.known_addon_imp.value

def clean(self):
try:
jsonschema.validate(
instance=self.operation_params,
instance=self.operation_kwargs,
schema=self.operation.params_jsonschema,
)
except jsonschema.exceptions.ValidationError as _error:
raise ValidationError(_error)

def execute(self):
def execute(self): # TODO: async_execute?
with dibs(self):
try:
# TODO: what if this raises database error?
_result = self.operation_imp.call_with_params(self.operation_params)
# wrap in a transaction to contain database errors,
# so status can be saved in the outer transaction
with transaction.atomic():
_result = self.operation_imp.call_with_kwargs(
self.imp_cls(), # TODO: allow imp init params
**self.operation_kwargs,
)
except Exception as _e:
self.operation_result = None
self.invocation_status = IntInvocationStatus.PROBLEM
print(_e)
# TODO: error/traceback
else:
# TODO: save message/traceback
raise
else: # no errors
self.operation_result = json_for_dataclass(_result)
self.invocation_status = IntInvocationStatus.SUCCESS
self.save() # TODO: finally?
finally:
self.save()
8 changes: 4 additions & 4 deletions addon_service/addon_operation_invocation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Meta:
fields = [
"url",
"invocation_status",
"operation_params",
"operation_kwargs",
"operation_result",
"operation",
"by_user",
Expand All @@ -45,7 +45,7 @@ class Meta:
internal_enum=IntInvocationStatus,
external_enum=InvocationStatus,
)
operation_params = serializers.JSONField()
operation_kwargs = serializers.JSONField()
operation_result = serializers.JSONField(read_only=True)
created = serializers.DateTimeField(read_only=True)
modified = serializers.DateTimeField(read_only=True)
Expand Down Expand Up @@ -77,13 +77,13 @@ def create(self, validated_data):
_operation = validated_data["operation"]
_invocation = AddonOperationInvocation.objects.create(
operation_identifier=_operation.name,
operation_params=validated_data["operation_params"],
operation_kwargs=validated_data["operation_kwargs"],
thru_addon=validated_data["thru_addon"],
by_user=UserReference.objects.all().first(), # TODO: infer user from request!
)
match _operation.operation_type:
case AddonOperationType.REDIRECT | AddonOperationType.IMMEDIATE:
_invocation.execute()
_invocation.execute() # block until done
case AddonOperationType.EVENTUAL:
raise NotImplementedError("TODO: enqueue task")
case _:
Expand Down
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 @@ -49,7 +49,7 @@ def authorized_capabilities_enum(self) -> list[enum.Enum]:
@property
def authorized_operations(self) -> list[AddonOperationModel]:
return [
AddonOperationModel.for_operation_imp(_imp)
AddonOperationModel(_imp)
for _imp in AddonOperationImplementation.on_implementation_cls(
self.external_storage_service.addon_imp.imp_cls,
capabilities=self.authorized_capabilities_enum,
Expand Down
2 changes: 1 addition & 1 deletion addon_service/configured_storage_addon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def connected_capabilities_enum(self) -> list[enum.Enum]:
@property
def connected_operations(self) -> list[AddonOperationModel]:
return [
AddonOperationModel.for_operation_imp(_imp)
AddonOperationModel(_imp)
for _imp in AddonOperationImplementation.on_implementation_cls(
self.base_account.external_storage_service.addon_imp.imp_cls,
capabilities=self.connected_capabilities_enum,
Expand Down
2 changes: 1 addition & 1 deletion addon_service/management/commands/fill_garbage.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def handle_label(self, label, **options):
_soi = db.AddonOperationInvocation.objects.create(
invocation_status=IntInvocationStatus.STARTING,
operation_identifier=_op.natural_key_str,
operation_params={"item_id": "foo"},
operation_kwargs={"item_id": "foo"},
thru_addon=_csa,
by_user=_iu,
)
Expand Down
2 changes: 1 addition & 1 deletion addon_service/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ class Migration(migrations.Migration):
),
),
("operation_identifier", models.TextField()),
("operation_params", models.JSONField(blank=True, default=dict)),
("operation_kwargs", models.JSONField(blank=True, default=dict)),
(
"operation_result",
models.JSONField(blank=True, default=None, null=True),
Expand Down
12 changes: 12 additions & 0 deletions addon_service/tests/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ class Meta:
credentials = factory.SubFactory(ExternalCredentialsFactory)


class AddonOperationInvocationFactory(DjangoModelFactory):
class Meta:
model = db.AddonOperationInvocation

operation_identifier = "download"
operation_kwargs = {"item_id": "foo"}
thru_addon = factory.SubFactory(
"addon_service.tests._factories.ConfiguredStorageAddonFactory"
)
by_user = factory.SubFactory(UserReferenceFactory)


###
# "Storage" models

Expand Down
57 changes: 34 additions & 23 deletions addon_service/tests/test_by_type/test_addon_operation_invocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,24 @@
)


class TestAddonOperationInvocationAPI(APITestCase):
class TestAddonOperationInvocationCreate(APITestCase):
@classmethod
def setUpTestData(cls):
cls._configured_addon = _factories.ConfiguredStorageAddonFactory()
cls._operation = models.AddonOperationModel.get_by_natural_key_str(
"blarg:download"
)

def _detail_path(self, invocation: models.AddonOperationInvocation):
return reverse(
"addon-operation-invocations-detail",
kwargs={"pk": invocation.pk},
)

@property
def _list_path(self):
return reverse("addon-operation-invocations-list")

def _related_path(self, invocation, related_field):
return reverse(
"addon-operation-invocations-related",
kwargs={
"pk": invocation.pk,
"related_field": related_field,
},
)

def _payload_for_post(self):
return {
"data": {
"type": "addon-operation-invocations",
"attributes": {
"operation_params": {"item_id": "foo"},
"operation_kwargs": {"item_id": "foo"},
},
"relationships": {
"operation": {
Expand Down Expand Up @@ -81,14 +66,40 @@ def test_post(self):
"success",
)

@unittest.skip("TODO")

@unittest.skip("TODO")
class TestAddonOperationInvocationErrors(APITestCase):
@classmethod
def setUpTestData(cls):
cls._invocation = _factories.AddonOperationInvocationFactory()

@property
def _detail_path(self):
return reverse(
"addon-operation-invocations-detail",
kwargs={"pk": self._invocation.pk},
)

@property
def _list_path(self):
return reverse("addon-operation-invocations-list")

def _related_path(self, related_field):
return reverse(
"addon-operation-invocations-related",
kwargs={
"pk": self._invocation.pk,
"related_field": related_field,
},
)

def test_methods_not_allowed(self):
_methods_not_allowed = {
self._detail_path: {"post"},
# TODO: self._list_path: {'get', 'patch', 'put', 'post'},
self._related_path("account_owner"): {"patch", "put", "post"},
self._related_path("external_storage_service"): {"patch", "put", "post"},
self._related_path("configured_storage_addons"): {"patch", "put", "post"},
self._list_path: {"get", "patch", "put"},
self._detail_path: {"patch", "put", "post", "delete"},
self._related_path("thru_addon"): {"patch", "put", "post", "delete"},
self._related_path("by_user"): {"patch", "put", "post", "delete"},
self._related_path("operation"): {"patch", "put", "post", "delete"},
}
for _path, _methods in _methods_not_allowed.items():
for _method in _methods:
Expand Down
2 changes: 1 addition & 1 deletion addon_toolkit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from .imp import AddonOperationImplementation
from .interface import (
AddonInterfaceDeclaration,
addon_interface,
Expand All @@ -9,6 +8,7 @@
immediate_operation,
redirect_operation,
)
from .operation_imp import AddonOperationImplementation


__all__ = (
Expand Down
Loading

0 comments on commit d3c0481

Please sign in to comment.