Skip to content

Commit aa902e6

Browse files
authored
Merge pull request #38 from PavlidisLab/feature-improved-auth-and-path
Improve authentication and path management
2 parents 5111665 + 4a713d3 commit aa902e6

File tree

2 files changed

+131
-27
lines changed

2 files changed

+131
-27
lines changed

gemmapy/gemmapy_api.py

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,105 @@
22
"""
33
Gemma python API (https://gemma.msl.ubc.ca/rest/v2/)
44
"""
5-
6-
from gemmapy import sdk
7-
from gemmapy import _processors as ps
8-
from gemmapy import _validators as vs
9-
from gemmapy import _subprocessors as sub
5+
import enum
6+
import json
7+
import logging
8+
import os
9+
import subprocess
10+
import warnings
11+
from getpass import getpass
12+
from io import StringIO
1013
from typing import Optional, List, Callable
11-
from pandas import DataFrame
12-
import pandas as pd
13-
import numpy as np
14+
1415
import anndata as ad
16+
import numpy as np
17+
import pandas as pd
1518
from anndata import AnnData
16-
from io import StringIO
17-
import warnings
18-
import json
19+
from pandas import DataFrame
20+
21+
from gemmapy import _processors as ps
22+
from gemmapy import _subprocessors as sub
23+
from gemmapy import _validators as vs
24+
from gemmapy import sdk
25+
26+
logger = logging.getLogger(__name__)
1927

28+
class GemmaPath(enum.Enum):
29+
PROD = "prod"
30+
DEV = "dev"
31+
STAGING = "staging"
2032

2133
class GemmaPy(object):
2234
"""
2335
Main API class
2436
"""
2537

26-
def __init__(self, auth:list|tuple=None, path="prod"):
38+
def __init__(self, auth: Optional[list | tuple] = None,
39+
path: Optional[GemmaPath | str] = None):
2740
"""
2841
:param list auth: (optional) A list or tuple of credential strings, e.g.
29-
(your_username, your_password)
30-
:param bool devel: (optional) If True development version of Gemma API will be
31-
used. Default is False.
42+
(your_username, your_password). Note that you may also define your Gemma
43+
credentials using `GEMMA_USERNAME` and `GEMMA_PASSWORD` environment
44+
variables. For a more secure approach, you can also provide a
45+
`GEMMA_PASSWORD_CMD` variable that produces your password
46+
(e.g. `pass gemma` using https://www.passwordstore.org/). If only a
47+
username is supplied, a password prompt will be used.
48+
:param str path: (optional) Override the path to use for the REST API.
49+
You may use one of the enumerated values in GemmaPath or a string.
50+
Three special values are recognized: "prod", "staging" and "dev",
51+
although only "prod" is publicly accessible. The default is the value
52+
from the OpenAPI specification used to generate the SDK, which is
53+
usually equivalent to PROD.
3254
"""
3355

3456
configuration = sdk.Configuration()
35-
if path == "prod":
36-
pass
37-
# configuration.host = 'https://gemma.msl.ubc.ca/rest/v2'
38-
elif path == 'dev':
57+
if path == GemmaPath.PROD or path == 'prod':
58+
logger.debug("Using production endpoint.")
59+
configuration.host = 'https://gemma.msl.ubc.ca/rest/v2'
60+
elif path == GemmaPath.DEV or path == 'dev':
3961
configuration.host = 'https://dev.gemma.msl.ubc.ca/rest/v2'
40-
elif path == 'staging':
62+
elif path == GemmaPath.STAGING or path == 'staging':
4163
configuration.host = "https://staging-gemma.msl.ubc.ca/rest/v2"
42-
else:
64+
elif path is not None:
4365
configuration.host = path
44-
66+
else:
67+
# use the default configuration in the openapi.json file
68+
pass
4569

4670
if auth is not None:
71+
if len(auth) != 1 and len(auth) != 2:
72+
raise ValueError(
73+
'There must be exactly one or two values in the auth parameter.')
4774
configuration.username = auth[0]
48-
configuration.password = auth[1]
75+
if len(auth) == 2:
76+
configuration.password = auth[1]
77+
else:
78+
configuration.password = getpass(
79+
f'Supply your password for {configuration.username}@{configuration.host}: ')
80+
elif os.environ.get('GEMMA_USERNAME'):
81+
logger.debug(
82+
'Reading username for %s from $GEMMA_USERNAME.',
83+
configuration.host)
84+
configuration.username = os.getenv('GEMMA_USERNAME')
85+
if os.getenv('GEMMA_PASSWORD'):
86+
logger.debug("Reading password for %s@%s from $GEMMA_PASSWORD.",
87+
configuration.username, configuration.host)
88+
configuration.password = os.getenv('GEMMA_PASSWORD')
89+
elif os.getenv('GEMMA_PASSWORD_CMD'):
90+
logger.debug(
91+
"Reading password for %s@%s from $GEMMA_PASSWORD_CMD (%s).",
92+
configuration.username, configuration.host,
93+
os.getenv('GEMMA_PASSWORD_CMD'))
94+
password = subprocess.run(os.getenv('GEMMA_PASSWORD_CMD'),
95+
shell=True, check=True,
96+
stdout=subprocess.PIPE,
97+
text=True).stdout
98+
configuration.password = password.splitlines()[0]
99+
else:
100+
logger.debug(
101+
'Could not read GEMMA_PASSWORD nor GEMMA_PASSWORD_CMD from environment, the password will be prompted.')
102+
configuration.password = getpass(
103+
f'Supply your password for {configuration.username}@{configuration.host}: ')
49104

50105
# create an instance of the API class
51106
self.raw = sdk.DefaultApi(sdk.ApiClient(configuration))

tests/test_basic.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
66
@author: omancarci
77
"""
8+
import os
9+
import time
10+
11+
import anndata as ad
12+
import pandas as pd
813
import pytest
14+
915
import gemmapy
10-
import pandas as pd
1116
from gemmapy import _subprocessors as sub
12-
import anndata as ad
13-
import time
14-
17+
from gemmapy.gemmapy_api import GemmaPath
1518

1619
api = gemmapy.GemmaPy()
1720

@@ -20,6 +23,52 @@ def slow_down_tests():
2023
yield
2124
time.sleep(1)
2225

26+
def test_path():
27+
client = gemmapy.GemmaPy(path=GemmaPath.PROD)
28+
assert client.raw.api_client.configuration.host == 'https://gemma.msl.ubc.ca/rest/v2'
29+
client = gemmapy.GemmaPy(path=GemmaPath.DEV)
30+
assert client.raw.api_client.configuration.host == 'https://dev.gemma.msl.ubc.ca/rest/v2'
31+
client = gemmapy.GemmaPy(path=GemmaPath.STAGING)
32+
assert client.raw.api_client.configuration.host == 'https://staging-gemma.msl.ubc.ca/rest/v2'
33+
client = gemmapy.GemmaPy(path='dev')
34+
assert client.raw.api_client.configuration.host == 'https://dev.gemma.msl.ubc.ca/rest/v2'
35+
client = gemmapy.GemmaPy(path='https://example.com/rest/v2')
36+
assert client.raw.api_client.configuration.host == 'https://example.com/rest/v2'
37+
38+
def test_auth(monkeypatch):
39+
monkeypatch.setitem(os.environ, 'GEMMA_USERNAME', '')
40+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD', '')
41+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD_CMD', '')
42+
43+
client = gemmapy.GemmaPy()
44+
assert client.raw.api_client.configuration.username == ''
45+
assert client.raw.api_client.configuration.password == ''
46+
47+
client = gemmapy.GemmaPy(auth=['foo', 'bar'])
48+
assert client.raw.api_client.configuration.username == 'foo'
49+
assert client.raw.api_client.configuration.password == 'bar'
50+
51+
with pytest.raises(OSError):
52+
gemmapy.GemmaPy(auth=('username',))
53+
54+
monkeypatch.setitem(os.environ, 'GEMMA_USERNAME', 'foo')
55+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD', 'bar')
56+
client = gemmapy.GemmaPy()
57+
assert client.raw.api_client.configuration.username == 'foo'
58+
assert client.raw.api_client.configuration.password == 'bar'
59+
60+
monkeypatch.setitem(os.environ, 'GEMMA_USERNAME', 'foo')
61+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD', '')
62+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD_CMD', 'echo 1234')
63+
client = gemmapy.GemmaPy()
64+
assert client.raw.api_client.configuration.username == 'foo'
65+
assert client.raw.api_client.configuration.password == '1234'
66+
67+
with pytest.raises(OSError):
68+
monkeypatch.setitem(os.environ, 'GEMMA_USERNAME', 'foo')
69+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD', '')
70+
monkeypatch.setitem(os.environ, 'GEMMA_PASSWORD_CMD', '')
71+
gemmapy.GemmaPy()
2372

2473
def test_get_result_sets():
2574
res = api.get_result_sets([200])

0 commit comments

Comments
 (0)