diff --git a/datalad_osf/create_sibling_osf.py b/datalad_osf/create_sibling_osf.py index a93c4ad..6a7bd91 100644 --- a/datalad_osf/create_sibling_osf.py +++ b/datalad_osf/create_sibling_osf.py @@ -7,7 +7,6 @@ # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -from os import environ from datalad.interface.base import ( Interface, build_doc, @@ -30,57 +29,12 @@ ) from datalad.interface.results import get_status_dict from datalad_osf.osfclient.osfclient import OSF -from datalad_osf.utils import create_project -from datalad.downloaders.credentials import ( - Token, - UserPassword, +from datalad_osf.utils import ( + create_project, + get_credentials, ) -def _get_credentials(): - """helper to read credentials - - for now go w/ env vars. can be refactored - to read from datalad configs, credential store, etc. - """ - # check if anything need to be done still - if 'OSF_TOKEN' in environ or all( - k in environ for k in ('OSF_USERNAME', 'OSF_PASSWORD')): - return dict( - token=environ.get('OSF_TOKEN', None), - username=environ.get('OSF_USERNAME', None), - password=environ.get('OSF_USERNAME', None), - ) - - token_auth = Token(name='https://osf.io', url=None) - up_auth = UserPassword(name='https://osf.io', url=None) - - # get auth token, form environment, or from datalad credential store - # if known-- we do not support first-time entry during a test run - token = environ.get( - 'OSF_TOKEN', - token_auth().get('token', None) if token_auth.is_known else None) - username = None - password = None - if not token: - # now same for user/password if there was no token - username = environ.get( - 'OSF_USERNAME', - up_auth().get('user', None) if up_auth.is_known else None) - password = environ.get( - 'OSF_PASSWORD', - up_auth().get('password', None) if up_auth.is_known else None) - - # place into environment, for now this is the only way the special remote - # can be supplied with credentials - for k, v in (('OSF_TOKEN', token), - ('OSF_USERNAME', username), - ('OSF_PASSWORD', password)): - if v: - environ[k] = v - return dict(token=token, username=username, password=password) - - @build_doc class CreateSiblingOSF(Interface): """Create a dataset representation at OSF @@ -162,7 +116,7 @@ def __call__(title, name="osf", dataset=None, mode="annex"): # - option: Make public! - cred = _get_credentials() + cred = get_credentials(allow_interactive=True) osf = OSF(**cred) proj_id, proj_url = create_project(osf_session=osf.session, title=title) yield get_status_dict(action="create-project-osf", diff --git a/datalad_osf/remote.py b/datalad_osf/remote.py index a5d6289..a4d8c41 100755 --- a/datalad_osf/remote.py +++ b/datalad_osf/remote.py @@ -98,16 +98,38 @@ def prepare(self): project_id = posixpath.basename( urlparse(self.annex.getconfig('project')).path.strip(posixpath.sep)) + try: + # make use of DataLad's credential manager for a more convenient + # out-of-the-box behavior + from datalad_osf.utils import get_credentials + # we must stay non-interactive, because this is running inside + # git-annex's special remote protocal + creds = get_credentials(allow_interactive=False) + except ImportError as e: + # whenever anything goes wrong here, stay clam and fall back + # on envvars. + # we want this special remote to be fully functional without + # datalad + creds = dict( + username=os.environ.get('OSF_USERNAME', None), + password=os.environ.get('OSF_PASSWORD', None), + token=os.environ.get('OSF_TOKEN', None), + ) + # next one just sets up the stage, no requests performed yet, hence + # no error checking needed # supply both auth credentials, so osfclient can fall back on user/pass # if needed - osf = OSF( - username=os.environ.get('OSF_USERNAME', None), - password=os.environ.get('OSF_PASSWORD', None), - token=os.environ.get('OSF_TOKEN', None), - ) # TODO: error checking etc + osf = OSF(**creds) # next one performs initial auth - self.project = osf.project(project_id) # errors ?? - + try: + self.project = osf.project(project_id) + except Exception as e: + # we need to raise RemoteError() such that PREPARE-FAILURE + # is reported, sadly that doesn't give users any clue + # TODO support datalad logging here + raise RemoteError( + 'Failed to obtain OSF project handle: {}'.format(e) + ) # which storage to use, defaults to 'osfstorage' # TODO a project could have more than one? Make parameter to select? self.storage = self.project.storage() diff --git a/datalad_osf/tests/test_create_sibling_osf.py b/datalad_osf/tests/test_create_sibling_osf.py index 957e05c..88226ad 100644 --- a/datalad_osf/tests/test_create_sibling_osf.py +++ b/datalad_osf/tests/test_create_sibling_osf.py @@ -20,12 +20,11 @@ with_tree ) from datalad.utils import Path -from datalad_osf.utils import delete_project -from datalad_osf.create_sibling_osf import _get_credentials -from datalad_osf.osfclient.osfclient import OSF -from datalad_osf.tests.utils import ( - setup_credentials, +from datalad_osf.utils import ( + delete_project, + get_credentials, ) +from datalad_osf.osfclient.osfclient import OSF minimal_repo = {'ds': {'file1.txt': 'content', @@ -43,7 +42,7 @@ def test_invalid_calls(path): raise SkipTest("TODO") -@skip_if(cond=not any(setup_credentials().values()), msg='no OSF credentials') +@skip_if(cond=not any(get_credentials().values()), msg='no OSF credentials') @with_tree(tree=minimal_repo) def test_create_osf_simple(path): @@ -87,12 +86,12 @@ def test_create_osf_simple(path): assert_in(here, whereis) finally: # clean remote end: - cred = _get_credentials() + cred = get_credentials(allow_interactive=False) osf = OSF(**cred) delete_project(osf.session, create_results[0]['id']) -@skip_if(cond=not any(setup_credentials().values()), msg='no OSF credentials') +@skip_if(cond=not any(get_credentials().values()), msg='no OSF credentials') @with_tree(tree=minimal_repo) def test_create_osf_export(path): @@ -114,12 +113,12 @@ def test_create_osf_export(path): finally: # clean remote end: - cred = _get_credentials() + cred = get_credentials(allow_interactive=False) osf = OSF(**cred) delete_project(osf.session, create_results[0]['id']) -@skip_if(cond=not any(setup_credentials().values()), msg='no OSF credentials') +@skip_if(cond=not any(get_credentials().values()), msg='no OSF credentials') def test_create_osf_existing(): raise SkipTest("TODO") diff --git a/datalad_osf/tests/test_remote.py b/datalad_osf/tests/test_remote.py index 446a7f7..66c0cb6 100644 --- a/datalad_osf/tests/test_remote.py +++ b/datalad_osf/tests/test_remote.py @@ -18,7 +18,9 @@ ) from datalad_osf.tests.utils import ( with_project, - setup_credentials, +) +from datalad_osf.utils import ( + get_credentials, ) common_init_opts = ["encryption=none", "type=external", "externaltype=osf", @@ -29,7 +31,7 @@ # remote. It might just be that the SHA256 key paths get too long # https://github.com/datalad/datalad-osf/issues/71 @skip_if_on_windows -@skip_if(cond=not any(setup_credentials().values()), msg='no OSF credentials') +@skip_if(cond=not any(get_credentials().values()), msg='no OSF credentials') @with_project(title="CI osf-special-remote") @with_tempfile def test_gitannex(osf_id, dspath): diff --git a/datalad_osf/tests/utils.py b/datalad_osf/tests/utils.py index 7fc524f..eb89b20 100644 --- a/datalad_osf/tests/utils.py +++ b/datalad_osf/tests/utils.py @@ -7,7 +7,6 @@ # # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -from os import environ from datalad.utils import ( optional_args, wraps @@ -15,56 +14,15 @@ from datalad_osf.utils import ( create_project, delete_project, -) -from datalad.downloaders.credentials import ( - Token, - UserPassword, + get_credentials, ) from datalad_osf.osfclient.osfclient import OSF -def setup_credentials(): - # check if anything need to be done still - if 'OSF_TOKEN' in environ or all( - k in environ for k in ('OSF_USERNAME', 'OSF_PASSWORD')): - return dict( - token=environ.get('OSF_TOKEN', None), - username=environ.get('OSF_USERNAME', None), - password=environ.get('OSF_USERNAME', None), - ) - - token_auth = Token(name='https://osf.io', url=None) - up_auth = UserPassword(name='https://osf.io', url=None) - - # get auth token, form environment, or from datalad credential store - # if known-- we do not support first-time entry during a test run - token = environ.get( - 'OSF_TOKEN', - token_auth().get('token', None) if token_auth.is_known else None) - username = None - password = None - if not token: - # now same for user/password if there was no token - username = environ.get( - 'OSF_USERNAME', - up_auth().get('user', None) if up_auth.is_known else None) - password = environ.get( - 'OSF_PASSWORD', - up_auth().get('password', None) if up_auth.is_known else None) - - # place into environment, for now this is the only way the special remote - # can be supplied with credentials - for k, v in (('OSF_TOKEN', token), - ('OSF_USERNAME', username), - ('OSF_PASSWORD', password)): - if v: - environ[k] = v - return dict(token=token, username=username, password=password) - - @optional_args def with_project(f, osf_session=None, title=None, category="project"): - creds = setup_credentials() + # we don't want the test hanging, no interaction + creds = get_credentials(allow_interactive=False) # supply all credentials, so osfclient can fall back on user/pass # if needed osf = OSF(**creds) diff --git a/datalad_osf/utils.py b/datalad_osf/utils.py index ceee051..0bccbd4 100644 --- a/datalad_osf/utils.py +++ b/datalad_osf/utils.py @@ -8,6 +8,11 @@ # ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## import json +from os import environ +from datalad.downloaders.credentials import ( + Token, + UserPassword, +) # Note: This should ultimately go into osfclient @@ -99,3 +104,42 @@ def initialize_osf_remote(remote, project, import subprocess subprocess.run(["git", "annex", "initremote", remote] + init_opts) + + +def get_credentials(allow_interactive=True): + # prefer the environment + if 'OSF_TOKEN' in environ or all( + k in environ for k in ('OSF_USERNAME', 'OSF_PASSWORD')): + return dict( + token=environ.get('OSF_TOKEN', None), + username=environ.get('OSF_USERNAME', None), + password=environ.get('OSF_USERNAME', None), + ) + + # fall back on DataLad credential manager + token_auth = Token(name='https://osf.io', url=None) + up_auth = UserPassword(name='https://osf.io', url=None) + + # get auth token, form environment, or from datalad credential store + # if known-- we do not support first-time entry during a test run + token = environ.get( + 'OSF_TOKEN', + token_auth().get('token', None) + if allow_interactive or token_auth.is_known + else None) + username = None + password = None + if not token: + # now same for user/password if there was no token + username = environ.get( + 'OSF_USERNAME', + up_auth().get('user', None) + if allow_interactive or up_auth.is_known + else None) + password = environ.get( + 'OSF_PASSWORD', + up_auth().get('password', None) + if allow_interactive or up_auth.is_known + else None) + + return dict(token=token, username=username, password=password)