mascope_sdk 0.1.0__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,23 @@
1
+ Metadata-Version: 2.3
2
+ Name: mascope_sdk
3
+ Version: 0.1.0
4
+ Summary: Mascope's public SDK library, wrapping the app's REST API.
5
+ License: GPL-3.0-or-later
6
+ Author: Oskari Kausiala
7
+ Author-email: oskari.kausiala@karsa.fi>
8
+ Maintainer: Oskari Kausiala
9
+ Maintainer-email: oskari.kausiala@karsa.fi>
10
+ Requires-Python: >=3.8
11
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Requires-Dist: requests (>=2.32,<3.0)
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.
23
+
@@ -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,767 @@
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(url: str, path: str, access_token: str, filepath: str):
145
+ """Send a POST request to the specified API endpoint with a path file to be uploaded.
146
+
147
+ :param url: The base URL of the server.
148
+ :type url: str
149
+ :param path: The specific API path to be appended to the base URL.
150
+ :type path: str
151
+ :param access_token: Authorization token for API access
152
+ :type access_token: str
153
+ :param filepath: Path to the file to be uploaded
154
+ :type filepath: str
155
+ :return: The response object if the request was successful, otherwise None.
156
+ :rtype: requests.Response or None
157
+ """
158
+ full_url = url + "/api/" + path
159
+ try:
160
+ headers = {
161
+ "Authorization": f"Bearer {access_token}",
162
+ "X-Service-Name": "tof-agent",
163
+ }
164
+ with open(filepath, "rb") as file:
165
+ resp = requests.post(
166
+ full_url,
167
+ files={"file": file},
168
+ headers=headers,
169
+ verify=False,
170
+ timeout=30,
171
+ )
172
+ resp.raise_for_status() # Raise HTTPError for bad responses
173
+ message = json.loads(resp.content).get("message", None)
174
+ if message is not None:
175
+ print(message)
176
+ except HTTPError as http_err:
177
+ if resp.status_code == 401 or resp.status_code == 403:
178
+ response = json.loads(resp.content)
179
+ error_message = response.get("detail", {}).get("error_message", None)
180
+ print(f"{error_message} Please check your API token.")
181
+ else:
182
+ try:
183
+ error_message = (
184
+ json.loads(resp.content)
185
+ .get("detail", {})
186
+ .get(
187
+ "error_message",
188
+ "No additional error information from the server.",
189
+ )
190
+ )
191
+ except json.JSONDecodeError:
192
+ error_message = "Failed to decode error message from server response."
193
+ print(
194
+ f"HTTP error: Unable to retrieve data from {full_url}. \nDetails: {http_err} \nServer message: {error_message}"
195
+ )
196
+ return None
197
+ except Timeout:
198
+ print(f"Timeout error: The request to {full_url} timed out.")
199
+ return None
200
+ except RequestException as req_err:
201
+ print(
202
+ f"Connection error: Could not connect to {full_url}. Please check the URL and your network connection. \nDetails: {req_err}"
203
+ )
204
+ return None
205
+ except Exception as e:
206
+ print(
207
+ f"Error: An unexpected error occurred while trying to reach {full_url}. \nDetails: {str(e)}"
208
+ )
209
+ return None
210
+ return resp
211
+
212
+
213
+ ################
214
+ # Workspaces API
215
+
216
+
217
+ def get_workspaces(mascope_url: str, access_token: str) -> list:
218
+ """Get Mascope workspaces from a URL
219
+
220
+ :param mascope_url: Mascope URL
221
+ :type mascope_url: str
222
+ :param access_token: Authorization token for API access
223
+ :type access_token: str
224
+ :return: List of workspace dictionaries.
225
+ :rtype: list
226
+ """
227
+ resp = api_get(url=mascope_url, path="workspaces", access_token=access_token)
228
+ # Check if the request was successful
229
+ if not resp:
230
+ print(
231
+ f"Failed to retrieve workspaces from {mascope_url}. Please check the URL and try again."
232
+ )
233
+ return []
234
+
235
+ content = json.loads(resp.content)
236
+ workspaces = content.get("data", [])
237
+ if not workspaces:
238
+ print("No workspaces found. Please create a new workspace.")
239
+
240
+ return workspaces
241
+
242
+
243
+ ####################
244
+ # Sample batches API
245
+
246
+
247
+ def get_sample_batches(mascope_url: str, access_token: str, workspace_id: str) -> list:
248
+ """
249
+ Get Mascope sample batches of a workspace.
250
+
251
+ :param mascope_url: The base URL of the Mascope instance.
252
+ :type mascope_url: str
253
+ :param access_token: Authorization token for API access
254
+ :type access_token: str
255
+ :param workspace_id: The ID of the workspace from which to retrieve sample batches.
256
+ :type workspace_id: str
257
+ :return: A list of sample batch dictionaries.
258
+ Returns an empty list if no sample batches are found or if an error occurs.
259
+ :rtype: list
260
+ """
261
+ # Prepare query parameters
262
+ query_params = {"workspace_id": workspace_id}
263
+
264
+ # Perform the GET request with query parameters
265
+ resp = api_get(
266
+ url=mascope_url,
267
+ path="sample/batches",
268
+ access_token=access_token,
269
+ params=query_params,
270
+ )
271
+
272
+ # Check if the request was successful
273
+ if not resp:
274
+ print(
275
+ f"Failed to retrieve sample batches from {mascope_url}. Please check the URL and try again."
276
+ )
277
+ return []
278
+
279
+ content = json.loads(resp.content)
280
+ batches = content.get("data", [])
281
+
282
+ if not batches:
283
+ print("No sample batches found. Please create a new sample batch.")
284
+
285
+ return batches
286
+
287
+
288
+ def get_sample_batch_data(
289
+ mascope_url: str,
290
+ access_token: str,
291
+ sample_batch_id: str,
292
+ ) -> dict:
293
+ """
294
+ Retrieve detailed data for all samples in a sample batch.
295
+
296
+ This function interacts with the Mascope API to fetch comprehensive data
297
+ for a given sample batch. It retrieves data for samples and combinned match/targets data
298
+ for compounds, ions, isotopes and interferences (included to isotopes).
299
+
300
+ :param mascope_url: The base URL of the Mascope instance.
301
+ :type mascope_url: str
302
+ :param access_token: Authorization token for API access
303
+ :type access_token: str
304
+ :param sample_batch_id: The ID of the sample batch to retrieve data for.
305
+ :type sample_batch_id: str
306
+ :return: A dictionary containing:
307
+ - `result`: Summary statistics about the retrieved data.
308
+ - `sample_batch`: Information about the sample batch.
309
+ - `samples`: A list of samples within the batch. Combination of samples (sample_item + sample_file) and match_samples
310
+ - `compounds`: Data for compounds. Combination of match_compounds and target_compounds
311
+ - `ions`: Data for ions. Combination of match_ions and target_ions
312
+ - `isotopes`: Data for isotopes. Combination of match_isotopes, match_interferences, and target_isotopes
313
+ Returns an empty dictionary if the request fails or no data is found.
314
+ :rtype: dict
315
+ """
316
+ # Step 1: Call the API to get the batch data (stored in database)
317
+ resp = api_get(
318
+ url=mascope_url,
319
+ path=f"match/targets/batch/{sample_batch_id}",
320
+ access_token=access_token,
321
+ )
322
+ if not resp:
323
+ print(
324
+ f"Failed to retrieve match data for sample batch with ID {sample_batch_id}."
325
+ )
326
+ return {}
327
+
328
+ # Step 2: Parse the response content
329
+ batch_data = json.loads(resp.content)
330
+ if not batch_data:
331
+ print(f"No data returned for sample batch with ID {sample_batch_id}.")
332
+ return {}
333
+
334
+ # Step 3: Extract relevant information from the aggregate match data
335
+ result = batch_data.get("result", {})
336
+ sample_batch = batch_data.get("data", {}).get("sample_batch", {})
337
+ samples = batch_data.get("data", {}).get("samples", [])
338
+ compounds = batch_data.get("data", {}).get("compounds", [])
339
+ ions = batch_data.get("data", {}).get("ions", [])
340
+ isotopes = batch_data.get("data", {}).get("isotopes", [])
341
+
342
+ # Step 4: Build the response structure
343
+ response = {
344
+ "result": result,
345
+ "sample_batch": sample_batch,
346
+ "samples": samples,
347
+ "compounds": compounds,
348
+ "ions": ions,
349
+ "isotopes": isotopes,
350
+ }
351
+
352
+ return response
353
+
354
+
355
+ #############
356
+ # Samples API
357
+
358
+
359
+ def get_samples(mascope_url: str, access_token: str, sample_batch_id: str) -> list:
360
+ """
361
+ Get Mascope samples of the specified sample batch.
362
+
363
+ :param mascope_url: The base URL of the Mascope instance.
364
+ :type mascope_url: str
365
+ :param access_token: Authorization token for API access
366
+ :type access_token: str
367
+ :param sample_batch_id: The ID of the sample batch from which to retrieve samples.
368
+ :type sample_batch_id: str
369
+ :return: A list of sample dictionaries.
370
+ Returns an empty list if no samples are found or if an error occurs.
371
+ :rtype: list
372
+ """
373
+ # Prepare query parameters
374
+ query_params = {"sample_batch_id": sample_batch_id}
375
+
376
+ # Perform the GET request with query parameters
377
+ resp = api_get(
378
+ url=mascope_url, path="samples", access_token=access_token, params=query_params
379
+ )
380
+
381
+ # Check if the API request was successful
382
+ if not resp:
383
+ print(
384
+ f"Failed to retrieve samples from {mascope_url}. Please check the URL and try again."
385
+ )
386
+ return []
387
+
388
+ content = json.loads(resp.content)
389
+ samples = content.get("data", [])
390
+ if not samples:
391
+ print(f"No samples found for sample batch with ID {sample_batch_id}.")
392
+
393
+ return samples
394
+
395
+
396
+ def get_sample(mascope_url: str, access_token: str, sample_item_id: str) -> dict:
397
+ """
398
+ Get details of a specific sample by its ID.
399
+
400
+ :param mascope_url: The base URL of the Mascope instance.
401
+ :type mascope_url: str
402
+ :param access_token: Authorization token for API access
403
+ :type access_token: str
404
+ :param sample_item_id: The ID of the sample item to retrieve.
405
+ :type sample_item_id: str
406
+ :return: The response dictionary containing the sample details, or None if an error occurs.
407
+ :rtype: dict
408
+ """
409
+ resp = api_get(
410
+ url=mascope_url,
411
+ path=f"samples/{sample_item_id}",
412
+ access_token=access_token,
413
+ )
414
+ if not resp:
415
+ print(f"Failed to retrieve sample details from {mascope_url}.")
416
+ return None
417
+
418
+ sample = json.loads(resp.content)
419
+ if not sample:
420
+ print(f"No sample with ID {sample_item_id} found.")
421
+ return sample
422
+
423
+
424
+ def get_sample_compound_matches(
425
+ mascope_url: str,
426
+ access_token: str,
427
+ sample_item_id: str,
428
+ target_compound_formula: str,
429
+ target_compound_name: str = "Unknown Compound",
430
+ match_params: dict = None,
431
+ ) -> dict:
432
+ """
433
+ Retrieves matches for compounds within a sample based on a target compound formula,
434
+ applying specified filter parameters to filter the matches.
435
+
436
+ :param mascope_url: Base URL of the Mascope API.
437
+ :type mascope_url: str
438
+ :param access_token: Authorization token for API access
439
+ :type access_token: str
440
+ :param sample_item_id: Unique identifier of the sample item to analyze.
441
+ :type sample_item_id: str
442
+ :param target_compound_formula: Chemical formula of the target compound.
443
+ :type target_compound_formula: str
444
+ :param target_compound_name: The name of the target compound, defaults to "Unknown Compound"
445
+ :type target_compound_name: str, optional
446
+ :param match_params: Parameters to filter the match results, affecting which matches are considered significant.
447
+ Should be a dictionary representing a MatchParams Pydantic model.
448
+ :type match_params: dict, optional
449
+ :return: A dictionary containing the match data (compound->ions->isotopes).
450
+ Returns None if no match data is found or if an error occurs.
451
+ :rtype: dict
452
+
453
+ Example of target compound and filter parameters data:
454
+ "target_compound_formula": "C6H12N2O6",
455
+ "target_compound_name": "Formic acid", # compound name is optional
456
+ "match_params": {
457
+ "mz_tolerance": 72,
458
+ "isotope_ratio_tolerance": 0.2,
459
+ "peak_min_intensity": 0.0,
460
+ "min_isotope_abundance": 0.15,
461
+ "min_isotope_correlation": 0.7,
462
+ "probable_match_threshold": 0.8,
463
+ "possible_match_threshold": 0.4,
464
+ }
465
+ """
466
+ # Construct the request body
467
+ body = {
468
+ "target_compound": {
469
+ "target_compound_formula": target_compound_formula,
470
+ "target_compound_name": target_compound_name,
471
+ }
472
+ }
473
+ if match_params is not None:
474
+ body["match_params"] = match_params
475
+
476
+ # Make the POST request for the specified sample
477
+ resp = api_post(
478
+ url=mascope_url,
479
+ path=f"match/aggregate/sample/{sample_item_id}/compound",
480
+ access_token=access_token,
481
+ data=body,
482
+ )
483
+
484
+ # Check if the API request was successful
485
+ if not resp:
486
+ print(
487
+ f"Failed to retrieve compound '{target_compound_formula}' match data for for sample item ID {sample_item_id} from {mascope_url}."
488
+ )
489
+ return None
490
+
491
+ # Parse the content of the response
492
+ response_json = resp.json()
493
+ match_data = response_json.get("data", None)
494
+
495
+ if not match_data:
496
+ print(
497
+ f"No compound matches found for sample item ID {sample_item_id} and target compound {target_compound_formula}."
498
+ )
499
+ return None
500
+
501
+ return match_data
502
+
503
+
504
+ ##################
505
+ # Sample files API
506
+
507
+
508
+ def get_sample_file_peaks(
509
+ mascope_url: str,
510
+ access_token: str,
511
+ sample_file_id: str,
512
+ areas: bool = True,
513
+ heights: bool = True,
514
+ ) -> dict:
515
+ """
516
+ Get peaks of a given sample file, with options to include areas and/or heights.
517
+
518
+ :param mascope_url: The base URL of the Mascope instance.
519
+ :type mascope_url: str
520
+ :param access_token: Authorization token for API access
521
+ :type access_token: str
522
+ :param sample_file_id: The ID of the sample file from which to retrieve peaks.
523
+ :type sample_file_id: str
524
+ :param areas: If True, include peak areas in the response, defaults to True.
525
+ :type areas: bool, optional
526
+ :param heights: If True, include peak heights in the response, defaults to True.
527
+ :type heights: bool, optional
528
+ :return: A dictionary with keys:
529
+ - "mz": list of m/z values of the peaks in the sample file
530
+ - "area": list of peak areas (if requested)
531
+ - "height": list of peak heights (if requested)
532
+ Returns None if no peaks are found or if an error occurs.
533
+ :rtype: dict or None
534
+ """
535
+ # Prepare query parameters for areas and heights
536
+ query_params = {
537
+ "areas": str(areas).lower(), # Convert bool to string (lowercase)
538
+ "heights": str(heights).lower(), # Convert bool to string (lowercase)
539
+ }
540
+ # Make API request with query parameters
541
+ resp = api_get(
542
+ url=mascope_url,
543
+ path=f"sample/files/{sample_file_id}/peaks",
544
+ access_token=access_token,
545
+ params=query_params,
546
+ )
547
+ # Check if the API request was successful
548
+ if not resp:
549
+ print(
550
+ f"Failed to retrieve peaks for sample file with ID {sample_file_id} from {mascope_url}."
551
+ )
552
+ return None
553
+
554
+ # Parse the content of the response
555
+ content = json.loads(resp.content)
556
+ peaks_data = content.get("data", None)
557
+
558
+ if not peaks_data:
559
+ print(f"No peaks found for sample file with ID {sample_file_id}.")
560
+ return None
561
+
562
+ # Return the peaks data
563
+ return peaks_data
564
+
565
+
566
+ def get_sample_file_peak_timeseries(
567
+ mascope_url: str,
568
+ access_token: str,
569
+ sample_file_id: str,
570
+ peak_mz: float,
571
+ peak_mz_tolerance_ppm: float = None,
572
+ ) -> dict:
573
+ """Get timeseries data for the specified peak of the sample file from the Mascope API.
574
+
575
+ :param mascope_url: The base URL of the Mascope instance.
576
+ :type mascope_url: str
577
+ :param access_token: Authorization token for API access
578
+ :type access_token: str
579
+ :param sample_file_id: The ID of the sample file from which to retrieve peak timeseries data.
580
+ :type sample_file_id: str
581
+ :param peak_mz: The m/z of the peak to request timeseries for.
582
+ :type peak_mz: float
583
+ :param peak_mz_tolerance_ppm: The m/z tolerance within which the peak should be compared (ppm), defaults to None.
584
+ :type peak_mz_tolerance_ppm: float, optional
585
+ :return: A dictionary with keys:
586
+ - "mz": m/z of the peak in sample file (None if no peak within tolerance)
587
+ - "height": list of peak intensity at time points (empty if no peak within tolerance)
588
+ - "time": list of time coordinates (empty if no peak within tolerance)
589
+ Returns None if no timeseries data is found or if an error occurs.
590
+ :rtype: dict or None
591
+ """
592
+ # Prepare the payload for the POST request
593
+ body = (
594
+ {"peak_mz": peak_mz, "peak_mz_tolerance_ppm": peak_mz_tolerance_ppm}
595
+ if peak_mz_tolerance_ppm is not None
596
+ else {"peak_mz": peak_mz}
597
+ )
598
+ resp = api_post(
599
+ url=mascope_url,
600
+ path=f"sample/files/{sample_file_id}/peaks/timeseries",
601
+ access_token=access_token,
602
+ data=body,
603
+ )
604
+ # Check if the API request was successful
605
+ if not resp:
606
+ print(
607
+ f"Failed to retrieve peak timeseries data from {mascope_url} for file ID {sample_file_id} and peak m/z {peak_mz}."
608
+ )
609
+ return None
610
+
611
+ # Parse the content of the response
612
+ content = json.loads(resp.content)
613
+ timeseries_data = content.get("data", None)
614
+
615
+ if not timeseries_data:
616
+ print(
617
+ f"No timeseries data found for sample file with ID {sample_file_id} and peak m/z {peak_mz}."
618
+ )
619
+ return None
620
+
621
+ # Return the timeseries data
622
+ return timeseries_data
623
+
624
+
625
+ def get_sample_file_spectrum(
626
+ mascope_url: str,
627
+ access_token: str,
628
+ sample_file_id: str,
629
+ t_min: float = None,
630
+ t_max: float = None,
631
+ mz_min: float = None,
632
+ mz_max: float = None,
633
+ ) -> dict:
634
+ """
635
+ Get the mass spectrum from a specified sample file within optional time and m/z ranges.
636
+
637
+ :param mascope_url: The base URL of the Mascope instance.
638
+ :type mascope_url: str
639
+ :param access_token: Authorization token for API access
640
+ :type access_token: str
641
+ :param sample_file_id: The ID of the sample file from which to retrieve the spectrum.
642
+ :type sample_file_id: str
643
+ :param t_min: Start of the time range, defaults to None.
644
+ :type t_min: float, optional
645
+ :param t_max: End of the time range, defaults to None.
646
+ :type t_max: float, optional
647
+ :param mz_min: Start of the m/z range, defaults to None.
648
+ :type mz_min: float, optional
649
+ :param mz_max: End of the m/z range, defaults to None.
650
+ :type mz_max: float, optional
651
+ :return: A dictionary with keys:
652
+ - "mz": list of m/z values
653
+ - "intensity": list of intensity values
654
+ - Optional: "results" and "spectrum_count" if available.
655
+ Returns None if no spectrum data is found or if an error occurs.
656
+ :rtype: dict or None
657
+ """
658
+ # Prepare query parameters as a dictionary
659
+ query_params = {}
660
+ if t_min is not None:
661
+ query_params["t_min"] = t_min
662
+ if t_max is not None:
663
+ query_params["t_max"] = t_max
664
+ if mz_min is not None:
665
+ query_params["mz_min"] = mz_min
666
+ if mz_max is not None:
667
+ query_params["mz_max"] = mz_max
668
+
669
+ # Make the GET request to the API endpoint with query parameters
670
+ resp = api_get(
671
+ url=mascope_url,
672
+ path=f"sample/files/{sample_file_id}/spectrum",
673
+ access_token=access_token,
674
+ params=query_params,
675
+ )
676
+
677
+ # Check if the API request was successful
678
+ if not resp:
679
+ print(
680
+ f"Failed to retrieve spectrum data for sample file with ID {sample_file_id} from {mascope_url}."
681
+ )
682
+ return None
683
+
684
+ # Parse the content of the response
685
+ content = json.loads(resp.content)
686
+ spectrum_data = content.get("data", None)
687
+
688
+ if not spectrum_data:
689
+ print(
690
+ f"No spectrum data found for sample file with ID {sample_file_id} and the given time or m/z ranges."
691
+ )
692
+ return None
693
+
694
+ return spectrum_data
695
+
696
+
697
+ ##########################
698
+ # Instrument functions API
699
+
700
+
701
+ def create_instrument_function(
702
+ mascope_url: str,
703
+ access_token: str,
704
+ instrument: str,
705
+ datetime_utc: str,
706
+ peakshape: dict,
707
+ resolution_function: list,
708
+ ) -> dict:
709
+ """
710
+ Create a new instrument function record in the database.
711
+
712
+ :param mascope_url: Base URL of the Mascope API.
713
+ :type mascope_url: str
714
+ :param access_token: Authorization token for API access
715
+ :type access_token: str
716
+ :param instrument: Name of the instrument.
717
+ :type instrument: str
718
+ :param datetime_utc: UTC timestamp of the instrument function.
719
+ :type datetime_utc: str
720
+ :param peakshape: Peak shape data containing 'x' and 'y' lists.
721
+ :type peakshape: dict
722
+ :param resolution_function: List containing resolution function parameters.
723
+ :type resolution_function: list
724
+ :return: The created instrument function details as received from the API response.
725
+ Returns None if creation failed or an error occurs.
726
+ :rtype: dict or None
727
+
728
+ Example instrument function input data:
729
+ instrument_function_data = {
730
+ "instrument": "KLTOF1",
731
+ "datetime_utc": "2024-04-04T07:51:00.717774",
732
+ "peakshape": {
733
+ "x": [-30.0, -29.9, -29.8, 29.8, 29.9, 30.0,],
734
+ "y": [0.0, 3.0326e-06, 4.8616e-06, 7.4314e-03, 1.2687e-02, 2.2572e-02,]
735
+ },
736
+ "resolution_function": [0.0001098, 0.0003524]
737
+ }
738
+ """
739
+ # Construct the request body based on the function parameters
740
+ data = {
741
+ "instrument": instrument,
742
+ "datetime_utc": datetime_utc,
743
+ "peakshape": peakshape,
744
+ "resolution_function": resolution_function,
745
+ }
746
+
747
+ # Make the POST request to the instrument_functions endpoint
748
+ resp = api_post(
749
+ url=mascope_url,
750
+ path="instrument_functions",
751
+ access_token=access_token,
752
+ data=data,
753
+ )
754
+ # Check if the API request was successful
755
+ if not resp:
756
+ print(f"Failed to create instrument function from {mascope_url}")
757
+ return None
758
+
759
+ # Successfully created the instrument function, extract 'data' from the response JSON
760
+ response_json = resp.json()
761
+ created_instrument_function = response_json.get("data", None)
762
+
763
+ if not created_instrument_function:
764
+ print(f"Failed to create instrument function. Status code: {resp.status_code}")
765
+ return None
766
+
767
+ return created_instrument_function
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "mascope_sdk"
3
+ description = "Mascope's public SDK library, wrapping the app's REST API."
4
+ classifiers = [
5
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
6
+ "Topic :: Software Development :: Libraries :: Python Modules",
7
+ ]
8
+ authors = [
9
+ { name = "Oskari Kausiala", email = "oskari.kausiala@karsa.fi>" },
10
+ { name = "Philip Chernonog", email = "philip.chernonog@karsa.fi>" },
11
+ ]
12
+ maintainers = [
13
+ { name = "Oskari Kausiala", email = "oskari.kausiala@karsa.fi>" },
14
+ { name = "Philip Chernonog", email = "philip.chernonog@karsa.fi>" },
15
+ ]
16
+ license = { text = "GPL-3.0-or-later" }
17
+ readme = "README.md"
18
+ requires-python = ">=3.8"
19
+ version = "0.1.0"
20
+
21
+ [project.urls]
22
+ homepage = "https://karsa.fi/"
23
+ repository = "https://github.com/Karsa-Oy/Mascope"
24
+ documentation = "https://github.com/Karsa-Oy/Mascope"
25
+ "Bug Tracker" = "https://github.com/Karsa-Oy/Mascope/issues"
26
+
27
+ [tool.poetry]
28
+ packages = [{ include = "mascope_sdk.py" }]
29
+
30
+ [build-system]
31
+ requires = ["poetry-core"]
32
+ build-backend = "poetry.core.masonry.api"
33
+
34
+ [tool.poetry.dependencies]
35
+ python = ">=3.8"
36
+
37
+ requests = "^2.32"
38
+
39
+ [tool.poetry.group.dev.dependencies]
40
+ deptry = "^0.16"