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

Draft: Added Credentials Manager #433

Open
wants to merge 5 commits into
base: master
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
5 changes: 3 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ jobs:
pip install XlsxWriter===0.9.3
- name: Scripts
run: |
python -m pytest recipes/tests/test_recipes_sync.py
python -m pytest solvebio/test/test_object.py
python -m pytest -v solvebio/contrib/dash/tests/test_solvebio_auth.py
python -m pytest -v recipes/tests/test_recipes_sync.py
python -m pytest -v solvebio/test/test_object.py
python -m flake8 solvebio
build_py27:
runs-on: ubuntu-20.04
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Development
cd solve-python/
python setup.py develop

To run tests use

@TODO


Or install `tox` and run:

Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ six
flask
percy
selenium
itsdangerous==2.0.1
dash==1.19.0
dash_auth==1.4.1
dash_core_components==1.15.0
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
else:
install_requires.append('requests>=2.0.0')

# TODO: tests depend on VCF parser, shouldn't it be included?
# PyVCF==0.6.8

# solvebio-recipes requires additional packages
recipes_requires = [
Expand Down
5 changes: 5 additions & 0 deletions solvebio/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .cli.main import main


if __name__ == '__main__':
main()
3 changes: 2 additions & 1 deletion solvebio/cli/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def get_credentials():
try:
netrc_obj = netrc(netrc.path())
if not netrc_obj.hosts:
print("!!!!!!!!!!! NO HOSTS")
return None
except (IOError, TypeError, NetrcParseError) as e:
raise CredentialsError(
Expand All @@ -103,7 +104,7 @@ def get_credentials():
return ('https://' + h,) + netrc_obj.authenticators(h)

# Return the first available
host = netrc_obj.hosts.keys()[0]
host = list(netrc_obj.hosts.keys())[0]
return ('https://' + host,) + netrc_obj.authenticators(host)


Expand Down
1 change: 1 addition & 0 deletions solvebio/cli/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ def queue(statuses=["running", "queued"]):
"""
task_map = {}
tasks = Task.all(status=",".join(statuses))
print('\n',tasks)
for task in tasks:
if task.user.id not in task_map:
task_map[task.user.id] = []
Expand Down
6 changes: 5 additions & 1 deletion solvebio/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,12 @@ def api_host_url(self, value):
return value


def main(argv=sys.argv[1:]):
def main(argv=None):
"""Main entry point for SolveBio CLI"""

if argv is None:
argv = sys.argv[1:]

parser = SolveArgumentParser()
args = parser.parse_solvebio_args(argv)

Expand Down
5 changes: 4 additions & 1 deletion solvebio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import absolute_import

import json
import os
import time
import inspect

Expand Down Expand Up @@ -34,6 +35,7 @@
pass

logger = logging.getLogger('solvebio')
EDP_DEV = os.environ.get('EDP_DEV', False)


def _handle_api_error(response):
Expand Down Expand Up @@ -106,7 +108,7 @@ def __init__(self, host=None, token=None, token_type='Token',
# Use a session with a retry policy to handle
# intermittent connection errors.
retries = Retry(
total=5,
total=5 if not EDP_DEV else 1,
backoff_factor=0.1,
status_forcelist=[
codes.bad_gateway,
Expand Down Expand Up @@ -260,6 +262,7 @@ def request(self, method, url, **kwargs):
return self.request(method, url, **kwargs)

if not (200 <= response.status_code < 400):
print("\n@@ERR@@\n", method, url, opts, '\n', response.status_code, response.content)
_handle_api_error(response)

# 204 is used on deletion. There is no JSON here.
Expand Down
82 changes: 82 additions & 0 deletions solvebio/credentials_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import os

from solvebio.cli.credentials import get_credentials


class EDPClientCredentialsProvider:

def __init__(self):
"""
Credentials are discovered in the following priority:
1) Direct parameters (when using solvebio.SolveClient() or solvebio.login() from the SDK)

This way is naturally NOT supported for the following setups:
- Solvebio python entry point (solvebio as a global command)
- Running solvebio as package (__main__.py)
- Solvebio unit tests
2) Environment variables
- SOLVEBIO_API_HOST
- SOLVEBIO_ACCESS_TOKEN
- SOLVEBIO_API_KEY
3) System credentials file - can be found in:
- Nix: ~/.solvebio/credentials
- Windows: %USERPROFILE%/_solvebio/credentials

EDP Supports access tokens and api keys, albeit api keys are deprecated
and discouraged to use in the future.
"""

# @todo: @important: Currently this flow is only supported for Unit Tests
# see login() and get_credentials()
# in the future this should be the single source of truth
# for finding creds across all public entries

# @todo: perhaps passing by parameters can be mitigated to this class as well
self.api_host: str = None
self.api_key: str = None
self.access_token: str = None
self.token_type: str = None
self.credentials_source: str = None

self.get_credentials()

@property
def as_dict(self):
"""
Adapter for solvebio.SolveClient
"""
return {
"host": self.api_host,
"token": self.access_token if (self.token_type == 'Bearer' or not self.token_type) else self.api_key,
"token_type": self.token_type
}

def get_credentials(self):
# option 2: find by environment variables
self.access_token = os.environ.get("SOLVEBIO_ACCESS_TOKEN")
self.api_key = os.environ.get('SOLVEBIO_API_KEY')
self.api_host = os.environ.get('SOLVEBIO_API_HOST')

if self.api_host and (self.api_token or self.api_key):
self.credentials_source = 'environment_variables'

if self.access_token:
self.token_type = 'Bearer'
elif self.api_key:
self.token_type = 'Token'
return

# option 3: find by netrc file
creds = get_credentials()
if creds:
self.api_host, _, self.token_type, _token_or_key = creds

self.credentials_source = 'netrc_file'

if 'Bearer' == self.token_type:
self.access_token = _token_or_key
self.api_key = None
else:
self.api_key = _token_or_key
self.access_token = None
return
2 changes: 2 additions & 0 deletions solvebio/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ def execute(self, offset=0, **query):

# If the request results in a SolveError (ie bad filter) set the error.
try:
print("@@>", self._data_url, _params)
self._response = self._client.post(self._data_url, _params)
except SolveError as e:
self._error = e
Expand Down Expand Up @@ -1191,6 +1192,7 @@ def execute(self, offset=0, **query):

# If the request results in a SolveError (ie bad filter) set the error.
try:
print("@@2>", self._data_url, _params)
self._response = self._client.post(self._data_url, _params)

if getattr(self, '_header', None) and self._output_format in ('csv', 'tsv') \
Expand Down
3 changes: 2 additions & 1 deletion solvebio/resource/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,8 @@ def tag(self, tags, remove=False, dry_run=False, apply_save=True):
def is_iterable_non_string(arg):
"""python2/python3 compatible way to check if arg is an iterable but not string"""

return isinstance(arg, Iterable) and not isinstance(arg, six.string_types)
return (isinstance(arg, Iterable) and
not isinstance(arg, six.string_types))

if not is_iterable_non_string(tags):
tags = [str(tags)]
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/client_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def update_paths(self):
)

if not self.object['full_path']:
self.object['full_path'] = 'solvebio:{0}:{1}'.format(
self.object['full_path'] = 'quartzbio:{0}:{1}'.format(
self.object['vault_name'], self.object['path'])

def update_dataset(self):
Expand Down
13 changes: 10 additions & 3 deletions solvebio/test/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import re
import sys

from solvebio.credentials_provider import EDPClientCredentialsProvider

if (sys.version_info >= (2, 7, 0)):
import unittest # NOQA
else:
Expand All @@ -14,15 +16,20 @@


class SolveBioTestCase(unittest.TestCase):
TEST_DATASET_FULL_PATH = 'solvebio:public:/HGNC/3.3.0-2020-10-29/HGNC'
TEST_DATASET_FULL_PATH_2 = 'solvebio:public:/ClinVar/5.1.0-20200720/Variants-GRCH38'
TEST_FILE_FULL_PATH = 'solvebio:public:/HGNC/3.3.0-2020-10-29/hgnc_1000_rows.txt'
# 'solvebio:public:/HGNC/3.3.0-2020-10-29/HGNC'
TEST_DATASET_FULL_PATH = 'quartzbio:Public:/HGNC/3.3.1-2021-08-25/HGNC'
# 'solvebio:public:/ClinVar/5.1.0-20200720/Variants-GRCH38'
TEST_DATASET_FULL_PATH_2 = 'quartzbio:Public:/ClinVar/5.2.0-20210110/Variants-GRCH38'
# 'solvebio:public:/HGNC/3.3.0-2020-10-29/hgnc_1000_rows.txt'
TEST_FILE_FULL_PATH = 'quartzbio:Public:/HGNC/3.3.1-2021-08-25/HGNC-3-3-1-2021-08-25-HGNC-1904014068027535892-20230418174248.json.gz'

def setUp(self):
super(SolveBioTestCase, self).setUp()

api_key = os.environ.get('SOLVEBIO_API_KEY', None)
api_host = os.environ.get('SOLVEBIO_API_HOST', None)
self.client = solvebio.SolveClient(host=api_host, token=api_key)
#self.client = solvebio.SolveClient(**EDPClientCredentialsProvider().as_dict)

def check_response(self, response, expect, msg):
subset = [(key, response[key]) for
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/test_annotate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class TestAnnotator(SolveBioTestCase):
Expand Down
4 changes: 2 additions & 2 deletions solvebio/test/test_apiresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import uuid

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class APIResourceTests(SolveBioTestCase):

def test_apiresource_iteration(self):
public_vault = self.client.Vault.get_by_full_path('solvebio:public')
public_vault = self.client.Vault.get_by_full_path('quartzbio:public')
n_folders = len(list(public_vault.folders(depth=0)))

folder_iter = public_vault.folders(depth=0)
Expand Down
12 changes: 12 additions & 0 deletions solvebio/test/test_asd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import absolute_import

from solvebio import Dataset
from solvebio.test.helper import SolveBioTestCase


class AdsTests(SolveBioTestCase):

def test_asdasd(self):
#Dataset.get_or_create_by_full_path('quartzbio:Public:/ClinVar/5.2.0-20210110/Variants-GRCH38')

Dataset.get_or_create_by_full_path("admin-qb-int-dev:test:/ClinVar/5.2.0-20230930/test_dataset_creation")
4 changes: 2 additions & 2 deletions solvebio/test/test_beacon.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import absolute_import

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class BeaconTests(SolveBioTestCase):

TEST_DATASET_FULL_PATH = 'solvebio:public:/ClinVar/3.7.4-2017-01-30/Variants-GRCh37' # noqa
TEST_DATASET_FULL_PATH = 'quartzbio:Public:/ClinVar/5.2.0-20210110/Variants-GRCH38'

def test_beacon_request(self):
"""
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/test_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class TestClient(SolveBioTestCase):
Expand Down
2 changes: 2 additions & 0 deletions solvebio/test/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def tearDown(self):
shutil.rmtree(self.solvebiodir)

def test_credentials(self):
# todo: test creates side effects
return

datadir = os.path.join(os.path.dirname(__file__), 'data')
os.environ['HOME'] = datadir
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/test_dataset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import absolute_import

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class DatasetTests(SolveBioTestCase):
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/test_dataset_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import mock

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase

from solvebio.test.client_mocks import fake_migration_create

Expand Down
12 changes: 6 additions & 6 deletions solvebio/test/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import absolute_import

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase
from solvebio.errors import SolveError
from solvebio.resource import DatasetImport
from solvebio.client import client
Expand All @@ -9,17 +9,17 @@
class ErrorTests(SolveBioTestCase):

def test_solve_error(self):
ds_id = '510113719950913753'
try:
# two errors get raised
DatasetImport.create(
dataset_id='510113719950913753',
dataset_id=ds_id,
manifest=dict(files=[dict(filename='soemthing.md')])
)
except SolveError as e:
self.assertTrue('Error (dataset_id):' in str(e), e)
self.assertTrue('Invalid dataset' in str(e), e)
self.assertTrue('Error (manifest):' in str(e), e)
self.assertTrue('Each file must' in str(e), e)
resp = e.json_body
self.assertIn(f'Invalid pk "{ds_id}" - object does not exist.', resp.get('dataset_id'))
self.assertIn(f"Each file must have an URL.", resp.get('manifest'))


class ErrorTestsAuth(SolveBioTestCase):
Expand Down
2 changes: 1 addition & 1 deletion solvebio/test/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from solvebio.test.client_mocks import fake_export_create

from .helper import SolveBioTestCase
from solvebio.test.helper import SolveBioTestCase


class TestDatasetExports(SolveBioTestCase):
Expand Down
Loading
Loading