-
Notifications
You must be signed in to change notification settings - Fork 6
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
[ENG-5196] addon capabilities (in api and implementers' toolkit) #12
Changes from all commits
7c64ba3
db817fc
886f259
f1f6e0d
e662835
403127e
5b4acc2
2e3cc00
218d4df
6d5e8d5
9393895
d4f8605
9c0f0c6
92f07f6
ddcab27
0410761
8d38b64
3436a26
4580a6f
38db11c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
) | ||
from rest_framework_json_api.utils import get_resource_type_from_model | ||
|
||
from addon_service.capability.serializers import StorageCapabilityListField | ||
from addon_service.models import ( | ||
AuthorizedStorageAccount, | ||
ConfiguredStorageAddon, | ||
|
@@ -43,6 +44,7 @@ def __init__(self, *args, **kwargs): | |
url = serializers.HyperlinkedIdentityField( | ||
view_name=f"{RESOURCE_NAME}-detail", required=False | ||
) | ||
authorized_capabilities = StorageCapabilityListField() | ||
account_owner = AccountOwnerField( | ||
many=False, | ||
queryset=UserReference.objects.all(), | ||
|
@@ -74,6 +76,7 @@ def __init__(self, *args, **kwargs): | |
|
||
def create(self, validate_data): | ||
account_owner = validate_data["account_owner"] | ||
authorized_capabilities = validate_data["authorized_capabilities"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can we change |
||
external_storage_service = validate_data["external_storage_service"] | ||
# TODO(ENG-5189): Update this once credentials format is finalized | ||
credentials, created = ExternalCredentials.objects.get_or_create( | ||
|
@@ -90,6 +93,7 @@ def create(self, validate_data): | |
return AuthorizedStorageAccount.objects.create( | ||
external_storage_service=external_storage_service, | ||
external_account=external_account, | ||
authorized_capabilities=authorized_capabilities, | ||
) | ||
|
||
class Meta: | ||
|
@@ -102,4 +106,5 @@ class Meta: | |
"external_storage_service", | ||
"username", | ||
"password", | ||
"authorized_capabilities", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from addon_service.common.enums import IntEnumForEnum | ||
from addon_toolkit.storage import StorageCapability | ||
|
||
|
||
__all__ = ("IntStorageCapability",) | ||
|
||
|
||
class IntStorageCapability(IntEnumForEnum, base_enum=StorageCapability): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose between the dual enums? Feels very complicated. On OSF, I was able to handle the translation from int to string for the API with a single enum here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm maybe it is too complicated, it sure did get finicky... bloomed from a conviction that also seemed nice code-aroma to use enums as explicit source-controlled maps between a python-name and both api and database representations -- the single-enum requires the api representation to be the python name... which may be fine, really for the operations api i did further complicate the idea, but may have better expressed intent? |
||
ACCESS = 1 | ||
UPDATE = 2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from addon_service.capability.models import IntStorageCapability | ||
from addon_service.common.enums.serializers import DualEnumsListField | ||
from addon_toolkit.storage import StorageCapability | ||
|
||
|
||
class StorageCapabilityListField( | ||
DualEnumsListField, | ||
external_enum=StorageCapability, | ||
internal_enum=IntStorageCapability, | ||
): | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import enum | ||
from typing import ClassVar | ||
|
||
|
||
def same_enum_names(enum_a: type[enum.Enum], enum_b: type[enum.Enum]) -> bool: | ||
# ensure enums have same names | ||
_names_a = {_item.name for _item in enum_a} | ||
_names_b = {_item.name for _item in enum_b} | ||
return _names_a == _names_b | ||
|
||
|
||
class IntEnumForEnum(enum.IntEnum): | ||
__base_enum: ClassVar[type[enum.Enum]] | ||
|
||
def __init_subclass__(cls, /, base_enum: type[enum.Enum], **kwargs): | ||
super().__init_subclass__(**kwargs) | ||
assert same_enum_names(base_enum, cls) | ||
cls.__base_enum = base_enum | ||
|
||
@classmethod | ||
def to_int(cls, base_enum_member): | ||
return cls[base_enum_member.name] | ||
|
||
@classmethod | ||
def as_django_choices(cls): | ||
return [(int(_item), _item.name) for _item in cls] | ||
|
||
def to_base_enum(self) -> enum.Enum: | ||
return self.__base_enum[self.name] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import enum | ||
from typing import ClassVar | ||
|
||
from rest_framework_json_api import serializers | ||
|
||
from addon_service.common.enums import same_enum_names | ||
|
||
|
||
class DualEnumsListField(serializers.MultipleChoiceField): | ||
"""use one enum in your database and another in your api!""" | ||
|
||
__internal_enum: ClassVar[type[enum.Enum]] | ||
__external_enum: ClassVar[type[enum.Enum]] | ||
|
||
def __init__(self, **kwargs): | ||
super().__init__( | ||
**kwargs, | ||
choices={ # valid serialized values come from the external enum | ||
_external_member.value for _external_member in self.__external_enum | ||
}, | ||
) | ||
|
||
def __init_subclass__( | ||
cls, | ||
/, | ||
internal_enum: type[enum.Enum], | ||
external_enum: type[enum.Enum], | ||
**kwargs, | ||
): | ||
super().__init_subclass__(**kwargs) | ||
assert same_enum_names(internal_enum, external_enum) | ||
cls.__internal_enum = internal_enum | ||
cls.__external_enum = external_enum | ||
|
||
def to_internal_value(self, data) -> list[enum.Enum]: | ||
_names: set = 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) -> enum.Enum: | ||
_external_member = self.__external_enum(external_value) | ||
return self.__internal_enum[_external_member.name] | ||
|
||
def _to_external_enum_value(self, internal_value: enum.Enum): | ||
_internal_member = self.__internal_enum(internal_value) | ||
_external_member = self.__external_enum[_internal_member.name] | ||
return _external_member.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: would rather enforce this differently, since setting it in
choices
requires migrations when options change.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fair enough; how about a validator?