mascope-sdk 2025.5.16__tar.gz
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.
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mascope-sdk
|
|
3
|
+
Version: 2025.5.16
|
|
4
|
+
Summary: Mascope's public SDK library, wrapping the app's REST API.
|
|
5
|
+
Author: Oskari Kausiala, Philip Chernonog
|
|
6
|
+
Author-email: Oskari Kausiala <oskari.kausiala@karsa.fi>>, Philip Chernonog <philip.chernonog@karsa.fi>>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
9
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
10
|
+
Requires-Dist: requests>=2.32.3
|
|
11
|
+
Maintainer: Oskari Kausiala, Philip Chernonog
|
|
12
|
+
Maintainer-email: Oskari Kausiala <oskari.kausiala@karsa.fi>>, Philip Chernonog <philip.chernonog@karsa.fi>>
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Project-URL: Bug Tracker, https://github.com/Karsa-Oy/Mascope/issues
|
|
15
|
+
Project-URL: documentation, https://github.com/Karsa-Oy/Mascope
|
|
16
|
+
Project-URL: homepage, https://karsa.fi/
|
|
17
|
+
Project-URL: repository, https://github.com/Karsa-Oy/Mascope
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Mascope SDK
|
|
21
|
+
|
|
22
|
+
This library exposes a public Mascope Python SDK for end-users to leverage especially in Jupyter notebooks.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mascope_sdk"
|
|
3
|
+
version = "2025.05.16"
|
|
4
|
+
description = "Mascope's public SDK library, wrapping the app's REST API."
|
|
5
|
+
classifiers = [
|
|
6
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
7
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
8
|
+
]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Oskari Kausiala", email = "oskari.kausiala@karsa.fi>" },
|
|
11
|
+
{ name = "Philip Chernonog", email = "philip.chernonog@karsa.fi>" },
|
|
12
|
+
]
|
|
13
|
+
maintainers = [
|
|
14
|
+
{ name = "Oskari Kausiala", email = "oskari.kausiala@karsa.fi>" },
|
|
15
|
+
{ name = "Philip Chernonog", email = "philip.chernonog@karsa.fi>" },
|
|
16
|
+
]
|
|
17
|
+
license.text = "MIT"
|
|
18
|
+
readme = "README.md"
|
|
19
|
+
requires-python = ">=3.8"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.32.3",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
homepage = "https://karsa.fi/"
|
|
26
|
+
repository = "https://github.com/Karsa-Oy/Mascope"
|
|
27
|
+
documentation = "https://github.com/Karsa-Oy/Mascope"
|
|
28
|
+
"Bug Tracker" = "https://github.com/Karsa-Oy/Mascope/issues"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.6,<0.7"]
|
|
32
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import warnings
|
|
4
|
+
from requests.exceptions import HTTPError, Timeout, RequestException
|
|
5
|
+
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
6
|
+
|
|
7
|
+
# TODO add the message to every response in the fastapi, including success with number of results.
|
|
8
|
+
|
|
9
|
+
# Suppress only the InsecureRequestWarning from requests
|
|
10
|
+
warnings.simplefilter("ignore", InsecureRequestWarning)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def api_get(url: str, path: str, access_token: str, params: dict = None):
|
|
14
|
+
"""
|
|
15
|
+
Send a GET request to the specified API endpoint with optional query parameters.
|
|
16
|
+
|
|
17
|
+
:param url: The base URL of the server.
|
|
18
|
+
:type url: str
|
|
19
|
+
:param path: The specific API path to be appended to the base URL.
|
|
20
|
+
:type path: str
|
|
21
|
+
:param access_token: Authorization token for API access
|
|
22
|
+
:type access_token: str
|
|
23
|
+
:param params: A dictionary of query parameters to include in the request.
|
|
24
|
+
:type params: dict, optional
|
|
25
|
+
:return: The response object if the request was successful, otherwise None.
|
|
26
|
+
:rtype: requests.Response or None
|
|
27
|
+
"""
|
|
28
|
+
full_url = url + "/api/" + path
|
|
29
|
+
try:
|
|
30
|
+
headers = {
|
|
31
|
+
"Authorization": f"Bearer {access_token}",
|
|
32
|
+
"X-Service-Name": "mascope_sdk",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Send GET request with query parameters (if provided)
|
|
36
|
+
resp = requests.get(
|
|
37
|
+
full_url, params=params, headers=headers, verify=False, timeout=30
|
|
38
|
+
)
|
|
39
|
+
resp.raise_for_status() # Raise HTTPError for bad responses
|
|
40
|
+
message = json.loads(resp.content).get("message", None)
|
|
41
|
+
if message is not None:
|
|
42
|
+
print(message)
|
|
43
|
+
except HTTPError as http_err:
|
|
44
|
+
if resp.status_code == 401 or resp.status_code == 403:
|
|
45
|
+
response = json.loads(resp.content)
|
|
46
|
+
error_message = response.get("detail", {}).get("error_message", None)
|
|
47
|
+
print(f"{error_message} Please check your API token.")
|
|
48
|
+
else:
|
|
49
|
+
try:
|
|
50
|
+
error_message = (
|
|
51
|
+
json.loads(resp.content)
|
|
52
|
+
.get("detail", {})
|
|
53
|
+
.get(
|
|
54
|
+
"error_message",
|
|
55
|
+
"No additional error information from the server.",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
error_message = "Failed to decode error message from server response."
|
|
60
|
+
print(
|
|
61
|
+
f"HTTP error: Unable to retrieve data from {full_url}. \nDetails: {http_err} \nServer message: {error_message}"
|
|
62
|
+
)
|
|
63
|
+
return None
|
|
64
|
+
except Timeout:
|
|
65
|
+
print(f"Timeout error: The request to {full_url} timed out.")
|
|
66
|
+
return None
|
|
67
|
+
except RequestException as req_err:
|
|
68
|
+
print(
|
|
69
|
+
f"Connection error: Could not connect to {full_url}. Please check the URL and your network connection. \nDetails: {req_err}"
|
|
70
|
+
)
|
|
71
|
+
return None
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(
|
|
74
|
+
f"Error: An unexpected error occurred while trying to reach {full_url}. \nDetails: {str(e)}"
|
|
75
|
+
)
|
|
76
|
+
return None
|
|
77
|
+
return resp
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def api_post(url: str, path: str, access_token: str, data: dict):
|
|
81
|
+
"""Send a POST request to the specified API endpoint with provided data.
|
|
82
|
+
|
|
83
|
+
:param url: The base URL of the server.
|
|
84
|
+
:type url: str
|
|
85
|
+
:param path: The specific API path to be appended to the base URL.
|
|
86
|
+
:type path: str
|
|
87
|
+
:param access_token: Authorization token for API access
|
|
88
|
+
:type access_token: str
|
|
89
|
+
:param data: The data payload to send in the POST request.
|
|
90
|
+
:type data: dict
|
|
91
|
+
:return: The response object if the request was successful, otherwise None.
|
|
92
|
+
:rtype: requests.Response or None
|
|
93
|
+
"""
|
|
94
|
+
full_url = url + "/api/" + path
|
|
95
|
+
try:
|
|
96
|
+
headers = {
|
|
97
|
+
"Authorization": f"Bearer {access_token}",
|
|
98
|
+
"X-Service-Name": "mascope_sdk",
|
|
99
|
+
}
|
|
100
|
+
resp = requests.post(
|
|
101
|
+
full_url, data=json.dumps(data), headers=headers, verify=False, timeout=30
|
|
102
|
+
)
|
|
103
|
+
resp.raise_for_status() # Raise HTTPError for bad responses
|
|
104
|
+
message = json.loads(resp.content).get("message", None)
|
|
105
|
+
if message is not None:
|
|
106
|
+
print(message)
|
|
107
|
+
except HTTPError as http_err:
|
|
108
|
+
if resp.status_code == 401 or resp.status_code == 403:
|
|
109
|
+
response = json.loads(resp.content)
|
|
110
|
+
error_message = response.get("detail", {}).get("error_message", None)
|
|
111
|
+
print(f"{error_message} Please check your API token.")
|
|
112
|
+
else:
|
|
113
|
+
try:
|
|
114
|
+
error_message = (
|
|
115
|
+
json.loads(resp.content)
|
|
116
|
+
.get("detail", {})
|
|
117
|
+
.get(
|
|
118
|
+
"error_message",
|
|
119
|
+
"No additional error information from the server.",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
error_message = "Failed to decode error message from server response."
|
|
124
|
+
print(
|
|
125
|
+
f"HTTP error: Unable to retrieve data from {full_url}. \nDetails: {http_err} \nServer message: {error_message}"
|
|
126
|
+
)
|
|
127
|
+
return None
|
|
128
|
+
except Timeout:
|
|
129
|
+
print(f"Timeout error: The request to {full_url} timed out.")
|
|
130
|
+
return None
|
|
131
|
+
except RequestException as req_err:
|
|
132
|
+
print(
|
|
133
|
+
f"Connection error: Could not connect to {full_url}. Please check the URL and your network connection. \nDetails: {req_err}"
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(
|
|
138
|
+
f"Error: An unexpected error occurred while trying to reach {full_url}. \nDetails: {str(e)}"
|
|
139
|
+
)
|
|
140
|
+
return None
|
|
141
|
+
return resp
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def api_post_file(
|
|
145
|
+
url: str,
|
|
146
|
+
path: str,
|
|
147
|
+
access_token: str,
|
|
148
|
+
filepath: str,
|
|
149
|
+
service_name: str = "mascope_sdk",
|
|
150
|
+
):
|
|
151
|
+
"""Send a POST request to the specified API endpoint with a path file to be uploaded.
|
|
152
|
+
|
|
153
|
+
:param url: The base URL of the server.
|
|
154
|
+
:type url: str
|
|
155
|
+
:param path: The specific API path to be appended to the base URL.
|
|
156
|
+
:type path: str
|
|
157
|
+
:param access_token: Authorization token for API access
|
|
158
|
+
:type access_token: str
|
|
159
|
+
:param filepath: Path to the file to be uploaded
|
|
160
|
+
:type filepath: str
|
|
161
|
+
:param service_name: The name of the service making the request, defaults to "mascope_sdk".
|
|
162
|
+
:type service_name: str, optional
|
|
163
|
+
:return: The response object if the request was successful, otherwise None.
|
|
164
|
+
:rtype: requests.Response or None
|
|
165
|
+
"""
|
|
166
|
+
full_url = url + "/api/" + path
|
|
167
|
+
try:
|
|
168
|
+
headers = {
|
|
169
|
+
"Authorization": f"Bearer {access_token}",
|
|
170
|
+
"X-Service-Name": service_name,
|
|
171
|
+
}
|
|
172
|
+
with open(filepath, "rb") as file:
|
|
173
|
+
resp = requests.post(
|
|
174
|
+
full_url,
|
|
175
|
+
files={"file": file},
|
|
176
|
+
headers=headers,
|
|
177
|
+
verify=False,
|
|
178
|
+
timeout=30,
|
|
179
|
+
)
|
|
180
|
+
resp.raise_for_status() # Raise HTTPError for bad responses
|
|
181
|
+
message = json.loads(resp.content).get("message", None)
|
|
182
|
+
if message is not None:
|
|
183
|
+
print(message)
|
|
184
|
+
except HTTPError as http_err:
|
|
185
|
+
if resp.status_code == 401 or resp.status_code == 403:
|
|
186
|
+
response = json.loads(resp.content)
|
|
187
|
+
error_message = response.get("detail", {}).get("error_message", None)
|
|
188
|
+
print(f"{error_message} Please check your API token.")
|
|
189
|
+
else:
|
|
190
|
+
try:
|
|
191
|
+
error_message = (
|
|
192
|
+
json.loads(resp.content)
|
|
193
|
+
.get("detail", {})
|
|
194
|
+
.get(
|
|
195
|
+
"error_message",
|
|
196
|
+
"No additional error information from the server.",
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
except json.JSONDecodeError:
|
|
200
|
+
error_message = "Failed to decode error message from server response."
|
|
201
|
+
print(
|
|
202
|
+
f"HTTP error: Unable to retrieve data from {full_url}. \nDetails: {http_err} \nServer message: {error_message}"
|
|
203
|
+
)
|
|
204
|
+
return None
|
|
205
|
+
except Timeout:
|
|
206
|
+
print(f"Timeout error: The request to {full_url} timed out.")
|
|
207
|
+
return None
|
|
208
|
+
except RequestException as req_err:
|
|
209
|
+
print(
|
|
210
|
+
f"Connection error: Could not connect to {full_url}. Please check the URL and your network connection. \nDetails: {req_err}"
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(
|
|
215
|
+
f"Error: An unexpected error occurred while trying to reach {full_url}. \nDetails: {str(e)}"
|
|
216
|
+
)
|
|
217
|
+
return None
|
|
218
|
+
return resp
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
################
|
|
222
|
+
# Workspaces API
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_workspaces(mascope_url: str, access_token: str) -> list:
|
|
226
|
+
"""Get Mascope workspaces from a URL
|
|
227
|
+
|
|
228
|
+
:param mascope_url: Mascope URL
|
|
229
|
+
:type mascope_url: str
|
|
230
|
+
:param access_token: Authorization token for API access
|
|
231
|
+
:type access_token: str
|
|
232
|
+
:return: List of workspace dictionaries.
|
|
233
|
+
:rtype: list
|
|
234
|
+
"""
|
|
235
|
+
resp = api_get(url=mascope_url, path="workspaces", access_token=access_token)
|
|
236
|
+
# Check if the request was successful
|
|
237
|
+
if not resp:
|
|
238
|
+
print(
|
|
239
|
+
f"Failed to retrieve workspaces from {mascope_url}. Please check the URL and try again."
|
|
240
|
+
)
|
|
241
|
+
return []
|
|
242
|
+
|
|
243
|
+
content = json.loads(resp.content)
|
|
244
|
+
workspaces = content.get("data", [])
|
|
245
|
+
if not workspaces:
|
|
246
|
+
print("No workspaces found. Please create a new workspace.")
|
|
247
|
+
|
|
248
|
+
return workspaces
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
####################
|
|
252
|
+
# Sample batches API
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_sample_batches(mascope_url: str, access_token: str, workspace_id: str) -> list:
|
|
256
|
+
"""
|
|
257
|
+
Get Mascope sample batches of a workspace.
|
|
258
|
+
|
|
259
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
260
|
+
:type mascope_url: str
|
|
261
|
+
:param access_token: Authorization token for API access
|
|
262
|
+
:type access_token: str
|
|
263
|
+
:param workspace_id: The ID of the workspace from which to retrieve sample batches.
|
|
264
|
+
:type workspace_id: str
|
|
265
|
+
:return: A list of sample batch dictionaries.
|
|
266
|
+
Returns an empty list if no sample batches are found or if an error occurs.
|
|
267
|
+
:rtype: list
|
|
268
|
+
"""
|
|
269
|
+
# Prepare query parameters
|
|
270
|
+
query_params = {"workspace_id": workspace_id}
|
|
271
|
+
|
|
272
|
+
# Perform the GET request with query parameters
|
|
273
|
+
resp = api_get(
|
|
274
|
+
url=mascope_url,
|
|
275
|
+
path="sample/batches",
|
|
276
|
+
access_token=access_token,
|
|
277
|
+
params=query_params,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check if the request was successful
|
|
281
|
+
if not resp:
|
|
282
|
+
print(
|
|
283
|
+
f"Failed to retrieve sample batches from {mascope_url}. Please check the URL and try again."
|
|
284
|
+
)
|
|
285
|
+
return []
|
|
286
|
+
|
|
287
|
+
content = json.loads(resp.content)
|
|
288
|
+
batches = content.get("data", [])
|
|
289
|
+
|
|
290
|
+
if not batches:
|
|
291
|
+
print("No sample batches found. Please create a new sample batch.")
|
|
292
|
+
|
|
293
|
+
return batches
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_sample_batch_data(
|
|
297
|
+
mascope_url: str,
|
|
298
|
+
access_token: str,
|
|
299
|
+
sample_batch_id: str,
|
|
300
|
+
) -> dict:
|
|
301
|
+
"""
|
|
302
|
+
Retrieve detailed data for all samples in a sample batch.
|
|
303
|
+
|
|
304
|
+
This function interacts with the Mascope API to fetch comprehensive data
|
|
305
|
+
for a given sample batch. It retrieves data for samples and combinned match/targets data
|
|
306
|
+
for compounds, ions, isotopes and interferences (included to isotopes).
|
|
307
|
+
|
|
308
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
309
|
+
:type mascope_url: str
|
|
310
|
+
:param access_token: Authorization token for API access
|
|
311
|
+
:type access_token: str
|
|
312
|
+
:param sample_batch_id: The ID of the sample batch to retrieve data for.
|
|
313
|
+
:type sample_batch_id: str
|
|
314
|
+
:return: A dictionary containing:
|
|
315
|
+
- `result`: Summary statistics about the retrieved data.
|
|
316
|
+
- `sample_batch`: Information about the sample batch.
|
|
317
|
+
- `samples`: A list of samples within the batch. Combination of samples (sample_item + sample_file) and match_samples
|
|
318
|
+
- `compounds`: Data for compounds. Combination of match_compounds and target_compounds
|
|
319
|
+
- `ions`: Data for ions. Combination of match_ions and target_ions
|
|
320
|
+
- `isotopes`: Data for isotopes. Combination of match_isotopes, match_interferences, and target_isotopes
|
|
321
|
+
Returns an empty dictionary if the request fails or no data is found.
|
|
322
|
+
:rtype: dict
|
|
323
|
+
"""
|
|
324
|
+
# Step 1: Call the API to get the batch data (stored in database)
|
|
325
|
+
resp = api_get(
|
|
326
|
+
url=mascope_url,
|
|
327
|
+
path=f"match/targets/batch/{sample_batch_id}",
|
|
328
|
+
access_token=access_token,
|
|
329
|
+
)
|
|
330
|
+
if not resp:
|
|
331
|
+
print(
|
|
332
|
+
f"Failed to retrieve match data for sample batch with ID {sample_batch_id}."
|
|
333
|
+
)
|
|
334
|
+
return {}
|
|
335
|
+
|
|
336
|
+
# Step 2: Parse the response content
|
|
337
|
+
batch_data = json.loads(resp.content)
|
|
338
|
+
if not batch_data:
|
|
339
|
+
print(f"No data returned for sample batch with ID {sample_batch_id}.")
|
|
340
|
+
return {}
|
|
341
|
+
|
|
342
|
+
# Step 3: Extract relevant information from the aggregate match data
|
|
343
|
+
result = batch_data.get("result", {})
|
|
344
|
+
sample_batch = batch_data.get("data", {}).get("sample_batch", {})
|
|
345
|
+
samples = batch_data.get("data", {}).get("samples", [])
|
|
346
|
+
compounds = batch_data.get("data", {}).get("compounds", [])
|
|
347
|
+
ions = batch_data.get("data", {}).get("ions", [])
|
|
348
|
+
isotopes = batch_data.get("data", {}).get("isotopes", [])
|
|
349
|
+
|
|
350
|
+
# Step 4: Build the response structure
|
|
351
|
+
response = {
|
|
352
|
+
"result": result,
|
|
353
|
+
"sample_batch": sample_batch,
|
|
354
|
+
"samples": samples,
|
|
355
|
+
"compounds": compounds,
|
|
356
|
+
"ions": ions,
|
|
357
|
+
"isotopes": isotopes,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return response
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
#############
|
|
364
|
+
# Samples API
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def get_samples(mascope_url: str, access_token: str, sample_batch_id: str) -> list:
|
|
368
|
+
"""
|
|
369
|
+
Get Mascope samples of the specified sample batch.
|
|
370
|
+
|
|
371
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
372
|
+
:type mascope_url: str
|
|
373
|
+
:param access_token: Authorization token for API access
|
|
374
|
+
:type access_token: str
|
|
375
|
+
:param sample_batch_id: The ID of the sample batch from which to retrieve samples.
|
|
376
|
+
:type sample_batch_id: str
|
|
377
|
+
:return: A list of sample dictionaries.
|
|
378
|
+
Returns an empty list if no samples are found or if an error occurs.
|
|
379
|
+
:rtype: list
|
|
380
|
+
"""
|
|
381
|
+
# Prepare query parameters
|
|
382
|
+
query_params = {"sample_batch_id": sample_batch_id}
|
|
383
|
+
|
|
384
|
+
# Perform the GET request with query parameters
|
|
385
|
+
resp = api_get(
|
|
386
|
+
url=mascope_url, path="samples", access_token=access_token, params=query_params
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Check if the API request was successful
|
|
390
|
+
if not resp:
|
|
391
|
+
print(
|
|
392
|
+
f"Failed to retrieve samples from {mascope_url}. Please check the URL and try again."
|
|
393
|
+
)
|
|
394
|
+
return []
|
|
395
|
+
|
|
396
|
+
content = json.loads(resp.content)
|
|
397
|
+
samples = content.get("data", [])
|
|
398
|
+
if not samples:
|
|
399
|
+
print(f"No samples found for sample batch with ID {sample_batch_id}.")
|
|
400
|
+
|
|
401
|
+
return samples
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def get_sample(mascope_url: str, access_token: str, sample_item_id: str) -> dict:
|
|
405
|
+
"""
|
|
406
|
+
Get details of a specific sample by its ID.
|
|
407
|
+
|
|
408
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
409
|
+
:type mascope_url: str
|
|
410
|
+
:param access_token: Authorization token for API access
|
|
411
|
+
:type access_token: str
|
|
412
|
+
:param sample_item_id: The ID of the sample item to retrieve.
|
|
413
|
+
:type sample_item_id: str
|
|
414
|
+
:return: The response dictionary containing the sample details, or None if an error occurs.
|
|
415
|
+
:rtype: dict
|
|
416
|
+
"""
|
|
417
|
+
resp = api_get(
|
|
418
|
+
url=mascope_url,
|
|
419
|
+
path=f"samples/{sample_item_id}",
|
|
420
|
+
access_token=access_token,
|
|
421
|
+
)
|
|
422
|
+
if not resp:
|
|
423
|
+
print(f"Failed to retrieve sample details from {mascope_url}.")
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
sample = json.loads(resp.content)
|
|
427
|
+
if not sample:
|
|
428
|
+
print(f"No sample with ID {sample_item_id} found.")
|
|
429
|
+
return sample
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def get_sample_compound_matches(
|
|
433
|
+
mascope_url: str,
|
|
434
|
+
access_token: str,
|
|
435
|
+
sample_item_id: str,
|
|
436
|
+
target_compound_formula: str,
|
|
437
|
+
target_compound_name: str = "Unknown Compound",
|
|
438
|
+
match_params: dict = None,
|
|
439
|
+
) -> dict:
|
|
440
|
+
"""
|
|
441
|
+
Retrieves matches for compounds within a sample based on a target compound formula,
|
|
442
|
+
applying specified filter parameters to filter the matches.
|
|
443
|
+
|
|
444
|
+
:param mascope_url: Base URL of the Mascope API.
|
|
445
|
+
:type mascope_url: str
|
|
446
|
+
:param access_token: Authorization token for API access
|
|
447
|
+
:type access_token: str
|
|
448
|
+
:param sample_item_id: Unique identifier of the sample item to analyze.
|
|
449
|
+
:type sample_item_id: str
|
|
450
|
+
:param target_compound_formula: Chemical formula of the target compound.
|
|
451
|
+
:type target_compound_formula: str
|
|
452
|
+
:param target_compound_name: The name of the target compound, defaults to "Unknown Compound"
|
|
453
|
+
:type target_compound_name: str, optional
|
|
454
|
+
:param match_params: Parameters to filter the match results, affecting which matches are considered significant.
|
|
455
|
+
Should be a dictionary representing a MatchParams Pydantic model.
|
|
456
|
+
:type match_params: dict, optional
|
|
457
|
+
:return: A dictionary containing the match data (compound->ions->isotopes).
|
|
458
|
+
Returns None if no match data is found or if an error occurs.
|
|
459
|
+
:rtype: dict
|
|
460
|
+
|
|
461
|
+
Example of target compound and filter parameters data:
|
|
462
|
+
"target_compound_formula": "C6H12N2O6",
|
|
463
|
+
"target_compound_name": "Formic acid", # compound name is optional
|
|
464
|
+
"match_params": {
|
|
465
|
+
"mz_tolerance": 72,
|
|
466
|
+
"isotope_ratio_tolerance": 0.2,
|
|
467
|
+
"peak_min_intensity": 0.0,
|
|
468
|
+
"min_isotope_abundance": 0.15,
|
|
469
|
+
"min_isotope_correlation": 0.7,
|
|
470
|
+
"probable_match_threshold": 0.8,
|
|
471
|
+
"possible_match_threshold": 0.4,
|
|
472
|
+
}
|
|
473
|
+
"""
|
|
474
|
+
# Construct the request body
|
|
475
|
+
body = {
|
|
476
|
+
"target_compound": {
|
|
477
|
+
"target_compound_formula": target_compound_formula,
|
|
478
|
+
"target_compound_name": target_compound_name,
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if match_params is not None:
|
|
482
|
+
body["match_params"] = match_params
|
|
483
|
+
|
|
484
|
+
# Make the POST request for the specified sample
|
|
485
|
+
resp = api_post(
|
|
486
|
+
url=mascope_url,
|
|
487
|
+
path=f"match/aggregate/sample/{sample_item_id}/compound",
|
|
488
|
+
access_token=access_token,
|
|
489
|
+
data=body,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Check if the API request was successful
|
|
493
|
+
if not resp:
|
|
494
|
+
print(
|
|
495
|
+
f"Failed to retrieve compound '{target_compound_formula}' match data for for sample item ID {sample_item_id} from {mascope_url}."
|
|
496
|
+
)
|
|
497
|
+
return None
|
|
498
|
+
|
|
499
|
+
# Parse the content of the response
|
|
500
|
+
response_json = resp.json()
|
|
501
|
+
match_data = response_json.get("data", None)
|
|
502
|
+
|
|
503
|
+
if not match_data:
|
|
504
|
+
print(
|
|
505
|
+
f"No compound matches found for sample item ID {sample_item_id} and target compound {target_compound_formula}."
|
|
506
|
+
)
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
return match_data
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
##################
|
|
513
|
+
# Sample files API
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def get_sample_file_peaks(
|
|
517
|
+
mascope_url: str,
|
|
518
|
+
access_token: str,
|
|
519
|
+
sample_file_id: str,
|
|
520
|
+
areas: bool = True,
|
|
521
|
+
heights: bool = True,
|
|
522
|
+
) -> dict:
|
|
523
|
+
"""
|
|
524
|
+
Get peaks of a given sample file, with options to include areas and/or heights.
|
|
525
|
+
|
|
526
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
527
|
+
:type mascope_url: str
|
|
528
|
+
:param access_token: Authorization token for API access
|
|
529
|
+
:type access_token: str
|
|
530
|
+
:param sample_file_id: The ID of the sample file from which to retrieve peaks.
|
|
531
|
+
:type sample_file_id: str
|
|
532
|
+
:param areas: If True, include peak areas in the response, defaults to True.
|
|
533
|
+
:type areas: bool, optional
|
|
534
|
+
:param heights: If True, include peak heights in the response, defaults to True.
|
|
535
|
+
:type heights: bool, optional
|
|
536
|
+
:return: A dictionary with keys:
|
|
537
|
+
- "mz": list of m/z values of the peaks in the sample file
|
|
538
|
+
- "area": list of peak areas (if requested)
|
|
539
|
+
- "height": list of peak heights (if requested)
|
|
540
|
+
Returns None if no peaks are found or if an error occurs.
|
|
541
|
+
:rtype: dict or None
|
|
542
|
+
"""
|
|
543
|
+
# Prepare query parameters for areas and heights
|
|
544
|
+
query_params = {
|
|
545
|
+
"areas": str(areas).lower(), # Convert bool to string (lowercase)
|
|
546
|
+
"heights": str(heights).lower(), # Convert bool to string (lowercase)
|
|
547
|
+
}
|
|
548
|
+
# Make API request with query parameters
|
|
549
|
+
resp = api_get(
|
|
550
|
+
url=mascope_url,
|
|
551
|
+
path=f"sample/files/{sample_file_id}/peaks",
|
|
552
|
+
access_token=access_token,
|
|
553
|
+
params=query_params,
|
|
554
|
+
)
|
|
555
|
+
# Check if the API request was successful
|
|
556
|
+
if not resp:
|
|
557
|
+
print(
|
|
558
|
+
f"Failed to retrieve peaks for sample file with ID {sample_file_id} from {mascope_url}."
|
|
559
|
+
)
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
# Parse the content of the response
|
|
563
|
+
content = json.loads(resp.content)
|
|
564
|
+
peaks_data = content.get("data", None)
|
|
565
|
+
|
|
566
|
+
if not peaks_data:
|
|
567
|
+
print(f"No peaks found for sample file with ID {sample_file_id}.")
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
# Return the peaks data
|
|
571
|
+
return peaks_data
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def get_sample_file_peak_noise(
|
|
575
|
+
mascope_url: str,
|
|
576
|
+
access_token: str,
|
|
577
|
+
sample_file_id: str,
|
|
578
|
+
mzs: list[float],
|
|
579
|
+
t_min: float = None,
|
|
580
|
+
t_max: float = None,
|
|
581
|
+
ppm: int = 1,
|
|
582
|
+
polarity: str = None,
|
|
583
|
+
) -> dict:
|
|
584
|
+
"""
|
|
585
|
+
Get noise values for given peak m/z values in a sample file.
|
|
586
|
+
|
|
587
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
588
|
+
:type mascope_url: str
|
|
589
|
+
:param access_token: Authorization token for API access
|
|
590
|
+
:type access_token: str
|
|
591
|
+
:param sample_file_id: The ID of the sample file.
|
|
592
|
+
:type sample_file_id: str
|
|
593
|
+
:param mzs: List of peak m/z values to compute noise for.
|
|
594
|
+
:type mzs: list[float]
|
|
595
|
+
:param t_min: Start time (optional).
|
|
596
|
+
:type t_min: float, optional
|
|
597
|
+
:param t_max: End time (optional).
|
|
598
|
+
:type t_max: float, optional
|
|
599
|
+
:param ppm: ppm precision for binning, defaults to 1.
|
|
600
|
+
:type ppm: int, optional
|
|
601
|
+
:param polarity: Polarity of the scans, "+" or "-", optional.
|
|
602
|
+
:type polarity: str, optional
|
|
603
|
+
:return: Dictionary with m/z values and corresponding noise values, or None if request fails.
|
|
604
|
+
:rtype: dict or None
|
|
605
|
+
"""
|
|
606
|
+
body = {
|
|
607
|
+
"mzs": mzs,
|
|
608
|
+
"t_min": t_min,
|
|
609
|
+
"t_max": t_max,
|
|
610
|
+
"ppm": ppm,
|
|
611
|
+
"polarity": polarity,
|
|
612
|
+
}
|
|
613
|
+
# Remove None values from body
|
|
614
|
+
body = {k: v for k, v in body.items() if v is not None}
|
|
615
|
+
|
|
616
|
+
resp = api_post(
|
|
617
|
+
url=mascope_url,
|
|
618
|
+
path=f"sample/files/{sample_file_id}/peaks/noise",
|
|
619
|
+
access_token=access_token,
|
|
620
|
+
data=body,
|
|
621
|
+
)
|
|
622
|
+
if not resp:
|
|
623
|
+
print(
|
|
624
|
+
f"Failed to retrieve peak noise data for sample file with ID {sample_file_id} from {mascope_url}."
|
|
625
|
+
)
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
content = resp.json()
|
|
629
|
+
peak_noise_data = content.get("data", None)
|
|
630
|
+
|
|
631
|
+
if not peak_noise_data:
|
|
632
|
+
print(f"No peak noise data found for sample file with ID {sample_file_id}.")
|
|
633
|
+
return None
|
|
634
|
+
|
|
635
|
+
return peak_noise_data
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def get_sample_file_peak_timeseries(
|
|
639
|
+
mascope_url: str,
|
|
640
|
+
access_token: str,
|
|
641
|
+
sample_file_id: str,
|
|
642
|
+
peak_mz: float,
|
|
643
|
+
peak_mz_tolerance_ppm: float = None,
|
|
644
|
+
) -> dict:
|
|
645
|
+
"""Get timeseries data for the specified peak of the sample file from the Mascope API.
|
|
646
|
+
|
|
647
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
648
|
+
:type mascope_url: str
|
|
649
|
+
:param access_token: Authorization token for API access
|
|
650
|
+
:type access_token: str
|
|
651
|
+
:param sample_file_id: The ID of the sample file from which to retrieve peak timeseries data.
|
|
652
|
+
:type sample_file_id: str
|
|
653
|
+
:param peak_mz: The m/z of the peak to request timeseries for.
|
|
654
|
+
:type peak_mz: float
|
|
655
|
+
:param peak_mz_tolerance_ppm: The m/z tolerance within which the peak should be compared (ppm), defaults to None.
|
|
656
|
+
:type peak_mz_tolerance_ppm: float, optional
|
|
657
|
+
:return: A dictionary with keys:
|
|
658
|
+
- "mz": m/z of the peak in sample file (None if no peak within tolerance)
|
|
659
|
+
- "height": list of peak intensity at time points (empty if no peak within tolerance)
|
|
660
|
+
- "time": list of time coordinates (empty if no peak within tolerance)
|
|
661
|
+
Returns None if no timeseries data is found or if an error occurs.
|
|
662
|
+
:rtype: dict or None
|
|
663
|
+
"""
|
|
664
|
+
# Prepare the payload for the POST request
|
|
665
|
+
body = (
|
|
666
|
+
{"peak_mz": peak_mz, "peak_mz_tolerance_ppm": peak_mz_tolerance_ppm}
|
|
667
|
+
if peak_mz_tolerance_ppm is not None
|
|
668
|
+
else {"peak_mz": peak_mz}
|
|
669
|
+
)
|
|
670
|
+
resp = api_post(
|
|
671
|
+
url=mascope_url,
|
|
672
|
+
path=f"sample/files/{sample_file_id}/peaks/timeseries",
|
|
673
|
+
access_token=access_token,
|
|
674
|
+
data=body,
|
|
675
|
+
)
|
|
676
|
+
# Check if the API request was successful
|
|
677
|
+
if not resp:
|
|
678
|
+
print(
|
|
679
|
+
f"Failed to retrieve peak timeseries data from {mascope_url} for file ID {sample_file_id} and peak m/z {peak_mz}."
|
|
680
|
+
)
|
|
681
|
+
return None
|
|
682
|
+
|
|
683
|
+
# Parse the content of the response
|
|
684
|
+
content = json.loads(resp.content)
|
|
685
|
+
timeseries_data = content.get("data", None)
|
|
686
|
+
|
|
687
|
+
if not timeseries_data:
|
|
688
|
+
print(
|
|
689
|
+
f"No timeseries data found for sample file with ID {sample_file_id} and peak m/z {peak_mz}."
|
|
690
|
+
)
|
|
691
|
+
return None
|
|
692
|
+
|
|
693
|
+
# Return the timeseries data
|
|
694
|
+
return timeseries_data
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def get_sample_file_spectrum(
|
|
698
|
+
mascope_url: str,
|
|
699
|
+
access_token: str,
|
|
700
|
+
sample_file_id: str,
|
|
701
|
+
t_min: float = None,
|
|
702
|
+
t_max: float = None,
|
|
703
|
+
mz_min: float = None,
|
|
704
|
+
mz_max: float = None,
|
|
705
|
+
) -> dict:
|
|
706
|
+
"""
|
|
707
|
+
Get the mass spectrum from a specified sample file within optional time and m/z ranges.
|
|
708
|
+
|
|
709
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
710
|
+
:type mascope_url: str
|
|
711
|
+
:param access_token: Authorization token for API access
|
|
712
|
+
:type access_token: str
|
|
713
|
+
:param sample_file_id: The ID of the sample file from which to retrieve the spectrum.
|
|
714
|
+
:type sample_file_id: str
|
|
715
|
+
:param t_min: Start of the time range, defaults to None.
|
|
716
|
+
:type t_min: float, optional
|
|
717
|
+
:param t_max: End of the time range, defaults to None.
|
|
718
|
+
:type t_max: float, optional
|
|
719
|
+
:param mz_min: Start of the m/z range, defaults to None.
|
|
720
|
+
:type mz_min: float, optional
|
|
721
|
+
:param mz_max: End of the m/z range, defaults to None.
|
|
722
|
+
:type mz_max: float, optional
|
|
723
|
+
:return: A dictionary with keys:
|
|
724
|
+
- "mz": list of m/z values
|
|
725
|
+
- "intensity": list of intensity values
|
|
726
|
+
- Optional: "results" and "spectrum_count" if available.
|
|
727
|
+
Returns None if no spectrum data is found or if an error occurs.
|
|
728
|
+
:rtype: dict or None
|
|
729
|
+
"""
|
|
730
|
+
# Prepare query parameters as a dictionary
|
|
731
|
+
query_params = {}
|
|
732
|
+
if t_min is not None:
|
|
733
|
+
query_params["t_min"] = t_min
|
|
734
|
+
if t_max is not None:
|
|
735
|
+
query_params["t_max"] = t_max
|
|
736
|
+
if mz_min is not None:
|
|
737
|
+
query_params["mz_min"] = mz_min
|
|
738
|
+
if mz_max is not None:
|
|
739
|
+
query_params["mz_max"] = mz_max
|
|
740
|
+
|
|
741
|
+
# Make the GET request to the API endpoint with query parameters
|
|
742
|
+
resp = api_get(
|
|
743
|
+
url=mascope_url,
|
|
744
|
+
path=f"sample/files/{sample_file_id}/spectrum",
|
|
745
|
+
access_token=access_token,
|
|
746
|
+
params=query_params,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# Check if the API request was successful
|
|
750
|
+
if not resp:
|
|
751
|
+
print(
|
|
752
|
+
f"Failed to retrieve spectrum data for sample file with ID {sample_file_id} from {mascope_url}."
|
|
753
|
+
)
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
# Parse the content of the response
|
|
757
|
+
content = json.loads(resp.content)
|
|
758
|
+
spectrum_data = content.get("data", None)
|
|
759
|
+
|
|
760
|
+
if not spectrum_data:
|
|
761
|
+
print(
|
|
762
|
+
f"No spectrum data found for sample file with ID {sample_file_id} and the given time or m/z ranges."
|
|
763
|
+
)
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
return spectrum_data
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def get_sample_file_instrument_config(
|
|
770
|
+
mascope_url: str,
|
|
771
|
+
access_token: str,
|
|
772
|
+
sample_file_name: str,
|
|
773
|
+
) -> dict:
|
|
774
|
+
"""
|
|
775
|
+
Retrieve the instrument config for a sample file using its filename.
|
|
776
|
+
|
|
777
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
778
|
+
:type mascope_url: str
|
|
779
|
+
:param access_token: Authorization token for API access
|
|
780
|
+
:type access_token: str
|
|
781
|
+
:param sample_file_name: The name of the sample file.
|
|
782
|
+
:type sample_file_name: str
|
|
783
|
+
:return: The instrument config dictionary, or None if not found.
|
|
784
|
+
:rtype: dict or None
|
|
785
|
+
"""
|
|
786
|
+
resp = api_get(
|
|
787
|
+
url=mascope_url,
|
|
788
|
+
path=f"instrument_configs/by_filename/{sample_file_name}",
|
|
789
|
+
access_token=access_token,
|
|
790
|
+
)
|
|
791
|
+
if not resp:
|
|
792
|
+
print(f"Failed to retrieve instrument config for filename {sample_file_name}.")
|
|
793
|
+
return None
|
|
794
|
+
|
|
795
|
+
content = json.loads(resp.content)
|
|
796
|
+
instrument_config = content.get("data", None)
|
|
797
|
+
if not instrument_config:
|
|
798
|
+
print(f"No instrument config found for filename {sample_file_name}.")
|
|
799
|
+
return None
|
|
800
|
+
|
|
801
|
+
return instrument_config
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def get_sample_file_metadata(
|
|
805
|
+
mascope_url: str,
|
|
806
|
+
access_token: str,
|
|
807
|
+
sample_file_id: str,
|
|
808
|
+
) -> dict | None:
|
|
809
|
+
"""
|
|
810
|
+
Retrieve metadata for a specific sample file by its ID.
|
|
811
|
+
|
|
812
|
+
:param mascope_url: The base URL of the Mascope instance.
|
|
813
|
+
:type mascope_url: str
|
|
814
|
+
:param access_token: Authorization token for API access
|
|
815
|
+
:type access_token: str
|
|
816
|
+
:param sample_file_id: The ID of the sample file.
|
|
817
|
+
:type sample_file_id: str
|
|
818
|
+
:return: Metadata dictionary for the sample file, or None if not found or error.
|
|
819
|
+
:rtype: dict or None
|
|
820
|
+
"""
|
|
821
|
+
resp = api_get(
|
|
822
|
+
url=mascope_url,
|
|
823
|
+
path=f"sample/files/{sample_file_id}/metadata",
|
|
824
|
+
access_token=access_token,
|
|
825
|
+
)
|
|
826
|
+
if not resp:
|
|
827
|
+
print(
|
|
828
|
+
f"Failed to retrieve metadata for sample file with ID {sample_file_id} from {mascope_url}."
|
|
829
|
+
)
|
|
830
|
+
return None
|
|
831
|
+
|
|
832
|
+
content = resp.json()
|
|
833
|
+
metadata = content.get("data", None)
|
|
834
|
+
if not metadata:
|
|
835
|
+
print(f"No metadata found for sample file with ID {sample_file_id}.")
|
|
836
|
+
return None
|
|
837
|
+
|
|
838
|
+
return metadata
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
##########################
|
|
842
|
+
# Instrument functions API
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def create_instrument_function(
|
|
846
|
+
mascope_url: str,
|
|
847
|
+
access_token: str,
|
|
848
|
+
instrument: str,
|
|
849
|
+
datetime_utc: str,
|
|
850
|
+
peakshape: dict,
|
|
851
|
+
resolution_function: list,
|
|
852
|
+
) -> dict:
|
|
853
|
+
"""
|
|
854
|
+
Create a new instrument function record in the database.
|
|
855
|
+
|
|
856
|
+
:param mascope_url: Base URL of the Mascope API.
|
|
857
|
+
:type mascope_url: str
|
|
858
|
+
:param access_token: Authorization token for API access
|
|
859
|
+
:type access_token: str
|
|
860
|
+
:param instrument: Name of the instrument.
|
|
861
|
+
:type instrument: str
|
|
862
|
+
:param datetime_utc: UTC timestamp of the instrument function.
|
|
863
|
+
:type datetime_utc: str
|
|
864
|
+
:param peakshape: Peak shape data containing 'x' and 'y' lists.
|
|
865
|
+
:type peakshape: dict
|
|
866
|
+
:param resolution_function: List containing resolution function parameters.
|
|
867
|
+
:type resolution_function: list
|
|
868
|
+
:return: The created instrument function details as received from the API response.
|
|
869
|
+
Returns None if creation failed or an error occurs.
|
|
870
|
+
:rtype: dict or None
|
|
871
|
+
|
|
872
|
+
Example instrument function input data:
|
|
873
|
+
instrument_function_data = {
|
|
874
|
+
"instrument": "KLTOF1",
|
|
875
|
+
"datetime_utc": "2024-04-04T07:51:00.717774",
|
|
876
|
+
"peakshape": {
|
|
877
|
+
"x": [-30.0, -29.9, -29.8, 29.8, 29.9, 30.0,],
|
|
878
|
+
"y": [0.0, 3.0326e-06, 4.8616e-06, 7.4314e-03, 1.2687e-02, 2.2572e-02,]
|
|
879
|
+
},
|
|
880
|
+
"resolution_function": [0.0001098, 0.0003524]
|
|
881
|
+
}
|
|
882
|
+
"""
|
|
883
|
+
# Construct the request body based on the function parameters
|
|
884
|
+
data = {
|
|
885
|
+
"instrument": instrument,
|
|
886
|
+
"datetime_utc": datetime_utc,
|
|
887
|
+
"peakshape": peakshape,
|
|
888
|
+
"resolution_function": resolution_function,
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
# Make the POST request to the instrument_functions endpoint
|
|
892
|
+
resp = api_post(
|
|
893
|
+
url=mascope_url,
|
|
894
|
+
path="instrument_functions",
|
|
895
|
+
access_token=access_token,
|
|
896
|
+
data=data,
|
|
897
|
+
)
|
|
898
|
+
# Check if the API request was successful
|
|
899
|
+
if not resp:
|
|
900
|
+
print(f"Failed to create instrument function from {mascope_url}")
|
|
901
|
+
return None
|
|
902
|
+
|
|
903
|
+
# Successfully created the instrument function, extract 'data' from the response JSON
|
|
904
|
+
response_json = resp.json()
|
|
905
|
+
created_instrument_function = response_json.get("data", None)
|
|
906
|
+
|
|
907
|
+
if not created_instrument_function:
|
|
908
|
+
print(f"Failed to create instrument function. Status code: {resp.status_code}")
|
|
909
|
+
return None
|
|
910
|
+
|
|
911
|
+
return created_instrument_function
|