Skip to content

Commit

Permalink
Merge pull request #74 from CenterForOpenScience/fix/debut-deployment
Browse files Browse the repository at this point in the history
fixups for deployment
  • Loading branch information
aaxelb authored Jul 23, 2024
2 parents b7a43cc + ec3b55a commit 71475e8
Show file tree
Hide file tree
Showing 45 changed files with 555 additions and 255 deletions.
16 changes: 8 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ COPY . /code/
WORKDIR /code
# END gv-base

# BEGIN gv-local
FROM gv-base as gv-local
# install dev and non-dev dependencies:
RUN pip3 install --no-cache-dir -r requirements/dev-requirements.txt
# Start the Django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8004"]
# END gv-local

# BEGIN gv-deploy
FROM gv-base as gv-deploy
# install non-dev and release-only dependencies:
Expand All @@ -16,11 +24,3 @@ RUN pip3 install --no-cache-dir -r requirements/release.txt
RUN python manage.py collectstatic --noinput
# note: no CMD in gv-deploy -- depends on deployment
# END gv-deploy

# BEGIN gv-local
FROM gv-base as gv-local
# install dev and non-dev dependencies:
RUN pip3 install --no-cache-dir -r requirements/dev-requirements.txt
# Start the Django development server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8004"]
# END gv-local
3 changes: 1 addition & 2 deletions addon_imps/storage/box_dot_com.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ async def list_child_items(
_parsed = _BoxDotComParsedJson(await _response.json_content())
return storage.ItemSampleResult(
items=list(_parsed.item_results(item_type=item_type)),
cursor=_parsed.cursor(),
)
).with_cursor(_parsed.cursor())

def _params_from_cursor(self, cursor: str = "") -> dict[str, str]:
# https://developer.box.com/guides/api-calls/pagination/offset-based/
Expand Down
10 changes: 8 additions & 2 deletions addon_service/addon_imp/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dataclasses
import typing
from functools import cached_property

from addon_service.addon_operation.models import AddonOperationModel
Expand All @@ -20,6 +21,11 @@ class AddonImpModel(StaticDataclassModel):
def init_args_from_static_key(cls, static_key: str) -> tuple:
return (known_imps.get_imp_by_name(static_key),)

@classmethod
def iter_all(cls) -> typing.Iterator[typing.Self]:
for _imp in known_imps.KnownAddonImps:
yield cls(_imp.value)

@property
def static_key(self) -> str:
return self.name
Expand All @@ -42,13 +48,13 @@ def interface_docstring(self) -> str:
@cached_property
def implemented_operations(self) -> tuple[AddonOperationModel, ...]:
return tuple(
AddonOperationModel(self.imp_cls, _operation)
AddonOperationModel(self.imp_cls.ADDON_INTERFACE, _operation)
for _operation in self.imp_cls.all_implemented_operations()
)

def get_operation_model(self, operation_name: str):
return AddonOperationModel(
self.imp_cls,
self.imp_cls.ADDON_INTERFACE,
self.imp_cls.get_operation_declaration(operation_name),
)

Expand Down
49 changes: 29 additions & 20 deletions addon_service/addon_operation/models.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
import dataclasses
import typing
from functools import cached_property

from addon_service.common import known_imps
from addon_service.common.static_dataclass_model import StaticDataclassModel
from addon_toolkit import (
AddonCapabilities,
AddonImp,
AddonOperationDeclaration,
AddonOperationType,
interfaces,
)
from addon_toolkit.json_arguments import (
jsonschema_for_dataclass,
jsonschema_for_signature_params,
)
from addon_toolkit.json_arguments import JsonschemaDocBuilder


# dataclass wrapper for an operation implemented on a concrete AddonImp subclass
# meets rest_framework_json_api expectations on a model class
@dataclasses.dataclass(frozen=True)
class AddonOperationModel(StaticDataclassModel):
imp_cls: type[AddonImp]
interface_cls: type[interfaces.BaseAddonInterface]
declaration: AddonOperationDeclaration

###
# StaticDataclassModel abstract methods

@classmethod
def init_args_from_static_key(cls, static_key: str) -> tuple:
(_imp_name, _operation_name) = static_key.split(":")
_imp_cls = known_imps.get_imp_by_name(_imp_name)
return (_imp_cls, _imp_cls.get_operation_declaration(_operation_name))
(_interface_name, _operation_name) = static_key.split(":")
_interface_cls = interfaces.AllAddonInterfaces[_interface_name].value
return (_interface_cls, _interface_cls.get_operation_by_name(_operation_name))

@classmethod
def iter_all(cls) -> typing.Iterator[typing.Self]:
"""yield all available static instances of this class (if any)"""
for _interface in interfaces.AllAddonInterfaces:
_interface_cls = _interface.value
for _operation_declaration in _interface_cls.iter_declared_operations():
yield cls(_interface_cls, _operation_declaration)

@property
def static_key(self) -> str:
return ":".join((known_imps.get_imp_name(self.imp_cls), self.declaration.name))
return ":".join((self.interface_name, self.name))

###
# fields for api
Expand All @@ -42,6 +47,10 @@ def static_key(self) -> str:
def name(self) -> str:
return self.declaration.name

@cached_property
def interface_name(self) -> str:
return interfaces.AllAddonInterfaces(self.interface_cls).name

@cached_property
def operation_type(self) -> AddonOperationType:
return self.declaration.operation_type
Expand All @@ -50,29 +59,29 @@ def operation_type(self) -> AddonOperationType:
def docstring(self) -> str:
return self.declaration.docstring

@cached_property
def implementation_docstring(self) -> str:
return self.imp_cls.get_imp_function(self.declaration).__doc__ or ""

@cached_property
def capability(self) -> AddonCapabilities:
return self.declaration.capability

@cached_property
def params_jsonschema(self) -> dict:
return jsonschema_for_signature_params(self.declaration.call_signature)
def kwargs_jsonschema(self) -> dict:
return JsonschemaDocBuilder(self.declaration.operation_fn).build()

@cached_property
def result_jsonschema(self) -> dict:
return jsonschema_for_dataclass(self.declaration.result_dataclass)
return JsonschemaDocBuilder(self.declaration.result_dataclass).build()

@cached_property
def implemented_by(self):
# local import to avoid circular import
# (AddonOperationModel and AddonImpModel need to be mutually aware of each other in order to populate their respective relationship fields)
from addon_service.addon_imp.models import AddonImpModel

return AddonImpModel(self.imp_cls)
_imps = set()
for _imp_model in AddonImpModel.iter_all():
if self.declaration in _imp_model.imp_cls.all_implemented_operations():
_imps.add(_imp_model)
return tuple(_imps)

class JSONAPIMeta:
resource_name = "addon-operation-imps"
resource_name = "addon-operations"
15 changes: 11 additions & 4 deletions addon_service/addon_operation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ class AddonOperationSerializer(serializers.Serializer):

name = serializers.CharField(read_only=True)
docstring = serializers.CharField(read_only=True)
implementation_docstring = serializers.CharField(read_only=True)
capability = EnumNameChoiceField(enum_cls=AddonCapabilities, read_only=True)
params_jsonschema = serializers.JSONField()
result_jsonschema = serializers.JSONField()
kwargs_jsonschema = serializers.JSONField(read_only=True)
result_jsonschema = serializers.JSONField(read_only=True)

###
# relationships
Expand All @@ -35,7 +34,7 @@ class AddonOperationSerializer(serializers.Serializer):
dataclass_model=AddonImpModel,
related_link_view_name=view_names.related_view(RESOURCE_TYPE),
read_only=True,
many=False,
many=True,
)

###
Expand All @@ -47,3 +46,11 @@ class AddonOperationSerializer(serializers.Serializer):

class Meta:
model = AddonOperationModel
fields = [
"id",
"name",
"docstring",
"capability",
"kwargs_jsonschema",
"result_jsonschema",
]
2 changes: 1 addition & 1 deletion addon_service/addon_operation_invocation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def clean_fields(self, *args, **kwargs):
try:
jsonschema.validate(
instance=self.operation_kwargs,
schema=self.operation.params_jsonschema,
schema=self.operation.kwargs_jsonschema,
)
except jsonschema.exceptions.ValidationError as _exception:
raise ValidationError(_exception)
Expand Down
2 changes: 1 addition & 1 deletion addon_service/addon_operation_invocation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def create(self, validated_data):
_user_uri = _request.session.get("user_reference_uri")
_user, _ = UserReference.objects.get_or_create(user_uri=_user_uri)
return AddonOperationInvocation(
operation=AddonOperationModel(_imp_cls, _operation),
operation=AddonOperationModel(_imp_cls.ADDON_INTERFACE, _operation),
operation_kwargs=validated_data["operation_kwargs"],
thru_addon=_thru_addon,
thru_account=_thru_account,
Expand Down
28 changes: 18 additions & 10 deletions addon_service/authorized_storage_account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def _set_credentials(self, credentials_field: str, credentials_data: Credentials
raise ValidationError("Trying to set credentials to non-existing field")
if creds_type is not self.credentials_format.dataclass:
raise ValidationError(
f"Expected credentials of type type {self.credentials_format.dataclass}."
f"Expected credentials of type {self.credentials_format.dataclass}."
f"Got credentials of type {creds_type}."
)
if not getattr(self, credentials_field, None):
Expand Down Expand Up @@ -177,7 +177,7 @@ def owner_uri(self) -> str:
def authorized_operations(self) -> list[AddonOperationModel]:
_imp_cls = self.imp_cls
return [
AddonOperationModel(_imp_cls, _operation)
AddonOperationModel(_imp_cls.ADDON_INTERFACE, _operation)
for _operation in _imp_cls.implemented_operations_for_capabilities(
self.authorized_capabilities
)
Expand All @@ -204,25 +204,27 @@ def auth_url(self) -> str | None:
return self.oauth2_auth_url
case CredentialsFormats.OAUTH1A:
return self.oauth1_auth_url
return None

@property
def oauth1_auth_url(self) -> str:
def oauth1_auth_url(self) -> str | None:
client_config = self.external_service.oauth1_client_config
if self._temporary_oauth1_credentials:
if client_config and self._temporary_oauth1_credentials:
return oauth1_utils.build_auth_url(
auth_uri=client_config.auth_url,
temporary_oauth_token=self.temporary_oauth1_credentials.oauth_token,
temporary_oauth_token=self._temporary_oauth1_credentials.oauth_token,
)
return None

@property
def oauth2_auth_url(self) -> str | None:
state_token = self.oauth2_token_metadata.state_token
if not state_token:
_token_metadata = self.oauth2_token_metadata
if not _token_metadata or not _token_metadata.state_token:
return None
return oauth2_utils.build_auth_url(
auth_uri=self.external_service.oauth2_client_config.auth_uri,
client_id=self.external_service.oauth2_client_config.client_id,
state_token=state_token,
state_token=_token_metadata.state_token,
authorized_scopes=self.oauth2_token_metadata.authorized_scopes,
redirect_uri=self.external_service.oauth2_client_config.auth_callback_url,
)
Expand All @@ -233,12 +235,18 @@ def api_base_url(self) -> str:

@api_base_url.setter
def api_base_url(self, value: str):
self._api_base_url = value
self._api_base_url = (
"" if value == self.external_service.api_base_url else value
)

@property
def imp_cls(self) -> type[AddonImp]:
return self.external_service.addon_imp.imp_cls

@property
def credentials_available(self) -> bool:
return self._credentials is not None

@transaction.atomic
def initiate_oauth1_flow(self):
if self.credentials_format is not CredentialsFormats.OAUTH1A:
Expand Down Expand Up @@ -285,7 +293,7 @@ def validate_api_base_url(self):
"api_base_url": f"Cannot specify an api_base_url for Public-only service {service.display_name}"
}
)
if ServiceTypes.PUBLIC not in service.service_type and not self.api_base_url:
if ServiceTypes.PUBLIC not in service.service_type and not self._api_base_url:
raise ValidationError(
{
"api_base_url": f"Must specify an api_base_url for Hosted-only service {service.display_name}"
Expand Down
53 changes: 43 additions & 10 deletions addon_service/authorized_storage_account/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def __init__(self, *args, **kwargs):
url = serializers.HyperlinkedIdentityField(
view_name=view_names.detail_view(RESOURCE_TYPE), required=False
)
display_name = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=256
)
authorized_capabilities = EnumNameMultipleChoiceField(enum_cls=AddonCapabilities)
authorized_operation_names = serializers.ListField(
child=serializers.CharField(),
Expand Down Expand Up @@ -69,6 +72,7 @@ def __init__(self, *args, **kwargs):
related_link_view_name=view_names.related_view(RESOURCE_TYPE),
)
credentials = CredentialsField(write_only=True, required=False)
initiate_oauth = serializers.BooleanField(write_only=True, required=False)

included_serializers = {
"account_owner": "addon_service.serializers.UserReferenceSerializer",
Expand All @@ -94,16 +98,23 @@ def create(self, validated_data):
except ModelValidationError as e:
raise serializers.ValidationError(e)

if external_service.credentials_format is CredentialsFormats.OAUTH2:
authorized_account.initiate_oauth2_flow(
validated_data.get("authorized_scopes")
)
elif external_service.credentials_format is CredentialsFormats.OAUTH1A:
authorized_account.initiate_oauth1_flow()
self.context["request"].session["oauth1a_account_id"] = encrypt_string(
authorized_account.pk
)
else:
if validated_data.get("initiate_oauth", False):
if external_service.credentials_format is CredentialsFormats.OAUTH2:
authorized_account.initiate_oauth2_flow(
validated_data.get("authorized_scopes")
)
elif external_service.credentials_format is CredentialsFormats.OAUTH1A:
authorized_account.initiate_oauth1_flow()
self.context["request"].session["oauth1a_account_id"] = encrypt_string(
authorized_account.pk
)
else:
raise serializers.ValidationError(
{
"initiate_oauth": "this external service is not configured for oauth"
}
)
elif validated_data.get("credentials"):
authorized_account.credentials = validated_data["credentials"]

try:
Expand All @@ -116,6 +127,26 @@ def create(self, validated_data):

return authorized_account

def update(self, instance, validated_data):
# only these fields may be PATCHed:
if "display_name" in validated_data:
instance.display_name = validated_data["display_name"]
if "authorized_capabilities" in validated_data:
instance.authorized_capabilities = validated_data["authorized_capabilities"]
if "api_base_url" in validated_data:
instance.api_base_url = validated_data["api_base_url"]
if "default_root_folder" in validated_data:
instance.default_root_folder = validated_data["default_root_folder"]
if validated_data.get("credentials"):
instance.credentials = validated_data["credentials"]
instance.save() # may raise ValidationError
if (
validated_data.get("initiate_oauth", False)
and instance.credentials_format is CredentialsFormats.OAUTH2
):
instance.initiate_oauth2_flow(validated_data.get("authorized_scopes"))
return instance

class Meta:
model = AuthorizedStorageAccount
fields = [
Expand All @@ -132,4 +163,6 @@ class Meta:
"credentials",
"default_root_folder",
"external_storage_service",
"initiate_oauth",
"credentials_available",
]
Loading

0 comments on commit 71475e8

Please sign in to comment.