Skip to content
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

add servable editing to sdk #161

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ formats:
- pdf

python:
version: 3.6
version: 3.8
install:
- method: pip
path: .
Expand Down
66 changes: 65 additions & 1 deletion dlhub_sdk/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from functools import reduce
import logging
import json
from jsonschema import SchemaError
import os
from tempfile import mkstemp
from typing import Union, Any, Optional, Tuple, Dict
from typing import Union, Any, Optional, Tuple, Dict, List
import requests
import warnings
import globus_sdk

from globus_sdk import BaseClient
Expand All @@ -15,6 +18,7 @@
from globus_sdk.scopes import AuthScopes, SearchScopes

from dlhub_sdk.config import DLHUB_SERVICE_ADDRESS, CLIENT_ID
from dlhub_sdk.models.servables import BaseServableModel
from dlhub_sdk.utils.futures import DLHubFuture
from dlhub_sdk.utils.schemas import validate_against_dlhub_schema
from dlhub_sdk.utils.search import DLHubSearchHelper, get_method_details, filter_latest
Expand All @@ -26,6 +30,10 @@
logger = logging.getLogger(__name__)


class PermissionWarning(UserWarning):
"""Brought to the user's attention when they attempt to perform an action without proper privileges"""


class DLHubClient(BaseClient):
"""Main class for interacting with the DLHub service

Expand Down Expand Up @@ -417,6 +425,62 @@ def publish_repository(self, repository):
task_id = response.data['task_id']
return task_id

def edit_servable(self, servable_name: str, *, model: BaseServableModel = None, changes: Dict[str, str] = None) -> dict:
"""Edit servable metadata from its name and model object or dict of changes

Args:
servable_name (string): name of the servable to be modified
model (BaseServableModel): the new metadata can be retrieved from a model object
changes (dict): keys represent the property to be edited using a period delimited path through the metadata structure
e.g. the input description is located at `servable.methods.run.input.description`
the full structure of the metadata can be found at <INSERT LINK TO DOCS HERE>
Returns:
(dict): the result of the edit request
Raises:
ValueError: if the user provides an invalid key in changes or an unsatisfactory servable_name (points to more than one servable)
Exception: if the user attempts to edit a servable they do not own or a property they cannot edit
"""
if model is not None:
metadata = model.to_dict()
else:
metadata = self.search_by_servable(servable_name, self.get_username(), only_latest=True)
if not isinstance(metadata, dict):
raise ValueError("Please provide the unique name of a servable that you own")

for key_str, value in changes.items():
metadata = self._edit_dict(metadata, key_str.split("."), value)

try:
validate_against_dlhub_schema(metadata, "servable")
except SchemaError:
raise ValueError("dl.edit_servable was supplied invalid replacement data") # traceback will show the SchemaError

res = self.put(f"/servables/{self.get_username()}/{servable_name}", json_body=metadata)

return res

def _edit_dict(self, dct: dict, keys: List[str], data: Any) -> dict:
"""Edit the given dict such that the value found by keys becomes data

Args:
dct (dict): the metadata that needs to be modified
keys (list): list representing the path through nested dictionaries to the desired key
data (Any): the value to be inserted at the location of keys
Returns:
(dict): dct after it has been modified
Raises:
ValueError: if keys does not point to a valid location
"""
# the user is not meant to modify any property within the dlhub field
if "dlhub" in keys:
warnings.warn(f"The property ({'.'.join(keys)}) cannot be modified without administrator attention", PermissionWarning, stacklevel=2)
return dct
try:
reduce(dict.__getitem__, keys[:-1], dct)[keys[-1]] = data # folds keys to get to the penultimate key and assigns to it
return dct
except KeyError:
raise ValueError(f"dl.edit_servable was supplied an invalid property: {'.'.join(keys)}") from None # hide KeyError from the traceback

def search(self, query, advanced=False, limit=None, only_latest=True):
"""Query the DLHub servable library

Expand Down
11 changes: 9 additions & 2 deletions dlhub_sdk/tests/test_dlhub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_get_servables(dl):

def test_run(dl):
user = "aristana_uchicago"
name = "noop_v10"
name = "noop_v11" # published 2/22/2022
data = True # accepts anything as input, but listed as Boolean in DLHub

# Test a synchronous request
Expand Down Expand Up @@ -88,6 +88,13 @@ def test_submit(dl):
dl.publish_servable(model)


@mark.skip # unimplemented on the service currently
def test_edit(dl):
res = dl.edit_servable("noop_v10", changes={"servable.methods.run.output.description": "'Hello, world!'"})
with open("test_res.txt", "w") as f:
f.write(str(res))


def test_describe_model(dl):
# Find the 1d_norm function from the test user (should be there)
description = dl.describe_servable('dlhub.test_gmail/1d_norm')
Expand Down Expand Up @@ -220,6 +227,6 @@ def test_namespace(dl):


def test_status(dl):
future = dl.run('aristana_uchicago/noop_v10', True, asynchronous=True)
future = dl.run('aristana_uchicago/noop_v11', True, asynchronous=True)
# Need spec for Fx status returns
assert isinstance(dl.get_task_status(future.task_id), dict)
4 changes: 4 additions & 0 deletions dlhub_sdk/utils/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ def __init__(self, search_client: SearchClient, **kwargs):
"""
super(DLHubSearchHelper, self).__init__("dlhub", search_client=search_client, **kwargs)

# a verison of this functionality needs to be implemented to handle the not yet implemented dl.delete_seravble() function
# def search(self, q=None, advanced=False, limit=None, info=False, reset_query=True):
# return super(DLHubSearchHelper, self.exclude_field("dlhub.deleted", True)).search(q, advanced, limit, info, reset_query)

def match_owner(self, owner):
"""Add a model owner to the query.

Expand Down
2 changes: 1 addition & 1 deletion dlhub_sdk/version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# single source of truth for package version,
# see https://packaging.python.org/en/latest/single_source_version/
__version__ = "0.10.2"
__version__ = "1.0.1"

# app name to send as part of SDK requests
app_name = "DLHub SDK v{}".format(__version__)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ mdf_toolbox>=0.5.4
jsonschema>=3.2.0
funcx>=1.0.0
pydantic
numpy