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,3 @@
1
+ # Mascope SDK
2
+
3
+ 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