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

added first iteration of easy_publish #153

Merged
merged 56 commits into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
828b678
remove exception chaining when given unknown type
isaac-darling Jul 21, 2022
c2af2f3
add easy_publish method to dlhub.client
isaac-darling Jul 21, 2022
9c6041f
fix style in easy_publish
isaac-darling Jul 21, 2022
4e0a17f
add missing whitespace
isaac-darling Jul 21, 2022
e16cc99
fix error message in HelpMessage
isaac-darling Jul 21, 2022
4812942
merge the two static init options into one method
isaac-darling Jul 22, 2022
9f299b8
reorder static create_model params for portability
isaac-darling Jul 22, 2022
4e58649
return the old model constructor for compatibility
isaac-darling Jul 24, 2022
760b703
remove walrus operator, fix help message, revert return to task id
isaac-darling Jul 25, 2022
8e1d3c6
patch publishing test to so it runs without bloat
isaac-darling Jul 25, 2022
03e948f
patch publishing test so that it runs without bloat
isaac-darling Jul 25, 2022
dc9fb36
add functionality for extracting metadata from hints
isaac-darling Jul 27, 2022
0524c89
add auto-inspecting to easy_publish
isaac-darling Jul 27, 2022
1fb6615
switch easy_publish test to a function with hints
isaac-darling Jul 27, 2022
3e573d2
add clarifying comments
isaac-darling Jul 31, 2022
d4e6232
remove commented test skips
isaac-darling Aug 9, 2022
981efe7
add affiliations parameter to easy_publish
isaac-darling Aug 9, 2022
5ba2463
add paper_doi parameter to easy_publish
isaac-darling Aug 11, 2022
026ef66
simplify paper_doi parameter of easy_publish
isaac-darling Aug 11, 2022
3521090
add clarifying comments and extract repeated code
isaac-darling Aug 12, 2022
af04041
Merge branch 'dev' into friendly-publish
isaac-darling Aug 15, 2022
802cfc0
Merge branch 'dev' into auto-extract-metadata
isaac-darling Aug 15, 2022
ba9e7e7
fix code style
isaac-darling Aug 15, 2022
0ecdbbc
add comments to bypass flake8
isaac-darling Aug 16, 2022
bf55004
add pytest-mock to testing-requirements
isaac-darling Aug 16, 2022
c93efeb
add new parameters to easy_publish test
isaac-darling Aug 19, 2022
e35b097
Merge pull request #163 from DLHub-Argonne/dev
ascourtas Aug 26, 2022
fcbd173
update version to 1.0.0 for funcx compat
ascourtas Aug 26, 2022
624a59b
switch to backwards compatible type hints
isaac-darling Sep 13, 2022
83c126f
switch to backwards compatible type hints
isaac-darling Sep 13, 2022
ac7f1d7
switch to backwards compatible type checking
isaac-darling Sep 13, 2022
873f34c
update test model to noop_v11 since v10 has been corrupted (#164)
ascourtas Sep 15, 2022
5e8d8c6
release for test update patch
ascourtas Sep 15, 2022
6b1af9c
Updating RTD Python version to 3.9
blaiszik Sep 15, 2022
1a77bfd
RTD Python to 3.8
blaiszik Sep 15, 2022
3b08006
Add numpy latest requirement
blaiszik Sep 15, 2022
dbc7126
Merge branch 'master' into friendly-publish
isaac-darling Sep 16, 2022
71cb851
Merge branch 'master' into auto-extract-metadata
isaac-darling Sep 16, 2022
5e5d34c
move inspect code to util module
isaac-darling Sep 16, 2022
9310f71
add support for typing type names
isaac-darling Sep 16, 2022
ee975d7
address the behavior of the type checker
isaac-darling Sep 16, 2022
f4ec3c2
Merge branch 'auto-extract-metadata' into friendly-publish
isaac-darling Sep 16, 2022
b1eac69
remove unnecessary comments
isaac-darling Sep 16, 2022
e8576bd
make auto-inspection the default behavior
isaac-darling Sep 16, 2022
08d255a
revert last commit
isaac-darling Sep 16, 2022
f7eb374
adjust publish test to use my function
isaac-darling Sep 16, 2022
ba0a4fe
update condition to isolate subscripted hints
isaac-darling Sep 16, 2022
7cbfded
add tests to improve coverage of python model
isaac-darling Sep 16, 2022
7213660
add test for auto inspecting class methods
isaac-darling Sep 16, 2022
21a2ea0
fix path to pickle file
isaac-darling Sep 16, 2022
88dab48
fix test to cooperate with pickle
isaac-darling Sep 16, 2022
a22de1d
add file for tests on inspect functions
isaac-darling Sep 20, 2022
a9ef0c3
ndarray hints are not subscriptable
isaac-darling Sep 21, 2022
62b77be
add tests for inspect library
isaac-darling Sep 21, 2022
666f5c8
add handling for ndarray shape of Any
isaac-darling Sep 21, 2022
4828b88
add tests for ndarrays of Any shape
isaac-darling Sep 21, 2022
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
77 changes: 76 additions & 1 deletion dlhub_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import os
from tempfile import mkstemp
from typing import Union, Any, Optional, Tuple, Dict
from typing import Sequence, Union, Any, Optional, Tuple, Dict, List
import requests
import globus_sdk

Expand All @@ -14,6 +14,13 @@
from funcx.sdk.client import FuncXClient
from globus_sdk.scopes import AuthScopes, SearchScopes

from dlhub_sdk.models.servables.keras import KerasModel
from dlhub_sdk.models.servables.pytorch import TorchModel
from dlhub_sdk.models.servables.python import PythonClassMethodModel
from dlhub_sdk.models.servables.python import PythonStaticMethodModel
from dlhub_sdk.models.servables.tensorflow import TensorFlowModel
from dlhub_sdk.models.servables.sklearn import ScikitLearnModel

from dlhub_sdk.config import DLHUB_SERVICE_ADDRESS, CLIENT_ID
from dlhub_sdk.utils.futures import DLHubFuture
from dlhub_sdk.utils.schemas import validate_against_dlhub_schema
Expand All @@ -26,6 +33,10 @@
logger = logging.getLogger(__name__)


class HelpMessage(Exception):
"""Raised from another error to provide the user an additional message"""


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

Expand Down Expand Up @@ -343,6 +354,70 @@ def get_result(self, task_id, verbose=False):
result = result[0]
return result

def easy_publish(self, title: str, creators: Union[str, List[str]], short_name: str, servable_type: str, serv_options: Dict[str, Any],
affiliations: List[Sequence[str]] = None, paper_doi: str = None):
"""Simplified publishing method for servables

Args:
title (string): title for the servable
creators (string | list): either the creator's name (FamilyName, GivenName) or a list of the creators' names
short_name (string): shorthand name for the servable
servable_type (string): the type of the servable, must be a member of ("static_method",
"class_method",
"keras",
"pytorch",
"tensorflow",
"sklearn") more information on servable types can be found here:
https://dlhub-sdk.readthedocs.io/en/latest/servable-types.html
serv_options (dict): the servable_type specific arguments that are necessary for publishing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add more context for the user on what the servable options are -- ideally, link to documentation

affiliations (list): list of affiliations for each author
paper_doi (str): DOI of a paper that describes the servable
Returns:
(string): task id of this submission, can be used to check for success
Raises:
ValueError: If the given servable_type is not in the list of acceptable types
Exception: If the serv_options are incomplete or the request to publish results in an error
"""
# conversion table for model string names to classes
models = {"static_method": PythonStaticMethodModel,
"class_method": PythonClassMethodModel,
"keras": KerasModel,
"pytorch": TorchModel,
"tensorflow": TensorFlowModel,
"sklearn": ScikitLearnModel}

# raise an error if the provided servable_type is invalid
model = models.get(servable_type)
if model is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you wanted to shorten this a bit, you could do if servable_type not in models -- slightly more pythonic imo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would, but I also need to store the model variable for later. I suppose lookups are quick, but it's a little more visual clutter. up to you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♀️ I like "not in models" but not enough to change this here.

raise ValueError(f"dl.easy_publish given invalid servable type: {servable_type}, please refer to the docstring")

# attempt to construct the model and raise a helpful error if needed
try:
# if the servable is a python function, set the parameter to attempt to auto-generate the inputs/outputs
if servable_type in {"static_method", "class_method"}:
serv_options["auto_inspect"] = True

model_info = model.create_model(**serv_options)
except Exception as e:
help_err = HelpMessage(f"Help can be found here:\nhttps://dlhub-sdk.readthedocs.io/en/latest/source/dlhub_sdk.models.servables.html#"
f"{model.__module__}.{model.__name__}.create_model")
raise help_err from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to clarify, this help_err won't eat the original exception message, correct?

Copy link
Contributor Author

@isaac-darling isaac-darling Jul 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. specifying from e means the traceback will show the original error and say something like "the following error is a direct result of the previous error" and then show the help message. docs on from Exception here.


# set the required datacite fields
model_info.set_title(title)
creators = [creators] if isinstance(creators, str) else creators # handle the case where creators is a string
model_info.set_creators(creators, affiliations or []) # affiliations if provided, else empty list
model_info.set_name(short_name)

if paper_doi is not None:
model_info.add_related_resource(paper_doi, "DOI", "IsDescribedBy")

# perform the publish
task_id = self.publish_servable(model_info)

# return the id of the publish task
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great comments in this method Isaac, keep it up! 🎉

return task_id

def publish_servable(self, model):
"""Submit a servable to DLHub

Expand Down
5 changes: 3 additions & 2 deletions dlhub_sdk/models/datacite.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ class DataciteRelationType(Enum):
class DataciteRelatedIdentifier(BaseModel):
"""Identifier of a related resource. Use this property to indicate subsets of properties, as appropriate."""
relatedIdentifier: str
relatedIdentifierType: DataciteRelatedIdentifierType
relatedMetadataScheme: str
relatedIdentifierType: Union[str, DataciteRelatedIdentifierType]
relationType: Union[str, DataciteRelationType]
relatedMetadataScheme: Optional[str]
schemeURI: Optional[AnyUrl] = None


Expand Down
54 changes: 48 additions & 6 deletions dlhub_sdk/models/servables/python.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""Tools to annotate generic operations (e.g., class method calls) in Python"""
import pickle as pkl
import importlib
from inspect import Signature

from dlhub_sdk.models.servables import BaseServableModel, ArgumentTypeMetadata
from dlhub_sdk.utils.types import compose_argument_block
from dlhub_sdk.utils.inspect import signature_to_input, signature_to_output


class BasePythonServableModel(BaseServableModel):
Expand Down Expand Up @@ -113,14 +116,15 @@ class PythonClassMethodModel(BasePythonServableModel):
any arguments of the class that should be set as defaults."""

@classmethod
def create_model(cls, path, method, function_kwargs=None) -> 'PythonClassMethodModel':
def create_model(cls, path, method, function_kwargs=None, *, auto_inspect=False) -> 'PythonClassMethodModel':
"""Initialize a model for a python object

Args:
path (string): Path to a pickled Python file
method (string): Name of the method for this class
function_kwargs (dict): Names and values of any other argument of the function to set
the values must be JSON serializable.
auto_inspect (boolean): Whether or not to attempt to automatically extract inputs from the function
"""
output = super(PythonClassMethodModel, cls).create_model(method, function_kwargs)

Expand All @@ -135,6 +139,11 @@ def create_model(cls, path, method, function_kwargs=None) -> 'PythonClassMethodM
'class_name': class_name
})

if auto_inspect:
func = getattr(obj, method)

output = add_extracted_metadata(func, output)

return output

def _get_handler(self):
Expand All @@ -153,8 +162,8 @@ class PythonStaticMethodModel(BasePythonServableModel):
"""

@classmethod
def create_model(cls, module, method, autobatch=False, function_kwargs=None):
"""Initialize the method
def create_model(cls, module=None, method=None, autobatch=False, function_kwargs=None, *, f=None, auto_inspect=False):
"""Initialize the method based on the provided arguments
ascourtas marked this conversation as resolved.
Show resolved Hide resolved

Args:
module (string): Name of the module holding the function
Expand All @@ -163,28 +172,61 @@ def create_model(cls, module, method, autobatch=False, function_kwargs=None):
Calls :code:`map(f, list)`
function_kwargs (dict): Names and values of any other argument of the function to set
the values must be JSON serializable.
f (object): function pointer to the desired python function
auto_inspect (boolean): Whether or not to attempt to automatically extract inputs from the function
Raises:
TypeError: If there is no valid way to process the given arguments
"""
# if a pointer is provided, get the module and method
if f is not None:
ascourtas marked this conversation as resolved.
Show resolved Hide resolved
module, method = f.__module__, f.__name__
func = f
# if it is not, ensure both the module and method are provided and get the function pointer
elif module is not None and method is not None:
module_obj = importlib.import_module(module)
func = getattr(module_obj, method)
else:
raise TypeError("PythonStaticMethodModel.create_model was not provided valid arguments. Please provide either a funtion pointer"
" or the module and name of the desired static function")

output = super(PythonStaticMethodModel, cls).create_model(method, function_kwargs)

output.servable.methods["run"].method_details.update({
'module': module,
'autobatch': autobatch
})

if auto_inspect:
output = add_extracted_metadata(func, output)

return output

@classmethod
def from_function_pointer(cls, f, autobatch=False, function_kwargs=None):
"""Construct the module given a function pointer

Args:
f (object): A function pointer
f (object): Function pointer to the Python function to be published
autobatch (bool): Whether to run function on an iterable of entries
function_kwargs (dict): Any default options for this function
"""
return cls.create_model(f.__module__, f.__name__, autobatch, function_kwargs)
return cls.create_model(f=f, autobatch=autobatch, function_kwargs=function_kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what to do about autobatch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that I understand..


def _get_handler(self):
return 'python.PythonStaticMethodServable'

def _get_type(self):
return 'Python static method'


def add_extracted_metadata(func, model: BasePythonServableModel) -> BasePythonServableModel:
"""Helper function for adding generated input/output metadata to a model object
Args:
func: a pointer to the function whose data is to be extracted
model (BasePythonServableModel): the model who needs its data to be updated
Returns:
(BasePythonServableModel): the model that was given after it is updated
"""
sig = Signature.from_callable(func)
model = model.set_inputs(**signature_to_input(sig))
model = model.set_outputs(**signature_to_output(sig))
return model
52 changes: 45 additions & 7 deletions dlhub_sdk/tests/test_dlhub_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import os
import pickle as pkl
from typing import Dict

import mdf_toolbox
from pytest import fixture, raises, mark
from pytest_mock import mocker # noqa: F401 (flake8 cannot detect usage)

from dlhub_sdk.models.servables.python import PythonStaticMethodModel
from dlhub_sdk.models.servables.python import PythonClassMethodModel, PythonStaticMethodModel
from dlhub_sdk.utils.futures import DLHubFuture
from dlhub_sdk.client import DLHubClient

Expand All @@ -13,6 +16,16 @@
client_secret = os.getenv('CLIENT_SECRET')
fx_scope = "https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all"
is_gha = os.getenv('GITHUB_ACTIONS')
_pickle_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "pickle.pkl"))


# make dummy reply for mocker patch to return
class DummyReply:
def __init__(self) -> None:
self.status_code = 200

def json(self) -> Dict[str, str]:
return {"task_id": "bf06d72e-0478-11ed-97f9-4b1381555b22"} # valid task id, status is known to be FAILED


@fixture()
Expand Down Expand Up @@ -53,7 +66,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 All @@ -73,9 +86,7 @@ def test_run(dl):
assert res.result(timeout=60) == 'Hello world!'


@mark.skipif(not is_gha, reason='Avoid running this test except on larger-scale tests of the system')
@mark.skip
def test_submit(dl):
def test_submit(dl, mocker): # noqa: F811 (flake8 does not understand usage)
# Make an example function
model = PythonStaticMethodModel.create_model('numpy.linalg', 'norm')
model.dlhub.test = True
Expand All @@ -84,8 +95,35 @@ def test_submit(dl):
model.set_inputs('ndarray', 'Array to be normed', shape=[None])
model.set_outputs('number', 'Norm of the array')

# patch requests.post
mocker.patch("requests.post", return_value=DummyReply())

# Submit the model
dl.publish_servable(model)
task_id = dl.publish_servable(model)
assert task_id == "bf06d72e-0478-11ed-97f9-4b1381555b22"

# pickle class method to test
with open(_pickle_path, 'wb') as fp:
pkl.dump(DummyReply(), fp)

# test auto_inspect for class methods
model = PythonClassMethodModel.create_model(_pickle_path, "json", auto_inspect=True)
model.dlhub.test = True
model.set_name("dummy_json")
model.set_title("Dummy JSON")

# Submit the model
task_id = dl.publish_servable(model)
assert task_id == "bf06d72e-0478-11ed-97f9-4b1381555b22"

# make sure invalid call raises proper error
with raises(TypeError):
model = PythonStaticMethodModel.create_model("dlhub_sdk.utils.validation")

# Submit the model using easy publish
task_id = dl.easy_publish("Validate dl.run Calls", "Darling, Isaac", "validate_run", "static_method",
{"module": "dlhub_sdk.utils.validation", "method": "validate"}, [["University of Chicago"]], "not-a-real-doi")
assert task_id == "bf06d72e-0478-11ed-97f9-4b1381555b22"


def test_describe_model(dl):
Expand Down Expand Up @@ -220,6 +258,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)
Loading