qmenta-client 1.1.dev1378__py3-none-any.whl → 1.1.dev1389__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qmenta/client/Project.py CHANGED
@@ -20,186 +20,6 @@ logger_name = "qmenta.client"
20
20
  OPERATOR_LIST = ["eq", "ne", "gt", "gte", "lt", "lte"]
21
21
 
22
22
 
23
- def show_progress(done, total, finish=False):
24
- bytes_in_mb = 1024 * 1024
25
- progress_message = "\r[{:.2f} %] Uploaded {:.2f} of {:.2f} Mb".format(
26
- done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb
27
- )
28
- sys.stdout.write(progress_message)
29
- sys.stdout.flush()
30
- if not finish:
31
- pass
32
- # sys.stdout.write("")
33
- # sys.stdout.flush()
34
- else:
35
- sys.stdout.write("\n")
36
-
37
-
38
- def get_session_id(file_path):
39
- m = hashlib.md5()
40
- m.update(file_path.encode("utf-8"))
41
- return str(time.time()).replace(".", "") + "_" + m.hexdigest()
42
-
43
-
44
- def check_upload_file(file_path):
45
- """
46
- Check whether a file has the correct extension to upload.
47
-
48
- Parameters
49
- ----------
50
- file_path : str
51
- Path to the file
52
-
53
- Returns
54
- -------
55
- bool
56
- True if correct extension, False otherwise.
57
- """
58
-
59
- # TODO: Add a file zipper here so zips files in a folder
60
-
61
- file_parts = file_path.split(".")
62
- extension = file_parts[-1]
63
-
64
- if extension != "zip":
65
- logging.getLogger(logger_name).error("You must upload a zip.")
66
- return False
67
- else:
68
- return True
69
-
70
-
71
- def operation(reference_value, operator, input_value):
72
- """
73
- The method performs an operation by comparing the two input values.
74
- The Operation is applied to the Input Value in comparison to the Reference
75
- Value.
76
-
77
- Parameters
78
- ----------
79
- reference_value : str, list, or int
80
- Reference value.
81
- operator : str
82
- Operation.
83
- input_value : str, list, or int
84
- Input value.
85
-
86
- Returns
87
- -------
88
- bool
89
- True if the operation is satisfied, False otherwise.
90
- """
91
- if input_value is None or input_value == "":
92
- return False
93
-
94
- if operator == "in":
95
- return reference_value in input_value
96
-
97
- elif operator == "in-list":
98
- return all([el in input_value for el in reference_value])
99
-
100
- elif operator == "eq":
101
- return input_value == reference_value
102
-
103
- elif operator == "gt":
104
- return input_value > reference_value
105
-
106
- elif operator == "gte":
107
- return input_value >= reference_value
108
-
109
- elif operator == "lt":
110
- return input_value < reference_value
111
-
112
- elif operator == "lte":
113
- return input_value <= reference_value
114
- else:
115
- return False
116
-
117
-
118
- def wrap_search_criteria(search_criteria={}):
119
- """
120
- Wraps the conditions specified within the Search Criteria in order for
121
- other methods to handle it easily. The conditions are grouped only into
122
- three groups: Modality, Tags and the File Metadata (if DICOM it corresponds
123
- to the DICOM information), and each of them is output in a different
124
- variable.
125
-
126
- Parameters
127
- ----------
128
- search_criteria : dict
129
- Each element is a string and is built using the formatting
130
- "KEYTYPE;VALUE", or "KEYTYPE;OPERATOR|VALUE".
131
-
132
- Full list of keys avaiable for the dictionary:
133
-
134
- search_criteria = {
135
- "pars_patient_secret_name": "string;SUBJECTID",
136
- "pars_ssid": "integer;OPERATOR|SSID",
137
- "pars_modalities": "string;MODALITY",
138
- "pars_tags": "tags;TAGS",
139
- "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
140
- "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
141
- "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
142
- }
143
-
144
- See documentation for a complete definition.
145
-
146
- Returns
147
- -------
148
- modality : str
149
- String containing the modality of the search criteria extracted from
150
- 'pars_modalities'
151
-
152
- tags : list of str
153
- List of strings containing the tags of the search criteria extracted
154
- 'from pars_tags'
155
-
156
- file_metadata : Dict
157
- Dictionary containing the file metadata of the search criteria
158
- extracted from 'pars_[dicom]_KEY'
159
- """
160
-
161
- # The keys not included bellow apply to the whole session.
162
- modality, tags, file_metadata = "", list(), dict()
163
- for key, value in search_criteria.items():
164
- if key == "pars_modalities":
165
- modalities = value.split(";")[1].split(",")
166
- if len(modalities) != 1:
167
- raise ValueError(f"A file can only have one modality. "
168
- f"Provided Modalities: "
169
- f"{', '.join(modalities)}.")
170
- modality = modalities[0]
171
- elif key == "pars_tags":
172
- tags = value.split(";")[1].split(",")
173
- elif "pars_[dicom]_" in key:
174
- d_tag = key.split("pars_[dicom]_")[1]
175
- d_type = value.split(";")[0]
176
- if d_type == "string":
177
- file_metadata[d_tag] = {
178
- "operation": "in",
179
- "value": value.replace(d_type + ";", "")
180
- }
181
- elif d_type == "integer":
182
- d_operator = value.split(";")[1].split("|")[0]
183
- d_value = value.split(";")[1].split("|")[1]
184
- file_metadata[d_tag] = {
185
- "operation": d_operator,
186
- "value": int(d_value)}
187
- elif d_type == "decimal":
188
- d_operator = value.split(";")[1].split("|")[0]
189
- d_value = value.split(";")[1].split("|")[1]
190
- file_metadata[d_tag] = {
191
- "operation": d_operator,
192
- "value": float(d_value)
193
- }
194
- elif d_type == "list":
195
- value.replace(d_type + ";", "")
196
- file_metadata[d_tag] = {
197
- "operation": "in-list",
198
- "value": value.replace(d_type + ";", "").split(";")
199
- }
200
- return modality, tags, file_metadata
201
-
202
-
203
23
  def convert_qc_value_to_qcstatus(value):
204
24
  """
205
25
  Convert input string to QCStatus class.
@@ -255,6 +75,7 @@ class Project:
255
75
 
256
76
  """
257
77
 
78
+ """ Project Related Methods """
258
79
  def __init__(self, account: Account, project_id, max_upload_retries=5):
259
80
  # if project_id is a string (the name of the project), get the
260
81
  # project id (int)
@@ -312,964 +133,998 @@ class Project:
312
133
  rep = "<Project {}>".format(self._project_name)
313
134
  return rep
314
135
 
315
- @property
316
- def subjects_metadata(self):
317
- """
318
- List all subject data from the selected project.
319
-
320
- Returns
321
- -------
322
- dict
323
- A list of dictionary of {"metadata_name": "metadata_value"}
324
- """
325
- return self.get_subjects_metadata()
326
-
327
- def get_subjects_metadata(self, search_criteria={}, items=(0, 9999)):
136
+ """ Upload / Download Data Related Methods """
137
+ def _upload_chunk(
138
+ self,
139
+ data,
140
+ range_str,
141
+ length,
142
+ session_id,
143
+ disposition,
144
+ last_chunk,
145
+ name="",
146
+ date_of_scan="",
147
+ description="",
148
+ subject_name="",
149
+ ssid="",
150
+ filename="DATA.zip",
151
+ input_data_type="mri_brain_data:1.0",
152
+ result=False,
153
+ add_to_container_id=0,
154
+ split_data=False,
155
+ ):
328
156
  """
329
- List all Subject ID/Session ID from the selected project that meet the
330
- defined search criteria at a session level.
157
+ Upload a chunk of a file to the platform.
331
158
 
332
159
  Parameters
333
160
  ----------
334
- search_criteria: dict
335
- Each element is a string and is built using the formatting
336
- "type;value", or "type;operation|value"
337
-
338
- Complete search_criteria Dictionary Explanation:
339
-
340
- search_criteria = {
341
- "pars_patient_secret_name": "string;SUBJECTID",
342
- "pars_ssid": "integer;OPERATOR|SSID",
343
- "pars_modalities": "string;MODALITY",
344
- "pars_tags": "tags;TAGS",
345
- "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
346
- "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
347
- "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
348
- }
349
-
350
- Where:
351
- "pars_patient_secret_name": Applies the search to the 'Subject ID'.
352
- SUBJECTID is a comma separated list of strings.
353
- "pars_ssid": Applies the search to the 'Session ID'.
354
- SSID is an integer.
355
- OPERATOR is the operator to apply. One of:
356
- - Equal: eq
357
- - Different Than: ne
358
- - Greater Than: gt
359
- - Greater/Equal To: gte
360
- - Lower Than: lt
361
- - Lower/Equal To: lte
362
-
363
- "pars_modalities": Applies the search to the file 'Modalities'
364
- available within each Subject ID.
365
- MODALITY is a comma separated list of string. A session is provided as
366
- long as one MODALITY is available.
367
- "pars_tags": Applies the search to the file 'Tags' available within
368
- each Subject ID and to the subject-level 'Tags'.
369
- TAGS is a comma separated list of strings. A session is provided as
370
- long as one tag is available.
371
- "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
372
- field.
373
- AGE_AT_SCAN is an integer.
374
- "pars_[dicom]_KEY": Applies the search to the metadata fields
375
- available within each file. KEY must be one of the
376
- metadata keys of the files. The full list of KEYS is shown above via
377
- 'file_m["metadata"]["info"].keys()'.
378
- KEYTYPE is the type of the KEY. One of:
379
- - integer
380
- - decimal
381
- - string
382
- - list
383
-
384
- if 'integer' or 'decimal' you must also include an OPERATOR
385
- (i.e., "integer;OPERATOR|KEYVALUE").
386
- if 'list' the KEYVALUE should be a semicolon separated list of
387
- values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
388
- KEYVALUEs must be strings.
389
- KEYVALUE is the expected value of the KEY.
390
- "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
391
- within the 'Metadata Manager' of the project.
392
- PROJECTMETADATA is the ID of the metadata field.
393
- METADATATYPE is the type of the metadata field. One of:
394
- - string
395
- - integer
396
- - list
397
- - decimal
398
- - single_option
399
- - multiple_option
400
-
401
- if 'integer' or 'decimal' you must also include an OPERATOR
402
- (i.e., "integer;OPERATOR|METADATAVALUE").
403
- KEYVALUE is the expected value of the metadata.
161
+ data
162
+ The file chunk to upload
163
+ range_str
164
+ The string to send that describes the content range
165
+ length
166
+ The content length of the chunk to send
167
+ session_id
168
+ The session ID from the file path
169
+ filename
170
+ The name of the file to be sent
171
+ disposition
172
+ The disposition of the content
173
+ last_chunk
174
+ Set this only for the last chunk to be uploaded.
175
+ All following parameters are ignored when False.
176
+ split_data
177
+ Sets the header that informs the platform to split
178
+ the uploaded file into multiple sessions.
179
+ """
404
180
 
405
- 1) Example:
406
- search_criteria = {
407
- "pars_patient_secret_name": "string;abide",
408
- "pars_ssid": "integer;eq|2"
181
+ request_headers = {
182
+ "Content-Type": "application/zip",
183
+ "Content-Range": range_str,
184
+ "Session-ID": str(session_id),
185
+ "Content-Length": str(length),
186
+ "Content-Disposition": disposition,
409
187
  }
410
188
 
411
- 2) Example:
412
- search_criteria = {
413
- "pars_modalities": "string;T1",
414
- "pars_tags": "tags;flair",
415
- "pars_[dicom]_Manufacturer": "string;ge",
416
- "pars_[dicom]_FlipAngle": "integer;gt|5",
417
- "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
418
- }
189
+ if last_chunk:
190
+ request_headers["X-Mint-Name"] = name
191
+ request_headers["X-Mint-Date"] = date_of_scan
192
+ request_headers["X-Mint-Description"] = description
193
+ request_headers["X-Mint-Patient-Secret"] = subject_name
194
+ request_headers["X-Mint-SSID"] = ssid
195
+ request_headers["X-Mint-Filename"] = filename
196
+ request_headers["X-Mint-Project-Id"] = str(self._project_id)
197
+ request_headers["X-Mint-Split-Data"] = str(int(split_data))
419
198
 
420
- Note the search criteria applies to all the files included within a
421
- session. Hence, it does not imply that all the criteria conditions
422
- are applied to the same files. In example 2) above, it means that
423
- any Subject ID/Session ID that has a file classified with a 'T1'
424
- modality, a file with a 'flair' tag, a file whose Manufacturer
425
- contains 'ge', a file whose FlipAngle is greater than '5º', and a
426
- file with ImageType with any of the values: PRIMARY or SECONDARY
427
- will be selected.
199
+ if input_data_type:
200
+ request_headers["X-Mint-Type"] = input_data_type
428
201
 
429
- Returns
430
- -------
431
- dict
432
- A list of dictionary of {"metadata_name": "metadata_value"}
202
+ if result:
203
+ request_headers["X-Mint-In-Out"] = "out"
204
+ else:
205
+ request_headers["X-Mint-In-Out"] = "in"
433
206
 
434
- """
207
+ if add_to_container_id > 0:
208
+ request_headers["X-Mint-Add-To"] = str(add_to_container_id)
435
209
 
436
- assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
437
- assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
210
+ request_headers["X-Requested-With"] = "XMLHttpRequest"
438
211
 
439
- assert all([key[:5] == "pars_" for key in search_criteria.keys()]), (
440
- f"All keys of the search_criteria dictionary " f"'{search_criteria.keys()}' must start with 'pars_'."
212
+ response_time = 900.0 if last_chunk else 120.0
213
+ response = platform.post(
214
+ auth=self._account.auth, endpoint="upload", data=data, headers=request_headers, timeout=response_time
441
215
  )
442
216
 
443
- for key, value in search_criteria.items():
444
- if value.split(";")[0] in ["integer", "decimal"]:
445
- assert value.split(";")[1].split("|")[0] in OPERATOR_LIST, (
446
- f"Search criteria of type '{value.split(';')[0]}' must "
447
- f"include an operator ({', '.join(OPERATOR_LIST)})."
448
- )
449
-
450
- content = platform.parse_response(
451
- platform.post(
452
- self._account.auth,
453
- "patient_manager/get_patient_list",
454
- data=search_criteria,
455
- headers={"X-Range": f"items={items[0]}-{items[1]}"},
456
- )
457
- )
458
- return content
217
+ return response
459
218
 
460
- def get_subjects_files_metadata(self, search_criteria={}, items=(0, 9999)):
461
- """
462
- List all Subject ID/Session ID from the selected project that meet the
463
- defined search criteria at a file level.
464
-
465
- Note, albeit the search criteria is similar to the one defined in
466
- method 'get_subjects_metadata()' (see differences below), the
467
- output is different as this method provides the sessions which
468
- have a file that satisfy all the conditions of the search criteria.
469
- This method is slow.
219
+ def upload_file(
220
+ self,
221
+ file_path,
222
+ subject_name,
223
+ ssid="",
224
+ date_of_scan="",
225
+ description="",
226
+ result=False,
227
+ name="",
228
+ input_data_type="qmenta_mri_brain_data:1.0",
229
+ add_to_container_id=0,
230
+ chunk_size=2**9,
231
+ split_data=False,
232
+ ):
233
+ """
234
+ Upload a ZIP file to the platform.
470
235
 
471
236
  Parameters
472
237
  ----------
473
- search_criteria: dict
474
- Each element is a string and is built using the formatting
475
- "type;value", or "type;operation|value"
476
-
477
- Complete search_criteria Dictionary Explanation:
478
-
479
- search_criteria = {
480
- "pars_patient_secret_name": "string;SUBJECTID",
481
- "pars_ssid": "integer;OPERATOR|SSID",
482
- "pars_modalities": "string;MODALITY",
483
- "pars_tags": "tags;TAGS",
484
- "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
485
- "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
486
- "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
487
- }
238
+ file_path : str
239
+ Path to the ZIP file to upload.
240
+ subject_name : str
241
+ Subject ID of the data to upload in the project in QMENTA Platform.
242
+ ssid : str
243
+ Session ID of the Subject ID (i.e., ID of the timepoint).
244
+ date_of_scan : str
245
+ Date of scan/creation of the file
246
+ description : str
247
+ Description of the file
248
+ result : bool
249
+ If result=True then the upload will be taken as an offline analysis
250
+ name : str
251
+ Name of the file in the platform
252
+ input_data_type : str
253
+ qmenta_medical_image_data:3.11
254
+ add_to_container_id : int
255
+ ID of the container to which this file should be added (if id > 0)
256
+ chunk_size : int
257
+ Size in kB of each chunk. Should be expressed as
258
+ a power of 2: 2**x. Default value of x is 9 (chunk_size = 512 kB)
259
+ split_data : bool
260
+ If True, the platform will try to split the uploaded file into
261
+ different sessions. It will be ignored when the ssid is given.
488
262
 
489
- Where:
490
- "pars_patient_secret_name": Applies the search to the 'Subject ID'.
491
- SUBJECTID is a comma separated list of strings.
492
- "pars_ssid": Applies the search to the 'Session ID'.
493
- SSID is an integer.
494
- OPERATOR is the operator to apply. One of:
495
- - Equal: eq
496
- - Different Than: ne
497
- - Greater Than: gt
498
- - Greater/Equal To: gte
499
- - Lower Than: lt
500
- - Lower/Equal To: lte
263
+ Returns
264
+ -------
265
+ bool
266
+ True if correctly uploaded, False otherwise.
267
+ """
501
268
 
502
- "pars_modalities": Applies the search to the file 'Modalities'
503
- available within each Subject ID.
504
- MODALITY is a string.
505
- "pars_tags": Applies only the search to the file 'Tags' available
506
- within each Subject ID.
507
- TAGS is a comma separated list of strings. All tags must be present in
508
- the same file.
509
- "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
510
- field.
511
- AGE_AT_SCAN is an integer.
512
- "pars_[dicom]_KEY": Applies the search to the metadata fields
513
- available within each file. KEY must be one of the
514
- metadata keys of the files. The full list of KEYS is shown above via
515
- 'file_m["metadata"]["info"].keys()'. # TODO: SEE HOW TO WRITE THIS
516
- KEYTYPE is the type of the KEY. One of:
517
- - integer
518
- - decimal
519
- - string
520
- - list
269
+ filename = os.path.split(file_path)[1]
270
+ input_data_type = "offline_analysis:1.0" if result else input_data_type
521
271
 
522
- if 'integer' or 'decimal' you must also include an OPERATOR
523
- (i.e., "integer;OPERATOR|KEYVALUE").
524
- if 'list' the KEYVALUE should be a semicolon separated list of
525
- values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
526
- KEYVALUEs must be strings.
527
- KEYVALUE is the expected value of the KEY.
528
- "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
529
- within the 'Metadata Manager' of the project.
530
- PROJECTMETADATA is the ID of the metadata field.
531
- METADATATYPE is the type of the metadata field. One of:
532
- - string
533
- - integer
534
- - list
535
- - decimal
536
- - single_option
537
- - multiple_option
272
+ chunk_size *= 1024
273
+ max_retries = 10
538
274
 
539
- if 'integer' or 'decimal' you must also include an OPERATOR
540
- (i.e., "integer;OPERATOR|METADATAVALUE").
541
- KEYVALUE is the expected value of the metadata.
275
+ name = name or os.path.split(file_path)[1]
542
276
 
543
- 1) Example:
544
- search_criteria = {
545
- "pars_patient_secret_name": "string;abide",
546
- "pars_ssid": "integer;eq|2"
547
- }
277
+ total_bytes = os.path.getsize(file_path)
548
278
 
549
- 2) Example:
550
- search_criteria = {
551
- "pars_patient_secret_name": "string;001"
552
- "pars_modalities": "string;T2",
553
- "pars_tags": "tags;flair",
554
- "pars_[dicom]_Manufacturer": "string;ge",
555
- "pars_[dicom]_FlipAngle": "integer;gt|5",
556
- "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
557
- }
279
+ # making chunks of the file and sending one by one
280
+ logger = logging.getLogger(logger_name)
281
+ with open(file_path, "rb") as file_object:
558
282
 
559
- Note the search criteria might apply to both the files metadata
560
- information available within a session and the metadata of the
561
- session. And the method provides a session only if all the file
562
- related conditions are satisfied within the same file.
563
- In example 2) above, it means that the output will contain any
564
- session whose Subject ID contains '001', and there is a file with
565
- modality 'T2', tag 'flair', FlipAngle greater than 5º, and
566
- ImageType with both values PRIMARY and SECONDARY.
567
- Further, the acquisition had to be performed in a Manufacturer
568
- containing 'ge'.
283
+ file_size = os.path.getsize(file_path)
284
+ if file_size == 0:
285
+ logger.error("Cannot upload empty file {}".format(file_path))
286
+ return False
287
+ uploaded = 0
288
+ session_id = self.__get_session_id(file_path)
289
+ chunk_num = 0
290
+ retries_count = 0
291
+ uploaded_bytes = 0
292
+ response = None
293
+ last_chunk = False
569
294
 
570
- Returns
571
- -------
572
- dict
573
- A list of dictionary of {"metadata_name": "metadata_value"}
295
+ if ssid and split_data:
296
+ logger.warning("split-data argument will be ignored because" + " ssid has been specified")
297
+ split_data = False
574
298
 
575
- """
299
+ while True:
300
+ data = file_object.read(chunk_size)
301
+ if not data:
302
+ break
576
303
 
577
- content = self.get_subjects_metadata(search_criteria, items=(0, 9999))
304
+ start_position = chunk_num * chunk_size
305
+ end_position = start_position + chunk_size - 1
306
+ bytes_to_send = chunk_size
578
307
 
579
- # Wrap search criteria.
580
- modality, tags, dicoms = wrap_search_criteria(search_criteria)
308
+ if end_position >= total_bytes:
309
+ last_chunk = True
310
+ end_position = total_bytes - 1
311
+ bytes_to_send = total_bytes - uploaded_bytes
581
312
 
582
- # Iterate over the files of each subject selected to include/exclude
583
- # them from the results.
584
- subjects = list()
585
- for subject in content:
586
- files = platform.parse_response(platform.post(
587
- self._account.auth, "file_manager/get_container_files",
588
- data={"container_id": str(int(subject["container_id"]))}
589
- ))
313
+ bytes_range = "bytes " + str(start_position) + "-" + str(end_position) + "/" + str(total_bytes)
590
314
 
591
- for file in files["meta"]:
592
- if modality and \
593
- modality != (file.get("metadata") or {}).get("modality"):
594
- continue
595
- if tags and not all([tag in file.get("tags") for tag in tags]):
596
- continue
597
- if dicoms:
598
- result_values = list()
599
- for key, dict_value in dicoms.items():
600
- f_value = ((file.get("metadata") or {})
601
- .get("info") or {}).get(key)
602
- d_operator = dict_value["operation"]
603
- d_value = dict_value["value"]
604
- result_values.append(
605
- operation(d_value, d_operator, f_value)
606
- )
315
+ dispstr = f"attachment; filename={filename}"
316
+ response = self._upload_chunk(
317
+ data,
318
+ bytes_range,
319
+ bytes_to_send,
320
+ session_id,
321
+ dispstr,
322
+ last_chunk,
323
+ name,
324
+ date_of_scan,
325
+ description,
326
+ subject_name,
327
+ ssid,
328
+ filename,
329
+ input_data_type,
330
+ result,
331
+ add_to_container_id,
332
+ split_data,
333
+ )
607
334
 
608
- if not all(result_values):
609
- continue
610
- subjects.append(subject)
611
- break
612
- return subjects
335
+ if response is None:
336
+ retries_count += 1
337
+ time.sleep(retries_count * 5)
338
+ if retries_count > max_retries:
339
+ error_message = "HTTP Connection Problem"
340
+ logger.error(error_message)
341
+ break
342
+ elif int(response.status_code) == 201:
343
+ chunk_num += 1
344
+ retries_count = 0
345
+ uploaded_bytes += chunk_size
346
+ elif int(response.status_code) == 200:
347
+ self.__show_progress(file_size, file_size, finish=True)
348
+ break
349
+ elif int(response.status_code) == 416:
350
+ retries_count += 1
351
+ time.sleep(retries_count * 5)
352
+ if retries_count > self.max_retries:
353
+ error_message = "Error Code: 416; " "Requested Range Not Satisfiable (NGINX)"
354
+ logger.error(error_message)
355
+ break
356
+ else:
357
+ retries_count += 1
358
+ time.sleep(retries_count * 5)
359
+ if retries_count > max_retries:
360
+ error_message = "Number of retries has been reached. " "Upload process stops here !"
361
+ logger.error(error_message)
362
+ break
613
363
 
614
- @property
615
- def subjects(self):
616
- """
617
- Return the list of subject names (Subject ID) from the selected
618
- project.
364
+ uploaded += chunk_size
365
+ self.__show_progress(uploaded, file_size)
619
366
 
620
- :return: a list of subject names
621
- :rtype: List(Strings)
622
- """
367
+ try:
368
+ platform.parse_response(response)
369
+ except errors.PlatformError as error:
370
+ logger.error(error)
371
+ return False
623
372
 
624
- subjects = self.subjects_metadata
625
- names = [s["patient_secret_name"] for s in subjects]
626
- return list(set(names))
373
+ message = "Your data was successfully uploaded."
374
+ message += "The uploaded file will be soon processed !"
375
+ logger.info(message)
376
+ return True
627
377
 
628
- def check_subject_name(self, subject_name):
378
+ def upload_mri(self, file_path, subject_name):
629
379
  """
630
- Check if a given subject name (Subject ID) exists in the selected
631
- project.
380
+ Upload new MRI data to the subject.
632
381
 
633
382
  Parameters
634
383
  ----------
635
- subject_name : str
636
- Subject ID of the subject to check
384
+ file_path : str
385
+ Path to the file to upload
386
+ subject_name: str
637
387
 
638
388
  Returns
639
389
  -------
640
390
  bool
641
- True if subject name exists in project, False otherwise
391
+ True if upload was correctly done, False otherwise.
642
392
  """
643
393
 
644
- return subject_name in self.subjects
394
+ if self.__check_upload_file(file_path):
395
+ return self.upload_file(file_path, subject_name)
645
396
 
646
- @property
647
- def metadata_parameters(self):
397
+ def upload_gametection(self, file_path, subject_name):
648
398
  """
649
- List all the parameters in the subject-level metadata.
399
+ Upload new Gametection data to the subject.
650
400
 
651
- Each project has a set of parameters that define the subjects-level
652
- metadata. This function returns all these parameters and its
653
- properties. New subject-level metadata parameters can be creted in the
654
- QMENTA Platform via the Metadata Manager. The API only allow
655
- modification of these subject-level metadata parameters via the
656
- 'change_subject_metadata()' method.
401
+ Parameters
402
+ ----------
403
+ file_path : str
404
+ Path to the file to upload
405
+ subject_name: str
657
406
 
658
407
  Returns
659
408
  -------
660
- dict[str] -> dict[str] -> x
661
- dictionary {"param_name":
662
- { "order": Int,
663
- "tags": [tag1, tag2, ..., ],
664
- "title: "Title",
665
- "type": "integer|string|date|list|decimal",
666
- "visible": 0|1
667
- }}
409
+ bool
410
+ True if upload was correctly done, False otherwise.
668
411
  """
669
- logger = logging.getLogger(logger_name)
670
- try:
671
- data = platform.parse_response(platform.post(self._account.auth, "patient_manager/module_config"))
672
- except errors.PlatformError:
673
- logger.error("Could not retrieve metadata parameters.")
674
- return None
675
- return data["fields"]
676
412
 
677
- def get_analysis(self, analysis_name_or_id):
413
+ if self.__check_upload_file(file_path):
414
+ return self.upload_file(file_path, subject_name, input_data_type="parkinson_gametection")
415
+ return False
416
+
417
+ def upload_result(self, file_path, subject_name):
678
418
  """
679
- Returns the analysis corresponding with the analysis id or analysis name
419
+ Upload new result data to the subject.
680
420
 
681
421
  Parameters
682
422
  ----------
683
- analysis_name_or_id : int, str
684
- analysis_id or analysis name to search for
423
+ file_path : str
424
+ Path to the file to upload
425
+ subject_name: str
685
426
 
686
427
  Returns
687
428
  -------
688
- analysis: dict
689
- A dictionary containing the analysis information
429
+ bool
430
+ True if upload was correctly done, False otherwise.
690
431
  """
691
- if isinstance(analysis_name_or_id, int):
692
- search_tag = "id"
693
- elif isinstance(analysis_name_or_id, str):
694
- if analysis_name_or_id.isdigit():
695
- search_tag = "id"
696
- analysis_name_or_id = int(analysis_name_or_id)
697
- else:
698
- search_tag = "p_n"
699
- else:
700
- raise Exception("The analysis identifier must be its name or an " "integer")
701
432
 
702
- search_condition = {
703
- search_tag: analysis_name_or_id,
704
- }
705
- response = platform.parse_response(
706
- platform.post(self._account.auth, "analysis_manager/get_analysis_list", data=search_condition)
707
- )
433
+ if self.__check_upload_file(file_path):
434
+ return self.upload_file(file_path, subject_name, result=True)
435
+ return False
708
436
 
709
- if len(response) > 1:
710
- raise Exception(f"multiple analyses with name " f"{analysis_name_or_id} found")
711
- elif len(response) == 1:
712
- return response[0]
713
- else:
714
- return None
437
+ def download_file(self, container_id, file_name, local_filename=False, overwrite=False):
438
+ """
439
+ Download a single file from a specific container.
715
440
 
716
- def list_analysis(self, search_condition={}, items=(0, 9999)):
441
+ Parameters
442
+ ----------
443
+ container_id : str
444
+ ID of the container inside which the file is.
445
+ file_name : str
446
+ Name of the file in the container.
447
+ local_filename : str
448
+ Name of the file to be created. By default, the same as file_name.
449
+ overwrite : bool
450
+ Whether to overwrite the file if existing.
717
451
  """
718
- List the analysis available to the user.\n
452
+ logger = logging.getLogger(logger_name)
453
+ if not isinstance(file_name, str):
454
+ raise ValueError("The name of the file to download (file_name) "
455
+ "should be of type string.")
456
+ if not isinstance(file_name, str):
457
+ raise ValueError("The name of the output file (local_filename) "
458
+ "should be of type string.")
719
459
 
720
- Examples
721
- --------
460
+ if file_name not in self.list_container_files(container_id):
461
+ msg = f'File "{file_name}" does not exist in container ' f"{container_id}"
462
+ logger.error(msg)
463
+ return False
722
464
 
723
- >>> search_condition = {
724
- "secret_name":"014_S_6920",
725
- "from_d": "06.02.2025",
726
- "with_child_analysis": 1,
727
- "state": "completed"
728
- }
729
- list_analysis(search_condition=search_condition)
465
+ local_filename = local_filename or file_name
730
466
 
731
- Parameters
732
- ----------
733
- search_condition : dict
734
- - p_n: str or None Analysis name
735
- - type: str or None Type
736
- - from_d: str or None dd.mm.yyyy Date from
737
- - to_d: str or None dd.mm.yyyy Date to
738
- - qa_status: str or None pass/fail/nd QC status
739
- - secret_name: str or None Subject ID
740
- - tags: str or None
741
- - with_child_analysis: 1 or None if 1, child analysis of workflows will appear
742
- - id: str or None ID
743
- - state: running, completed, pending, exception or None
744
- - username: str or None
467
+ if os.path.exists(local_filename) and not overwrite:
468
+ msg = f"File {local_filename} already exists"
469
+ logger.error(msg)
470
+ return False
745
471
 
746
- items : List[int]
747
- list containing two elements [min, max] that correspond to the
748
- mininum and maximum range of analysis listed
472
+ params = {"container_id": container_id, "files": file_name}
749
473
 
750
- Returns
751
- -------
752
- dict
753
- List of analysis, each a dictionary
754
- """
755
- assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
756
- assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
757
- search_keys = {
758
- "p_n": str,
759
- "type": str,
760
- "from_d": str,
761
- "to_d": str,
762
- "qa_status": str,
763
- "secret_name": str,
764
- "tags": str,
765
- "with_child_analysis": int,
766
- "id": int,
767
- "state": str,
768
- "username": str,
769
- }
770
- for key in search_condition.keys():
771
- if key not in search_keys.keys():
772
- raise Exception((f"This key '{key}' is not accepted by this" "search condition"))
773
- if not isinstance(search_condition[key], search_keys[key]) and search_condition[key] is not None:
774
- raise Exception((f"The key {key} in the search condition" f"is not type {search_keys[key]}"))
775
- req_headers = {"X-Range": f"items={items[0]}-{items[1] - 1}"}
776
- return platform.parse_response(
777
- platform.post(
778
- auth=self._account.auth,
779
- endpoint="analysis_manager/get_analysis_list",
780
- headers=req_headers,
781
- data=search_condition,
782
- )
783
- )
474
+ with platform.post(
475
+ self._account.auth, "file_manager/download_file", data=params, stream=True
476
+ ) as response, open(local_filename, "wb") as f:
477
+
478
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
479
+ f.write(chunk)
480
+ f.flush()
481
+
482
+ logger.info(f"File {file_name} from container {container_id} saved to" f" {local_filename}")
483
+ return True
784
484
 
785
- def get_subject_container_id(self, subject_name, ssid):
485
+ def download_files(self, container_id, filenames, zip_name="files.zip", overwrite=False):
786
486
  """
787
- Given a Subject ID and Session ID, return its Container ID.
487
+ Download a set of files from a given container.
788
488
 
789
489
  Parameters
790
490
  ----------
791
- subject_name : str
792
- Subject ID of the subject in the project.
793
- ssid : str
794
- Session ID of the subject in the project.
795
-
796
- Returns
797
- -------
798
- int or bool
799
- The Container ID of the subject in the project, or False if
800
- the subject is not found.
491
+ container_id : int
492
+ ID of the container inside which the file is.
493
+ filenames : list[str]
494
+ List of files to download.
495
+ overwrite : bool
496
+ Whether to overwrite the file if existing.
497
+ zip_name : str
498
+ Name of the zip where the downloaded files are stored.
801
499
  """
500
+ logger = logging.getLogger(logger_name)
802
501
 
803
- search_criteria = {"s_n": subject_name, "ssid": ssid}
804
- response = self.list_input_containers(search_criteria=search_criteria)
502
+ if not all([isinstance(file_name, str) for file_name in filenames]):
503
+ raise ValueError("The name of the files to download (filenames) "
504
+ "should be of type string.")
505
+ if not isinstance(zip_name, str):
506
+ raise ValueError("The name of the output ZIP file (zip_name) "
507
+ "should be of type string.")
805
508
 
806
- for subject in response:
807
- if subject["patient_secret_name"] == subject_name and subject["ssid"] == ssid:
808
- return subject["container_id"]
809
- return False
509
+ files_not_in_container = list(filter(lambda f: f not in self.list_container_files(container_id), filenames))
810
510
 
811
- def list_input_containers(self, search_criteria={}, items=(0, 9999)):
511
+ if files_not_in_container:
512
+ msg = (
513
+ f"The following files are missing in container " f"{container_id}: {', '.join(files_not_in_container)}"
514
+ )
515
+ logger.error(msg)
516
+ return False
517
+
518
+ if os.path.exists(zip_name) and not overwrite:
519
+ msg = f'File "{zip_name}" already exists'
520
+ logger.error(msg)
521
+ return False
522
+
523
+ params = {"container_id": container_id, "files": ";".join(filenames)}
524
+ with platform.post(
525
+ self._account.auth, "file_manager/download_file", data=params, stream=True
526
+ ) as response, open(zip_name, "wb") as f:
527
+
528
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
529
+ f.write(chunk)
530
+ f.flush()
531
+
532
+ logger.info("Files from container {} saved to {}".format(container_id, zip_name))
533
+ return True
534
+
535
+ def copy_container_to_project(self, container_id, project_id):
812
536
  """
813
- Retrieve the list of input containers available to the user under a
814
- certain search criteria.
537
+ Copy a container to another project.
815
538
 
816
539
  Parameters
817
540
  ----------
818
- search_criteria : dict
819
- Each element is a string and is built using the formatting
820
- "type;value".
821
-
822
- List of possible keys:
823
- d_n: container_name # TODO: WHAT IS THIS???
824
- s_n: subject_id
825
- Subject ID of the subject in the platform.
826
- ssid: session_id
827
- Session ID of the subejct in the platform.
828
- from_d: from date
829
- Starting date in which perform the search. Format: DD.MM.YYYY
830
- to_d: to date
831
- End date in which perform the search. Format: DD.MM.YYYY
832
- sets: data sets (modalities) # TODO: WHAT IS THIS???
833
-
834
- items: Tuple(int, int)
835
- Starting and ending element of the search.
541
+ container_id : int
542
+ ID of the container to copy.
543
+ project_id : int or str
544
+ ID of the project to retrieve, either the numeric ID or the name
836
545
 
837
546
  Returns
838
547
  -------
839
- dict
840
- List of containers, each a dictionary containing the following
841
- information:
842
- {"container_name", "container_id", "patient_secret_name", "ssid"}
548
+ bool
549
+ True on success, False on fail
843
550
  """
844
551
 
845
- assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
846
- assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
552
+ if type(project_id) == int or type(project_id) == float:
553
+ p_id = int(project_id)
554
+ elif type(project_id) == str:
555
+ projects = self._account.projects
556
+ projects_match = [proj for proj in projects if proj["name"] == project_id]
557
+ if not projects_match:
558
+ raise Exception(f"Project {project_id}" + " does not exist or is not available for this user.")
559
+ p_id = int(projects_match[0]["id"])
560
+ else:
561
+ raise TypeError("project_id")
562
+ data = {
563
+ "container_id": container_id,
564
+ "project_id": p_id,
565
+ }
847
566
 
848
- response = platform.parse_response(
849
- platform.post(
850
- self._account.auth,
851
- "file_manager/get_container_list",
852
- data=search_criteria,
853
- headers={"X-Range": f"items={items[0]}-{items[1]}"},
567
+ try:
568
+ platform.parse_response(
569
+ platform.post(self._account.auth, "file_manager/copy_container_to_another_project", data=data)
854
570
  )
855
- )
856
- containers = [
857
- {
858
- "patient_secret_name": container_item["patient_secret_name"],
859
- "container_name": container_item["name"],
860
- "container_id": container_item["_id"],
861
- "ssid": container_item["ssid"],
862
- }
863
- for container_item in response
864
- ]
865
- return containers
571
+ except errors.PlatformError as e:
572
+ logging.getLogger(logger_name).error("Couldn not copy container: {}".format(e))
573
+ return False
866
574
 
867
- def list_result_containers(self, search_condition={}, items=(0, 9999)):
575
+ return True
576
+
577
+ """ Subject/Session Related Methods """
578
+ @property
579
+ def subjects(self):
868
580
  """
869
- List the result containers available to the user.
870
- Examples
871
- --------
581
+ Return the list of subject names (Subject ID) from the selected
582
+ project.
872
583
 
873
- >>> search_condition = {
874
- "secret_name":"014_S_6920",
875
- "from_d": "06.02.2025",
876
- "with_child_analysis": 1,
877
- "state": "completed"
878
- }
879
- list_result_containers(search_condition=search_condition)
584
+ :return: a list of subject names
585
+ :rtype: List(Strings)
586
+ """
880
587
 
881
- Parameters
882
- ----------
883
- search_condition : dict
884
- - p_n: str or None Analysis name
885
- - type: str or None Type
886
- - from_d: str or None dd.mm.yyyy Date from
887
- - to_d: str or None dd.mm.yyyy Date to
888
- - qa_status: str or None pass/fail/nd QC status
889
- - secret_name: str or None Subject ID
890
- - tags: str or None
891
- - with_child_analysis: 1 or None if 1, child analysis of workflows will appear
892
- - id: str or None ID
893
- - state: running, completed, pending, exception or None
894
- - username: str or None
895
- items : List[int]
896
- list containing two elements [min, max] that correspond to the
897
- mininum and maximum range of analysis listed
588
+ subjects = self.subjects_metadata
589
+ names = [s["patient_secret_name"] for s in subjects]
590
+ return list(set(names))
591
+
592
+ @property
593
+ def subjects_metadata(self):
594
+ """
595
+ List all subject data from the selected project.
898
596
 
899
597
  Returns
900
598
  -------
901
599
  dict
902
- List of containers, each a dictionary
903
- {"name": "container-name", "id": "container_id"}
904
- if "id": None, that analysis did not had an output container,
905
- probably it is a workflow
600
+ A list of dictionary of {"metadata_name": "metadata_value"}
906
601
  """
907
- analyses = self.list_analysis(search_condition, items)
908
- return [{"name": analysis["name"], "id": (analysis.get("out_container_id") or None)} for analysis in analyses]
602
+ return self.get_subjects_metadata()
909
603
 
910
- def list_container_files(
911
- self,
912
- container_id,
913
- ):
604
+ @property
605
+ def metadata_parameters(self):
914
606
  """
915
- List the name of the files available inside a given container.
916
- Parameters
917
- ----------
918
- container_id : str or int
919
- Container identifier.
607
+ List all the parameters in the subject-level metadata.
608
+
609
+ Each project has a set of parameters that define the subjects-level
610
+ metadata. This function returns all these parameters and its
611
+ properties. New subject-level metadata parameters can be creted in the
612
+ QMENTA Platform via the Metadata Manager. The API only allow
613
+ modification of these subject-level metadata parameters via the
614
+ 'change_subject_metadata()' method.
920
615
 
921
616
  Returns
922
617
  -------
923
- list[str]
924
- List of file names (strings)
618
+ dict[str] -> dict[str] -> x
619
+ dictionary {"param_name":
620
+ { "order": Int,
621
+ "tags": [tag1, tag2, ..., ],
622
+ "title: "Title",
623
+ "type": "integer|string|date|list|decimal",
624
+ "visible": 0|1
625
+ }}
925
626
  """
627
+ logger = logging.getLogger(logger_name)
926
628
  try:
927
- content = platform.parse_response(
928
- platform.post(
929
- self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
930
- )
931
- )
932
- except errors.PlatformError as e:
933
- logging.getLogger(logger_name).error(e)
934
- return False
935
- if "files" not in content.keys():
936
- logging.getLogger(logger_name).error("Could not get files")
937
- return False
938
- return content["files"]
629
+ data = platform.parse_response(platform.post(self._account.auth, "patient_manager/module_config"))
630
+ except errors.PlatformError:
631
+ logger.error("Could not retrieve metadata parameters.")
632
+ return None
633
+ return data["fields"]
939
634
 
940
- def list_container_filter_files(
941
- self,
942
- container_id,
943
- modality="",
944
- metadata_info={},
945
- tags=[]
946
- ):
635
+ def check_subject_name(self, subject_name):
947
636
  """
948
- List the name of the files available inside a given container.
949
- search condition example:
950
- "metadata": {"SliceThickness":1},
951
- }
637
+ Check if a given subject name (Subject ID) exists in the selected
638
+ project.
639
+
952
640
  Parameters
953
641
  ----------
954
- container_id : str or int
955
- Container identifier.
956
-
957
- modality: str
958
- String containing the modality of the files being filtered
959
-
960
- metadata_info: dict
961
- Dictionary containing the file metadata of the files being filtered
962
-
963
- tags: list[str]
964
- List of strings containing the tags of the files being filtered
642
+ subject_name : str
643
+ Subject ID of the subject to check
965
644
 
966
645
  Returns
967
646
  -------
968
- selected_files: list[str]
969
- List of file names (strings)
647
+ bool
648
+ True if subject name exists in project, False otherwise
970
649
  """
971
- content_files = self.list_container_files(container_id)
972
- content_meta = self.list_container_files_metadata(container_id)
973
- selected_files = []
974
- for index, file in enumerate(content_files):
975
- metadata_file = content_meta[index]
976
- tags_file = metadata_file.get("tags")
977
- tags_bool = [tag in tags_file for tag in tags]
978
- info_bool = []
979
- if modality == "":
980
- modality_bool = True
981
- else:
982
- modality_bool = modality == metadata_file["metadata"].get(
983
- "modality"
984
- )
985
- for key in metadata_info.keys():
986
- meta_key = (
987
- (
988
- metadata_file.get("metadata") or {}
989
- ).get("info") or {}).get(
990
- key
991
- )
992
- if meta_key is None:
993
- logging.getLogger(logger_name).warning(
994
- f"{key} is not in file_info from file {file}"
995
- )
996
- info_bool.append(
997
- metadata_info[key] == meta_key
998
- )
999
- if all(tags_bool) and all(info_bool) and modality_bool:
1000
- selected_files.append(file)
1001
- return selected_files
1002
650
 
1003
- def list_container_files_metadata(self, container_id):
651
+ return subject_name in self.subjects
652
+
653
+ def get_subject_container_id(self, subject_name, ssid):
1004
654
  """
1005
- List all the metadata of the files available inside a given container.
655
+ Given a Subject ID and Session ID, return its Container ID.
1006
656
 
1007
657
  Parameters
1008
658
  ----------
1009
- container_id : str
1010
- Container identifier.
659
+ subject_name : str
660
+ Subject ID of the subject in the project.
661
+ ssid : str
662
+ Session ID of the subject in the project.
1011
663
 
1012
664
  Returns
1013
665
  -------
1014
- dict
1015
- Dictionary of {"metadata_name": "metadata_value"}
666
+ int or bool
667
+ The Container ID of the subject in the project, or False if
668
+ the subject is not found.
1016
669
  """
1017
670
 
1018
- try:
1019
- data = platform.parse_response(
1020
- platform.post(
1021
- self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
1022
- )
1023
- )
1024
- except errors.PlatformError as e:
1025
- logging.getLogger(logger_name).error(e)
1026
- return False
671
+ search_criteria = {"s_n": subject_name, "ssid": ssid}
672
+ response = self.list_input_containers(search_criteria=search_criteria)
1027
673
 
1028
- return data["meta"]
674
+ for subject in response:
675
+ if subject["patient_secret_name"] == subject_name and subject["ssid"] == ssid:
676
+ return subject["container_id"]
677
+ return False
1029
678
 
1030
- def get_file_metadata(self, container_id, filename):
679
+ def get_subject_id(self, subject_name, ssid):
1031
680
  """
1032
- Retrieve the metadata from a particular file in a particular container.
681
+ Given a Subject ID and Session ID, return its Patient ID in the
682
+ project.
1033
683
 
1034
684
  Parameters
1035
685
  ----------
1036
- container_id : str
1037
- Container identifier.
1038
- filename : str
1039
- Name of the file.
686
+ subject_name : str
687
+ Subject ID of the subject in the project.
688
+ ssid : str
689
+ Session ID of the subject in the project.
1040
690
 
1041
691
  Returns
1042
692
  -------
1043
- dict
1044
- Dictionary with the metadata. False otherwise.
693
+ int or bool
694
+ The ID of the subject in the project, or False if
695
+ the subject is not found.
1045
696
  """
1046
- all_metadata = self.list_container_files_metadata(container_id)
1047
- if all_metadata:
1048
- for file_meta in all_metadata:
1049
- if file_meta["name"] == filename:
1050
- return file_meta
1051
- else:
1052
- return False
1053
697
 
1054
- def change_file_metadata(self, container_id, filename, modality, tags):
698
+ for user in self.get_subjects_metadata():
699
+ if user["patient_secret_name"] == str(subject_name) and user["ssid"] == str(ssid):
700
+ return int(user["_id"])
701
+ return False
702
+
703
+ def get_subjects_metadata(self, search_criteria={}, items=(0, 9999)):
1055
704
  """
1056
- Change modality and tags of `filename` in `container_id`
705
+ List all Subject ID/Session ID from the selected project that meet the
706
+ defined search criteria at a session level.
1057
707
 
1058
708
  Parameters
1059
709
  ----------
1060
- container_id : int
1061
- Container identifier.
1062
- filename : str
1063
- Name of the file to be edited.
1064
- modality : str or None
1065
- Modality identifier, or None if the file shouldn't have
1066
- any modality
1067
- tags : list[str] or None
1068
- List of tags, or None if the filename shouldn't have any tags
710
+ search_criteria: dict
711
+ Each element is a string and is built using the formatting
712
+ "type;value", or "type;operation|value"
713
+
714
+ Complete search_criteria Dictionary Explanation:
715
+
716
+ search_criteria = {
717
+ "pars_patient_secret_name": "string;SUBJECTID",
718
+ "pars_ssid": "integer;OPERATOR|SSID",
719
+ "pars_modalities": "string;MODALITY",
720
+ "pars_tags": "tags;TAGS",
721
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
722
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
723
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
724
+ }
725
+
726
+ Where:
727
+ "pars_patient_secret_name": Applies the search to the 'Subject ID'.
728
+ SUBJECTID is a comma separated list of strings.
729
+ "pars_ssid": Applies the search to the 'Session ID'.
730
+ SSID is an integer.
731
+ OPERATOR is the operator to apply. One of:
732
+ - Equal: eq
733
+ - Different Than: ne
734
+ - Greater Than: gt
735
+ - Greater/Equal To: gte
736
+ - Lower Than: lt
737
+ - Lower/Equal To: lte
738
+
739
+ "pars_modalities": Applies the search to the file 'Modalities'
740
+ available within each Subject ID.
741
+ MODALITY is a comma separated list of string. A session is provided as
742
+ long as one MODALITY is available.
743
+ "pars_tags": Applies the search to the file 'Tags' available within
744
+ each Subject ID and to the subject-level 'Tags'.
745
+ TAGS is a comma separated list of strings. A session is provided as
746
+ long as one tag is available.
747
+ "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
748
+ field.
749
+ AGE_AT_SCAN is an integer.
750
+ "pars_[dicom]_KEY": Applies the search to the metadata fields
751
+ available within each file. KEY must be one of the
752
+ metadata keys of the files. The full list of KEYS for a given
753
+ file is shown in the QMENTA Platform within the File Information
754
+ of such File.
755
+ KEYTYPE is the type of the KEY. One of:
756
+ - integer
757
+ - decimal
758
+ - string
759
+ - list
760
+
761
+ if 'integer' or 'decimal' you must also include an OPERATOR
762
+ (i.e., "integer;OPERATOR|KEYVALUE").
763
+ if 'list' the KEYVALUE should be a semicolon separated list of
764
+ values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
765
+ KEYVALUEs must be strings.
766
+ KEYVALUE is the expected value of the KEY.
767
+ "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
768
+ within the 'Metadata Manager' of the project.
769
+ PROJECTMETADATA is the ID of the metadata field.
770
+ METADATATYPE is the type of the metadata field. One of:
771
+ - string
772
+ - integer
773
+ - list
774
+ - decimal
775
+ - single_option
776
+ - multiple_option
777
+
778
+ if 'integer' or 'decimal' you must also include an OPERATOR
779
+ (i.e., "integer;OPERATOR|METADATAVALUE").
780
+ KEYVALUE is the expected value of the metadata.
781
+
782
+ 1) Example:
783
+ search_criteria = {
784
+ "pars_patient_secret_name": "string;abide",
785
+ "pars_ssid": "integer;eq|2"
786
+ }
787
+
788
+ 2) Example:
789
+ search_criteria = {
790
+ "pars_modalities": "string;T1",
791
+ "pars_tags": "tags;flair",
792
+ "pars_[dicom]_Manufacturer": "string;ge",
793
+ "pars_[dicom]_FlipAngle": "integer;gt|5",
794
+ "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
795
+ }
796
+
797
+ Note the search criteria applies to all the files included within a
798
+ session. Hence, it does not imply that all the criteria conditions
799
+ are applied to the same files. In example 2) above, it means that
800
+ any Subject ID/Session ID that has a file classified with a 'T1'
801
+ modality, a file with a 'flair' tag, a file whose Manufacturer
802
+ contains 'ge', a file whose FlipAngle is greater than '5º', and a
803
+ file with ImageType with any of the values: PRIMARY or SECONDARY
804
+ will be selected.
805
+
806
+ Returns
807
+ -------
808
+ dict
809
+ A list of dictionary of {"metadata_name": "metadata_value"}
810
+
1069
811
  """
1070
812
 
1071
- tags_str = "" if tags is None else ";".join(tags)
1072
- platform.parse_response(
813
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
814
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
815
+
816
+ assert all([key[:5] == "pars_" for key in search_criteria.keys()]), (
817
+ f"All keys of the search_criteria dictionary " f"'{search_criteria.keys()}' must start with 'pars_'."
818
+ )
819
+
820
+ for key, value in search_criteria.items():
821
+ if value.split(";")[0] in ["integer", "decimal"]:
822
+ assert value.split(";")[1].split("|")[0] in OPERATOR_LIST, (
823
+ f"Search criteria of type '{value.split(';')[0]}' must "
824
+ f"include an operator ({', '.join(OPERATOR_LIST)})."
825
+ )
826
+
827
+ content = platform.parse_response(
1073
828
  platform.post(
1074
829
  self._account.auth,
1075
- "file_manager/edit_file",
1076
- data={"container_id": container_id, "filename": filename, "tags": tags_str, "modality": modality},
830
+ "patient_manager/get_patient_list",
831
+ data=search_criteria,
832
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
1077
833
  )
1078
834
  )
835
+ return content
836
+
837
+ def change_subject_metadata(self, patient_id, subject_name, ssid, tags, age_at_scan, metadata):
838
+ """
839
+ Change the Subject ID, Session ID, Tags, Age at Scan and Metadata of
840
+ the session with Patient ID
841
+
842
+ Parameters
843
+ ----------
844
+ patient_id : Integer
845
+ Patient ID representing the session to modify.
846
+ subject_name : String
847
+ Represents the new Subject ID.
848
+ ssid : String
849
+ Represents the new Session ID.
850
+ tags : list of strings in lowercase
851
+ Represents the new tags of the session.
852
+ age_at_scan : Integer
853
+ Represents the new Age at Scan of the Session.
854
+ metadata : Dictionary
855
+ Each pair key/value representing the new metadata values.
856
+
857
+ The keys must either all start with "md\\_" or none start
858
+ with "md\\_".
1079
859
 
1080
- def download_file(self, container_id, file_name, local_filename=False, overwrite=False):
1081
- """
1082
- Download a single file from a specific container.
860
+ The key represents the ID of the metadata field.
1083
861
 
1084
- Parameters
1085
- ----------
1086
- container_id : str
1087
- ID of the container inside which the file is.
1088
- file_name : str
1089
- Name of the file in the container.
1090
- local_filename : str
1091
- Name of the file to be created. By default, the same as file_name.
1092
- overwrite : bool
1093
- Whether to overwrite the file if existing.
862
+ Returns
863
+ -------
864
+ bool
865
+ True if correctly modified, False otherwise
1094
866
  """
1095
867
  logger = logging.getLogger(logger_name)
1096
- if file_name not in self.list_container_files(container_id):
1097
- msg = f'File "{file_name}" does not exist in container ' f"{container_id}"
1098
- logger.error(msg)
1099
- return False
1100
868
 
1101
- local_filename = local_filename or file_name
869
+ try:
870
+ patient_id = str(int(patient_id))
871
+ except ValueError:
872
+ raise ValueError(f"'patient_id': '{patient_id}' not valid. " f"Must be convertible to int.")
1102
873
 
1103
- if os.path.exists(local_filename) and not overwrite:
1104
- msg = f"File {local_filename} already exists"
1105
- logger.error(msg)
1106
- return False
874
+ assert isinstance(tags, list) and all(
875
+ isinstance(item, str) for item in tags
876
+ ), f"tags: '{tags}' should be a list of strings."
877
+ tags = [tag.lower() for tag in tags]
1107
878
 
1108
- params = {"container_id": container_id, "files": file_name}
879
+ assert subject_name is not None and subject_name != "", "subject_name must be a non empty string."
880
+ assert ssid is not None and ssid != "", "ssid must be a non empty string."
1109
881
 
1110
- with platform.post(
1111
- self._account.auth, "file_manager/download_file", data=params, stream=True
1112
- ) as response, open(local_filename, "wb") as f:
882
+ try:
883
+ age_at_scan = str(int(age_at_scan)) if age_at_scan else None
884
+ except ValueError:
885
+ raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. " f"Must be an integer.")
1113
886
 
1114
- for chunk in response.iter_content(chunk_size=2**9 * 1024):
1115
- f.write(chunk)
1116
- f.flush()
887
+ assert isinstance(metadata, dict), f"metadata: '{metadata}' should be a dictionary."
1117
888
 
1118
- logger.info(f"File {file_name} from container {container_id} saved to" f" {local_filename}")
889
+ assert all("md_" == key[:3] for key in metadata.keys()) or all("md_" != key[:3] for key in metadata.keys()), (
890
+ f"metadata: '{metadata}' must be a dictionary whose keys " f"are either all starting with 'md_' or none."
891
+ )
892
+
893
+ metadata_keys = self.metadata_parameters.keys()
894
+ assert all(
895
+ [key[3:] in metadata_keys if "md_" == key[:3] else key in metadata_keys for key in metadata.keys()]
896
+ ), (
897
+ f"Some metadata keys provided ({', '.join(metadata.keys())}) "
898
+ f"are not available in the project. They can be added via the "
899
+ f"Metadata Manager via the QMENTA Platform graphical user "
900
+ f"interface (GUI)."
901
+ )
902
+
903
+ post_data = {
904
+ "patient_id": patient_id,
905
+ "secret_name": str(subject_name),
906
+ "ssid": str(ssid),
907
+ "tags": ",".join(tags),
908
+ "age_at_scan": age_at_scan,
909
+ }
910
+ for key, value in metadata.items():
911
+ id = key[3:] if "md_" == key[:3] else key
912
+ post_data[f"last_vals.{id}"] = value
913
+
914
+ try:
915
+ platform.parse_response(platform.post(self._account.auth, "patient_manager/upsert_patient", data=post_data))
916
+ except errors.PlatformError:
917
+ logger.error(f"Patient ID '{patient_id}' could not be modified.")
918
+ return False
919
+
920
+ logger.info(f"Patient ID '{patient_id}' successfully modified.")
1119
921
  return True
1120
922
 
1121
- def download_files(self, container_id, filenames, zip_name="files.zip", overwrite=False):
923
+ def get_subjects_files_metadata(self, search_criteria={}, items=(0, 9999)):
1122
924
  """
1123
- Download a set of files from a given container.
925
+ List all Subject ID/Session ID from the selected project that meet the
926
+ defined search criteria at a file level.
927
+
928
+ Note, albeit the search criteria is similar to the one defined in
929
+ method 'get_subjects_metadata()' (see differences below), the
930
+ output is different as this method provides the sessions which
931
+ have a file that satisfy all the conditions of the search criteria.
932
+ This method is slow.
1124
933
 
1125
934
  Parameters
1126
935
  ----------
1127
- container_id : int
1128
- ID of the container inside which the file is.
1129
- filenames : list[str]
1130
- List of files to download.
1131
- overwrite : bool
1132
- Whether to overwrite the file if existing.
1133
- zip_name : str
1134
- Name of the zip where the downloaded files are stored.
1135
- """
1136
- logger = logging.getLogger(logger_name)
1137
- files_not_in_container = list(filter(lambda f: f not in self.list_container_files(container_id), filenames))
936
+ search_criteria: dict
937
+ Each element is a string and is built using the formatting
938
+ "type;value", or "type;operation|value"
1138
939
 
1139
- if files_not_in_container:
1140
- msg = (
1141
- f"The following files are missing in container " f"{container_id}: {', '.join(files_not_in_container)}"
1142
- )
1143
- logger.error(msg)
1144
- return False
940
+ Complete search_criteria Dictionary Explanation:
1145
941
 
1146
- if os.path.exists(zip_name) and not overwrite:
1147
- msg = f'File "{zip_name}" already exists'
1148
- logger.error(msg)
1149
- return False
942
+ search_criteria = {
943
+ "pars_patient_secret_name": "string;SUBJECTID",
944
+ "pars_ssid": "integer;OPERATOR|SSID",
945
+ "pars_modalities": "string;MODALITY",
946
+ "pars_tags": "tags;TAGS",
947
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
948
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
949
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
950
+ }
1150
951
 
1151
- params = {"container_id": container_id, "files": ";".join(filenames)}
1152
- with platform.post(
1153
- self._account.auth, "file_manager/download_file", data=params, stream=True
1154
- ) as response, open(zip_name, "wb") as f:
952
+ Where:
953
+ "pars_patient_secret_name": Applies the search to the 'Subject ID'.
954
+ SUBJECTID is a comma separated list of strings.
955
+ "pars_ssid": Applies the search to the 'Session ID'.
956
+ SSID is an integer.
957
+ OPERATOR is the operator to apply. One of:
958
+ - Equal: eq
959
+ - Different Than: ne
960
+ - Greater Than: gt
961
+ - Greater/Equal To: gte
962
+ - Lower Than: lt
963
+ - Lower/Equal To: lte
1155
964
 
1156
- for chunk in response.iter_content(chunk_size=2**9 * 1024):
1157
- f.write(chunk)
1158
- f.flush()
965
+ "pars_modalities": Applies the search to the file 'Modalities'
966
+ available within each Subject ID.
967
+ MODALITY is a string.
968
+ "pars_tags": Applies only the search to the file 'Tags' available
969
+ within each Subject ID.
970
+ TAGS is a comma separated list of strings. All tags must be present in
971
+ the same file.
972
+ "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
973
+ field.
974
+ AGE_AT_SCAN is an integer.
975
+ "pars_[dicom]_KEY": Applies the search to the metadata fields
976
+ available within each file. KEY must be one of the
977
+ metadata keys of the files. The full list of KEYS for a given
978
+ file is shown in the QMENTA Platform within the File Information
979
+ of such File.
980
+ KEYTYPE is the type of the KEY. One of:
981
+ - integer
982
+ - decimal
983
+ - string
984
+ - list
1159
985
 
1160
- logger.info("Files from container {} saved to {}".format(container_id, zip_name))
1161
- return True
986
+ if 'integer' or 'decimal' you must also include an OPERATOR
987
+ (i.e., "integer;OPERATOR|KEYVALUE").
988
+ if 'list' the KEYVALUE should be a semicolon separated list of
989
+ values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
990
+ KEYVALUEs must be strings.
991
+ KEYVALUE is the expected value of the KEY.
992
+ "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
993
+ within the 'Metadata Manager' of the project.
994
+ PROJECTMETADATA is the ID of the metadata field.
995
+ METADATATYPE is the type of the metadata field. One of:
996
+ - string
997
+ - integer
998
+ - list
999
+ - decimal
1000
+ - single_option
1001
+ - multiple_option
1162
1002
 
1163
- def get_subject_id(self, subject_name, ssid):
1164
- """
1165
- Given a Subject ID and Session ID, return its Patient ID in the
1166
- project.
1003
+ if 'integer' or 'decimal' you must also include an OPERATOR
1004
+ (i.e., "integer;OPERATOR|METADATAVALUE").
1005
+ KEYVALUE is the expected value of the metadata.
1167
1006
 
1168
- Parameters
1169
- ----------
1170
- subject_name : str
1171
- Subject ID of the subject in the project.
1172
- ssid : str
1173
- Session ID of the subject in the project.
1007
+ 1) Example:
1008
+ search_criteria = {
1009
+ "pars_patient_secret_name": "string;abide",
1010
+ "pars_ssid": "integer;eq|2"
1011
+ }
1012
+
1013
+ 2) Example:
1014
+ search_criteria = {
1015
+ "pars_patient_secret_name": "string;001"
1016
+ "pars_modalities": "string;T2",
1017
+ "pars_tags": "tags;flair",
1018
+ "pars_[dicom]_Manufacturer": "string;ge",
1019
+ "pars_[dicom]_FlipAngle": "integer;gt|5",
1020
+ "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
1021
+ }
1022
+
1023
+ Note the search criteria might apply to both the files metadata
1024
+ information available within a session and the metadata of the
1025
+ session. And the method provides a session only if all the file
1026
+ related conditions are satisfied within the same file.
1027
+ In example 2) above, it means that the output will contain any
1028
+ session whose Subject ID contains '001', and there is a file with
1029
+ modality 'T2', tag 'flair', FlipAngle greater than 5º, and
1030
+ ImageType with both values PRIMARY and SECONDARY.
1031
+ Further, the acquisition had to be performed in a Manufacturer
1032
+ containing 'ge'.
1174
1033
 
1175
1034
  Returns
1176
1035
  -------
1177
- int or bool
1178
- The ID of the subject in the project, or False if
1179
- the subject is not found.
1036
+ dict
1037
+ A list of dictionary of {"metadata_name": "metadata_value"}
1038
+
1180
1039
  """
1181
1040
 
1182
- for user in self.get_subjects_metadata():
1183
- if user["patient_secret_name"] == str(subject_name) and user["ssid"] == str(ssid):
1184
- return int(user["_id"])
1185
- return False
1041
+ content = self.get_subjects_metadata(search_criteria, items=(0, 9999))
1042
+
1043
+ # Wrap search criteria.
1044
+ modality, tags, dicoms = self.__wrap_search_criteria(search_criteria)
1045
+
1046
+ # Iterate over the files of each subject selected to include/exclude
1047
+ # them from the results.
1048
+ subjects = list()
1049
+ for subject in content:
1050
+ files = platform.parse_response(platform.post(
1051
+ self._account.auth, "file_manager/get_container_files",
1052
+ data={"container_id": str(int(subject["container_id"]))}
1053
+ ))
1054
+
1055
+ for file in files["meta"]:
1056
+ if modality and \
1057
+ modality != (file.get("metadata") or {}).get("modality"):
1058
+ continue
1059
+ if tags and not all([tag in file.get("tags") for tag in tags]):
1060
+ continue
1061
+ if dicoms:
1062
+ result_values = list()
1063
+ for key, dict_value in dicoms.items():
1064
+ f_value = ((file.get("metadata") or {})
1065
+ .get("info") or {}).get(key)
1066
+ d_operator = dict_value["operation"]
1067
+ d_value = dict_value["value"]
1068
+ result_values.append(
1069
+ self.__operation(d_value, d_operator, f_value)
1070
+ )
1186
1071
 
1187
- def change_subject_metadata(self, patient_id, subject_name, ssid, tags, age_at_scan, metadata):
1072
+ if not all(result_values):
1073
+ continue
1074
+ subjects.append(subject)
1075
+ break
1076
+ return subjects
1077
+
1078
+ def get_file_metadata(self, container_id, filename):
1188
1079
  """
1189
- Change the Subject ID, Session ID, Tags, Age at Scan and Metadata of
1190
- the session with Patient ID
1080
+ Retrieve the metadata from a particular file in a particular container.
1191
1081
 
1192
1082
  Parameters
1193
1083
  ----------
1194
- patient_id : Integer
1195
- Patient ID representing the session to modify.
1196
- subject_name : String
1197
- Represents the new Subject ID.
1198
- ssid : String
1199
- Represents the new Session ID.
1200
- tags : list of strings in lowercase
1201
- Represents the new tags of the session.
1202
- age_at_scan : Integer
1203
- Represents the new Age at Scan of the Session.
1204
- metadata : Dictionary
1205
- Each pair key/value representing the new metadata values.
1206
-
1207
- The keys must either all start with "md\\_" or none start
1208
- with "md\\_".
1209
-
1210
- The key represents the ID of the metadata field.
1084
+ container_id : str
1085
+ Container identifier.
1086
+ filename : str
1087
+ Name of the file.
1211
1088
 
1212
1089
  Returns
1213
1090
  -------
1214
- bool
1215
- True if correctly modified, False otherwise
1091
+ dict
1092
+ Dictionary with the metadata. False otherwise.
1216
1093
  """
1217
- logger = logging.getLogger(logger_name)
1218
-
1219
- try:
1220
- patient_id = str(int(patient_id))
1221
- except ValueError:
1222
- raise ValueError(f"'patient_id': '{patient_id}' not valid. " f"Must be convertible to int.")
1223
-
1224
- assert isinstance(tags, list) and all(
1225
- isinstance(item, str) for item in tags
1226
- ), f"tags: '{tags}' should be a list of strings."
1227
- tags = [tag.lower() for tag in tags]
1228
-
1229
- assert subject_name is not None and subject_name != "", "subject_name must be a non empty string."
1230
- assert ssid is not None and ssid != "", "ssid must be a non empty string."
1231
-
1232
- try:
1233
- age_at_scan = str(int(age_at_scan)) if age_at_scan else None
1234
- except ValueError:
1235
- raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. " f"Must be an integer.")
1094
+ all_metadata = self.list_container_files_metadata(container_id)
1095
+ if all_metadata:
1096
+ for file_meta in all_metadata:
1097
+ if file_meta["name"] == filename:
1098
+ return file_meta
1099
+ else:
1100
+ return False
1236
1101
 
1237
- assert isinstance(metadata, dict), f"metadata: '{metadata}' should be a dictionary."
1102
+ def change_file_metadata(self, container_id, filename, modality, tags):
1103
+ """
1104
+ Change modality and tags of `filename` in `container_id`
1238
1105
 
1239
- assert all("md_" == key[:3] for key in metadata.keys()) or all("md_" != key[:3] for key in metadata.keys()), (
1240
- f"metadata: '{metadata}' must be a dictionary whose keys " f"are either all starting with 'md_' or none."
1241
- )
1106
+ Parameters
1107
+ ----------
1108
+ container_id : int
1109
+ Container identifier.
1110
+ filename : str
1111
+ Name of the file to be edited.
1112
+ modality : str or None
1113
+ Modality identifier, or None if the file shouldn't have
1114
+ any modality
1115
+ tags : list[str] or None
1116
+ List of tags, or None if the filename shouldn't have any tags
1117
+ """
1242
1118
 
1243
- metadata_keys = self.metadata_parameters.keys()
1244
- assert all(
1245
- [key[3:] in metadata_keys if "md_" == key[:3] else key in metadata_keys for key in metadata.keys()]
1246
- ), (
1247
- f"Some metadata keys provided ({', '.join(metadata.keys())}) "
1248
- f"are not available in the project. They can be added via the "
1249
- f"Metadata Manager via the QMENTA Platform graphical user "
1250
- f"interface (GUI)."
1119
+ tags_str = "" if tags is None else ";".join(tags)
1120
+ platform.parse_response(
1121
+ platform.post(
1122
+ self._account.auth,
1123
+ "file_manager/edit_file",
1124
+ data={"container_id": container_id, "filename": filename, "tags": tags_str, "modality": modality},
1125
+ )
1251
1126
  )
1252
1127
 
1253
- post_data = {
1254
- "patient_id": patient_id,
1255
- "secret_name": str(subject_name),
1256
- "ssid": str(ssid),
1257
- "tags": ",".join(tags),
1258
- "age_at_scan": age_at_scan,
1259
- }
1260
- for key, value in metadata.items():
1261
- id = key[3:] if "md_" == key[:3] else key
1262
- post_data[f"last_vals.{id}"] = value
1263
-
1264
- try:
1265
- platform.parse_response(platform.post(self._account.auth, "patient_manager/upsert_patient", data=post_data))
1266
- except errors.PlatformError:
1267
- logger.error(f"Patient ID '{patient_id}' could not be modified.")
1268
- return False
1269
-
1270
- logger.info(f"Patient ID '{patient_id}' successfully modified.")
1271
- return True
1272
-
1273
1128
  def delete_session(self, subject_name, session_id):
1274
1129
  """
1275
1130
  Delete a session from a subject within a project providing the
@@ -1305,424 +1160,420 @@ class Project:
1305
1160
  session = session_to_del[0]
1306
1161
 
1307
1162
  try:
1308
- platform.parse_response(
1309
- platform.post(
1310
- self._account.auth,
1311
- "patient_manager/delete_patient",
1312
- data={"patient_id": str(int(session["_id"])), "delete_files": 1},
1313
- )
1314
- )
1315
- except errors.PlatformError:
1316
- logger.error(f"Session \"{subject_name}/{session['ssid']}\" could" f" not be deleted.")
1317
- return False
1318
-
1319
- logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully " f"deleted.")
1320
- return True
1321
-
1322
- def delete_session_by_patientid(self, patient_id):
1323
- """
1324
- Delete a session from a subject within a project providing the
1325
- Patient ID.
1326
-
1327
- Parameters
1328
- ----------
1329
- patient_id : str
1330
- Patient ID of the Session ID/Subject ID
1331
-
1332
- Returns
1333
- -------
1334
- bool
1335
- True if correctly deleted, False otherwise.
1336
- """
1337
- logger = logging.getLogger(logger_name)
1338
-
1339
- try:
1340
- platform.parse_response(
1341
- platform.post(
1342
- self._account.auth,
1343
- "patient_manager/delete_patient",
1344
- data={"patient_id": str(int(patient_id)), "delete_files": 1},
1345
- )
1346
- )
1347
- except errors.PlatformError:
1348
- logger.error(f"Patient ID {patient_id} could not be deleted.")
1349
- return False
1350
-
1351
- logger.info(f"Patient ID {patient_id} successfully deleted.")
1352
- return True
1353
-
1354
- def delete_subject(self, subject_name):
1355
- """
1356
- Delete a subject from the project. It deletes all its available
1357
- sessions only providing the Subject ID.
1358
-
1359
- Parameters
1360
- ----------
1361
- subject_name : str
1362
- Subject ID of the subject to be deleted.
1363
-
1364
- Returns
1365
- -------
1366
- bool
1367
- True if correctly deleted, False otherwise.
1368
- """
1369
-
1370
- logger = logging.getLogger(logger_name)
1371
- # Always fetch the session IDs from the platform before deleting them
1372
- all_sessions = self.get_subjects_metadata()
1373
-
1374
- sessions_to_del = [s for s in all_sessions if s["patient_secret_name"] == subject_name]
1375
-
1376
- if not sessions_to_del:
1377
- logger.error("Subject {} cannot be found in this project.".format(subject_name))
1378
- return False
1379
-
1380
- for ssid in [s["ssid"] for s in sessions_to_del]:
1381
- if not self.delete_session(subject_name, ssid):
1382
- return False
1383
- return True
1384
-
1385
- def _upload_chunk(
1386
- self,
1387
- data,
1388
- range_str,
1389
- length,
1390
- session_id,
1391
- disposition,
1392
- last_chunk,
1393
- name="",
1394
- date_of_scan="",
1395
- description="",
1396
- subject_name="",
1397
- ssid="",
1398
- filename="DATA.zip",
1399
- input_data_type="mri_brain_data:1.0",
1400
- result=False,
1401
- add_to_container_id=0,
1402
- split_data=False,
1403
- ):
1404
- """
1405
- Upload a chunk of a file to the platform.
1406
-
1407
- Parameters
1408
- ----------
1409
- data
1410
- The file chunk to upload
1411
- range_str
1412
- The string to send that describes the content range
1413
- length
1414
- The content length of the chunk to send
1415
- session_id
1416
- The session ID from the file path
1417
- filename
1418
- The name of the file to be sent
1419
- disposition
1420
- The disposition of the content
1421
- last_chunk
1422
- Set this only for the last chunk to be uploaded.
1423
- All following parameters are ignored when False.
1424
- split_data
1425
- Sets the header that informs the platform to split
1426
- the uploaded file into multiple sessions.
1427
- """
1428
-
1429
- request_headers = {
1430
- "Content-Type": "application/zip",
1431
- "Content-Range": range_str,
1432
- "Session-ID": str(session_id),
1433
- "Content-Length": str(length),
1434
- "Content-Disposition": disposition,
1435
- }
1436
-
1437
- if last_chunk:
1438
- request_headers["X-Mint-Name"] = name
1439
- request_headers["X-Mint-Date"] = date_of_scan
1440
- request_headers["X-Mint-Description"] = description
1441
- request_headers["X-Mint-Patient-Secret"] = subject_name
1442
- request_headers["X-Mint-SSID"] = ssid
1443
- request_headers["X-Mint-Filename"] = filename
1444
- request_headers["X-Mint-Project-Id"] = str(self._project_id)
1445
- request_headers["X-Mint-Split-Data"] = str(int(split_data))
1163
+ platform.parse_response(
1164
+ platform.post(
1165
+ self._account.auth,
1166
+ "patient_manager/delete_patient",
1167
+ data={"patient_id": str(int(session["_id"])), "delete_files": 1},
1168
+ )
1169
+ )
1170
+ except errors.PlatformError:
1171
+ logger.error(f"Session \"{subject_name}/{session['ssid']}\" could" f" not be deleted.")
1172
+ return False
1446
1173
 
1447
- if input_data_type:
1448
- request_headers["X-Mint-Type"] = input_data_type
1174
+ logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully " f"deleted.")
1175
+ return True
1449
1176
 
1450
- if result:
1451
- request_headers["X-Mint-In-Out"] = "out"
1452
- else:
1453
- request_headers["X-Mint-In-Out"] = "in"
1177
+ def delete_session_by_patientid(self, patient_id):
1178
+ """
1179
+ Delete a session from a subject within a project providing the
1180
+ Patient ID.
1454
1181
 
1455
- if add_to_container_id > 0:
1456
- request_headers["X-Mint-Add-To"] = str(add_to_container_id)
1182
+ Parameters
1183
+ ----------
1184
+ patient_id : str
1185
+ Patient ID of the Session ID/Subject ID
1457
1186
 
1458
- request_headers["X-Requested-With"] = "XMLHttpRequest"
1187
+ Returns
1188
+ -------
1189
+ bool
1190
+ True if correctly deleted, False otherwise.
1191
+ """
1192
+ logger = logging.getLogger(logger_name)
1459
1193
 
1460
- response_time = 900.0 if last_chunk else 120.0
1461
- response = platform.post(
1462
- auth=self._account.auth, endpoint="upload", data=data, headers=request_headers, timeout=response_time
1463
- )
1194
+ try:
1195
+ platform.parse_response(
1196
+ platform.post(
1197
+ self._account.auth,
1198
+ "patient_manager/delete_patient",
1199
+ data={"patient_id": str(int(patient_id)), "delete_files": 1},
1200
+ )
1201
+ )
1202
+ except errors.PlatformError:
1203
+ logger.error(f"Patient ID {patient_id} could not be deleted.")
1204
+ return False
1464
1205
 
1465
- return response
1206
+ logger.info(f"Patient ID {patient_id} successfully deleted.")
1207
+ return True
1466
1208
 
1467
- def upload_file(
1468
- self,
1469
- file_path,
1470
- subject_name,
1471
- ssid="",
1472
- date_of_scan="",
1473
- description="",
1474
- result=False,
1475
- name="",
1476
- input_data_type="qmenta_mri_brain_data:1.0",
1477
- add_to_container_id=0,
1478
- chunk_size=2**9,
1479
- split_data=False,
1480
- ):
1209
+ def delete_subject(self, subject_name):
1481
1210
  """
1482
- Upload a ZIP file to the platform.
1211
+ Delete a subject from the project. It deletes all its available
1212
+ sessions only providing the Subject ID.
1483
1213
 
1484
1214
  Parameters
1485
1215
  ----------
1486
- file_path : str
1487
- Path to the ZIP file to upload.
1488
1216
  subject_name : str
1489
- Subject ID of the data to upload in the project in QMENTA Platform.
1490
- ssid : str
1491
- Session ID of the Subject ID (i.e., ID of the timepoint).
1492
- date_of_scan : str
1493
- Date of scan/creation of the file
1494
- description : str
1495
- Description of the file
1496
- result : bool
1497
- If result=True then the upload will be taken as an offline analysis
1498
- name : str
1499
- Name of the file in the platform
1500
- input_data_type : str
1501
- qmenta_medical_image_data:3.11
1502
- add_to_container_id : int
1503
- ID of the container to which this file should be added (if id > 0)
1504
- chunk_size : int
1505
- Size in kB of each chunk. Should be expressed as
1506
- a power of 2: 2**x. Default value of x is 9 (chunk_size = 512 kB)
1507
- split_data : bool
1508
- If True, the platform will try to split the uploaded file into
1509
- different sessions. It will be ignored when the ssid is given.
1217
+ Subject ID of the subject to be deleted.
1510
1218
 
1511
1219
  Returns
1512
1220
  -------
1513
1221
  bool
1514
- True if correctly uploaded, False otherwise.
1222
+ True if correctly deleted, False otherwise.
1515
1223
  """
1516
1224
 
1517
- filename = os.path.split(file_path)[1]
1518
- input_data_type = "offline_analysis:1.0" if result else input_data_type
1519
-
1520
- chunk_size *= 1024
1521
- max_retries = 10
1522
-
1523
- name = name or os.path.split(file_path)[1]
1225
+ logger = logging.getLogger(logger_name)
1226
+ # Always fetch the session IDs from the platform before deleting them
1227
+ all_sessions = self.get_subjects_metadata()
1524
1228
 
1525
- total_bytes = os.path.getsize(file_path)
1229
+ sessions_to_del = [s for s in all_sessions if s["patient_secret_name"] == subject_name]
1526
1230
 
1527
- # making chunks of the file and sending one by one
1528
- logger = logging.getLogger(logger_name)
1529
- with open(file_path, "rb") as file_object:
1231
+ if not sessions_to_del:
1232
+ logger.error("Subject {} cannot be found in this project.".format(subject_name))
1233
+ return False
1530
1234
 
1531
- file_size = os.path.getsize(file_path)
1532
- if file_size == 0:
1533
- logger.error("Cannot upload empty file {}".format(file_path))
1235
+ for ssid in [s["ssid"] for s in sessions_to_del]:
1236
+ if not self.delete_session(subject_name, ssid):
1534
1237
  return False
1535
- uploaded = 0
1536
- session_id = get_session_id(file_path)
1537
- chunk_num = 0
1538
- retries_count = 0
1539
- uploaded_bytes = 0
1540
- response = None
1541
- last_chunk = False
1238
+ return True
1542
1239
 
1543
- if ssid and split_data:
1544
- logger.warning("split-data argument will be ignored because" + " ssid has been specified")
1545
- split_data = False
1240
+ """ Container Related Methods """
1546
1241
 
1547
- while True:
1548
- data = file_object.read(chunk_size)
1549
- if not data:
1550
- break
1242
+ def list_input_containers(self, search_criteria={}, items=(0, 9999)):
1243
+ """
1244
+ Retrieve the list of input containers available to the user under a
1245
+ certain search criteria.
1551
1246
 
1552
- start_position = chunk_num * chunk_size
1553
- end_position = start_position + chunk_size - 1
1554
- bytes_to_send = chunk_size
1247
+ Parameters
1248
+ ----------
1249
+ search_criteria : dict
1250
+ Each element is a string and is built using the formatting
1251
+ "type;value".
1555
1252
 
1556
- if end_position >= total_bytes:
1557
- last_chunk = True
1558
- end_position = total_bytes - 1
1559
- bytes_to_send = total_bytes - uploaded_bytes
1253
+ List of possible keys:
1254
+ d_n: container_name # TODO: WHAT IS THIS???
1255
+ s_n: subject_id
1256
+ Subject ID of the subject in the platform.
1257
+ ssid: session_id
1258
+ Session ID of the subejct in the platform.
1259
+ from_d: from date
1260
+ Starting date in which perform the search. Format: DD.MM.YYYY
1261
+ to_d: to date
1262
+ End date in which perform the search. Format: DD.MM.YYYY
1263
+ sets: data sets (modalities) # TODO: WHAT IS THIS???
1560
1264
 
1561
- bytes_range = "bytes " + str(start_position) + "-" + str(end_position) + "/" + str(total_bytes)
1265
+ items: Tuple(int, int)
1266
+ Starting and ending element of the search.
1562
1267
 
1563
- dispstr = f"attachment; filename={filename}"
1564
- response = self._upload_chunk(
1565
- data,
1566
- bytes_range,
1567
- bytes_to_send,
1568
- session_id,
1569
- dispstr,
1570
- last_chunk,
1571
- name,
1572
- date_of_scan,
1573
- description,
1574
- subject_name,
1575
- ssid,
1576
- filename,
1577
- input_data_type,
1578
- result,
1579
- add_to_container_id,
1580
- split_data,
1581
- )
1268
+ Returns
1269
+ -------
1270
+ dict
1271
+ List of containers, each a dictionary containing the following
1272
+ information:
1273
+ {"container_name", "container_id", "patient_secret_name", "ssid"}
1274
+ """
1582
1275
 
1583
- if response is None:
1584
- retries_count += 1
1585
- time.sleep(retries_count * 5)
1586
- if retries_count > max_retries:
1587
- error_message = "HTTP Connection Problem"
1588
- logger.error(error_message)
1589
- break
1590
- elif int(response.status_code) == 201:
1591
- chunk_num += 1
1592
- retries_count = 0
1593
- uploaded_bytes += chunk_size
1594
- elif int(response.status_code) == 200:
1595
- show_progress(file_size, file_size, finish=True)
1596
- break
1597
- elif int(response.status_code) == 416:
1598
- retries_count += 1
1599
- time.sleep(retries_count * 5)
1600
- if retries_count > self.max_retries:
1601
- error_message = "Error Code: 416; " "Requested Range Not Satisfiable (NGINX)"
1602
- logger.error(error_message)
1603
- break
1604
- else:
1605
- retries_count += 1
1606
- time.sleep(retries_count * 5)
1607
- if retries_count > max_retries:
1608
- error_message = "Number of retries has been reached. " "Upload process stops here !"
1609
- logger.error(error_message)
1610
- break
1276
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
1277
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
1611
1278
 
1612
- uploaded += chunk_size
1613
- show_progress(uploaded, file_size)
1279
+ response = platform.parse_response(
1280
+ platform.post(
1281
+ self._account.auth,
1282
+ "file_manager/get_container_list",
1283
+ data=search_criteria,
1284
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
1285
+ )
1286
+ )
1287
+ containers = [
1288
+ {
1289
+ "patient_secret_name": container_item["patient_secret_name"],
1290
+ "container_name": container_item["name"],
1291
+ "container_id": container_item["_id"],
1292
+ "ssid": container_item["ssid"],
1293
+ }
1294
+ for container_item in response
1295
+ ]
1296
+ return containers
1614
1297
 
1615
- try:
1616
- platform.parse_response(response)
1617
- except errors.PlatformError as error:
1618
- logger.error(error)
1619
- return False
1298
+ def list_result_containers(self, search_condition={}, items=(0, 9999)):
1299
+ """
1300
+ List the result containers available to the user.
1301
+ Examples
1302
+ --------
1620
1303
 
1621
- message = "Your data was successfully uploaded."
1622
- message += "The uploaded file will be soon processed !"
1623
- logger.info(message)
1624
- return True
1304
+ >>> search_condition = {
1305
+ "secret_name":"014_S_6920",
1306
+ "from_d": "06.02.2025",
1307
+ "with_child_analysis": 1,
1308
+ "state": "completed"
1309
+ }
1310
+ list_result_containers(search_condition=search_condition)
1625
1311
 
1626
- def upload_mri(self, file_path, subject_name):
1627
- """
1628
- Upload new MRI data to the subject.
1312
+ Note the keys not needed for the search do not have to be included in
1313
+ the search condition.
1629
1314
 
1630
1315
  Parameters
1631
1316
  ----------
1632
- file_path : str
1633
- Path to the file to upload
1634
- subject_name: str
1317
+ search_condition : dict
1318
+ - p_n: str or None Analysis name
1319
+ - type: str or None Type
1320
+ - from_d: str or None dd.mm.yyyy Date from
1321
+ - to_d: str or None dd.mm.yyyy Date to
1322
+ - qa_status: str or None pass/fail/nd QC status
1323
+ - secret_name: str or None Subject ID
1324
+ - tags: str or None
1325
+ - with_child_analysis: 1 or None if 1, child analysis of workflows will appear
1326
+ - id: str or None ID
1327
+ - state: running, completed, pending, exception or None
1328
+ - username: str or None
1329
+
1330
+ items : List[int]
1331
+ list containing two elements [min, max] that correspond to the
1332
+ mininum and maximum range of analysis listed
1635
1333
 
1636
1334
  Returns
1637
1335
  -------
1638
- bool
1639
- True if upload was correctly done, False otherwise.
1336
+ dict
1337
+ List of containers, each a dictionary
1338
+ {"name": "container-name", "id": "container_id"}
1339
+ if "id": None, that analysis did not had an output container,
1340
+ probably it is a workflow
1640
1341
  """
1342
+ analyses = self.list_analysis(search_condition, items)
1343
+ return [{"name": analysis["name"], "id": (analysis.get("out_container_id") or None)} for analysis in analyses]
1641
1344
 
1642
- if check_upload_file(file_path):
1643
- return self.upload_file(file_path, subject_name)
1345
+ def list_container_files(
1346
+ self,
1347
+ container_id,
1348
+ ):
1349
+ """
1350
+ List the name of the files available inside a given container.
1351
+ Parameters
1352
+ ----------
1353
+ container_id : str or int
1354
+ Container identifier.
1644
1355
 
1645
- def upload_gametection(self, file_path, subject_name):
1356
+ Returns
1357
+ -------
1358
+ list[str]
1359
+ List of file names (strings)
1646
1360
  """
1647
- Upload new Gametection data to the subject.
1361
+ try:
1362
+ content = platform.parse_response(
1363
+ platform.post(
1364
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
1365
+ )
1366
+ )
1367
+ except errors.PlatformError as e:
1368
+ logging.getLogger(logger_name).error(e)
1369
+ return False
1370
+ if "files" not in content.keys():
1371
+ logging.getLogger(logger_name).error("Could not get files")
1372
+ return False
1373
+ return content["files"]
1648
1374
 
1375
+ def list_container_filter_files(
1376
+ self,
1377
+ container_id,
1378
+ modality="",
1379
+ metadata_info={},
1380
+ tags=[]
1381
+ ):
1382
+ """
1383
+ List the name of the files available inside a given container.
1384
+ search condition example:
1385
+ "metadata": {"SliceThickness":1},
1386
+ }
1649
1387
  Parameters
1650
1388
  ----------
1651
- file_path : str
1652
- Path to the file to upload
1653
- subject_name: str
1389
+ container_id : str or int
1390
+ Container identifier.
1391
+
1392
+ modality: str
1393
+ String containing the modality of the files being filtered
1394
+
1395
+ metadata_info: dict
1396
+ Dictionary containing the file metadata of the files being filtered
1397
+
1398
+ tags: list[str]
1399
+ List of strings containing the tags of the files being filtered
1654
1400
 
1655
1401
  Returns
1656
1402
  -------
1657
- bool
1658
- True if upload was correctly done, False otherwise.
1403
+ selected_files: list[str]
1404
+ List of file names (strings)
1659
1405
  """
1406
+ content_files = self.list_container_files(container_id)
1407
+ content_meta = self.list_container_files_metadata(container_id)
1408
+ selected_files = []
1409
+ for index, file in enumerate(content_files):
1410
+ metadata_file = content_meta[index]
1411
+ tags_file = metadata_file.get("tags")
1412
+ tags_bool = [tag in tags_file for tag in tags]
1413
+ info_bool = []
1414
+ if modality == "":
1415
+ modality_bool = True
1416
+ else:
1417
+ modality_bool = modality == metadata_file["metadata"].get(
1418
+ "modality"
1419
+ )
1420
+ for key in metadata_info.keys():
1421
+ meta_key = (
1422
+ (
1423
+ metadata_file.get("metadata") or {}
1424
+ ).get("info") or {}).get(
1425
+ key
1426
+ )
1427
+ if meta_key is None:
1428
+ logging.getLogger(logger_name).warning(
1429
+ f"{key} is not in file_info from file {file}"
1430
+ )
1431
+ info_bool.append(
1432
+ metadata_info[key] == meta_key
1433
+ )
1434
+ if all(tags_bool) and all(info_bool) and modality_bool:
1435
+ selected_files.append(file)
1436
+ return selected_files
1660
1437
 
1661
- if check_upload_file(file_path):
1662
- return self.upload_file(file_path, subject_name, input_data_type="parkinson_gametection")
1663
- return False
1664
-
1665
- def upload_result(self, file_path, subject_name):
1438
+ def list_container_files_metadata(self, container_id):
1666
1439
  """
1667
- Upload new result data to the subject.
1440
+ List all the metadata of the files available inside a given container.
1668
1441
 
1669
1442
  Parameters
1670
1443
  ----------
1671
- file_path : str
1672
- Path to the file to upload
1673
- subject_name: str
1444
+ container_id : str
1445
+ Container identifier.
1674
1446
 
1675
1447
  Returns
1676
1448
  -------
1677
- bool
1678
- True if upload was correctly done, False otherwise.
1449
+ dict
1450
+ Dictionary of {"metadata_name": "metadata_value"}
1679
1451
  """
1680
1452
 
1681
- if check_upload_file(file_path):
1682
- return self.upload_file(file_path, subject_name, result=True)
1683
- return False
1453
+ try:
1454
+ data = platform.parse_response(
1455
+ platform.post(
1456
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
1457
+ )
1458
+ )
1459
+ except errors.PlatformError as e:
1460
+ logging.getLogger(logger_name).error(e)
1461
+ return False
1684
1462
 
1685
- def copy_container_to_project(self, container_id, project_id):
1463
+ return data["meta"]
1464
+
1465
+ """ Analysis Related Methods """
1466
+
1467
+ def get_analysis(self, analysis_name_or_id):
1686
1468
  """
1687
- Copy a container to another project.
1469
+ Returns the analysis corresponding with the analysis id or analysis name
1688
1470
 
1689
1471
  Parameters
1690
1472
  ----------
1691
- container_id : int
1692
- ID of the container to copy.
1693
- project_id : int or str
1694
- ID of the project to retrieve, either the numeric ID or the name
1473
+ analysis_name_or_id : int, str
1474
+ analysis_id or analysis name to search for
1695
1475
 
1696
1476
  Returns
1697
1477
  -------
1698
- bool
1699
- True on success, False on fail
1478
+ analysis: dict
1479
+ A dictionary containing the analysis information
1700
1480
  """
1481
+ if isinstance(analysis_name_or_id, int):
1482
+ search_tag = "id"
1483
+ elif isinstance(analysis_name_or_id, str):
1484
+ if analysis_name_or_id.isdigit():
1485
+ search_tag = "id"
1486
+ analysis_name_or_id = int(analysis_name_or_id)
1487
+ else:
1488
+ search_tag = "p_n"
1489
+ else:
1490
+ raise Exception("The analysis identifier must be its name or an " "integer")
1701
1491
 
1702
- if type(project_id) == int or type(project_id) == float:
1703
- p_id = int(project_id)
1704
- elif type(project_id) == str:
1705
- projects = self._account.projects
1706
- projects_match = [proj for proj in projects if proj["name"] == project_id]
1707
- if not projects_match:
1708
- raise Exception(f"Project {project_id}" + " does not exist or is not available for this user.")
1709
- p_id = int(projects_match[0]["id"])
1492
+ search_condition = {
1493
+ search_tag: analysis_name_or_id,
1494
+ }
1495
+ response = platform.parse_response(
1496
+ platform.post(self._account.auth, "analysis_manager/get_analysis_list", data=search_condition)
1497
+ )
1498
+
1499
+ if len(response) > 1:
1500
+ raise Exception(f"multiple analyses with name " f"{analysis_name_or_id} found")
1501
+ elif len(response) == 1:
1502
+ return response[0]
1710
1503
  else:
1711
- raise TypeError("project_id")
1712
- data = {
1713
- "container_id": container_id,
1714
- "project_id": p_id,
1504
+ return None
1505
+
1506
+ def list_analysis(self, search_condition={}, items=(0, 9999)):
1507
+ """
1508
+ List the analysis available to the user.
1509
+
1510
+ Examples
1511
+ --------
1512
+
1513
+ >>> search_condition = {
1514
+ "secret_name":"014_S_6920",
1515
+ "from_d": "06.02.2025",
1516
+ "with_child_analysis": 1,
1517
+ "state": "completed"
1715
1518
  }
1519
+ list_analysis(search_condition=search_condition)
1716
1520
 
1717
- try:
1718
- platform.parse_response(
1719
- platform.post(self._account.auth, "file_manager/copy_container_to_another_project", data=data)
1521
+ Note the keys not needed for the search do not have to be included in
1522
+ the search condition.
1523
+
1524
+ Parameters
1525
+ ----------
1526
+ search_condition : dict
1527
+ - p_n: str or None Analysis name
1528
+ - type: str or None Type
1529
+ - from_d: str or None dd.mm.yyyy Date from
1530
+ - to_d: str or None dd.mm.yyyy Date to
1531
+ - qa_status: str or None pass/fail/nd QC status
1532
+ - secret_name: str or None Subject ID
1533
+ - tags: str or None
1534
+ - with_child_analysis: 1 or None if 1, child analysis of workflows will appear
1535
+ - id: str or None ID
1536
+ - state: running, completed, pending, exception or None
1537
+ - username: str or None
1538
+
1539
+ items : List[int]
1540
+ list containing two elements [min, max] that correspond to the
1541
+ mininum and maximum range of analysis listed
1542
+
1543
+ Returns
1544
+ -------
1545
+ dict
1546
+ List of analysis, each a dictionary
1547
+ """
1548
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
1549
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
1550
+ search_keys = {
1551
+ "p_n": str,
1552
+ "type": str,
1553
+ "from_d": str,
1554
+ "to_d": str,
1555
+ "qa_status": str,
1556
+ "secret_name": str,
1557
+ "tags": str,
1558
+ "with_child_analysis": int,
1559
+ "id": int,
1560
+ "state": str,
1561
+ "username": str,
1562
+ }
1563
+ for key in search_condition.keys():
1564
+ if key not in search_keys.keys():
1565
+ raise Exception((f"This key '{key}' is not accepted by this" "search condition"))
1566
+ if not isinstance(search_condition[key], search_keys[key]) and search_condition[key] is not None:
1567
+ raise Exception((f"The key {key} in the search condition" f"is not type {search_keys[key]}"))
1568
+ req_headers = {"X-Range": f"items={items[0]}-{items[1] - 1}"}
1569
+ return platform.parse_response(
1570
+ platform.post(
1571
+ auth=self._account.auth,
1572
+ endpoint="analysis_manager/get_analysis_list",
1573
+ headers=req_headers,
1574
+ data=search_condition,
1720
1575
  )
1721
- except errors.PlatformError as e:
1722
- logging.getLogger(logger_name).error("Couldn not copy container: {}".format(e))
1723
- return False
1724
-
1725
- return True
1576
+ )
1726
1577
 
1727
1578
  def start_analysis(
1728
1579
  self,
@@ -1830,94 +1681,7 @@ class Project:
1830
1681
 
1831
1682
  return True
1832
1683
 
1833
- def __handle_start_analysis(self, post_data, ignore_warnings=False, n_calls=0):
1834
- """
1835
- Handle the possible responses from the server after start_analysis.
1836
- Sometimes we have to send a request again, and then check again the
1837
- response. That"s why this function is separated from start_analysis.
1838
-
1839
- Since this function sometimes calls itself, n_calls avoids entering an
1840
- infinite loop due to some misbehaviour in the server.
1841
- """
1842
-
1843
- call_limit = 10
1844
- n_calls += 1
1845
-
1846
- logger = logging.getLogger(logger_name)
1847
- if n_calls > call_limit:
1848
- logger.error(
1849
- f"__handle_start_analysis_response called itself more\
1850
- than {n_calls} times: aborting."
1851
- )
1852
- return None
1853
-
1854
- try:
1855
- response = platform.parse_response(
1856
- platform.post(self._account.auth, "analysis_manager/analysis_registration", data=post_data)
1857
- )
1858
- logger.info(response["message"])
1859
- return int(response["analysis_id"])
1860
- except platform.ChooseDataError as choose_data:
1861
- has_warning = False
1862
-
1863
- # logging any warning that we have
1864
- if choose_data.warning:
1865
- has_warning = True
1866
- logger.warning(response["warning"])
1867
-
1868
- new_post = {
1869
- "analysis_id": choose_data.analysis_id,
1870
- "script_name": post_data["script_name"],
1871
- "version": post_data["version"],
1872
- }
1873
-
1874
- if choose_data.data_to_choose:
1875
- # in case we have data to choose
1876
- chosen_files = {}
1877
- for settings_key in choose_data.data_to_choose:
1878
- chosen_files[settings_key] = {}
1879
- filters = choose_data.data_to_choose[settings_key]["filters"]
1880
- for filter_key in filters:
1881
- filter_data = filters[filter_key]
1882
-
1883
- # skip the filters that did not pass
1884
- if not filter_data["passed"]:
1885
- continue
1886
-
1887
- number_of_files_to_select = 1
1888
- if filter_data["range"][0] != 0:
1889
- number_of_files_to_select = filter_data["range"][0]
1890
- elif filter_data["range"][1] != 0:
1891
- number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
1892
- else:
1893
- number_of_files_to_select = len(filter_data["files"])
1894
-
1895
- files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
1896
- chosen_files[settings_key][filter_key] = files_selection
1897
-
1898
- new_post["user_preference"] = json.dumps(chosen_files)
1899
- else:
1900
- if has_warning and not ignore_warnings:
1901
- logger.info("cancelling analysis due to warnings, " + 'set "ignore_warnings" to True to override')
1902
- new_post["cancel"] = "1"
1903
- else:
1904
- logger.info("suppressing warnings")
1905
- new_post["user_preference"] = "{}"
1906
- new_post["_mint_only_warning"] = "1"
1907
-
1908
- return self.__handle_start_analysis(new_post, ignore_warnings=ignore_warnings, n_calls=n_calls)
1909
- except platform.ActionFailedError as e:
1910
- logger.error(f"Unable to start the analysis: {e}")
1911
- return None
1912
-
1913
- @staticmethod
1914
- def __get_modalities(files):
1915
- modalities = []
1916
- for file_ in files:
1917
- modality = file_["metadata"]["modality"]
1918
- if modality not in modalities:
1919
- modalities.append(modality)
1920
- return modalities
1684
+ """ QC Status Related Methods """
1921
1685
 
1922
1686
  def set_qc_status_analysis(self, analysis_id,
1923
1687
  status=QCStatus.UNDERTERMINED, comments=""):
@@ -2099,6 +1863,7 @@ class Project:
2099
1863
  raise ValueError("Either 'patient_id' or 'subject_name' and 'ssid'"
2100
1864
  " must not be empty.")
2101
1865
 
1866
+ """ Protocol Adherence Related Methods """
2102
1867
  def set_project_pa_rules(self, rules_file_path, guidance_text=""):
2103
1868
  """
2104
1869
  Updates the active project's protocol adherence rules using the
@@ -2183,3 +1948,268 @@ class Project:
2183
1948
  return False
2184
1949
 
2185
1950
  return res["guidance_text"]
1951
+
1952
+ """ Helper Methods """
1953
+ def __handle_start_analysis(self, post_data, ignore_warnings=False, n_calls=0):
1954
+ """
1955
+ Handle the possible responses from the server after start_analysis.
1956
+ Sometimes we have to send a request again, and then check again the
1957
+ response. That"s why this function is separated from start_analysis.
1958
+
1959
+ Since this function sometimes calls itself, n_calls avoids entering an
1960
+ infinite loop due to some misbehaviour in the server.
1961
+ """
1962
+
1963
+ call_limit = 10
1964
+ n_calls += 1
1965
+
1966
+ logger = logging.getLogger(logger_name)
1967
+ if n_calls > call_limit:
1968
+ logger.error(
1969
+ f"__handle_start_analysis_response called itself more\
1970
+ than {n_calls} times: aborting."
1971
+ )
1972
+ return None
1973
+
1974
+ try:
1975
+ response = platform.parse_response(
1976
+ platform.post(self._account.auth, "analysis_manager/analysis_registration", data=post_data)
1977
+ )
1978
+ logger.info(response["message"])
1979
+ return int(response["analysis_id"])
1980
+ except platform.ChooseDataError as choose_data:
1981
+ has_warning = False
1982
+
1983
+ # logging any warning that we have
1984
+ if choose_data.warning:
1985
+ has_warning = True
1986
+ logger.warning(response["warning"])
1987
+
1988
+ new_post = {
1989
+ "analysis_id": choose_data.analysis_id,
1990
+ "script_name": post_data["script_name"],
1991
+ "version": post_data["version"],
1992
+ }
1993
+
1994
+ if choose_data.data_to_choose:
1995
+ # in case we have data to choose
1996
+ chosen_files = {}
1997
+ for settings_key in choose_data.data_to_choose:
1998
+ chosen_files[settings_key] = {}
1999
+ filters = choose_data.data_to_choose[settings_key]["filters"]
2000
+ for filter_key in filters:
2001
+ filter_data = filters[filter_key]
2002
+
2003
+ # skip the filters that did not pass
2004
+ if not filter_data["passed"]:
2005
+ continue
2006
+
2007
+ number_of_files_to_select = 1
2008
+ if filter_data["range"][0] != 0:
2009
+ number_of_files_to_select = filter_data["range"][0]
2010
+ elif filter_data["range"][1] != 0:
2011
+ number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
2012
+ else:
2013
+ number_of_files_to_select = len(filter_data["files"])
2014
+
2015
+ files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
2016
+ chosen_files[settings_key][filter_key] = files_selection
2017
+
2018
+ new_post["user_preference"] = json.dumps(chosen_files)
2019
+ else:
2020
+ if has_warning and not ignore_warnings:
2021
+ logger.info("cancelling analysis due to warnings, " + 'set "ignore_warnings" to True to override')
2022
+ new_post["cancel"] = "1"
2023
+ else:
2024
+ logger.info("suppressing warnings")
2025
+ new_post["user_preference"] = "{}"
2026
+ new_post["_mint_only_warning"] = "1"
2027
+
2028
+ return self.__handle_start_analysis(new_post, ignore_warnings=ignore_warnings, n_calls=n_calls)
2029
+ except platform.ActionFailedError as e:
2030
+ logger.error(f"Unable to start the analysis: {e}")
2031
+ return None
2032
+
2033
+ @staticmethod
2034
+ def __get_modalities(files):
2035
+ modalities = []
2036
+ for file_ in files:
2037
+ modality = file_["metadata"]["modality"]
2038
+ if modality not in modalities:
2039
+ modalities.append(modality)
2040
+ return modalities
2041
+
2042
+ def __show_progress(self, done, total, finish=False):
2043
+ bytes_in_mb = 1024 * 1024
2044
+ progress_message = "\r[{:.2f} %] Uploaded {:.2f} of {:.2f} Mb".format(
2045
+ done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb
2046
+ )
2047
+ sys.stdout.write(progress_message)
2048
+ sys.stdout.flush()
2049
+ if not finish:
2050
+ pass
2051
+ # sys.stdout.write("")
2052
+ # sys.stdout.flush()
2053
+ else:
2054
+ sys.stdout.write("\n")
2055
+
2056
+ def __get_session_id(self, file_path):
2057
+ m = hashlib.md5()
2058
+ m.update(file_path.encode("utf-8"))
2059
+ return str(time.time()).replace(".", "") + "_" + m.hexdigest()
2060
+
2061
+ def __check_upload_file(self, file_path):
2062
+ """
2063
+ Check whether a file has the correct extension to upload.
2064
+
2065
+ Parameters
2066
+ ----------
2067
+ file_path : str
2068
+ Path to the file
2069
+
2070
+ Returns
2071
+ -------
2072
+ bool
2073
+ True if correct extension, False otherwise.
2074
+ """
2075
+
2076
+ # TODO: Add a file zipper here so zips files in a folder
2077
+
2078
+ file_parts = file_path.split(".")
2079
+ extension = file_parts[-1]
2080
+
2081
+ if extension != "zip":
2082
+ logging.getLogger(logger_name).error("You must upload a zip.")
2083
+ return False
2084
+ else:
2085
+ return True
2086
+
2087
+ def __operation(self, reference_value, operator, input_value):
2088
+ """
2089
+ The method performs an operation by comparing the two input values.
2090
+ The Operation is applied to the Input Value in comparison to the Reference
2091
+ Value.
2092
+
2093
+ Parameters
2094
+ ----------
2095
+ reference_value : str, list, or int
2096
+ Reference value.
2097
+ operator : str
2098
+ Operation.
2099
+ input_value : str, list, or int
2100
+ Input value.
2101
+
2102
+ Returns
2103
+ -------
2104
+ bool
2105
+ True if the operation is satisfied, False otherwise.
2106
+ """
2107
+ if input_value is None or input_value == "":
2108
+ return False
2109
+
2110
+ if operator == "in":
2111
+ return reference_value in input_value
2112
+
2113
+ elif operator == "in-list":
2114
+ return all([el in input_value for el in reference_value])
2115
+
2116
+ elif operator == "eq":
2117
+ return input_value == reference_value
2118
+
2119
+ elif operator == "gt":
2120
+ return input_value > reference_value
2121
+
2122
+ elif operator == "gte":
2123
+ return input_value >= reference_value
2124
+
2125
+ elif operator == "lt":
2126
+ return input_value < reference_value
2127
+
2128
+ elif operator == "lte":
2129
+ return input_value <= reference_value
2130
+ else:
2131
+ return False
2132
+
2133
+ def __wrap_search_criteria(self, search_criteria={}):
2134
+ """
2135
+ Wraps the conditions specified within the Search Criteria in order for
2136
+ other methods to handle it easily. The conditions are grouped only into
2137
+ three groups: Modality, Tags and the File Metadata (if DICOM it corresponds
2138
+ to the DICOM information), and each of them is output in a different
2139
+ variable.
2140
+
2141
+ Parameters
2142
+ ----------
2143
+ search_criteria : dict
2144
+ Each element is a string and is built using the formatting
2145
+ "KEYTYPE;VALUE", or "KEYTYPE;OPERATOR|VALUE".
2146
+
2147
+ Full list of keys avaiable for the dictionary:
2148
+
2149
+ search_criteria = {
2150
+ "pars_patient_secret_name": "string;SUBJECTID",
2151
+ "pars_ssid": "integer;OPERATOR|SSID",
2152
+ "pars_modalities": "string;MODALITY",
2153
+ "pars_tags": "tags;TAGS",
2154
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
2155
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
2156
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
2157
+ }
2158
+
2159
+ See documentation for a complete definition.
2160
+
2161
+ Returns
2162
+ -------
2163
+ modality : str
2164
+ String containing the modality of the search criteria extracted from
2165
+ 'pars_modalities'
2166
+
2167
+ tags : list of str
2168
+ List of strings containing the tags of the search criteria extracted
2169
+ 'from pars_tags'
2170
+
2171
+ file_metadata : Dict
2172
+ Dictionary containing the file metadata of the search criteria
2173
+ extracted from 'pars_[dicom]_KEY'
2174
+ """
2175
+
2176
+ # The keys not included bellow apply to the whole session.
2177
+ modality, tags, file_metadata = "", list(), dict()
2178
+ for key, value in search_criteria.items():
2179
+ if key == "pars_modalities":
2180
+ modalities = value.split(";")[1].split(",")
2181
+ if len(modalities) != 1:
2182
+ raise ValueError(f"A file can only have one modality. "
2183
+ f"Provided Modalities: "
2184
+ f"{', '.join(modalities)}.")
2185
+ modality = modalities[0]
2186
+ elif key == "pars_tags":
2187
+ tags = value.split(";")[1].split(",")
2188
+ elif "pars_[dicom]_" in key:
2189
+ d_tag = key.split("pars_[dicom]_")[1]
2190
+ d_type = value.split(";")[0]
2191
+ if d_type == "string":
2192
+ file_metadata[d_tag] = {
2193
+ "operation": "in",
2194
+ "value": value.replace(d_type + ";", "")
2195
+ }
2196
+ elif d_type == "integer":
2197
+ d_operator = value.split(";")[1].split("|")[0]
2198
+ d_value = value.split(";")[1].split("|")[1]
2199
+ file_metadata[d_tag] = {
2200
+ "operation": d_operator,
2201
+ "value": int(d_value)}
2202
+ elif d_type == "decimal":
2203
+ d_operator = value.split(";")[1].split("|")[0]
2204
+ d_value = value.split(";")[1].split("|")[1]
2205
+ file_metadata[d_tag] = {
2206
+ "operation": d_operator,
2207
+ "value": float(d_value)
2208
+ }
2209
+ elif d_type == "list":
2210
+ value.replace(d_type + ";", "")
2211
+ file_metadata[d_tag] = {
2212
+ "operation": "in-list",
2213
+ "value": value.replace(d_type + ";", "").split(";")
2214
+ }
2215
+ return modality, tags, file_metadata