Skip to content

Commit

Permalink
wip (fixed tests)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Mar 4, 2024
1 parent f4abf60 commit f2baa74
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 70 deletions.
2 changes: 2 additions & 0 deletions addon_service/tests/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from factory.django import DjangoModelFactory

from addon_service import models as db
from addon_service.addon_imp.known import IntAddonImp
from addon_service.common.capability import IntStorageCapability


Expand Down Expand Up @@ -52,6 +53,7 @@ class Meta:
max_upload_mb = factory.Faker("pyint")
auth_uri = factory.Sequence(lambda n: f"http://auth.example/{n}")
credentials_issuer = factory.SubFactory(CredentialsIssuerFactory)
int_addon_imp = IntAddonImp.blarg


class AuthorizedStorageAccountFactory(DjangoModelFactory):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def _related_path(self, related_field):
},
)

def test_get(self):
def test_get_detail(self):
_resp = self.client.get(self._detail_path)
self.assertEqual(_resp.status_code, HTTPStatus.OK)
self.assertEqual(
Expand Down Expand Up @@ -123,6 +123,7 @@ def test_get(self):
"account_owner",
"external_storage_service",
"configured_storage_addons",
"authorized_operations",
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ def test_get(self):
self.assertEqual(
set(_content["data"]["relationships"].keys()),
{
"base_account",
"authorized_resource",
"base_account",
"connected_operations",
},
)

Expand Down
25 changes: 4 additions & 21 deletions addon_service/tests/test_by_type/test_external_storage_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_get(self):
self.assertEqual(
set(_content["data"]["relationships"].keys()),
{
"authorized_storage_accounts",
"addon_imp",
},
)

Expand All @@ -141,28 +141,11 @@ def setUpTestData(cls):
{"get": "retrieve_related"},
)

def test_get_related__empty(self):
def test_get_related(self):
_resp = self._related_view(
get_test_request(),
pk=self._ess.pk,
related_field="authorized_storage_accounts",
related_field="addon_imp",
)
self.assertEqual(_resp.status_code, HTTPStatus.OK)
self.assertEqual(_resp.data, [])

def test_get_related__several(self):
_accounts = _factories.AuthorizedStorageAccountFactory.create_batch(
size=5,
external_storage_service=self._ess,
)
_resp = self._related_view(
get_test_request(),
pk=self._ess.pk,
related_field="authorized_storage_accounts",
)
self.assertEqual(_resp.status_code, HTTPStatus.OK)
_content = json.loads(_resp.rendered_content)
self.assertEqual(
{_datum["id"] for _datum in _content["data"]},
{str(_account.pk) for _account in _accounts},
)
self.assertEqual(_resp.data["name"], self._ess.addon_imp.name)
2 changes: 2 additions & 0 deletions addon_toolkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
)
from .operation import (
AddonOperationDeclaration,
RedirectResult,
immediate_operation,
redirect_operation,
)
Expand All @@ -14,6 +15,7 @@
"AddonInterfaceDeclaration",
"AddonOperationDeclaration",
"AddonOperationImplementation",
"RedirectResult",
"addon_interface",
"immediate_operation",
"redirect_operation",
Expand Down
4 changes: 3 additions & 1 deletion addon_toolkit/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@
_logger = logging.getLogger(__name__)


@dataclasses.dataclass
@dataclasses.dataclass(frozen=True)
class AddonInterfaceDeclaration:
"""dataclass for the operations declared on a class decorated with `addon_interface`"""

interface_cls: type
capability_enum: type[enum.Enum]
method_name_by_op: dict[AddonOperationDeclaration, str] = dataclasses.field(
default_factory=dict,
compare=False,
)
ops_by_capability: dict[
enum.Enum, set[AddonOperationDeclaration]
] = dataclasses.field(
default_factory=dict,
compare=False,
)

@classmethod
Expand Down
60 changes: 42 additions & 18 deletions addon_toolkit/operation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import dataclasses
import enum
from typing import Callable
import inspect
from http import HTTPMethod
from typing import (
Any,
Callable,
)

from .declarator import Declarator

Expand Down Expand Up @@ -30,8 +35,8 @@ class AddonOperationDeclaration:
operation_type: AddonOperationType
capability: enum.Enum
operation_fn: Callable # the decorated function
params_dataclass: type = dataclasses.field(init=False)
success_dataclass: type = dataclasses.field(init=False)
return_dataclass: type = dataclasses.field(default=type(None), compare=False)
params_dataclass: type = dataclasses.field(init=False, compare=False)

@classmethod
def for_function(self, fn: Callable) -> "AddonOperationDeclaration":
Expand All @@ -40,7 +45,19 @@ def for_function(self, fn: Callable) -> "AddonOperationDeclaration":
def __post_init__(self):
# use __setattr__ to ignore dataclass frozenness
super().__setattr__("params_dataclass", self._build_params_dataclass())
super().__setattr__("success_dataclass", self._get_success_dataclass())
_return_type = self.get_return_type()
if self.return_dataclass is type(None):
assert dataclasses.is_dataclass(
_return_type
), f"operation methods must return a dataclass (got {_return_type} on {self.operation_fn})"
super().__setattr__("return_dataclass", _return_type)
else:
assert dataclasses.is_dataclass(
self.return_dataclass
), f"return_dataclass must be a dataclass (got {self.return_dataclass})"
assert issubclass(
_return_type, self.return_dataclass
), f"expected return type {self.return_dataclass} on operation function {self.operation_fn} (got {_return_type})"

@property
def name(self):
Expand All @@ -53,37 +70,44 @@ def docstring(self) -> str:
# TODO: consider docstring param on operation decorators, allow overriding __doc__
return self.operation_fn.__doc__ or ""

def get_return_type(self) -> type:
return inspect.get_annotations(self.operation_fn)["return"]

def get_param_types(self) -> tuple[tuple[str, Any], ...]:
return tuple(
(_name, _type)
for _name, _type in inspect.get_annotations(self.operation_fn).items()
if _name != "return"
)

def _build_params_dataclass(self):
_fields = [
(_param_name, _param_type)
for _param_name, _param_type in self.operation_fn.__annotations__.items()
if _param_name != "return"
]
return dataclasses.make_dataclass(
f"ParamsDataclass__{self.__class__.__qualname__}__{self.name}",
_fields,
f"ParamsDataclass__{self.__class__.__name__}__{self.name}",
fields=self.get_param_types(),
)

def _get_success_dataclass(self):
_return_type = self.operation_fn.__annotations__["return"]
# TODO: except KeyError
assert dataclasses.is_dataclass(_return_type) and isinstance(_return_type, type)
return _return_type


# declarator for all types of operations -- use operation_type-specific decorators below
_operation_declarator = Declarator(
declaration_dataclass=AddonOperationDeclaration,
object_field="operation_fn",
)


@dataclasses.dataclass
class RedirectResult:
url: str
method: HTTPMethod = HTTPMethod.GET


# decorator for operations that may be performed by a client request (e.g. redirect to waterbutler)
redirect_operation = _operation_declarator.with_kwargs(
operation_type=AddonOperationType.REDIRECT,
return_dataclass=RedirectResult,
# TODO: consider adding `save_invocation: bool = True`, set False here
)

# decorator for operations that must be performed by the server but will take only
# decorator for operations that must be performed by the server but should take only
# a short time, so a server may wait on the operation before responding to a request
# (e.g. get a metadata description of an item, list items in a given folder)
immediate_operation = _operation_declarator.with_kwargs(
Expand Down
8 changes: 1 addition & 7 deletions addon_toolkit/storage.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import dataclasses
import enum
from http import HTTPMethod

from addon_toolkit import (
RedirectResult,
addon_interface,
immediate_operation,
redirect_operation,
Expand All @@ -17,12 +17,6 @@ class StorageCapability(enum.Enum):
UPDATE = "update"


@dataclasses.dataclass
class RedirectResult:
url: str
method: HTTPMethod = HTTPMethod.GET


@dataclasses.dataclass
class PagedResult:
items: list
Expand Down
Loading

0 comments on commit f2baa74

Please sign in to comment.