Skip to content

Commit 4a713d3

Browse files
committed
Improve authentication and path management
Add an enumeration for all the possible path values. Allow username to be supplied and password to be prompted with getpass. Add support for GEMMA_USERNAME, GEMMA_PASSWORD and GEMMA_PASSWORD_CMD environment variables.
1 parent 5111665 commit 4a713d3

2 files changed

Lines changed: 131 additions & 27 deletions

File tree

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)