qmenta-client 1.1.dev1289__py3-none-any.whl → 1.1.dev1311__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
@@ -17,12 +17,14 @@ if sys.version_info[0] == 3:
17
17
  unicode = str
18
18
 
19
19
  logger_name = "qmenta.client"
20
+ OPERATOR_LIST = ["eq", "ne", "gt", "gte", "lt", "lte"]
20
21
 
21
22
 
22
23
  def show_progress(done, total, finish=False):
23
24
  bytes_in_mb = 1024 * 1024
24
25
  progress_message = "\r[{:.2f} %] Uploaded {:.2f} of {:.2f} Mb".format(
25
- done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb)
26
+ done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb
27
+ )
26
28
  sys.stdout.write(progress_message)
27
29
  sys.stdout.flush()
28
30
  if not finish:
@@ -66,11 +68,144 @@ def check_upload_file(file_path):
66
68
  return True
67
69
 
68
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 teh search criteria
158
+ exgtracted 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
+
69
203
  class QCStatus(Enum):
70
204
  """
71
205
  Enum with the following options:
72
206
  FAIL, PASS
73
207
  """
208
+
74
209
  PASS = "pass"
75
210
  FAIL = "fail"
76
211
 
@@ -94,15 +229,11 @@ class Project:
94
229
  # project id (int)
95
230
  if isinstance(project_id, str):
96
231
  project_name = project_id
97
- project_id = next(iter(filter(
98
- lambda proj: proj["name"] == project_id, account.projects)
99
- ))["id"]
232
+ project_id = next(iter(filter(lambda proj: proj["name"] == project_id, account.projects)))["id"]
100
233
  else:
101
234
  if isinstance(project_id, float):
102
235
  project_id = int(project_id)
103
- project_name = next(iter(filter(
104
- lambda proj: proj["id"] == project_id, account.projects)
105
- ))["name"]
236
+ project_name = next(iter(filter(lambda proj: proj["id"] == project_id, account.projects)))["name"]
106
237
 
107
238
  self._account = account
108
239
  self._project_id = project_id
@@ -133,11 +264,11 @@ class Project:
133
264
  """
134
265
  logger = logging.getLogger(logger_name)
135
266
  try:
136
- platform.parse_response(platform.post(
137
- self._account.auth,
138
- "projectset_manager/activate_project",
139
- data={"project_id": int(project_id)}
140
- ))
267
+ platform.parse_response(
268
+ platform.post(
269
+ self._account.auth, "projectset_manager/activate_project", data={"project_id": int(project_id)}
270
+ )
271
+ )
141
272
  except errors.PlatformError:
142
273
  logger.error("Unable to activate the project.")
143
274
  return False
@@ -164,8 +295,8 @@ class Project:
164
295
 
165
296
  def get_subjects_metadata(self, search_criteria={}, items=(0, 9999)):
166
297
  """
167
- List all subjects data from the selected project that meet the defined
168
- search criteria.
298
+ List all Subject ID/Session ID from the selected project that meet the
299
+ defined search criteria at a session level.
169
300
 
170
301
  Parameters
171
302
  ----------
@@ -200,10 +331,12 @@ class Project:
200
331
 
201
332
  "pars_modalities": Applies the search to the file 'Modalities'
202
333
  available within each Subject ID.
203
- MODALITY is a comma separated list of string.
334
+ MODALITY is a comma separated list of string. A session is provided as
335
+ long as one MODALITY is available.
204
336
  "pars_tags": Applies the search to the file 'Tags' available within
205
337
  each Subject ID and to the subject-level 'Tags'.
206
- TAGS is a comma separated list of strings.
338
+ TAGS is a comma separated list of strings. A session is provided as
339
+ long as one tag is available.
207
340
  "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
208
341
  field.
209
342
  AGE_AT_SCAN is an integer.
@@ -213,11 +346,15 @@ class Project:
213
346
  'file_m["metadata"]["info"].keys()'.
214
347
  KEYTYPE is the type of the KEY. One of:
215
348
  - integer
349
+ - decimal
216
350
  - string
217
351
  - list
218
352
 
219
- if 'integer' you must also include an OPERATOR
353
+ if 'integer' or 'decimal' you must also include an OPERATOR
220
354
  (i.e., "integer;OPERATOR|KEYVALUE").
355
+ if 'list' the KEYVALUE should be a semicolon separated list of
356
+ values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
357
+ KEYVALUEs must be strings.
221
358
  KEYVALUE is the expected value of the KEY.
222
359
  "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
223
360
  within the 'Metadata Manager' of the project.
@@ -246,6 +383,7 @@ class Project:
246
383
  "pars_tags": "tags;flair",
247
384
  "pars_[dicom]_Manufacturer": "string;ge",
248
385
  "pars_[dicom]_FlipAngle": "integer;gt|5",
386
+ "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
249
387
  }
250
388
 
251
389
  Note the search criteria applies to all the files included within a
@@ -253,8 +391,9 @@ class Project:
253
391
  are applied to the same files. In example 2) above, it means that
254
392
  any Subject ID/Session ID that has a file classified with a 'T1'
255
393
  modality, a file with a 'flair' tag, a file whose Manufacturer
256
- contains 'ge', and a file whose FlipAngle is greater than '5º' will
257
- be selected.
394
+ contains 'ge', a file whose FlipAngle is greater than '5º', and a
395
+ file with ImageType with any of the values: PRIMARY or SECONDARY
396
+ will be selected.
258
397
 
259
398
  Returns
260
399
  -------
@@ -263,29 +402,184 @@ class Project:
263
402
 
264
403
  """
265
404
 
266
- assert len(items) == 2, f"The number of elements in items " \
267
- f"'{len(items)}' should be equal to two."
268
- assert all([isinstance(item, int) for item in items]), \
269
- f"All items elements '{items}' should be integers."
405
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
406
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
270
407
 
271
- assert all([key[:5] == "pars_" for key in search_criteria.keys()]), \
272
- f"All keys of the search_criteria dictionary " \
273
- f"'{search_criteria.keys()}' must start with 'pars_'."
408
+ assert all([key[:5] == "pars_" for key in search_criteria.keys()]), (
409
+ f"All keys of the search_criteria dictionary " f"'{search_criteria.keys()}' must start with 'pars_'."
410
+ )
274
411
 
275
- operator_list = ["eq", "ne", "gt", "gte", "lt", "lte"]
276
412
  for key, value in search_criteria.items():
277
413
  if value.split(";")[0] in ["integer", "decimal"]:
278
- assert value.split(";")[1].split("|")[0] in operator_list, \
279
- f"Search criteria of type '{value.split(';')[0]}' must " \
280
- f"include an operator ({', '.join(operator_list)})."
281
-
282
- content = platform.parse_response(platform.post(
283
- self._account.auth, "patient_manager/get_patient_list",
284
- data=search_criteria,
285
- headers={"X-Range": f"items={items[0]}-{items[1]}"}
286
- ))
414
+ assert value.split(";")[1].split("|")[0] in OPERATOR_LIST, (
415
+ f"Search criteria of type '{value.split(';')[0]}' must "
416
+ f"include an operator ({', '.join(OPERATOR_LIST)})."
417
+ )
418
+
419
+ content = platform.parse_response(
420
+ platform.post(
421
+ self._account.auth,
422
+ "patient_manager/get_patient_list",
423
+ data=search_criteria,
424
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
425
+ )
426
+ )
287
427
  return content
288
428
 
429
+ def get_subjects_files_metadata(self, search_criteria={}, items=(0, 9999)):
430
+ """
431
+ List all Subject ID/Session ID from the selected project that meet the
432
+ defined search criteria at a file level.
433
+
434
+ Note, albeit the search criteria is similar to the one defined in
435
+ method 'get_subjects_metadata()' (see differences below), the
436
+ output is different as this method provides the sessions which
437
+ have a file that satisfy all the conditions of the search criteria.
438
+ This method is slow.
439
+
440
+ Parameters
441
+ ----------
442
+ search_criteria: dict
443
+ Each element is a string and is built using the formatting
444
+ "type;value", or "type;operation|value"
445
+
446
+ Complete search_criteria Dictionary Explanation:
447
+
448
+ search_criteria = {
449
+ "pars_patient_secret_name": "string;SUBJECTID",
450
+ "pars_ssid": "integer;OPERATOR|SSID",
451
+ "pars_modalities": "string;MODALITY",
452
+ "pars_tags": "tags;TAGS",
453
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
454
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
455
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
456
+ }
457
+
458
+ Where:
459
+ "pars_patient_secret_name": Applies the search to the 'Subject ID'.
460
+ SUBJECTID is a comma separated list of strings.
461
+ "pars_ssid": Applies the search to the 'Session ID'.
462
+ SSID is an integer.
463
+ OPERATOR is the operator to apply. One of:
464
+ - Equal: eq
465
+ - Different Than: ne
466
+ - Greater Than: gt
467
+ - Greater/Equal To: gte
468
+ - Lower Than: lt
469
+ - Lower/Equal To: lte
470
+
471
+ "pars_modalities": Applies the search to the file 'Modalities'
472
+ available within each Subject ID.
473
+ MODALITY is a string.
474
+ "pars_tags": Applies only the search to the file 'Tags' available
475
+ within each Subject ID.
476
+ TAGS is a comma separated list of strings. All tags must be present in
477
+ the same file.
478
+ "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
479
+ field.
480
+ AGE_AT_SCAN is an integer.
481
+ "pars_[dicom]_KEY": Applies the search to the metadata fields
482
+ available within each file. KEY must be one of the
483
+ metadata keys of the files. The full list of KEYS is shown above via
484
+ 'file_m["metadata"]["info"].keys()'. # TODO: SEE HOW TO WRITE THIS
485
+ KEYTYPE is the type of the KEY. One of:
486
+ - integer
487
+ - decimal
488
+ - string
489
+ - list
490
+
491
+ if 'integer' or 'decimal' you must also include an OPERATOR
492
+ (i.e., "integer;OPERATOR|KEYVALUE").
493
+ if 'list' the KEYVALUE should be a semicolon separated list of
494
+ values. (i.e., "list;KEYVALUE1;KEYVALUE2;KEYVALUE3)
495
+ KEYVALUEs must be strings.
496
+ KEYVALUE is the expected value of the KEY.
497
+ "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
498
+ within the 'Metadata Manager' of the project.
499
+ PROJECTMETADATA is the ID of the metadata field.
500
+ METADATATYPE is the type of the metadata field. One of:
501
+ - string
502
+ - integer
503
+ - list
504
+ - decimal
505
+ - single_option
506
+ - multiple_option
507
+
508
+ if 'integer' or 'decimal' you must also include an OPERATOR
509
+ (i.e., "integer;OPERATOR|METADATAVALUE").
510
+ KEYVALUE is the expected value of the metadata.
511
+
512
+ 1) Example:
513
+ search_criteria = {
514
+ "pars_patient_secret_name": "string;abide",
515
+ "pars_ssid": "integer;eq|2"
516
+ }
517
+
518
+ 2) Example:
519
+ search_criteria = {
520
+ "pars_patient_secret_name": "string;001"
521
+ "pars_modalities": "string;T2",
522
+ "pars_tags": "tags;flair",
523
+ "pars_[dicom]_Manufacturer": "string;ge",
524
+ "pars_[dicom]_FlipAngle": "integer;gt|5",
525
+ "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
526
+ }
527
+
528
+ Note the search criteria might apply to both the files metadata
529
+ information available within a session and the metadata of the
530
+ session. And the method provides a session only if all the file
531
+ related conditions are satisfied within the same file.
532
+ In example 2) above, it means that the output will contain any
533
+ session whose Subject ID contains '001', and there is a file with
534
+ modality 'T2', tag 'flair', FlipAngle greater than 5º, and
535
+ ImageType with both values PRIMARY and SECONDARY.
536
+ Further, the acquisition had to be performed in a Manufacturer
537
+ containing 'ge'.
538
+
539
+ Returns
540
+ -------
541
+ dict
542
+ A list of dictionary of {"metadata_name": "metadata_value"}
543
+
544
+ """
545
+
546
+ content = self.get_subjects_metadata(search_criteria, items=(0, 9999))
547
+
548
+ # Wrap search criteria.
549
+ modality, tags, dicoms = wrap_search_criteria(search_criteria)
550
+
551
+ # Iterate over the files of each subject selected to include/exclude
552
+ # them from the results.
553
+ subjects = list()
554
+ for subject in content:
555
+ files = platform.parse_response(platform.post(
556
+ self._account.auth, "file_manager/get_container_files",
557
+ data={"container_id": str(int(subject["container_id"]))}
558
+ ))
559
+
560
+ for file in files["meta"]:
561
+ if modality and \
562
+ modality != (file.get("metadata") or {}).get("modality"):
563
+ continue
564
+ if tags and not all([tag in file.get("tags") for tag in tags]):
565
+ continue
566
+ if dicoms:
567
+ result_values = list()
568
+ for key, dict_value in dicoms.items():
569
+ f_value = ((file.get("metadata") or {})
570
+ .get("info") or {}).get(key)
571
+ d_operator = dict_value["operation"]
572
+ d_value = dict_value["value"]
573
+ result_values.append(
574
+ operation(d_value, d_operator, f_value)
575
+ )
576
+
577
+ if not all(result_values):
578
+ continue
579
+ subjects.append(subject)
580
+ break
581
+ return subjects
582
+
289
583
  @property
290
584
  def subjects(self):
291
585
  """
@@ -321,10 +615,14 @@ class Project:
321
615
  @property
322
616
  def metadata_parameters(self):
323
617
  """
324
- List all the parameters in the subject metadata.
618
+ List all the parameters in the subject-level metadata.
325
619
 
326
- Each project has a set of parameters that define the subjects metadata.
327
- This function returns all these parameters and its properties.
620
+ Each project has a set of parameters that define the subjects-level
621
+ metadata. This function returns all these parameters and its
622
+ properties. New subject-level metadata parameters can be creted in the
623
+ QMENTA Platform via the Metadata Manager. The API only allow
624
+ modification of these subject-level metadata parameters via the
625
+ 'change_subject_metadata()' method.
328
626
 
329
627
  Returns
330
628
  -------
@@ -339,83 +637,29 @@ class Project:
339
637
  """
340
638
  logger = logging.getLogger(logger_name)
341
639
  try:
342
- data = platform.parse_response(platform.post(
343
- self._account.auth, "patient_manager/module_config"
344
- ))
640
+ data = platform.parse_response(platform.post(self._account.auth, "patient_manager/module_config"))
345
641
  except errors.PlatformError:
346
642
  logger.error("Could not retrieve metadata parameters.")
347
643
  return None
348
644
  return data["fields"]
349
645
 
350
- def add_metadata_parameter(self, title, param_id=None,
351
- param_type="string", visible=False):
352
- """
353
- Add a metadata parameter to the project.
354
-
355
- Parameters
356
- ----------
357
- title : str
358
- Identifier of this new parameter
359
- param_id : str
360
- Title of this new parameter
361
- param_type : str
362
- Type of the parameter. One of:
363
- "integer", "date", "string", "list", "decimal"
364
- visible : bool
365
- whether the parameter will be visible in the table of patients
366
-
367
- Returns
368
- -------
369
- bool
370
- True if parameter was correctly added, False otherwise.
371
- """
372
- # use param_id equal to title if param_id is not provided
373
- param_id = param_id or title
374
-
375
- param_properties = [title, param_id, param_type, str(int(visible))]
376
-
377
- post_data = {"add": "|".join(param_properties),
378
- "edit": "",
379
- "delete": ""
380
- }
381
-
382
- logger = logging.getLogger(logger_name)
383
- try:
384
- answer = platform.parse_response(platform.post(
385
- self._account.auth,
386
- "patient_manager/save_metadata_changes",
387
- data=post_data
388
- ))
389
- except errors.PlatformError:
390
- answer = {}
391
-
392
- if title not in answer:
393
- logger.error(f"Could not add new parameter: {title}")
394
- return False
395
-
396
- logger.info("New parameter added:", title, param_properties)
397
- return True
398
-
399
646
  def get_analysis(self, analysis_name_or_id):
400
647
  if isinstance(analysis_name_or_id, int):
401
648
  search_tag = "id"
402
649
  elif isinstance(analysis_name_or_id, str):
403
650
  search_tag = "p_n"
404
651
  else:
405
- raise Exception("The analysis identifier must be its name or an "
406
- "integer")
652
+ raise Exception("The analysis identifier must be its name or an " "integer")
407
653
 
408
654
  search_condition = {
409
655
  search_tag: analysis_name_or_id,
410
656
  }
411
- response = platform.parse_response(platform.post(
412
- self._account.auth, "analysis_manager/get_analysis_list",
413
- data=search_condition
414
- ))
657
+ response = platform.parse_response(
658
+ platform.post(self._account.auth, "analysis_manager/get_analysis_list", data=search_condition)
659
+ )
415
660
 
416
661
  if len(response) > 1:
417
- raise Exception(f"multiple analyses with name "
418
- f"{analysis_name_or_id} found")
662
+ raise Exception(f"multiple analyses with name " f"{analysis_name_or_id} found")
419
663
  elif len(response) == 1:
420
664
  return response[0]
421
665
  else:
@@ -455,10 +699,8 @@ class Project:
455
699
  dict
456
700
  List of analysis, each a dictionary
457
701
  """
458
- assert len(items) == 2, f"The number of elements in items " \
459
- f"'{len(items)}' should be equal to two."
460
- assert all([isinstance(item, int) for item in items]), \
461
- f"All items elements '{items}' should be integers."
702
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
703
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
462
704
  search_keys = {
463
705
  "p_n": str,
464
706
  "type": str,
@@ -474,26 +716,18 @@ class Project:
474
716
  }
475
717
  for key in search_condition.keys():
476
718
  if key not in search_keys.keys():
477
- raise Exception(
478
- (
479
- f"This key '{key}' is not accepted by this"
480
- "search condition"
481
- )
482
- )
719
+ raise Exception((f"This key '{key}' is not accepted by this" "search condition"))
483
720
  if not isinstance(search_condition[key], search_keys[key]):
484
- raise Exception(
485
- (
486
- f"The key {key} in the search condition"
487
- f"is not type {search_keys[key]}"
488
- )
489
- )
721
+ raise Exception((f"The key {key} in the search condition" f"is not type {search_keys[key]}"))
490
722
  req_headers = {"X-Range": f"items={items[0]}-{items[1] - 1}"}
491
- return platform.parse_response(platform.post(
492
- auth=self._account.auth,
493
- endpoint="analysis_manager/get_analysis_list",
494
- headers=req_headers,
495
- data=search_condition
496
- ))
723
+ return platform.parse_response(
724
+ platform.post(
725
+ auth=self._account.auth,
726
+ endpoint="analysis_manager/get_analysis_list",
727
+ headers=req_headers,
728
+ data=search_condition,
729
+ )
730
+ )
497
731
 
498
732
  def get_subject_container_id(self, subject_name, ssid):
499
733
  """
@@ -513,17 +747,11 @@ class Project:
513
747
  the subject is not found.
514
748
  """
515
749
 
516
- search_criteria = {
517
- "s_n": subject_name,
518
- "ssid": ssid
519
- }
520
- response = self.list_input_containers(
521
- search_criteria=search_criteria
522
- )
750
+ search_criteria = {"s_n": subject_name, "ssid": ssid}
751
+ response = self.list_input_containers(search_criteria=search_criteria)
523
752
 
524
753
  for subject in response:
525
- if subject["patient_secret_name"] == subject_name and \
526
- subject["ssid"] == ssid:
754
+ if subject["patient_secret_name"] == subject_name and subject["ssid"] == ssid:
527
755
  return subject["container_id"]
528
756
  return False
529
757
 
@@ -561,16 +789,17 @@ class Project:
561
789
  {"container_name", "container_id", "patient_secret_name", "ssid"}
562
790
  """
563
791
 
564
- assert len(items) == 2, f"The number of elements in items " \
565
- f"'{len(items)}' should be equal to two."
566
- assert all([isinstance(item, int) for item in items]), \
567
- f"All items elements '{items}' should be integers."
792
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
793
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
568
794
 
569
- response = platform.parse_response(platform.post(
570
- self._account.auth, "file_manager/get_container_list",
571
- data=search_criteria,
572
- headers={"X-Range": f"items={items[0]}-{items[1]}"}
573
- ))
795
+ response = platform.parse_response(
796
+ platform.post(
797
+ self._account.auth,
798
+ "file_manager/get_container_list",
799
+ data=search_criteria,
800
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
801
+ )
802
+ )
574
803
  containers = [
575
804
  {
576
805
  "patient_secret_name": container_item["patient_secret_name"],
@@ -598,8 +827,7 @@ class Project:
598
827
  {"name": "container-name", "id": "container_id"}
599
828
  """
600
829
  analysis = self.list_analysis(limit)
601
- return [{"name": a["name"],
602
- "id": a["out_container_id"]} for a in analysis]
830
+ return [{"name": a["name"], "id": a["out_container_id"]} for a in analysis]
603
831
 
604
832
  def list_container_files(self, container_id):
605
833
  """
@@ -616,10 +844,11 @@ class Project:
616
844
  List of file names (strings)
617
845
  """
618
846
  try:
619
- content = platform.parse_response(platform.post(
620
- self._account.auth, "file_manager/get_container_files",
621
- data={"container_id": container_id}
622
- ))
847
+ content = platform.parse_response(
848
+ platform.post(
849
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
850
+ )
851
+ )
623
852
  except errors.PlatformError as e:
624
853
  logging.getLogger(logger_name).error(e)
625
854
  return False
@@ -646,10 +875,11 @@ class Project:
646
875
  """
647
876
 
648
877
  try:
649
- data = platform.parse_response(platform.post(
650
- self._account.auth, "file_manager/get_container_files",
651
- data={"container_id": container_id}
652
- ))
878
+ data = platform.parse_response(
879
+ platform.post(
880
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
881
+ )
882
+ )
653
883
  except errors.PlatformError as e:
654
884
  logging.getLogger(logger_name).error(e)
655
885
  return False
@@ -698,18 +928,15 @@ class Project:
698
928
  """
699
929
 
700
930
  tags_str = "" if tags is None else ";".join(tags)
701
- platform.parse_response(platform.post(
702
- self._account.auth, "file_manager/edit_file",
703
- data={
704
- "container_id": container_id,
705
- "filename": filename,
706
- "tags": tags_str,
707
- "modality": modality
708
- }
709
- ))
931
+ platform.parse_response(
932
+ platform.post(
933
+ self._account.auth,
934
+ "file_manager/edit_file",
935
+ data={"container_id": container_id, "filename": filename, "tags": tags_str, "modality": modality},
936
+ )
937
+ )
710
938
 
711
- def download_file(self, container_id, file_name, local_filename=False,
712
- overwrite=False):
939
+ def download_file(self, container_id, file_name, local_filename=False, overwrite=False):
713
940
  """
714
941
  Download a single file from a specific container.
715
942
 
@@ -726,8 +953,7 @@ class Project:
726
953
  """
727
954
  logger = logging.getLogger(logger_name)
728
955
  if file_name not in self.list_container_files(container_id):
729
- msg = (f"File \"{file_name}\" does not exist in container "
730
- f"{container_id}")
956
+ msg = f'File "{file_name}" does not exist in container ' f"{container_id}"
731
957
  logger.error(msg)
732
958
  return False
733
959
 
@@ -740,22 +966,18 @@ class Project:
740
966
 
741
967
  params = {"container_id": container_id, "files": file_name}
742
968
 
743
- with platform.post(self._account.auth, "file_manager/download_file",
744
- data=params, stream=True) as response, \
745
- open(local_filename, "wb") as f:
969
+ with platform.post(
970
+ self._account.auth, "file_manager/download_file", data=params, stream=True
971
+ ) as response, open(local_filename, "wb") as f:
746
972
 
747
- for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
973
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
748
974
  f.write(chunk)
749
975
  f.flush()
750
976
 
751
- logger.info(
752
- f"File {file_name} from container {container_id} saved to"
753
- f" {local_filename}"
754
- )
977
+ logger.info(f"File {file_name} from container {container_id} saved to" f" {local_filename}")
755
978
  return True
756
979
 
757
- def download_files(self, container_id, filenames, zip_name="files.zip",
758
- overwrite=False):
980
+ def download_files(self, container_id, filenames, zip_name="files.zip", overwrite=False):
759
981
  """
760
982
  Download a set of files from a given container.
761
983
 
@@ -771,34 +993,30 @@ class Project:
771
993
  Name of the zip where the downloaded files are stored.
772
994
  """
773
995
  logger = logging.getLogger(logger_name)
774
- files_not_in_container = list(
775
- filter(lambda f: f not in self.list_container_files(container_id),
776
- filenames)
777
- )
996
+ files_not_in_container = list(filter(lambda f: f not in self.list_container_files(container_id), filenames))
778
997
 
779
998
  if files_not_in_container:
780
- msg = (f"The following files are missing in container "
781
- f"{container_id}: {', '.join(files_not_in_container)}")
999
+ msg = (
1000
+ f"The following files are missing in container " f"{container_id}: {', '.join(files_not_in_container)}"
1001
+ )
782
1002
  logger.error(msg)
783
1003
  return False
784
1004
 
785
1005
  if os.path.exists(zip_name) and not overwrite:
786
- msg = f"File \"{zip_name}\" already exists"
1006
+ msg = f'File "{zip_name}" already exists'
787
1007
  logger.error(msg)
788
1008
  return False
789
1009
 
790
1010
  params = {"container_id": container_id, "files": ";".join(filenames)}
791
- with platform.post(self._account.auth,
792
- "file_manager/download_file",
793
- data=params, stream=True) as response, \
794
- open(zip_name, "wb") as f:
1011
+ with platform.post(
1012
+ self._account.auth, "file_manager/download_file", data=params, stream=True
1013
+ ) as response, open(zip_name, "wb") as f:
795
1014
 
796
- for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
1015
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
797
1016
  f.write(chunk)
798
1017
  f.flush()
799
1018
 
800
- logger.info("Files from container {} saved to {}".format(
801
- container_id, zip_name))
1019
+ logger.info("Files from container {} saved to {}".format(container_id, zip_name))
802
1020
  return True
803
1021
 
804
1022
  def get_subject_id(self, subject_name, ssid):
@@ -821,48 +1039,11 @@ class Project:
821
1039
  """
822
1040
 
823
1041
  for user in self.get_subjects_metadata():
824
- if user["patient_secret_name"] == str(subject_name) and \
825
- user["ssid"] == str(ssid):
1042
+ if user["patient_secret_name"] == str(subject_name) and user["ssid"] == str(ssid):
826
1043
  return int(user["_id"])
827
1044
  return False
828
1045
 
829
- def add_subject(self, subject):
830
- """
831
- Add a subject to the project.
832
-
833
- Parameters
834
- ----------
835
- subject : Subject
836
- Instance of Subject representing the subject to add.
837
-
838
- Returns
839
- -------
840
- bool
841
- True if correctly added, False otherwise
842
- """
843
- logger = logging.getLogger(logger_name)
844
- if self.check_subject_name(subject.name):
845
- logger.error(f"Subject with name {subject.name} already exists in "
846
- f"project!")
847
- return False
848
-
849
- try:
850
- platform.parse_response(platform.post(
851
- self._account.auth, "patient_manager/upsert_patient",
852
- data={"secret_name": subject.name}
853
- ))
854
- except errors.PlatformError:
855
- logger.error(f"Subject {subject.name} could not be created.")
856
- return False
857
-
858
- subject.subject_id = self.get_subject_id(subject.name)
859
- subject.project = self
860
- logger.info(
861
- "Subject {0} was successfully created".format(subject.name))
862
- return True
863
-
864
- def change_subject_metadata(self, patient_id, subject_name, ssid, tags,
865
- age_at_scan, metadata):
1046
+ def change_subject_metadata(self, patient_id, subject_name, ssid, tags, age_at_scan, metadata):
866
1047
  """
867
1048
  Change the Subject ID, Session ID, Tags, Age at Scan and Metadata of
868
1049
  the session with Patient ID
@@ -897,42 +1078,36 @@ class Project:
897
1078
  try:
898
1079
  patient_id = str(int(patient_id))
899
1080
  except ValueError:
900
- raise ValueError(f"'patient_id': '{patient_id}' not valid. "
901
- f"Must be convertible to int.")
1081
+ raise ValueError(f"'patient_id': '{patient_id}' not valid. " f"Must be convertible to int.")
902
1082
 
903
- assert isinstance(tags, list) and \
904
- all(isinstance(item, str) for item in tags), \
905
- f"tags: '{tags}' should be a list of strings."
1083
+ assert isinstance(tags, list) and all(
1084
+ isinstance(item, str) for item in tags
1085
+ ), f"tags: '{tags}' should be a list of strings."
906
1086
  tags = [tag.lower() for tag in tags]
907
1087
 
908
- assert subject_name is not None and subject_name != "", \
909
- "subject_name must be a non empty string."
910
- assert ssid is not None and ssid != "", \
911
- "ssid must be a non empty string."
1088
+ assert subject_name is not None and subject_name != "", "subject_name must be a non empty string."
1089
+ assert ssid is not None and ssid != "", "ssid must be a non empty string."
912
1090
 
913
1091
  try:
914
1092
  age_at_scan = str(int(age_at_scan)) if age_at_scan else None
915
1093
  except ValueError:
916
- raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. "
917
- f"Must be an integer.")
1094
+ raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. " f"Must be an integer.")
918
1095
 
919
- assert isinstance(metadata, dict), \
920
- f"metadata: '{metadata}' should be a dictionary."
1096
+ assert isinstance(metadata, dict), f"metadata: '{metadata}' should be a dictionary."
921
1097
 
922
- assert all("md_" == key[:3] for key in metadata.keys()) or \
923
- all("md_" != key[:3] for key in metadata.keys()), \
924
- f"metadata: '{metadata}' must be a dictionary whose keys " \
925
- f"are either all starting with 'md_' or none."
1098
+ assert all("md_" == key[:3] for key in metadata.keys()) or all("md_" != key[:3] for key in metadata.keys()), (
1099
+ f"metadata: '{metadata}' must be a dictionary whose keys " f"are either all starting with 'md_' or none."
1100
+ )
926
1101
 
927
1102
  metadata_keys = self.metadata_parameters.keys()
928
- assert \
929
- all([key[3:] in metadata_keys
930
- if "md_" == key[:3] else key in metadata_keys
931
- for key in metadata.keys()]), \
932
- f"Some metadata keys provided ({', '.join(metadata.keys())}) " \
933
- f"are not available in the project. They can be added via the " \
934
- f"Metadata Manager via the QMENTA Platform graphical user " \
1103
+ assert all(
1104
+ [key[3:] in metadata_keys if "md_" == key[:3] else key in metadata_keys for key in metadata.keys()]
1105
+ ), (
1106
+ f"Some metadata keys provided ({', '.join(metadata.keys())}) "
1107
+ f"are not available in the project. They can be added via the "
1108
+ f"Metadata Manager via the QMENTA Platform graphical user "
935
1109
  f"interface (GUI)."
1110
+ )
936
1111
 
937
1112
  post_data = {
938
1113
  "patient_id": patient_id,
@@ -946,11 +1121,7 @@ class Project:
946
1121
  post_data[f"last_vals.{id}"] = value
947
1122
 
948
1123
  try:
949
- platform.parse_response(platform.post(
950
- self._account.auth,
951
- "patient_manager/upsert_patient",
952
- data=post_data
953
- ))
1124
+ platform.parse_response(platform.post(self._account.auth, "patient_manager/upsert_patient", data=post_data))
954
1125
  except errors.PlatformError:
955
1126
  logger.error(f"Patient ID '{patient_id}' could not be modified.")
956
1127
  return False
@@ -979,45 +1150,32 @@ class Project:
979
1150
  all_sessions = self.get_subjects_metadata()
980
1151
 
981
1152
  session_to_del = [
982
- s for s in all_sessions if
983
- s["patient_secret_name"] == subject_name and
984
- s["ssid"] == session_id
1153
+ s for s in all_sessions if s["patient_secret_name"] == subject_name and s["ssid"] == session_id
985
1154
  ]
986
1155
 
987
1156
  if not session_to_del:
988
- logger.error(
989
- f"Session {subject_name}/{session_id} could not be found "
990
- f"in this project."
991
- )
1157
+ logger.error(f"Session {subject_name}/{session_id} could not be found " f"in this project.")
992
1158
  return False
993
1159
  elif len(session_to_del) > 1:
994
- raise RuntimeError(
995
- "Multiple sessions with same Subject ID and Session ID."
996
- " Contact support."
997
- )
1160
+ raise RuntimeError("Multiple sessions with same Subject ID and Session ID." " Contact support.")
998
1161
  else:
999
- logger.info("{}/{} found (id {})".format(
1000
- subject_name, session_id, session_to_del[0]["_id"]
1001
- ))
1162
+ logger.info("{}/{} found (id {})".format(subject_name, session_id, session_to_del[0]["_id"]))
1002
1163
 
1003
1164
  session = session_to_del[0]
1004
1165
 
1005
1166
  try:
1006
- platform.parse_response(platform.post(
1007
- self._account.auth, "patient_manager/delete_patient",
1008
- data={
1009
- "patient_id": str(int(session["_id"])), "delete_files": 1
1010
- }
1011
- ))
1167
+ platform.parse_response(
1168
+ platform.post(
1169
+ self._account.auth,
1170
+ "patient_manager/delete_patient",
1171
+ data={"patient_id": str(int(session["_id"])), "delete_files": 1},
1172
+ )
1173
+ )
1012
1174
  except errors.PlatformError:
1013
- logger.error(f"Session \"{subject_name}/{session['ssid']}\" could"
1014
- f" not be deleted.")
1175
+ logger.error(f"Session \"{subject_name}/{session['ssid']}\" could" f" not be deleted.")
1015
1176
  return False
1016
1177
 
1017
- logger.info(
1018
- f"Session \"{subject_name}/{session['ssid']}\" successfully "
1019
- f"deleted."
1020
- )
1178
+ logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully " f"deleted.")
1021
1179
  return True
1022
1180
 
1023
1181
  def delete_session_by_patientid(self, patient_id):
@@ -1038,12 +1196,13 @@ class Project:
1038
1196
  logger = logging.getLogger(logger_name)
1039
1197
 
1040
1198
  try:
1041
- platform.parse_response(platform.post(
1042
- self._account.auth, "patient_manager/delete_patient",
1043
- data={
1044
- "patient_id": str(int(patient_id)), "delete_files": 1
1045
- }
1046
- ))
1199
+ platform.parse_response(
1200
+ platform.post(
1201
+ self._account.auth,
1202
+ "patient_manager/delete_patient",
1203
+ data={"patient_id": str(int(patient_id)), "delete_files": 1},
1204
+ )
1205
+ )
1047
1206
  except errors.PlatformError:
1048
1207
  logger.error(f"Patient ID {patient_id} could not be deleted.")
1049
1208
  return False
@@ -1071,16 +1230,10 @@ class Project:
1071
1230
  # Always fetch the session IDs from the platform before deleting them
1072
1231
  all_sessions = self.get_subjects_metadata()
1073
1232
 
1074
- sessions_to_del = [
1075
- s for s in all_sessions if s["patient_secret_name"] == subject_name
1076
- ]
1233
+ sessions_to_del = [s for s in all_sessions if s["patient_secret_name"] == subject_name]
1077
1234
 
1078
1235
  if not sessions_to_del:
1079
- logger.error(
1080
- "Subject {} cannot be found in this project.".format(
1081
- subject_name
1082
- )
1083
- )
1236
+ logger.error("Subject {} cannot be found in this project.".format(subject_name))
1084
1237
  return False
1085
1238
 
1086
1239
  for ssid in [s["ssid"] for s in sessions_to_del]:
@@ -1088,15 +1241,25 @@ class Project:
1088
1241
  return False
1089
1242
  return True
1090
1243
 
1091
- def _upload_chunk(self, data, range_str, length, session_id,
1092
- disposition,
1093
- last_chunk,
1094
- name="", date_of_scan="", description="",
1095
- subject_name="", ssid="", filename="DATA.zip",
1096
- input_data_type="mri_brain_data:1.0",
1097
- result=False, add_to_container_id=0,
1098
- split_data=False
1099
- ):
1244
+ def _upload_chunk(
1245
+ self,
1246
+ data,
1247
+ range_str,
1248
+ length,
1249
+ session_id,
1250
+ disposition,
1251
+ last_chunk,
1252
+ name="",
1253
+ date_of_scan="",
1254
+ description="",
1255
+ subject_name="",
1256
+ ssid="",
1257
+ filename="DATA.zip",
1258
+ input_data_type="mri_brain_data:1.0",
1259
+ result=False,
1260
+ add_to_container_id=0,
1261
+ split_data=False,
1262
+ ):
1100
1263
  """
1101
1264
  Upload a chunk of a file to the platform.
1102
1265
 
@@ -1123,10 +1286,11 @@ class Project:
1123
1286
  """
1124
1287
 
1125
1288
  request_headers = {
1126
- "Content-Type": "application/zip", "Content-Range":
1127
- range_str, "Session-ID": str(session_id),
1289
+ "Content-Type": "application/zip",
1290
+ "Content-Range": range_str,
1291
+ "Session-ID": str(session_id),
1128
1292
  "Content-Length": str(length),
1129
- "Content-Disposition": disposition
1293
+ "Content-Disposition": disposition,
1130
1294
  }
1131
1295
 
1132
1296
  if last_chunk:
@@ -1154,20 +1318,25 @@ class Project:
1154
1318
 
1155
1319
  response_time = 900.0 if last_chunk else 120.0
1156
1320
  response = platform.post(
1157
- auth=self._account.auth,
1158
- endpoint="upload",
1159
- data=data,
1160
- headers=request_headers,
1161
- timeout=response_time
1321
+ auth=self._account.auth, endpoint="upload", data=data, headers=request_headers, timeout=response_time
1162
1322
  )
1163
1323
 
1164
1324
  return response
1165
1325
 
1166
- def upload_file(self, file_path, subject_name, ssid="", date_of_scan="",
1167
- description="", result=False, name="",
1168
- input_data_type="qmenta_mri_brain_data:1.0",
1169
- add_to_container_id=0, chunk_size=2 ** 9,
1170
- split_data=False):
1326
+ def upload_file(
1327
+ self,
1328
+ file_path,
1329
+ subject_name,
1330
+ ssid="",
1331
+ date_of_scan="",
1332
+ description="",
1333
+ result=False,
1334
+ name="",
1335
+ input_data_type="qmenta_mri_brain_data:1.0",
1336
+ add_to_container_id=0,
1337
+ chunk_size=2**9,
1338
+ split_data=False,
1339
+ ):
1171
1340
  """
1172
1341
  Upload a ZIP file to the platform.
1173
1342
 
@@ -1231,8 +1400,7 @@ class Project:
1231
1400
  last_chunk = False
1232
1401
 
1233
1402
  if ssid and split_data:
1234
- logger.warning("split-data argument will be ignored because" +
1235
- " ssid has been specified")
1403
+ logger.warning("split-data argument will be ignored because" + " ssid has been specified")
1236
1404
  split_data = False
1237
1405
 
1238
1406
  while True:
@@ -1249,16 +1417,27 @@ class Project:
1249
1417
  end_position = total_bytes - 1
1250
1418
  bytes_to_send = total_bytes - uploaded_bytes
1251
1419
 
1252
- bytes_range = "bytes " + str(start_position) + "-" + \
1253
- str(end_position) + "/" + str(total_bytes)
1420
+ bytes_range = "bytes " + str(start_position) + "-" + str(end_position) + "/" + str(total_bytes)
1254
1421
 
1255
1422
  dispstr = f"attachment; filename={filename}"
1256
1423
  response = self._upload_chunk(
1257
- data, bytes_range, bytes_to_send, session_id, dispstr,
1424
+ data,
1425
+ bytes_range,
1426
+ bytes_to_send,
1427
+ session_id,
1428
+ dispstr,
1258
1429
  last_chunk,
1259
- name, date_of_scan, description, subject_name, ssid,
1260
- filename, input_data_type, result, add_to_container_id,
1261
- split_data)
1430
+ name,
1431
+ date_of_scan,
1432
+ description,
1433
+ subject_name,
1434
+ ssid,
1435
+ filename,
1436
+ input_data_type,
1437
+ result,
1438
+ add_to_container_id,
1439
+ split_data,
1440
+ )
1262
1441
 
1263
1442
  if response is None:
1264
1443
  retries_count += 1
@@ -1278,17 +1457,14 @@ class Project:
1278
1457
  retries_count += 1
1279
1458
  time.sleep(retries_count * 5)
1280
1459
  if retries_count > self.max_retries:
1281
- error_message = (
1282
- "Error Code: 416; "
1283
- "Requested Range Not Satisfiable (NGINX)")
1460
+ error_message = "Error Code: 416; " "Requested Range Not Satisfiable (NGINX)"
1284
1461
  logger.error(error_message)
1285
1462
  break
1286
1463
  else:
1287
1464
  retries_count += 1
1288
1465
  time.sleep(retries_count * 5)
1289
1466
  if retries_count > max_retries:
1290
- error_message = ("Number of retries has been reached. "
1291
- "Upload process stops here !")
1467
+ error_message = "Number of retries has been reached. " "Upload process stops here !"
1292
1468
  logger.error(error_message)
1293
1469
  break
1294
1470
 
@@ -1342,9 +1518,7 @@ class Project:
1342
1518
  """
1343
1519
 
1344
1520
  if check_upload_file(file_path):
1345
- return self.upload_file(
1346
- file_path, subject_name,
1347
- input_data_type="parkinson_gametection")
1521
+ return self.upload_file(file_path, subject_name, input_data_type="parkinson_gametection")
1348
1522
  return False
1349
1523
 
1350
1524
  def upload_result(self, file_path, subject_name):
@@ -1388,13 +1562,9 @@ class Project:
1388
1562
  p_id = int(project_id)
1389
1563
  elif type(project_id) == str:
1390
1564
  projects = self._account.projects
1391
- projects_match = [proj for proj in projects
1392
- if proj["name"] == project_id]
1565
+ projects_match = [proj for proj in projects if proj["name"] == project_id]
1393
1566
  if not projects_match:
1394
- raise Exception(
1395
- f"Project {project_id}" +
1396
- " does not exist or is not available for this user."
1397
- )
1567
+ raise Exception(f"Project {project_id}" + " does not exist or is not available for this user.")
1398
1568
  p_id = int(projects_match[0]["id"])
1399
1569
  else:
1400
1570
  raise TypeError("project_id")
@@ -1404,30 +1574,26 @@ class Project:
1404
1574
  }
1405
1575
 
1406
1576
  try:
1407
- platform.parse_response(platform.post(
1408
- self._account.auth,
1409
- "file_manager/copy_container_to_another_project",
1410
- data=data
1411
- ))
1412
- except errors.PlatformError as e:
1413
- logging.getLogger(logger_name).error(
1414
- "Couldn not copy container: {}".format(e)
1577
+ platform.parse_response(
1578
+ platform.post(self._account.auth, "file_manager/copy_container_to_another_project", data=data)
1415
1579
  )
1580
+ except errors.PlatformError as e:
1581
+ logging.getLogger(logger_name).error("Couldn not copy container: {}".format(e))
1416
1582
  return False
1417
1583
 
1418
1584
  return True
1419
1585
 
1420
1586
  def start_analysis(
1421
- self,
1422
- script_name,
1423
- version,
1424
- in_container_id=None,
1425
- analysis_name=None,
1426
- analysis_description=None,
1427
- ignore_warnings=False,
1428
- settings=None,
1429
- tags=None,
1430
- preferred_destination=None
1587
+ self,
1588
+ script_name,
1589
+ version,
1590
+ in_container_id=None,
1591
+ analysis_name=None,
1592
+ analysis_description=None,
1593
+ ignore_warnings=False,
1594
+ settings=None,
1595
+ tags=None,
1596
+ preferred_destination=None,
1431
1597
  ):
1432
1598
  """
1433
1599
  Starts an analysis on a subject.
@@ -1466,13 +1632,9 @@ class Project:
1466
1632
  logger = logging.getLogger(logger_name)
1467
1633
 
1468
1634
  if in_container_id is None and settings is None:
1469
- raise ValueError(
1470
- "Pass a value for either in_container_id or settings.")
1635
+ raise ValueError("Pass a value for either in_container_id or settings.")
1471
1636
 
1472
- post_data = {
1473
- "script_name": script_name,
1474
- "version": version
1475
- }
1637
+ post_data = {"script_name": script_name, "version": version}
1476
1638
 
1477
1639
  settings = settings or {}
1478
1640
 
@@ -1502,9 +1664,7 @@ class Project:
1502
1664
  post_data["preferred_destination"] = preferred_destination
1503
1665
 
1504
1666
  logger.debug(f"post_data = {post_data}")
1505
- return self.__handle_start_analysis(
1506
- post_data, ignore_warnings=ignore_warnings
1507
- )
1667
+ return self.__handle_start_analysis(post_data, ignore_warnings=ignore_warnings)
1508
1668
 
1509
1669
  def delete_analysis(self, analysis_id):
1510
1670
  """
@@ -1516,19 +1676,20 @@ class Project:
1516
1676
  logger = logging.getLogger(logger_name)
1517
1677
 
1518
1678
  try:
1519
- platform.parse_response(platform.post(
1520
- auth=self._account.auth,
1521
- endpoint="analysis_manager/delete_analysis",
1522
- data={"project_id": analysis_id}
1523
- ))
1679
+ platform.parse_response(
1680
+ platform.post(
1681
+ auth=self._account.auth,
1682
+ endpoint="analysis_manager/delete_analysis",
1683
+ data={"project_id": analysis_id},
1684
+ )
1685
+ )
1524
1686
  except errors.PlatformError as error:
1525
1687
  logger.error("Could not delete analysis: {}".format(error))
1526
1688
  return False
1527
1689
 
1528
1690
  return True
1529
1691
 
1530
- def __handle_start_analysis(self, post_data, ignore_warnings=False,
1531
- n_calls=0):
1692
+ def __handle_start_analysis(self, post_data, ignore_warnings=False, n_calls=0):
1532
1693
  """
1533
1694
  Handle the possible responses from the server after start_analysis.
1534
1695
  Sometimes we have to send a request again, and then check again the
@@ -1543,16 +1704,16 @@ class Project:
1543
1704
 
1544
1705
  logger = logging.getLogger(logger_name)
1545
1706
  if n_calls > call_limit:
1546
- logger.error(f"__handle_start_analysis_response called itself more\
1547
- than {n_calls} times: aborting.")
1707
+ logger.error(
1708
+ f"__handle_start_analysis_response called itself more\
1709
+ than {n_calls} times: aborting."
1710
+ )
1548
1711
  return None
1549
1712
 
1550
1713
  try:
1551
- response = platform.parse_response(platform.post(
1552
- self._account.auth,
1553
- "analysis_manager/analysis_registration",
1554
- data=post_data
1555
- ))
1714
+ response = platform.parse_response(
1715
+ platform.post(self._account.auth, "analysis_manager/analysis_registration", data=post_data)
1716
+ )
1556
1717
  logger.info(response["message"])
1557
1718
  return int(response["analysis_id"])
1558
1719
  except platform.ChooseDataError as choose_data:
@@ -1574,8 +1735,7 @@ class Project:
1574
1735
  chosen_files = {}
1575
1736
  for settings_key in choose_data.data_to_choose:
1576
1737
  chosen_files[settings_key] = {}
1577
- filters = choose_data.data_to_choose[
1578
- settings_key]["filters"]
1738
+ filters = choose_data.data_to_choose[settings_key]["filters"]
1579
1739
  for filter_key in filters:
1580
1740
  filter_data = filters[filter_key]
1581
1741
 
@@ -1587,35 +1747,24 @@ class Project:
1587
1747
  if filter_data["range"][0] != 0:
1588
1748
  number_of_files_to_select = filter_data["range"][0]
1589
1749
  elif filter_data["range"][1] != 0:
1590
- number_of_files_to_select = min(
1591
- filter_data["range"][1],
1592
- len(filter_data["files"])
1593
- )
1750
+ number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
1594
1751
  else:
1595
- number_of_files_to_select = len(
1596
- filter_data["files"]
1597
- )
1752
+ number_of_files_to_select = len(filter_data["files"])
1598
1753
 
1599
- files_selection = [ff["_id"] for ff in
1600
- filter_data["files"]
1601
- [:number_of_files_to_select]]
1602
- chosen_files[settings_key][filter_key] = \
1603
- files_selection
1754
+ files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
1755
+ chosen_files[settings_key][filter_key] = files_selection
1604
1756
 
1605
1757
  new_post["user_preference"] = json.dumps(chosen_files)
1606
1758
  else:
1607
1759
  if has_warning and not ignore_warnings:
1608
- logger.info("cancelling analysis due to warnings, " +
1609
- "set \"ignore_warnings\" to True to override")
1760
+ logger.info("cancelling analysis due to warnings, " + 'set "ignore_warnings" to True to override')
1610
1761
  new_post["cancel"] = "1"
1611
1762
  else:
1612
1763
  logger.info("suppressing warnings")
1613
1764
  new_post["user_preference"] = "{}"
1614
1765
  new_post["_mint_only_warning"] = "1"
1615
1766
 
1616
- return self.__handle_start_analysis(
1617
- new_post, ignore_warnings=ignore_warnings, n_calls=n_calls
1618
- )
1767
+ return self.__handle_start_analysis(new_post, ignore_warnings=ignore_warnings, n_calls=n_calls)
1619
1768
  except platform.ActionFailedError as e:
1620
1769
  logger.error(f"Unable to start the analysis: {e}")
1621
1770
  return None
@@ -1646,19 +1795,15 @@ class Project:
1646
1795
  logger = logging.getLogger(__name__)
1647
1796
  logger.info(f"Setting QC status to {status}: {comments}")
1648
1797
 
1649
- platform.parse_response(platform.post(
1650
- auth=self._account.auth,
1651
- endpoint="projectset_manager/set_qa_status",
1652
- data={
1653
- "item_ids": analysis_id,
1654
- "status": status.value,
1655
- "comments": comments,
1656
- "entity": "analysis"
1657
- }
1658
- ))
1798
+ platform.parse_response(
1799
+ platform.post(
1800
+ auth=self._account.auth,
1801
+ endpoint="projectset_manager/set_qa_status",
1802
+ data={"item_ids": analysis_id, "status": status.value, "comments": comments, "entity": "analysis"},
1803
+ )
1804
+ )
1659
1805
 
1660
- def get_qc_status(
1661
- self, patient_secret_name=None, ssid=None, analysis_id=None):
1806
+ def get_qc_status(self, patient_secret_name=None, ssid=None, analysis_id=None):
1662
1807
  """
1663
1808
  Gets the session QC status of a session. If the analysis_id is
1664
1809
  specified, it returns the QC of the
@@ -1668,17 +1813,15 @@ class Project:
1668
1813
  if patient_secret_name and ssid:
1669
1814
  session = self.get_subjects_metadata(
1670
1815
  search_criteria={
1671
- "pars_patient_secret_name": f"string;"
1672
- f"{patient_secret_name}",
1673
- "pars_ssid": f"integer;eq|{ssid}"
1816
+ "pars_patient_secret_name": f"string;" f"{patient_secret_name}",
1817
+ "pars_ssid": f"integer;eq|{ssid}",
1674
1818
  }
1675
1819
  )
1676
1820
  to_return = session["qa_status"], session["qa_comments"]
1677
1821
  elif analysis_id:
1678
1822
  try:
1679
1823
  to_return = [
1680
- analysis["qa_data"] for analysis in self.list_analysis()
1681
- if analysis["_id"] == analysis_id
1824
+ analysis["qa_data"] for analysis in self.list_analysis() if analysis["_id"] == analysis_id
1682
1825
  ][0]
1683
1826
  to_return = to_return["qa_status"], to_return["qa_comments"]
1684
1827
  except IndexError:
@@ -1689,22 +1832,21 @@ class Project:
1689
1832
  print(f"An error occurred: {e}")
1690
1833
  to_return = None
1691
1834
  else:
1692
- raise Exception(f"Must specify {patient_secret_name} and "
1693
- f"{ssid} or {analysis_id}.")
1835
+ raise Exception(f"Must specify {patient_secret_name} and {ssid} or {analysis_id}.")
1694
1836
  return to_return
1695
1837
 
1696
1838
  def start_multiple_analyses(
1697
- self,
1698
- script_name,
1699
- version,
1700
- n_times,
1701
- in_container_id=None,
1702
- analysis_name=None,
1703
- analysis_description=None,
1704
- ignore_warnings=False,
1705
- settings=None,
1706
- tags=None,
1707
- preferred_destination=None
1839
+ self,
1840
+ script_name,
1841
+ version,
1842
+ n_times,
1843
+ in_container_id=None,
1844
+ analysis_name=None,
1845
+ analysis_description=None,
1846
+ ignore_warnings=False,
1847
+ settings=None,
1848
+ tags=None,
1849
+ preferred_destination=None,
1708
1850
  ):
1709
1851
  """
1710
1852
  Starts multiple times the same analysis on a subject with the same
@@ -1745,9 +1887,7 @@ class Project:
1745
1887
  """
1746
1888
  logger = logging.getLogger(logger_name)
1747
1889
  for n in range(n_times):
1748
- logger.info(
1749
- f"Running tool {script_name}:{version} {n + 1}/{n_times}"
1750
- )
1890
+ logger.info(f"Running tool {script_name}:{version} {n + 1}/{n_times}")
1751
1891
  yield self.start_analysis(
1752
1892
  script_name,
1753
1893
  version,
@@ -1757,5 +1897,40 @@ class Project:
1757
1897
  ignore_warnings=ignore_warnings,
1758
1898
  settings=settings,
1759
1899
  tags=tags,
1760
- preferred_destination=preferred_destination
1900
+ preferred_destination=preferred_destination,
1761
1901
  )
1902
+
1903
+ def set_project_qa_rules(self, rules_file_path, guidance_text=""):
1904
+ """
1905
+ Logs in to the Qmenta platform, retrieves the project ID based on the project name,
1906
+ and updates the project's QA rules using the provided rules file.
1907
+
1908
+ Args:
1909
+ rules_file_path (str): The file path to the JSON file containing the QA rules.
1910
+ guidance_text (str): Description of the rules. Only visible for Platform admins.
1911
+
1912
+ Returns:
1913
+ bool: True if the rules were set successfully, False otherwise.
1914
+ """
1915
+ # Read the rules from the JSON file
1916
+ try:
1917
+ with open(rules_file_path, "r") as fr:
1918
+ rules = json.load(fr)
1919
+ except FileNotFoundError:
1920
+ print(f"ERROR: Rules file '{rules_file_path}' not found.")
1921
+ return False
1922
+
1923
+ # Update the project's QA rules
1924
+ res = platform.post(
1925
+ auth=self._account.auth,
1926
+ endpoint="projectset_manager/set_session_qa_requirements",
1927
+ data={"project_id": self._project_id, "rules": json.dumps(rules), "guidance_text": guidance_text},
1928
+ )
1929
+
1930
+ if res.json().get("success") == 1:
1931
+ print("Rules set up successfully!")
1932
+ return True
1933
+ else:
1934
+ print("ERROR setting the rules")
1935
+ print(res.json())
1936
+ return False