Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
aaxelb committed Jan 29, 2024
1 parent 036b34d commit a488d24
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 143 deletions.
131 changes: 19 additions & 112 deletions addon_toolkit/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,72 +9,23 @@
AuthorizedStorageAccount,
ConfiguredStorageAddon,
)
from addon_toolkit.namespaces import GRAVY
from addon_service.operation import _get_operation_iri


__all__ = ( # public module attrs:
"BaseAddonInterface",
"immediate_capability",
"proxy_read_capability",
"proxy_act_capability",
)
__all__ = ("BaseAddonInterface",)


_logger = logging.getLogger(__name__)


###
# decorators to declare capability identifiers on interface methods


def immediate_capability(capability_iri, *, requires):
# decorator for capabilities that can be computed immediately,
# without sending any requests or waiting on external resources
# (e.g. build a url in a known pattern or return declared static metadata)
def _decorator(fn):
# TODO: helpful error messaging for implementers
assert inspect.isfunction(fn)
assert not inspect.isawaitable(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
_set_capability_iri(fn, capability_iri)
return fn # decorator stub (TODO: register someway addon_service can use it)

return _decorator


def proxy_read_capability(capability_iri, *, requires):
# decorator for capabilities that require fetching data from elsewhere,
# but make no changes (e.g. get a metadata description of an item,
# list items in a given folder)
def _decorator(fn):
assert inspect.isasyncgenfunction(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
# TODO: assert based on return value?
return fn

return _decorator


def proxy_act_capability(capability_iri, *, requires):
# decorator for capabilities that initiate change, may take some time,
# and may fail in strange ways (e.g. delete an item, copy a file tree)
def _decorator(fn):
assert inspect.iscoroutine(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
# TODO: assert based on return value?
return fn

return _decorator


###
# addon interface


@dataclasses.dataclass
class BaseAddonInterface:
###
# public api for use on `self` when implementing capabilities
# public api for use on `self` when implementing operations

# TODO: consider intermediate dataclasses to limit direct use of data models
authorized_account: AuthorizedStorageAccount
Expand All @@ -96,83 +47,39 @@ async def send_request(self, http_method: HTTPMethod, url: str, **kwargs):
return _response

###
# private api for capability book-keeping
# private api for operation book-keeping

@classmethod
def __declared_capabilities(cls):
def __declared_operations(cls):
try:
return cls.__declared_capabilities
return cls.__declared_operations
except AttributeError:
_declared_capabilities = cls.__declared_capabilities = dict(
cls.__iter_declared_capabilities()
_declared_operations = cls.__declared_operations = dict(
cls.__iter_declared_operations()
)
return _declared_capabilities
return _declared_operations

@classmethod
def __iter_declared_capabilities(cls):
def __iter_declared_operations(cls):
for _methodname, _fn in inspect.getmembers(cls, inspect.ismethod):
try:
_capability_iri = _get_capability_iri(_fn)
_operation_iri = _get_operation_iri(_fn)
except AttributeError:
pass
else: # is capability
yield (_capability_iri, (_methodname, _fn))
else: # is operation
yield (_operation_iri, (_methodname, _fn))

raise NotImplementedError # TODO

def __get_capability_method(self, capability_iri: str):
_declared_capabilities = self.__declared_capabilities()
def __get_operation_method(self, operation_iri: str):
_declared_operations = self.__declared_operations()
try:
_methodname, _fn = _declared_capabilities[capability_iri]
_methodname, _fn = _declared_operations[operation_iri]
except AttributeError:
return NotImplemented
# TODO: _method = getattr(...


###
# module-private helpers


def _get_capability_iri(fn):
# may raise AttributeError
return getattr(fn, GRAVY.capability)


def _set_capability_iri(capability_fn, capability_iri):
try:
_prior_value = _get_capability_iri(capability_fn)
except AttributeError:
_prior_value = None
if _prior_value is not None:
raise ValueError("cannot call _set_capability_iri twice (on %r)", capability_fn)
setattr(capability_fn, GRAVY.capability, capability_iri)


def _get_capability_method_map(obj):
try:
return getattr(obj, GRAVY.capability_map)
except AttributeError:
return _compute_capability_method_map(obj)


def _compute_capability_method_map(obj):
_capability_method_map = {}
for _methodname, _fn in inspect.getmembers(obj, inspect.ismethod):
# TODO: intent is to make it easy to implement the capabilities you are
# trying to support while ignoring all the rest (until you want them).
# on the base class, declare and decorate methods for each supported
# capability, then implementers may implement (or not implement) any or
# all of them -- this doesn't quite do all that, maybe try from __new__?
try:
_capability_iri = getattr(_fn, GRAVY.capability)
except AttributeError:
pass # not a capability implementation
else:
assert _capability_iri not in _capability_method_map, (
f"duplicate implementations of capability <{_capability_iri}>"
f"(conflicting: {_fn}, {_capability_method_map[_capability_iri]})"
)
_capability_method_map[_capability_iri] = _methodname
_logger.info("found capability methods on %r: %r", obj, _capability_method_map)
setattr(obj, GRAVY.capability_map, _capability_method_map)
return _capability_method_map
immediate_operation = BaseAddonInterface._immediate_operation
proxy_read_operation = BaseAddonInterface._proxy_read_operation
proxy_act_operation = BaseAddonInterface._proxy_act_operation
2 changes: 1 addition & 1 deletion addon_toolkit/namespaces.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from primitive_metadata import primitive_rdf as rdf


GRAVY = rdf.IriNamespace("https://addons.osf.example/vocab/2023/")
GRAVY = rdf.IriNamespace("https://addons.osf.example/vocab/2024/")
93 changes: 93 additions & 0 deletions addon_toolkit/operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import dataclasses
import inspect
import logging
from typing import Callable

from addon_toolkit.namespaces import GRAVY


__all__ = ( # public module attrs:
"immediate_operation",
"proxy_read_operation",
"proxy_act_operation",
)

_logger = logging.getLogger(__name__)


###
# decorators to declare operations on interface classes


def immediate_operation(fn):
# decorator for operations that can be computed immediately,
# without sending any requests or waiting on external resources
# (e.g. build a url in a known pattern or return declared static metadata)
assert inspect.isfunction(fn)
assert not inspect.isawaitable(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
# TODO: helpful error messaging for implementers
return _DecoratedOperation(fn)


def proxy_read_operation(fn):
# decorator for operations that require fetching data from elsewhere,
# but make no changes (e.g. get a metadata description of an item,
# list items in a given folder)
assert inspect.isasyncgenfunction(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
# TODO: assert based on return value?
return _DecoratedOperation(fn)


def proxy_act_operation(fn):
# decorator for operations that initiate change, may take some time,
# and may fail in strange ways (e.g. delete an item, copy a file tree)
assert inspect.iscoroutine(fn)
# TODO: assert based on `inspect.signature(fn).parameters`
# TODO: assert based on return value?
return _DecoratedOperation(fn)


###
# module-private helpers


@dataclasses.dataclass
class _DecoratedOperation:
"""a temporary object for decorated operation methods"""

operation_fn: Callable

def __set_name__(self, cls, name):
# called for each decorated class method
_operation_method_map = _get_operation_method_map(cls)
assert name not in _operation_method_map
_operation_method_map[name]
# overwrite this _DecoratedOperation with the operation_fn
# now that operation record-keeping has completed
setattr(cls, name, self.operation_fn)


def _get_operation_iri(fn):
# may raise AttributeError
return getattr(fn, GRAVY.operation)


def _set_operation_iri(operation_fn, operation_iri):
try:
_prior_value = _get_operation_iri(operation_fn)
except AttributeError:
_prior_value = None
if _prior_value is not None:
raise ValueError("cannot call _set_operation_iri twice (on %r)", operation_fn)
setattr(operation_fn, GRAVY.operation, operation_iri)


def _get_operation_method_map(obj):
try:
return getattr(obj, GRAVY.operation_map)
except AttributeError:
_operation_method_map = {}
setattr(obj, GRAVY.operation_map, _operation_method_map)
return _operation_method_map
54 changes: 24 additions & 30 deletions addon_toolkit/storage.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,82 @@
from addon_service.namespaces import GRAVY

from .capability import (
immediate_capability,
proxy_act_capability,
proxy_read_capability,
)
from .interfaces import (
BaseAddonInterface,
PagedResult,
)
from .operation import (
immediate_operation,
proxy_act_operation,
proxy_read_operation,
)


# what a base StorageInterface could be like (incomplete)
class StorageInterface(BaseAddonInterface):
##
# "item-read" capabilities:
# "item-read" operations:

@immediate_capability(GRAVY.item_download_url, requires={GRAVY.read})
@immediate_operation
def item_download_url(self, item_id: str) -> str:
raise NotImplementedError # e.g. waterbutler url, when appropriate

@proxy_read_capability(GRAVY.get_item_description, requires={GRAVY.read})
@proxy_read_operation
async def get_item_description(self, item_id: str) -> dict:
raise NotImplementedError

##
# "item-write" capabilities:
# "item-write" operations:

@immediate_capability(GRAVY.item_upload_url, requires={GRAVY.write})
@immediate_operation
def item_upload_url(self, item_id: str) -> str:
raise NotImplementedError

@proxy_act_capability(GRAVY.pls_delete_item, requires={GRAVY.write})
@proxy_act_operation
async def pls_delete_item(self, item_id: str):
raise NotImplementedError

##
# "tree-read" capabilities:
# "tree-read" operations:

@proxy_read_capability(GRAVY.get_root_item_ids, requires={GRAVY.read, GRAVY.tree})
@proxy_read_operation
async def get_root_item_ids(self) -> PagedResult[str]:
raise NotImplementedError

@proxy_read_capability(GRAVY.get_parent_item_id, requires={GRAVY.read, GRAVY.tree})
@proxy_read_operation
async def get_parent_item_id(self, item_id: str) -> str | None:
raise NotImplementedError

@proxy_read_capability(GRAVY.get_item_path, requires={GRAVY.read, GRAVY.tree})
@proxy_read_operation
async def get_item_path(self, item_id: str) -> str:
raise NotImplementedError

@proxy_read_capability(GRAVY.get_child_item_ids, requires={GRAVY.read, GRAVY.tree})
@proxy_read_operation
async def get_child_item_ids(self, item_id: str) -> PagedResult[str]:
raise NotImplementedError

##
# "tree-write" capabilities
# "tree-write" operations

@proxy_act_capability(GRAVY.pls_move_item, requires={GRAVY.write, GRAVY.tree})
@proxy_act_operation
async def pls_move_item(self, item_id: str, new_treepath: str):
raise NotImplementedError

@proxy_act_capability(GRAVY.pls_copy_item, requires={GRAVY.write, GRAVY.tree})
@proxy_act_operation
async def pls_copy_item(self, item_id: str, new_treepath: str):
raise NotImplementedError

##
# "version-read" capabilities
# "version-read" operations

@proxy_read_capability(
GRAVY.get_current_version_id, requires={GRAVY.read, GRAVY.version}
)
@proxy_read_operation
async def get_current_version_id(self, item_id: str) -> str:
raise NotImplementedError

@proxy_read_capability(GRAVY.get_version_ids, requires={GRAVY.read, GRAVY.version})
@proxy_read_operation
async def get_version_ids(self, item_id: str) -> PagedResult[str]:
raise NotImplementedError

##
# "version-write" capabilities
# "version-write" operations

@proxy_act_capability(
GRAVY.pls_restore_version, requires={GRAVY.write, GRAVY.version}
)
@proxy_act_operation
async def pls_restore_version(self, item_id: str, version_id: str):
raise NotImplementedError

0 comments on commit a488d24

Please sign in to comment.