Skip to content

Commit

Permalink
working invocation
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Mar 6, 2024
1 parent f2baa74 commit 37a4bef
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 37 deletions.
13 changes: 9 additions & 4 deletions addon_service/addon_operation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.operation import AddonOperationType
from addon_toolkit.storage import StorageCapability


Expand All @@ -14,25 +15,25 @@
@dataclasses.dataclass
class AddonOperationModel(BaseDataclassModel):
name: str
operation_type: AddonOperationType
docstring: str
implementation_docstring: str
capability: enum.Enum
params_jsonschema: dict
imp_cls: type
params_dataclass: type

@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),
params_jsonschema=jsonschema_for_dataclass(
op_imp.operation.params_dataclass
),
imp_cls=op_imp.implementation_cls,
params_dataclass=op_imp.operation.params_dataclass,
)

@classmethod
Expand All @@ -55,5 +56,9 @@ def natural_key(self) -> list:
def implemented_by(self):
return AddonImpModel.for_imp(KnownAddonImp(self.imp_cls))

@property
def params_jsonschema(self) -> dict:
return jsonschema_for_dataclass(self.params_dataclass)

class JSONAPIMeta:
resource_name = "addon-operations"
1 change: 1 addition & 0 deletions addon_service/addon_operation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class AddonOperationSerializer(serializers.Serializer):
implemented_by = DataclassRelatedDataField(
dataclass_model=AddonImpModel,
related_link_view_name=view_names.related_view(RESOURCE_TYPE),
read_only=True,
many=False,
)

Expand Down
34 changes: 32 additions & 2 deletions addon_service/addon_operation_invocation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
from django.core.exceptions import ValidationError
from django.db import models

from addon_service.addon_operation.models import AddonOperationModel
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


class AddonOperationInvocation(AddonsServiceBaseModel):
Expand All @@ -28,7 +32,18 @@ class JSONAPIMeta:

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

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

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

def clean(self):
try:
Expand All @@ -38,3 +53,18 @@ def clean(self):
)
except jsonschema.exceptions.ValidationError as _error:
raise ValidationError(_error)

def execute(self):
with dibs(self):
try:
# TODO: what if this raises database error?
_result = self.operation_imp.call_with_params(self.operation_params)
except Exception as _e:
self.operation_result = None
self.invocation_status = IntInvocationStatus.PROBLEM
print(_e)
# TODO: error/traceback
else:
self.operation_result = json_for_dataclass(_result)
self.invocation_status = IntInvocationStatus.SUCCESS
self.save() # TODO: finally?
23 changes: 18 additions & 5 deletions addon_service/addon_operation_invocation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ConfiguredStorageAddon,
UserReference,
)
from addon_toolkit.operation import AddonOperationType


RESOURCE_TYPE = get_resource_type_from_model(AddonOperationInvocation)
Expand All @@ -40,6 +41,7 @@ class Meta:
)
invocation_status = DualEnumsChoiceField(
read_only=True,
required=False,
internal_enum=IntInvocationStatus,
external_enum=InvocationStatus,
)
Expand All @@ -56,7 +58,7 @@ class Meta:

by_user = ResourceRelatedField(
many=False,
queryset=UserReference.objects.all(),
read_only=True,
related_link_view_name=view_names.related_view(RESOURCE_TYPE),
)

Expand All @@ -72,7 +74,18 @@ class Meta:
}

def create(self, validated_data):
# TODO: on create:
# if immediate operation, wait on it
# if eventual operation, celery task or something
raise NotImplementedError
_operation = validated_data["operation"]
_invocation = AddonOperationInvocation.objects.create(
operation_identifier=_operation.name,
operation_params=validated_data["operation_params"],
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()
case AddonOperationType.EVENTUAL:
raise NotImplementedError("TODO: enqueue task")
case _:
raise ValueError(f"unknown operation type: {_operation.operation_type}")
return _invocation
33 changes: 33 additions & 0 deletions addon_service/common/dibs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import contextlib
import logging

from django.db import transaction


__all__ = ("dibs",)


_logger = logging.getLogger()


@contextlib.contextmanager
def dibs(model_instance, *, refresh=True):
"""context manager that locks the database row for a given model instance
a dibs'd block cannot be running twice for the same model instance at the same time
"""
with transaction.atomic():
_locked_obj = (
model_instance.__class__.objects.select_for_update()
.filter(pk=model_instance.pk)
.first()
)
if _locked_obj is None:
raise ValueError(f"dibs: could not find {model_instance}")
_logger.debug("dibs: locked %r", _locked_obj)
if refresh: # ensure the original object is up to date
model_instance.refresh_from_db()
yield
else: # avoid a query, but the original object may be stale
yield _locked_obj
_logger.debug("dibs: unlocked %r", _locked_obj)
6 changes: 3 additions & 3 deletions addon_service/common/enums/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def to_representation(self, value: enum.Enum):

@dataclasses.dataclass
class DualEnums:
internal_enum: type[enum.Enum]
external_enum: type[enum.Enum]
internal_enum: type[enum.Enum] # for use in models
external_enum: type[enum.Enum] # for use in api

def __post_init__(self):
assert same_enum_names(self.internal_enum, self.external_enum)
Expand All @@ -56,8 +56,8 @@ def __init__(self, *, internal_enum, external_enum, choices=None, **kwargs):
), f"{self.__class__} expects internal_enum and external_enum, not choices"
self.enums = DualEnums(internal_enum, external_enum)
super().__init__(
# valid serialized values come from external_enum
choices=_values(external_enum),
**kwargs,
)


Expand Down
14 changes: 12 additions & 2 deletions addon_service/common/serializer_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ def __init__(self, /, dataclass_model, read_only=True, **kwargs):


class DataclassRelatedDataField(ResourceRelatedField):
def __init__(self, /, dataclass_model, read_only=True, **kwargs):
def __init__(self, /, dataclass_model, **kwargs):
assert dataclasses.is_dataclass(dataclass_model)
return super().__init__(
read_only=read_only,
model=dataclass_model,
**kwargs,
)

def get_queryset(self):
return _FakeQuerysetForDataclassModel(self.model)


class _FakeQuerysetForDataclassModel:
def __init__(self, dataclass_model):
self.model = dataclass_model

def get(self, *, pk):
return self.model.get_by_pk(pk)
11 changes: 9 additions & 2 deletions addon_service/external_storage_service/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from django.db import models

from addon_service.addon_imp.known import IntAddonImp
from addon_service.addon_imp.known import (
IntAddonImp,
KnownAddonImp,
)
from addon_service.addon_imp.models import AddonImpModel
from addon_service.common.base_model import AddonsServiceBaseModel

Expand Down Expand Up @@ -28,6 +31,10 @@ class Meta:
class JSONAPIMeta:
resource_name = "external-storage-services"

@property
def known_addon_imp(self) -> KnownAddonImp:
return IntAddonImp(self.int_addon_imp).to_original_enum()

@property
def addon_imp(self) -> AddonImpModel:
return AddonImpModel.for_imp(IntAddonImp(self.int_addon_imp).to_original_enum())
return AddonImpModel.for_imp(self.known_addon_imp)
10 changes: 10 additions & 0 deletions addon_service/tests/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
APIRequestFactory,
force_authenticate,
)
from rest_framework_json_api.utils import get_resource_type_from_model


def get_test_request(user=None, method="get", path=""):
Expand All @@ -10,3 +11,12 @@ def get_test_request(user=None, method="get", path=""):
if user is not None:
force_authenticate(_request, user=user)
return _request


# TODO: use this more often in tests
def jsonapi_ref(obj) -> dict:
"""return a jsonapi resource reference (as json-serializable dict)"""
return {
"type": get_resource_type_from_model(obj.__class__),
"id": obj.pk,
}
Loading

0 comments on commit 37a4bef

Please sign in to comment.