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

Hook into DataLad credentials (optionally) #95

Merged
merged 1 commit into from
Jun 20, 2020
Merged
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
54 changes: 4 additions & 50 deletions datalad_osf/create_sibling_osf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##

from os import environ
from datalad.interface.base import (
Interface,
build_doc,
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand Down
36 changes: 29 additions & 7 deletions datalad_osf/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
19 changes: 9 additions & 10 deletions datalad_osf/tests/test_create_sibling_osf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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):

Expand Down Expand Up @@ -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):

Expand All @@ -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")
6 changes: 4 additions & 2 deletions datalad_osf/tests/test_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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):
Expand Down
48 changes: 3 additions & 45 deletions datalad_osf/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,64 +7,22 @@
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##

from os import environ
from datalad.utils import (
optional_args,
wraps
)
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)
Expand Down
44 changes: 44 additions & 0 deletions datalad_osf/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##

import json
from os import environ
from datalad.downloaders.credentials import (
Token,
UserPassword,
)


# Note: This should ultimately go into osfclient
Expand Down Expand Up @@ -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)