seer-pas-sdk 1.0.0__py3-none-any.whl → 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seer_pas_sdk/auth/auth.py +174 -15
- seer_pas_sdk/common/__init__.py +46 -5
- seer_pas_sdk/core/sdk.py +1474 -183
- seer_pas_sdk/core/unsupported.py +423 -230
- seer_pas_sdk/objects/__init__.py +1 -0
- seer_pas_sdk/objects/headers.py +144 -0
- seer_pas_sdk/objects/volcanoplot.py +3 -2
- {seer_pas_sdk-1.0.0.dist-info → seer_pas_sdk-1.1.1.dist-info}/METADATA +1 -2
- seer_pas_sdk-1.1.1.dist-info/RECORD +19 -0
- seer_pas_sdk-1.0.0.dist-info/RECORD +0 -18
- {seer_pas_sdk-1.0.0.dist-info → seer_pas_sdk-1.1.1.dist-info}/WHEEL +0 -0
- {seer_pas_sdk-1.0.0.dist-info → seer_pas_sdk-1.1.1.dist-info}/licenses/LICENSE.txt +0 -0
- {seer_pas_sdk-1.0.0.dist-info → seer_pas_sdk-1.1.1.dist-info}/top_level.txt +0 -0
seer_pas_sdk/auth/auth.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from datetime import datetime
|
|
1
3
|
import requests
|
|
2
4
|
import jwt
|
|
5
|
+
from ..common import get_version
|
|
6
|
+
from ..common.errors import ServerError
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
class Auth:
|
|
@@ -24,6 +28,7 @@ class Auth:
|
|
|
24
28
|
|
|
25
29
|
self.username = username
|
|
26
30
|
self.__password = password
|
|
31
|
+
self.version = get_version()
|
|
27
32
|
|
|
28
33
|
if instance not in Auth._instances:
|
|
29
34
|
if instance.startswith("https://") or instance.startswith(
|
|
@@ -35,7 +40,10 @@ class Auth:
|
|
|
35
40
|
raise ValueError("Invalid PAS instance.")
|
|
36
41
|
else:
|
|
37
42
|
self.url = Auth._instances[instance]
|
|
38
|
-
|
|
43
|
+
self.refresh_token = None
|
|
44
|
+
self.id_token = None
|
|
45
|
+
self.access_token = None
|
|
46
|
+
self.refresh_token_expiry = 0
|
|
39
47
|
self.instance = instance
|
|
40
48
|
|
|
41
49
|
# Null initialize multi tenant attributes
|
|
@@ -46,7 +54,9 @@ class Auth:
|
|
|
46
54
|
self.active_role,
|
|
47
55
|
) = [None] * 4
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
atexit.register(self._logout)
|
|
58
|
+
|
|
59
|
+
def _login(self):
|
|
50
60
|
"""
|
|
51
61
|
Logs into the PAS instance using the mapped URL and the login credentials (username and password) provided in the constructor.
|
|
52
62
|
|
|
@@ -55,7 +65,11 @@ class Auth:
|
|
|
55
65
|
dict
|
|
56
66
|
A dictionary containing the login response from the PAS instance.
|
|
57
67
|
"""
|
|
58
|
-
|
|
68
|
+
s = requests.Session()
|
|
69
|
+
s.headers.update(
|
|
70
|
+
{"x-seer-source": "sdk", "x-seer-id": f"{self.version}/login"}
|
|
71
|
+
)
|
|
72
|
+
response = s.post(
|
|
59
73
|
f"{self.url}auth/login",
|
|
60
74
|
json={"username": self.username, "password": self.__password},
|
|
61
75
|
)
|
|
@@ -65,35 +79,180 @@ class Auth:
|
|
|
65
79
|
"Check if the credentials are correct or if the backend is running or not."
|
|
66
80
|
)
|
|
67
81
|
|
|
82
|
+
try:
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ServerError("Could not login to the PAS instance")
|
|
86
|
+
|
|
87
|
+
res = response.json()
|
|
88
|
+
mfa_challenge = res.get("challenge", None)
|
|
89
|
+
if mfa_challenge:
|
|
90
|
+
mfa_args = {}
|
|
91
|
+
if any(
|
|
92
|
+
x not in res
|
|
93
|
+
for x in ["challenge", "session", "challengeParameters"]
|
|
94
|
+
):
|
|
95
|
+
raise ServerError(
|
|
96
|
+
"Unexpected return from server during MFA challenge. Please check the PAS SDK version and update the PAS SDK if necessary."
|
|
97
|
+
)
|
|
98
|
+
mfa_args["challengeName"] = res["challenge"]
|
|
99
|
+
mfa_args["session"] = res["session"]
|
|
100
|
+
mfa_args["username"] = res["challengeParameters"].get(
|
|
101
|
+
"USER_ID_FOR_SRP", ""
|
|
102
|
+
)
|
|
103
|
+
if not mfa_args["username"]:
|
|
104
|
+
raise ServerError(
|
|
105
|
+
"Unexpected return from server during MFA challenge. Please check the PAS SDK version and update the PAS SDK if necessary."
|
|
106
|
+
)
|
|
107
|
+
print(
|
|
108
|
+
"Multi-factor authentication (MFA) is enabled for your account."
|
|
109
|
+
)
|
|
110
|
+
mfa_code = input(
|
|
111
|
+
"Please enter the code from your authenticator app:"
|
|
112
|
+
)
|
|
113
|
+
mfa_response = s.post(
|
|
114
|
+
f"{self.url}auth/confirmMFA",
|
|
115
|
+
json={
|
|
116
|
+
"username": mfa_args["username"],
|
|
117
|
+
"mfaCode": mfa_code,
|
|
118
|
+
"challengeName": mfa_args["challengeName"],
|
|
119
|
+
"session": mfa_args["session"],
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
if mfa_response.status_code != 200:
|
|
123
|
+
raise ServerError(
|
|
124
|
+
"Could not confirm MFA for the PAS instance."
|
|
125
|
+
)
|
|
126
|
+
mfa_response = mfa_response.json()
|
|
127
|
+
if "AuthenticationResult" not in mfa_response:
|
|
128
|
+
raise ServerError(
|
|
129
|
+
"Unexpected return from server during MFA confirmation. Please check the PAS SDK version and update the PAS SDK if necessary."
|
|
130
|
+
)
|
|
131
|
+
mfa_response = mfa_response["AuthenticationResult"]
|
|
132
|
+
if any(
|
|
133
|
+
x not in mfa_response
|
|
134
|
+
for x in [
|
|
135
|
+
"AccessToken",
|
|
136
|
+
"IdToken",
|
|
137
|
+
"ExpiresIn",
|
|
138
|
+
"RefreshToken",
|
|
139
|
+
]
|
|
140
|
+
):
|
|
141
|
+
raise ServerError(
|
|
142
|
+
"Unexpected return from server during MFA confirmation. Please check the PAS SDK version and update the PAS SDK if necessary."
|
|
143
|
+
)
|
|
144
|
+
res["id_token"] = mfa_response["IdToken"]
|
|
145
|
+
res["access_token"] = mfa_response["AccessToken"]
|
|
146
|
+
res["expiresIn"] = mfa_response["ExpiresIn"]
|
|
147
|
+
res["refresh_token"] = mfa_response["RefreshToken"]
|
|
148
|
+
|
|
149
|
+
decoded_token = jwt.decode(
|
|
150
|
+
res["id_token"], options={"verify_signature": False}
|
|
151
|
+
)
|
|
152
|
+
self.base_tenant_id = decoded_token["custom:tenantId"]
|
|
153
|
+
self.base_role = decoded_token["custom:role"]
|
|
154
|
+
|
|
155
|
+
if not self.active_tenant_id:
|
|
156
|
+
self.active_tenant_id = self.base_tenant_id
|
|
157
|
+
|
|
158
|
+
if not self.active_role:
|
|
159
|
+
self.active_role = self.base_role
|
|
160
|
+
|
|
161
|
+
self.id_token = res["id_token"]
|
|
162
|
+
self.access_token = res["access_token"]
|
|
163
|
+
self.refresh_token = res.get("refresh_token", None)
|
|
164
|
+
self.token_expiry = int(datetime.now().timestamp()) + res.get(
|
|
165
|
+
"expiresIn", 0
|
|
166
|
+
)
|
|
167
|
+
return res["id_token"], res["access_token"]
|
|
168
|
+
|
|
169
|
+
def _logout(self):
|
|
170
|
+
if not self.has_valid_token():
|
|
171
|
+
return True
|
|
172
|
+
s = requests.Session()
|
|
173
|
+
s.headers.update(
|
|
174
|
+
{"x-seer-source": "sdk", "x-seer-id": f"{self.version}/logout"}
|
|
175
|
+
)
|
|
176
|
+
response = s.post(
|
|
177
|
+
f"{self.url}auth/logout",
|
|
178
|
+
json={
|
|
179
|
+
"username": self.username,
|
|
180
|
+
"refreshtoken": self.refresh_token,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
response.raise_for_status()
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise ServerError("Could not logout from the PAS instance")
|
|
187
|
+
self.refresh_token = None
|
|
188
|
+
self.token_expiry = 0
|
|
189
|
+
print(
|
|
190
|
+
f"User {self.username} logged out successfully. Thank you for using the PAS SDK."
|
|
191
|
+
)
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
def _refresh_token(self):
|
|
195
|
+
"""Refreshes the token using the refresh token.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ServerError: If the token could not be refreshed.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
dict: The response from the server containing the new token.
|
|
202
|
+
"""
|
|
203
|
+
s = requests.Session()
|
|
204
|
+
s.headers.update(
|
|
205
|
+
{
|
|
206
|
+
"x-seer-source": "sdk",
|
|
207
|
+
"x-seer-id": f"{self.version}/refresh_token",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
response = s.post(
|
|
211
|
+
f"{self.url}auth/refreshtoken",
|
|
212
|
+
json={"refreshtoken": self.refresh_token},
|
|
213
|
+
)
|
|
68
214
|
if response.status_code == 200:
|
|
69
215
|
return response.json()
|
|
216
|
+
else:
|
|
217
|
+
raise ServerError("Could not refresh token")
|
|
70
218
|
|
|
71
219
|
def get_token(self):
|
|
72
220
|
"""
|
|
73
|
-
Gets the token
|
|
221
|
+
Gets the current token. If the token is expired, it refreshes the token using the refresh token.
|
|
74
222
|
|
|
75
223
|
Returns
|
|
76
224
|
-------
|
|
77
225
|
str
|
|
78
226
|
The token from the login response.
|
|
79
227
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
228
|
+
if self.has_valid_token():
|
|
229
|
+
return self.id_token, self.access_token
|
|
230
|
+
else:
|
|
231
|
+
res = self._refresh_token()
|
|
82
232
|
|
|
83
233
|
if "id_token" not in res or "access_token" not in res:
|
|
84
234
|
raise ValueError(
|
|
85
235
|
"Check if the credentials are correct or if the backend is running or not."
|
|
86
236
|
)
|
|
87
|
-
|
|
88
|
-
|
|
237
|
+
self.token_expiry = int(datetime.now().timestamp()) + res.get(
|
|
238
|
+
"expiresIn", 0
|
|
89
239
|
)
|
|
90
|
-
self.base_tenant_id = decoded_token["custom:tenantId"]
|
|
91
|
-
self.base_role = decoded_token["custom:role"]
|
|
92
240
|
|
|
93
|
-
|
|
94
|
-
|
|
241
|
+
self.id_token = res["id_token"]
|
|
242
|
+
self.access_token = res["access_token"]
|
|
243
|
+
return res["id_token"], res["access_token"]
|
|
95
244
|
|
|
96
|
-
|
|
97
|
-
|
|
245
|
+
def has_valid_token(self):
|
|
246
|
+
"""
|
|
247
|
+
Check if the id and access tokens are valid.
|
|
98
248
|
|
|
99
|
-
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
bool
|
|
252
|
+
True if the refresh token is valid, False otherwise.
|
|
253
|
+
"""
|
|
254
|
+
return (
|
|
255
|
+
self.access_token is not None
|
|
256
|
+
and self.id_token is not None
|
|
257
|
+
and self.token_expiry > int(datetime.now().timestamp())
|
|
258
|
+
)
|
seer_pas_sdk/common/__init__.py
CHANGED
|
@@ -13,7 +13,8 @@ import json
|
|
|
13
13
|
import zipfile
|
|
14
14
|
import tempfile
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
import subprocess
|
|
17
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
17
18
|
|
|
18
19
|
from .groupanalysis import *
|
|
19
20
|
|
|
@@ -98,7 +99,7 @@ def dict_to_df(data):
|
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
# Most cases appear to be a .tsv file.
|
|
101
|
-
def
|
|
102
|
+
def download_df(url, is_tsv=True, dtype={}):
|
|
102
103
|
"""
|
|
103
104
|
Fetches a TSV/CSV file from a URL and returns as a Pandas DataFrame.
|
|
104
105
|
|
|
@@ -110,6 +111,9 @@ def url_to_df(url, is_tsv=True):
|
|
|
110
111
|
is_tsv : bool
|
|
111
112
|
True if the file is a TSV file, False if it is a CSV file.
|
|
112
113
|
|
|
114
|
+
dtype : dict
|
|
115
|
+
Data type conversion when intaking columns. e.g. {'a': str, 'b': np.float64}
|
|
116
|
+
|
|
113
117
|
Returns
|
|
114
118
|
-------
|
|
115
119
|
pandas.core.frame.DataFrame
|
|
@@ -122,7 +126,7 @@ def url_to_df(url, is_tsv=True):
|
|
|
122
126
|
|
|
123
127
|
Examples
|
|
124
128
|
--------
|
|
125
|
-
>>> csv =
|
|
129
|
+
>>> csv = download_df("link_to_csv_file")
|
|
126
130
|
>>> print(csv)
|
|
127
131
|
>>> Sample ID Sample name Well location MS file name
|
|
128
132
|
0 1 SampleName1 1 SDKTest1.raw
|
|
@@ -137,9 +141,9 @@ def url_to_df(url, is_tsv=True):
|
|
|
137
141
|
return pd.DataFrame()
|
|
138
142
|
url_content = io.StringIO(requests.get(url).content.decode("utf-8"))
|
|
139
143
|
if is_tsv:
|
|
140
|
-
csv = pd.read_csv(url_content, sep="\t")
|
|
144
|
+
csv = pd.read_csv(url_content, sep="\t", dtype=dtype)
|
|
141
145
|
else:
|
|
142
|
-
csv = pd.read_csv(url_content)
|
|
146
|
+
csv = pd.read_csv(url_content, dtype=dtype)
|
|
143
147
|
return csv
|
|
144
148
|
|
|
145
149
|
|
|
@@ -720,3 +724,40 @@ def rename_d_zip_file(source, destination):
|
|
|
720
724
|
zip_ref.write(file_path, arcname)
|
|
721
725
|
|
|
722
726
|
print(f"Renamed {source} to {destination}")
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def get_version():
|
|
730
|
+
"""
|
|
731
|
+
Returns the version of the seer-pas-sdk package.
|
|
732
|
+
"""
|
|
733
|
+
v = ""
|
|
734
|
+
try:
|
|
735
|
+
v = version("seer-pas-sdk")
|
|
736
|
+
v = "v" + v
|
|
737
|
+
if "dev" in v:
|
|
738
|
+
v = v[: v.index("dev") - 1].strip()
|
|
739
|
+
v += "-dev"
|
|
740
|
+
except Exception as e:
|
|
741
|
+
pass
|
|
742
|
+
|
|
743
|
+
if not v:
|
|
744
|
+
try:
|
|
745
|
+
v = subprocess.check_output(
|
|
746
|
+
["git", "describe", "--tags"], text=True
|
|
747
|
+
).strip()
|
|
748
|
+
if "-" in v:
|
|
749
|
+
v = v.split("-")[0]
|
|
750
|
+
v += "-dev"
|
|
751
|
+
except subprocess.CalledProcessError:
|
|
752
|
+
v = "unknown"
|
|
753
|
+
return f"{v}"
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def title_case_to_snake_case(s: str):
|
|
757
|
+
"""
|
|
758
|
+
Converts a title case string to snake case.
|
|
759
|
+
|
|
760
|
+
"""
|
|
761
|
+
s = s.replace(" ", "_").replace(".", "_").casefold()
|
|
762
|
+
|
|
763
|
+
return s
|