qmenta-client 1.1.dev1245__py3-none-any.whl → 1.1.dev1295__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
@@ -11,7 +11,6 @@ from enum import Enum
11
11
  from qmenta.client import Account
12
12
  from qmenta.core import errors
13
13
  from qmenta.core import platform
14
- from .Subject import Subject
15
14
 
16
15
  if sys.version_info[0] == 3:
17
16
  # Note: this branch & variable is only needed for python 2/3 compatibility
@@ -23,7 +22,8 @@ logger_name = "qmenta.client"
23
22
  def show_progress(done, total, finish=False):
24
23
  bytes_in_mb = 1024 * 1024
25
24
  progress_message = "\r[{:.2f} %] Uploaded {:.2f} of {:.2f} Mb".format(
26
- done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb)
25
+ done / float(total) * 100, done / bytes_in_mb, total / bytes_in_mb
26
+ )
27
27
  sys.stdout.write(progress_message)
28
28
  sys.stdout.flush()
29
29
  if not finish:
@@ -72,6 +72,7 @@ class QCStatus(Enum):
72
72
  Enum with the following options:
73
73
  FAIL, PASS
74
74
  """
75
+
75
76
  PASS = "pass"
76
77
  FAIL = "fail"
77
78
 
@@ -95,15 +96,11 @@ class Project:
95
96
  # project id (int)
96
97
  if isinstance(project_id, str):
97
98
  project_name = project_id
98
- project_id = next(iter(filter(
99
- lambda proj: proj["name"] == project_id, account.projects)
100
- ))["id"]
99
+ project_id = next(iter(filter(lambda proj: proj["name"] == project_id, account.projects)))["id"]
101
100
  else:
102
101
  if isinstance(project_id, float):
103
102
  project_id = int(project_id)
104
- project_name = next(iter(filter(
105
- lambda proj: proj["id"] == project_id, account.projects)
106
- ))["name"]
103
+ project_name = next(iter(filter(lambda proj: proj["id"] == project_id, account.projects)))["name"]
107
104
 
108
105
  self._account = account
109
106
  self._project_id = project_id
@@ -134,11 +131,11 @@ class Project:
134
131
  """
135
132
  logger = logging.getLogger(logger_name)
136
133
  try:
137
- platform.parse_response(platform.post(
138
- self._account.auth,
139
- "projectset_manager/activate_project",
140
- data={"project_id": int(project_id)}
141
- ))
134
+ platform.parse_response(
135
+ platform.post(
136
+ self._account.auth, "projectset_manager/activate_project", data={"project_id": int(project_id)}
137
+ )
138
+ )
142
139
  except errors.PlatformError:
143
140
  logger.error("Unable to activate the project.")
144
141
  return False
@@ -161,45 +158,139 @@ class Project:
161
158
  dict
162
159
  A list of dictionary of {"metadata_name": "metadata_value"}
163
160
  """
164
- return self.get_subjects_metadata(cache=False)
161
+ return self.get_subjects_metadata()
165
162
 
166
- def get_subjects_metadata(self, cache=True, search_criteria=None):
163
+ def get_subjects_metadata(self, search_criteria={}, items=(0, 9999)):
167
164
  """
168
- List all subject data from the selected project.
165
+ List all subjects data from the selected project that meet the defined
166
+ search criteria.
167
+
169
168
  Parameters
170
169
  ----------
171
- cache: bool
172
- Whether to use the cached metadata or not
173
-
174
170
  search_criteria: dict
175
171
  Each element is a string and is built using the formatting
176
- "type;value", or "type;operation|value"
172
+ "type;value", or "type;operation|value"
173
+
174
+ Complete search_criteria Dictionary Explanation:
175
+
176
+ search_criteria = {
177
+ "pars_patient_secret_name": "string;SUBJECTID",
178
+ "pars_ssid": "integer;OPERATOR|SSID",
179
+ "pars_modalities": "string;MODALITY",
180
+ "pars_tags": "tags;TAGS",
181
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
182
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
183
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
184
+ }
185
+
186
+ Where:
187
+ "pars_patient_secret_name": Applies the search to the 'Subject ID'.
188
+ SUBJECTID is a comma separated list of strings.
189
+ "pars_ssid": Applies the search to the 'Session ID'.
190
+ SSID is an integer.
191
+ OPERATOR is the operator to apply. One of:
192
+ - Equal: eq
193
+ - Different Than: ne
194
+ - Greater Than: gt
195
+ - Greater/Equal To: gte
196
+ - Lower Than: lt
197
+ - Lower/Equal To: lte
198
+
199
+ "pars_modalities": Applies the search to the file 'Modalities'
200
+ available within each Subject ID.
201
+ MODALITY is a comma separated list of string.
202
+ "pars_tags": Applies the search to the file 'Tags' available within
203
+ each Subject ID and to the subject-level 'Tags'.
204
+ TAGS is a comma separated list of strings.
205
+ "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
206
+ field.
207
+ AGE_AT_SCAN is an integer.
208
+ "pars_[dicom]_KEY": Applies the search to the metadata fields
209
+ available within each file. KEY must be one of the
210
+ metadata keys of the files. The full list of KEYS is shown above via
211
+ 'file_m["metadata"]["info"].keys()'.
212
+ KEYTYPE is the type of the KEY. One of:
213
+ - integer
214
+ - string
215
+ - list
216
+
217
+ if 'integer' you must also include an OPERATOR
218
+ (i.e., "integer;OPERATOR|KEYVALUE").
219
+ KEYVALUE is the expected value of the KEY.
220
+ "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
221
+ within the 'Metadata Manager' of the project.
222
+ PROJECTMETADATA is the ID of the metadata field.
223
+ METADATATYPE is the type of the metadata field. One of:
224
+ - string
225
+ - integer
226
+ - list
227
+ - decimal
228
+ - single_option
229
+ - multiple_option
230
+
231
+ if 'integer' or 'decimal' you must also include an OPERATOR
232
+ (i.e., "integer;OPERATOR|METADATAVALUE").
233
+ KEYVALUE is the expected value of the metadata.
234
+
235
+ 1) Example:
236
+ search_criteria = {
237
+ "pars_patient_secret_name": "string;abide",
238
+ "pars_ssid": "integer;eq|2"
239
+ }
240
+
241
+ 2) Example:
242
+ search_criteria = {
243
+ "pars_modalities": "string;T1",
244
+ "pars_tags": "tags;flair",
245
+ "pars_[dicom]_Manufacturer": "string;ge",
246
+ "pars_[dicom]_FlipAngle": "integer;gt|5",
247
+ }
177
248
 
178
- Example:
179
- search_criteria = {"pars_patient_secret_name": "string;abide",
180
- "pars_ssid": "integer;eq|2"}
249
+ Note the search criteria applies to all the files included within a
250
+ session. Hence, it does not imply that all the criteria conditions
251
+ are applied to the same files. In example 2) above, it means that
252
+ any Subject ID/Session ID that has a file classified with a 'T1'
253
+ modality, a file with a 'flair' tag, a file whose Manufacturer
254
+ contains 'ge', and a file whose FlipAngle is greater than '5º' will
255
+ be selected.
181
256
 
182
257
  Returns
183
258
  -------
184
259
  dict
185
260
  A list of dictionary of {"metadata_name": "metadata_value"}
261
+
186
262
  """
187
263
 
188
- if not cache or not self._subjects_metadata:
189
- content = platform.parse_response(platform.post(
190
- self._account.auth, "patient_manager/get_patient_list",
264
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
265
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
266
+
267
+ assert all([key[:5] == "pars_" for key in search_criteria.keys()]), (
268
+ f"All keys of the search_criteria dictionary " f"'{search_criteria.keys()}' must start with 'pars_'."
269
+ )
270
+
271
+ operator_list = ["eq", "ne", "gt", "gte", "lt", "lte"]
272
+ for key, value in search_criteria.items():
273
+ if value.split(";")[0] in ["integer", "decimal"]:
274
+ assert value.split(";")[1].split("|")[0] in operator_list, (
275
+ f"Search criteria of type '{value.split(';')[0]}' must "
276
+ f"include an operator ({', '.join(operator_list)})."
277
+ )
278
+
279
+ content = platform.parse_response(
280
+ platform.post(
281
+ self._account.auth,
282
+ "patient_manager/get_patient_list",
191
283
  data=search_criteria,
192
- headers={"X-Range": "items=0-9999"}
193
- ))
194
- self._subjects_metadata = content
195
- else:
196
- content = self._subjects_metadata
284
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
285
+ )
286
+ )
197
287
  return content
198
288
 
199
289
  @property
200
290
  def subjects(self):
201
291
  """
202
- Return the list of subject names from the selected project.
292
+ Return the list of subject names (Subject ID) from the selected
293
+ project.
203
294
 
204
295
  :return: a list of subject names
205
296
  :rtype: List(Strings)
@@ -211,12 +302,13 @@ class Project:
211
302
 
212
303
  def check_subject_name(self, subject_name):
213
304
  """
214
- Check if a given subject name exists in the selected project.
305
+ Check if a given subject name (Subject ID) exists in the selected
306
+ project.
215
307
 
216
308
  Parameters
217
309
  ----------
218
310
  subject_name : str
219
- name of the subject to check
311
+ Subject ID of the subject to check
220
312
 
221
313
  Returns
222
314
  -------
@@ -247,16 +339,13 @@ class Project:
247
339
  """
248
340
  logger = logging.getLogger(logger_name)
249
341
  try:
250
- data = platform.parse_response(platform.post(
251
- self._account.auth, "patient_manager/module_config"
252
- ))
342
+ data = platform.parse_response(platform.post(self._account.auth, "patient_manager/module_config"))
253
343
  except errors.PlatformError:
254
344
  logger.error("Could not retrieve metadata parameters.")
255
345
  return None
256
346
  return data["fields"]
257
347
 
258
- def add_metadata_parameter(self, title, param_id=None,
259
- param_type="string", visible=False):
348
+ def add_metadata_parameter(self, title, param_id=None, param_type="string", visible=False):
260
349
  """
261
350
  Add a metadata parameter to the project.
262
351
 
@@ -282,18 +371,13 @@ class Project:
282
371
 
283
372
  param_properties = [title, param_id, param_type, str(int(visible))]
284
373
 
285
- post_data = {"add": "|".join(param_properties),
286
- "edit": "",
287
- "delete": ""
288
- }
374
+ post_data = {"add": "|".join(param_properties), "edit": "", "delete": ""}
289
375
 
290
376
  logger = logging.getLogger(logger_name)
291
377
  try:
292
- answer = platform.parse_response(platform.post(
293
- self._account.auth,
294
- "patient_manager/save_metadata_changes",
295
- data=post_data
296
- ))
378
+ answer = platform.parse_response(
379
+ platform.post(self._account.auth, "patient_manager/save_metadata_changes", data=post_data)
380
+ )
297
381
  except errors.PlatformError:
298
382
  answer = {}
299
383
 
@@ -310,20 +394,17 @@ class Project:
310
394
  elif isinstance(analysis_name_or_id, str):
311
395
  search_tag = "p_n"
312
396
  else:
313
- raise Exception("The analysis identifier must be its name or an "
314
- "integer")
397
+ raise Exception("The analysis identifier must be its name or an " "integer")
315
398
 
316
399
  search_condition = {
317
400
  search_tag: analysis_name_or_id,
318
401
  }
319
- response = platform.parse_response(platform.post(
320
- self._account.auth, "analysis_manager/get_analysis_list",
321
- data=search_condition
322
- ))
402
+ response = platform.parse_response(
403
+ platform.post(self._account.auth, "analysis_manager/get_analysis_list", data=search_condition)
404
+ )
323
405
 
324
406
  if len(response) > 1:
325
- raise Exception(f"multiple analyses with name "
326
- f"{analysis_name_or_id} found")
407
+ raise Exception(f"multiple analyses with name " f"{analysis_name_or_id} found")
327
408
  elif len(response) == 1:
328
409
  return response[0]
329
410
  else:
@@ -363,10 +444,8 @@ class Project:
363
444
  dict
364
445
  List of analysis, each a dictionary
365
446
  """
366
- assert len(items) == 2, f"The number of elements in items " \
367
- f"'{len(items)}' should be equal to two."
368
- assert all([isinstance(item, int) for item in items]), \
369
- f"All items elements '{items}' should be integers."
447
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
448
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
370
449
  search_keys = {
371
450
  "p_n": str,
372
451
  "type": str,
@@ -382,70 +461,90 @@ class Project:
382
461
  }
383
462
  for key in search_condition.keys():
384
463
  if key not in search_keys.keys():
385
- raise Exception(
386
- (
387
- f"This key '{key}' is not accepted by this"
388
- "search condition"
389
- )
390
- )
464
+ raise Exception((f"This key '{key}' is not accepted by this" "search condition"))
391
465
  if not isinstance(search_condition[key], search_keys[key]):
392
- raise Exception(
393
- (
394
- f"The key {key} in the search condition"
395
- f"is not type {search_keys[key]}"
396
- )
397
- )
466
+ raise Exception((f"The key {key} in the search condition" f"is not type {search_keys[key]}"))
398
467
  req_headers = {"X-Range": f"items={items[0]}-{items[1] - 1}"}
399
- return platform.parse_response(platform.post(
400
- auth=self._account.auth,
401
- endpoint="analysis_manager/get_analysis_list",
402
- headers=req_headers,
403
- data=search_condition
404
- ))
405
-
406
- def get_container(self, subject_name):
407
- search_condition = {
408
- "s_n": subject_name,
409
- }
410
- response = self.list_input_containers(
411
- search_condition=search_condition
468
+ return platform.parse_response(
469
+ platform.post(
470
+ auth=self._account.auth,
471
+ endpoint="analysis_manager/get_analysis_list",
472
+ headers=req_headers,
473
+ data=search_condition,
474
+ )
412
475
  )
413
476
 
414
- if len(response) > 1:
415
- raise Exception(f"multiple containers for subject {subject_name} "
416
- f"found")
417
- elif len(response) == 1:
418
- return response[0]
419
- else:
420
- return None
477
+ def get_subject_container_id(self, subject_name, ssid):
478
+ """
479
+ Given a Subject ID and Session ID, return its Container ID.
480
+
481
+ Parameters
482
+ ----------
483
+ subject_name : str
484
+ Subject ID of the subject in the project.
485
+ ssid : str
486
+ Session ID of the subject in the project.
487
+
488
+ Returns
489
+ -------
490
+ int or bool
491
+ The Container ID of the subject in the project, or False if
492
+ the subject is not found.
493
+ """
494
+
495
+ search_criteria = {"s_n": subject_name, "ssid": ssid}
496
+ response = self.list_input_containers(search_criteria=search_criteria)
421
497
 
422
- def list_input_containers(self, search_condition=None, limit=1000):
498
+ for subject in response:
499
+ if subject["patient_secret_name"] == subject_name and subject["ssid"] == ssid:
500
+ return subject["container_id"]
501
+ return False
502
+
503
+ def list_input_containers(self, search_criteria={}, items=(0, 9999)):
423
504
  """
424
- List the containers available to the user.
505
+ Retrieve the list of input containers available to the user under a
506
+ certain search criteria.
425
507
 
426
508
  Parameters
427
509
  ----------
428
- search_condition : dict
429
- d_n: container_name
430
- s_n: subject_id
431
- from_d: from date
432
- to_d: to date
433
- sets: data sets (modalities)
434
- limit : int
435
- Max number of results
510
+ search_criteria : dict
511
+ Each element is a string and is built using the formatting
512
+ "type;value".
513
+
514
+ List of possible keys:
515
+ d_n: container_name # TODO: WHAT IS THIS???
516
+ s_n: subject_id
517
+ Subject ID of the subject in the platform.
518
+ ssid: session_id
519
+ Session ID of the subejct in the platform.
520
+ from_d: from date
521
+ Starting date in which perform the search. Format: DD.MM.YYYY
522
+ to_d: to date
523
+ End date in which perform the search. Format: DD.MM.YYYY
524
+ sets: data sets (modalities) # TODO: WHAT IS THIS???
525
+
526
+ items: Tuple(int, int)
527
+ Starting and ending element of the search.
436
528
 
437
529
  Returns
438
530
  -------
439
531
  dict
440
- List of containers, each a dictionary
441
- {"name": "container-name", "id": "container_id"}
532
+ List of containers, each a dictionary containing the following
533
+ information:
534
+ {"container_name", "container_id", "patient_secret_name", "ssid"}
442
535
  """
443
536
 
444
- req_headers = {"X-Range": "items=0-" + str(limit - 1)}
445
- response = platform.parse_response(platform.post(
446
- self._account.auth, "file_manager/get_container_list",
447
- data=search_condition, headers=req_headers
448
- ))
537
+ assert len(items) == 2, f"The number of elements in items " f"'{len(items)}' should be equal to two."
538
+ assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
539
+
540
+ response = platform.parse_response(
541
+ platform.post(
542
+ self._account.auth,
543
+ "file_manager/get_container_list",
544
+ data=search_criteria,
545
+ headers={"X-Range": f"items={items[0]}-{items[1]}"},
546
+ )
547
+ )
449
548
  containers = [
450
549
  {
451
550
  "patient_secret_name": container_item["patient_secret_name"],
@@ -473,8 +572,7 @@ class Project:
473
572
  {"name": "container-name", "id": "container_id"}
474
573
  """
475
574
  analysis = self.list_analysis(limit)
476
- return [{"name": a["name"],
477
- "id": a["out_container_id"]} for a in analysis]
575
+ return [{"name": a["name"], "id": a["out_container_id"]} for a in analysis]
478
576
 
479
577
  def list_container_files(self, container_id):
480
578
  """
@@ -491,10 +589,11 @@ class Project:
491
589
  List of file names (strings)
492
590
  """
493
591
  try:
494
- content = platform.parse_response(platform.post(
495
- self._account.auth, "file_manager/get_container_files",
496
- data={"container_id": container_id}
497
- ))
592
+ content = platform.parse_response(
593
+ platform.post(
594
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
595
+ )
596
+ )
498
597
  except errors.PlatformError as e:
499
598
  logging.getLogger(logger_name).error(e)
500
599
  return False
@@ -521,10 +620,11 @@ class Project:
521
620
  """
522
621
 
523
622
  try:
524
- data = platform.parse_response(platform.post(
525
- self._account.auth, "file_manager/get_container_files",
526
- data={"container_id": container_id}
527
- ))
623
+ data = platform.parse_response(
624
+ platform.post(
625
+ self._account.auth, "file_manager/get_container_files", data={"container_id": container_id}
626
+ )
627
+ )
528
628
  except errors.PlatformError as e:
529
629
  logging.getLogger(logger_name).error(e)
530
630
  return False
@@ -545,12 +645,15 @@ class Project:
545
645
  Returns
546
646
  -------
547
647
  dict
548
- Dictionary with the metadata.
648
+ Dictionary with the metadata. False otherwise.
549
649
  """
550
650
  all_metadata = self.list_container_files_metadata(container_id)
551
- for file_meta in all_metadata:
552
- if file_meta["name"] == filename:
553
- return file_meta
651
+ if all_metadata:
652
+ for file_meta in all_metadata:
653
+ if file_meta["name"] == filename:
654
+ return file_meta
655
+ else:
656
+ return False
554
657
 
555
658
  def change_file_metadata(self, container_id, filename, modality, tags):
556
659
  """
@@ -570,18 +673,15 @@ class Project:
570
673
  """
571
674
 
572
675
  tags_str = "" if tags is None else ";".join(tags)
573
- platform.parse_response(platform.post(
574
- self._account.auth, "file_manager/edit_file",
575
- data={
576
- "container_id": container_id,
577
- "filename": filename,
578
- "tags": tags_str,
579
- "modality": modality
580
- }
581
- ))
676
+ platform.parse_response(
677
+ platform.post(
678
+ self._account.auth,
679
+ "file_manager/edit_file",
680
+ data={"container_id": container_id, "filename": filename, "tags": tags_str, "modality": modality},
681
+ )
682
+ )
582
683
 
583
- def download_file(self, container_id, file_name, local_filename=False,
584
- overwrite=False):
684
+ def download_file(self, container_id, file_name, local_filename=False, overwrite=False):
585
685
  """
586
686
  Download a single file from a specific container.
587
687
 
@@ -598,8 +698,7 @@ class Project:
598
698
  """
599
699
  logger = logging.getLogger(logger_name)
600
700
  if file_name not in self.list_container_files(container_id):
601
- msg = (f"File \"{file_name}\" does not exist in container "
602
- f"{container_id}")
701
+ msg = f'File "{file_name}" does not exist in container ' f"{container_id}"
603
702
  logger.error(msg)
604
703
  return False
605
704
 
@@ -612,22 +711,18 @@ class Project:
612
711
 
613
712
  params = {"container_id": container_id, "files": file_name}
614
713
 
615
- with platform.post(self._account.auth, "file_manager/download_file",
616
- data=params, stream=True) as response, \
617
- open(local_filename, "wb") as f:
714
+ with platform.post(
715
+ self._account.auth, "file_manager/download_file", data=params, stream=True
716
+ ) as response, open(local_filename, "wb") as f:
618
717
 
619
- for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
718
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
620
719
  f.write(chunk)
621
720
  f.flush()
622
721
 
623
- logger.info(
624
- f"File {file_name} from container {container_id} saved to"
625
- f" {local_filename}"
626
- )
722
+ logger.info(f"File {file_name} from container {container_id} saved to" f" {local_filename}")
627
723
  return True
628
724
 
629
- def download_files(self, container_id, filenames, zip_name="files.zip",
630
- overwrite=False):
725
+ def download_files(self, container_id, filenames, zip_name="files.zip", overwrite=False):
631
726
  """
632
727
  Download a set of files from a given container.
633
728
 
@@ -643,46 +738,43 @@ class Project:
643
738
  Name of the zip where the downloaded files are stored.
644
739
  """
645
740
  logger = logging.getLogger(logger_name)
646
- files_not_in_container = list(
647
- filter(lambda f: f not in self.list_container_files(container_id),
648
- filenames)
649
- )
741
+ files_not_in_container = list(filter(lambda f: f not in self.list_container_files(container_id), filenames))
650
742
 
651
743
  if files_not_in_container:
652
- msg = (f"The following files are missing in container "
653
- f"{container_id}: {', '.join(files_not_in_container)}")
744
+ msg = (
745
+ f"The following files are missing in container " f"{container_id}: {', '.join(files_not_in_container)}"
746
+ )
654
747
  logger.error(msg)
655
748
  return False
656
749
 
657
750
  if os.path.exists(zip_name) and not overwrite:
658
- msg = f"File \"{zip_name}\" already exists"
751
+ msg = f'File "{zip_name}" already exists'
659
752
  logger.error(msg)
660
753
  return False
661
754
 
662
755
  params = {"container_id": container_id, "files": ";".join(filenames)}
663
- with platform.post(self._account.auth,
664
- "file_manager/download_file",
665
- data=params, stream=True) as response, \
666
- open(zip_name, "wb") as f:
756
+ with platform.post(
757
+ self._account.auth, "file_manager/download_file", data=params, stream=True
758
+ ) as response, open(zip_name, "wb") as f:
667
759
 
668
- for chunk in response.iter_content(chunk_size=2 ** 9 * 1024):
760
+ for chunk in response.iter_content(chunk_size=2**9 * 1024):
669
761
  f.write(chunk)
670
762
  f.flush()
671
763
 
672
- logger.info("Files from container {} saved to {}".format(
673
- container_id, zip_name))
764
+ logger.info("Files from container {} saved to {}".format(container_id, zip_name))
674
765
  return True
675
766
 
676
- def get_subject_id(self, subject_name, cache=False):
767
+ def get_subject_id(self, subject_name, ssid):
677
768
  """
678
- Given a subject name, return its ID in the project.
769
+ Given a Subject ID and Session ID, return its Patient ID in the
770
+ project.
679
771
 
680
772
  Parameters
681
773
  ----------
682
774
  subject_name : str
683
- Name of the subject in the project.
684
- cache : bool
685
- Whether to use the cached metadata or not
775
+ Subject ID of the subject in the project.
776
+ ssid : str
777
+ Session ID of the subject in the project.
686
778
 
687
779
  Returns
688
780
  -------
@@ -691,37 +783,11 @@ class Project:
691
783
  the subject is not found.
692
784
  """
693
785
 
694
- for user in self.get_subjects_metadata(cache):
695
- if user["patient_secret_name"] == subject_name:
786
+ for user in self.get_subjects_metadata():
787
+ if user["patient_secret_name"] == str(subject_name) and user["ssid"] == str(ssid):
696
788
  return int(user["_id"])
697
789
  return False
698
790
 
699
- def get_subject(self, subject_name, cache=True):
700
- """
701
- Return a subject object, representing a subject from the project.
702
-
703
- Parameters
704
- ----------
705
- subject_name : str
706
- Name of the subject.
707
- cache: bool
708
- Whether to use the cached metadata or not
709
-
710
- Returns
711
- -------
712
- Subject or bool
713
- A Subject instance representing the desired subject, or
714
- False if the subject was not found.
715
-
716
- """
717
- subject_id = self.get_subject_id(subject_name, cache=cache)
718
- if subject_id is False:
719
- return False
720
- subj = Subject(subject_name)
721
- subj.subject_id = subject_id
722
- subj.project = self
723
- return subj
724
-
725
791
  def add_subject(self, subject):
726
792
  """
727
793
  Add a subject to the project.
@@ -738,37 +804,119 @@ class Project:
738
804
  """
739
805
  logger = logging.getLogger(logger_name)
740
806
  if self.check_subject_name(subject.name):
741
- logger.error(f"Subject with name {subject.name} already exists in "
742
- f"project!")
807
+ logger.error(f"Subject with name {subject.name} already exists in " f"project!")
743
808
  return False
744
809
 
745
810
  try:
746
- platform.parse_response(platform.post(
747
- self._account.auth, "patient_manager/upsert_patient",
748
- data={"secret_name": subject.name}
749
- ))
811
+ platform.parse_response(
812
+ platform.post(self._account.auth, "patient_manager/upsert_patient", data={"secret_name": subject.name})
813
+ )
750
814
  except errors.PlatformError:
751
815
  logger.error(f"Subject {subject.name} could not be created.")
752
816
  return False
753
817
 
754
818
  subject.subject_id = self.get_subject_id(subject.name)
755
819
  subject.project = self
756
- logger.info(
757
- "Subject {0} was successfully created".format(subject.name))
820
+ logger.info("Subject {0} was successfully created".format(subject.name))
821
+ return True
822
+
823
+ def change_subject_metadata(self, patient_id, subject_name, ssid, tags, age_at_scan, metadata):
824
+ """
825
+ Change the Subject ID, Session ID, Tags, Age at Scan and Metadata of
826
+ the session with Patient ID
827
+
828
+ Parameters
829
+ ----------
830
+ patient_id : Integer
831
+ Patient ID representing the session to modify.
832
+ subject_name : String
833
+ Represents the new Subject ID.
834
+ ssid : String
835
+ Represents the new Session ID.
836
+ tags : list of strings in lowercase
837
+ Represents the new tags of the session.
838
+ age_at_scan : Integer
839
+ Represents the new Age at Scan of the Session.
840
+ metadata : Dictionary
841
+ Each pair key/value representing the new metadata values.
842
+
843
+ The keys must either all start with "md\\_" or none start
844
+ with "md\\_".
845
+
846
+ The key represents the ID of the metadata field.
847
+
848
+ Returns
849
+ -------
850
+ bool
851
+ True if correctly modified, False otherwise
852
+ """
853
+ logger = logging.getLogger(logger_name)
854
+
855
+ try:
856
+ patient_id = str(int(patient_id))
857
+ except ValueError:
858
+ raise ValueError(f"'patient_id': '{patient_id}' not valid. " f"Must be convertible to int.")
859
+
860
+ assert isinstance(tags, list) and all(
861
+ isinstance(item, str) for item in tags
862
+ ), f"tags: '{tags}' should be a list of strings."
863
+ tags = [tag.lower() for tag in tags]
864
+
865
+ assert subject_name is not None and subject_name != "", "subject_name must be a non empty string."
866
+ assert ssid is not None and ssid != "", "ssid must be a non empty string."
867
+
868
+ try:
869
+ age_at_scan = str(int(age_at_scan)) if age_at_scan else None
870
+ except ValueError:
871
+ raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. " f"Must be an integer.")
872
+
873
+ assert isinstance(metadata, dict), f"metadata: '{metadata}' should be a dictionary."
874
+
875
+ assert all("md_" == key[:3] for key in metadata.keys()) or all("md_" != key[:3] for key in metadata.keys()), (
876
+ f"metadata: '{metadata}' must be a dictionary whose keys " f"are either all starting with 'md_' or none."
877
+ )
878
+
879
+ metadata_keys = self.metadata_parameters.keys()
880
+ assert all(
881
+ [key[3:] in metadata_keys if "md_" == key[:3] else key in metadata_keys for key in metadata.keys()]
882
+ ), (
883
+ f"Some metadata keys provided ({', '.join(metadata.keys())}) "
884
+ f"are not available in the project. They can be added via the "
885
+ f"Metadata Manager via the QMENTA Platform graphical user "
886
+ f"interface (GUI)."
887
+ )
888
+
889
+ post_data = {
890
+ "patient_id": patient_id,
891
+ "secret_name": str(subject_name),
892
+ "ssid": str(ssid),
893
+ "tags": ",".join(tags),
894
+ "age_at_scan": age_at_scan,
895
+ }
896
+ for key, value in metadata.items():
897
+ id = key[3:] if "md_" == key[:3] else key
898
+ post_data[f"last_vals.{id}"] = value
899
+
900
+ try:
901
+ platform.parse_response(platform.post(self._account.auth, "patient_manager/upsert_patient", data=post_data))
902
+ except errors.PlatformError:
903
+ logger.error(f"Patient ID '{patient_id}' could not be modified.")
904
+ return False
905
+
906
+ logger.info(f"Patient ID '{patient_id}' successfully modified.")
758
907
  return True
759
908
 
760
- def delete_session(self, subject_name, session_id, cache=False):
909
+ def delete_session(self, subject_name, session_id):
761
910
  """
762
- Delete a session from a subject within a project.
911
+ Delete a session from a subject within a project providing the
912
+ Subject ID and Session ID.
763
913
 
764
914
  Parameters
765
915
  ----------
766
916
  subject_name : str
767
- Name of the subject
917
+ Subject ID of the subject
768
918
  session_id : int
769
- The SSID of the session that will be deleted
770
- cache : bool
771
- Whether to use the cached metadata or not
919
+ The Session ID of the session that will be deleted
772
920
 
773
921
  Returns
774
922
  -------
@@ -776,58 +924,78 @@ class Project:
776
924
  True if correctly deleted, False otherwise.
777
925
  """
778
926
  logger = logging.getLogger(logger_name)
779
- all_sessions = self.get_subjects_metadata(cache)
927
+ all_sessions = self.get_subjects_metadata()
780
928
 
781
- sessions_to_del = [
782
- s for s in all_sessions if
783
- s["patient_secret_name"] == subject_name and int(
784
- s["ssid"]
785
- ) == session_id
929
+ session_to_del = [
930
+ s for s in all_sessions if s["patient_secret_name"] == subject_name and s["ssid"] == session_id
786
931
  ]
787
932
 
788
- if not sessions_to_del:
789
- logger.error(
790
- f"Session {subject_name}/{session_id} could not be found "
791
- f"in this project."
792
- )
933
+ if not session_to_del:
934
+ logger.error(f"Session {subject_name}/{session_id} could not be found " f"in this project.")
793
935
  return False
794
- elif len(sessions_to_del) > 1:
795
- raise RuntimeError(
796
- "Multiple sessions with same SID and SSID. Contact support."
797
- )
936
+ elif len(session_to_del) > 1:
937
+ raise RuntimeError("Multiple sessions with same Subject ID and Session ID." " Contact support.")
798
938
  else:
799
- logger.info("{}/{} found (id {})".format(
800
- subject_name, session_id, sessions_to_del[0]["_id"]
801
- ))
939
+ logger.info("{}/{} found (id {})".format(subject_name, session_id, session_to_del[0]["_id"]))
802
940
 
803
- session = sessions_to_del[0]
941
+ session = session_to_del[0]
804
942
 
805
943
  try:
806
- platform.parse_response(platform.post(
807
- self._account.auth, "patient_manager/delete_patient",
808
- data={
809
- "patient_id": str(int(session["_id"])), "delete_files": 1
810
- }
811
- ))
944
+ platform.parse_response(
945
+ platform.post(
946
+ self._account.auth,
947
+ "patient_manager/delete_patient",
948
+ data={"patient_id": str(int(session["_id"])), "delete_files": 1},
949
+ )
950
+ )
812
951
  except errors.PlatformError:
813
- logger.error(f"Session \"{subject_name}/{session['ssid']}\" could"
814
- f" not be deleted.")
952
+ logger.error(f"Session \"{subject_name}/{session['ssid']}\" could" f" not be deleted.")
815
953
  return False
816
954
 
817
- logger.info(
818
- f"Session \"{subject_name}/{session['ssid']}\" successfully "
819
- f"deleted."
820
- )
955
+ logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully " f"deleted.")
956
+ return True
957
+
958
+ def delete_session_by_patientid(self, patient_id):
959
+ """
960
+ Delete a session from a subject within a project providing the
961
+ Patient ID.
962
+
963
+ Parameters
964
+ ----------
965
+ patient_id : str
966
+ Patient ID of the Session ID/Subject ID
967
+
968
+ Returns
969
+ -------
970
+ bool
971
+ True if correctly deleted, False otherwise.
972
+ """
973
+ logger = logging.getLogger(logger_name)
974
+
975
+ try:
976
+ platform.parse_response(
977
+ platform.post(
978
+ self._account.auth,
979
+ "patient_manager/delete_patient",
980
+ data={"patient_id": str(int(patient_id)), "delete_files": 1},
981
+ )
982
+ )
983
+ except errors.PlatformError:
984
+ logger.error(f"Patient ID {patient_id} could not be deleted.")
985
+ return False
986
+
987
+ logger.info(f"Patient ID {patient_id} successfully deleted.")
821
988
  return True
822
989
 
823
990
  def delete_subject(self, subject_name):
824
991
  """
825
- Delete a subject from the project.
992
+ Delete a subject from the project. It deletes all its available
993
+ sessions only providing the Subject ID.
826
994
 
827
995
  Parameters
828
996
  ----------
829
997
  subject_name : str
830
- Name of the subject to be deleted.
998
+ Subject ID of the subject to be deleted.
831
999
 
832
1000
  Returns
833
1001
  -------
@@ -837,34 +1005,38 @@ class Project:
837
1005
 
838
1006
  logger = logging.getLogger(logger_name)
839
1007
  # Always fetch the session IDs from the platform before deleting them
840
- all_sessions = self.get_subjects_metadata(False)
1008
+ all_sessions = self.get_subjects_metadata()
841
1009
 
842
- sessions_to_del = [
843
- s for s in all_sessions if s["patient_secret_name"] == subject_name
844
- ]
1010
+ sessions_to_del = [s for s in all_sessions if s["patient_secret_name"] == subject_name]
845
1011
 
846
1012
  if not sessions_to_del:
847
- logger.error(
848
- "Subject {} cannot be found in this project.".format(
849
- subject_name
850
- )
851
- )
1013
+ logger.error("Subject {} cannot be found in this project.".format(subject_name))
852
1014
  return False
853
1015
 
854
1016
  for ssid in [s["ssid"] for s in sessions_to_del]:
855
- if not self.delete_session(subject_name, ssid, cache=True):
1017
+ if not self.delete_session(subject_name, ssid):
856
1018
  return False
857
1019
  return True
858
1020
 
859
- def _upload_chunk(self, data, range_str, length, session_id,
860
- disposition,
861
- last_chunk,
862
- name="", date_of_scan="", description="",
863
- subject_name="", ssid="", filename="DATA.zip",
864
- input_data_type="mri_brain_data:1.0",
865
- result=False, add_to_container_id=0,
866
- split_data=False
867
- ):
1021
+ def _upload_chunk(
1022
+ self,
1023
+ data,
1024
+ range_str,
1025
+ length,
1026
+ session_id,
1027
+ disposition,
1028
+ last_chunk,
1029
+ name="",
1030
+ date_of_scan="",
1031
+ description="",
1032
+ subject_name="",
1033
+ ssid="",
1034
+ filename="DATA.zip",
1035
+ input_data_type="mri_brain_data:1.0",
1036
+ result=False,
1037
+ add_to_container_id=0,
1038
+ split_data=False,
1039
+ ):
868
1040
  """
869
1041
  Upload a chunk of a file to the platform.
870
1042
 
@@ -891,10 +1063,11 @@ class Project:
891
1063
  """
892
1064
 
893
1065
  request_headers = {
894
- "Content-Type": "application/zip", "Content-Range":
895
- range_str, "Session-ID": str(session_id),
1066
+ "Content-Type": "application/zip",
1067
+ "Content-Range": range_str,
1068
+ "Session-ID": str(session_id),
896
1069
  "Content-Length": str(length),
897
- "Content-Disposition": disposition
1070
+ "Content-Disposition": disposition,
898
1071
  }
899
1072
 
900
1073
  if last_chunk:
@@ -922,31 +1095,36 @@ class Project:
922
1095
 
923
1096
  response_time = 900.0 if last_chunk else 120.0
924
1097
  response = platform.post(
925
- auth=self._account.auth,
926
- endpoint="upload",
927
- data=data,
928
- headers=request_headers,
929
- timeout=response_time
1098
+ auth=self._account.auth, endpoint="upload", data=data, headers=request_headers, timeout=response_time
930
1099
  )
931
1100
 
932
1101
  return response
933
1102
 
934
- def upload_file(self, file_path, subject_name, ssid="", date_of_scan="",
935
- description="", result=False, name="",
936
- input_data_type="qmenta_mri_brain_data:1.0",
937
- add_to_container_id=0, chunk_size=2 ** 9,
938
- split_data=False):
1103
+ def upload_file(
1104
+ self,
1105
+ file_path,
1106
+ subject_name,
1107
+ ssid="",
1108
+ date_of_scan="",
1109
+ description="",
1110
+ result=False,
1111
+ name="",
1112
+ input_data_type="qmenta_mri_brain_data:1.0",
1113
+ add_to_container_id=0,
1114
+ chunk_size=2**9,
1115
+ split_data=False,
1116
+ ):
939
1117
  """
940
- Upload a file to the platform, associated with the current user.
1118
+ Upload a ZIP file to the platform.
941
1119
 
942
1120
  Parameters
943
1121
  ----------
944
1122
  file_path : str
945
- Path to the file to upload.
1123
+ Path to the ZIP file to upload.
946
1124
  subject_name : str
947
- Subject to which this file will belong
1125
+ Subject ID of the data to upload in the project in QMENTA Platform.
948
1126
  ssid : str
949
- The ID of the timepoint
1127
+ Session ID of the Subject ID (i.e., ID of the timepoint).
950
1128
  date_of_scan : str
951
1129
  Date of scan/creation of the file
952
1130
  description : str
@@ -956,7 +1134,7 @@ class Project:
956
1134
  name : str
957
1135
  Name of the file in the platform
958
1136
  input_data_type : str
959
- mri_brain_data:1.0 or gametection:1.0
1137
+ qmenta_medical_image_data:3.11
960
1138
  add_to_container_id : int
961
1139
  ID of the container to which this file should be added (if id > 0)
962
1140
  chunk_size : int
@@ -999,8 +1177,7 @@ class Project:
999
1177
  last_chunk = False
1000
1178
 
1001
1179
  if ssid and split_data:
1002
- logger.warning("split-data argument will be ignored because" +
1003
- " ssid has been specified")
1180
+ logger.warning("split-data argument will be ignored because" + " ssid has been specified")
1004
1181
  split_data = False
1005
1182
 
1006
1183
  while True:
@@ -1017,16 +1194,27 @@ class Project:
1017
1194
  end_position = total_bytes - 1
1018
1195
  bytes_to_send = total_bytes - uploaded_bytes
1019
1196
 
1020
- bytes_range = "bytes " + str(start_position) + "-" + \
1021
- str(end_position) + "/" + str(total_bytes)
1197
+ bytes_range = "bytes " + str(start_position) + "-" + str(end_position) + "/" + str(total_bytes)
1022
1198
 
1023
1199
  dispstr = f"attachment; filename={filename}"
1024
1200
  response = self._upload_chunk(
1025
- data, bytes_range, bytes_to_send, session_id, dispstr,
1201
+ data,
1202
+ bytes_range,
1203
+ bytes_to_send,
1204
+ session_id,
1205
+ dispstr,
1026
1206
  last_chunk,
1027
- name, date_of_scan, description, subject_name, ssid,
1028
- filename, input_data_type, result, add_to_container_id,
1029
- split_data)
1207
+ name,
1208
+ date_of_scan,
1209
+ description,
1210
+ subject_name,
1211
+ ssid,
1212
+ filename,
1213
+ input_data_type,
1214
+ result,
1215
+ add_to_container_id,
1216
+ split_data,
1217
+ )
1030
1218
 
1031
1219
  if response is None:
1032
1220
  retries_count += 1
@@ -1046,17 +1234,14 @@ class Project:
1046
1234
  retries_count += 1
1047
1235
  time.sleep(retries_count * 5)
1048
1236
  if retries_count > self.max_retries:
1049
- error_message = (
1050
- "Error Code: 416; "
1051
- "Requested Range Not Satisfiable (NGINX)")
1237
+ error_message = "Error Code: 416; " "Requested Range Not Satisfiable (NGINX)"
1052
1238
  logger.error(error_message)
1053
1239
  break
1054
1240
  else:
1055
1241
  retries_count += 1
1056
1242
  time.sleep(retries_count * 5)
1057
1243
  if retries_count > max_retries:
1058
- error_message = ("Number of retries has been reached. "
1059
- "Upload process stops here !")
1244
+ error_message = "Number of retries has been reached. " "Upload process stops here !"
1060
1245
  logger.error(error_message)
1061
1246
  break
1062
1247
 
@@ -1110,9 +1295,7 @@ class Project:
1110
1295
  """
1111
1296
 
1112
1297
  if check_upload_file(file_path):
1113
- return self.upload_file(
1114
- file_path, subject_name,
1115
- input_data_type="parkinson_gametection")
1298
+ return self.upload_file(file_path, subject_name, input_data_type="parkinson_gametection")
1116
1299
  return False
1117
1300
 
1118
1301
  def upload_result(self, file_path, subject_name):
@@ -1156,13 +1339,9 @@ class Project:
1156
1339
  p_id = int(project_id)
1157
1340
  elif type(project_id) == str:
1158
1341
  projects = self._account.projects
1159
- projects_match = [proj for proj in projects
1160
- if proj["name"] == project_id]
1342
+ projects_match = [proj for proj in projects if proj["name"] == project_id]
1161
1343
  if not projects_match:
1162
- raise Exception(
1163
- f"Project {project_id}" +
1164
- " does not exist or is not available for this user."
1165
- )
1344
+ raise Exception(f"Project {project_id}" + " does not exist or is not available for this user.")
1166
1345
  p_id = int(projects_match[0]["id"])
1167
1346
  else:
1168
1347
  raise TypeError("project_id")
@@ -1172,30 +1351,26 @@ class Project:
1172
1351
  }
1173
1352
 
1174
1353
  try:
1175
- platform.parse_response(platform.post(
1176
- self._account.auth,
1177
- "file_manager/copy_container_to_another_project",
1178
- data=data
1179
- ))
1180
- except errors.PlatformError as e:
1181
- logging.getLogger(logger_name).error(
1182
- "Couldn not copy container: {}".format(e)
1354
+ platform.parse_response(
1355
+ platform.post(self._account.auth, "file_manager/copy_container_to_another_project", data=data)
1183
1356
  )
1357
+ except errors.PlatformError as e:
1358
+ logging.getLogger(logger_name).error("Couldn not copy container: {}".format(e))
1184
1359
  return False
1185
1360
 
1186
1361
  return True
1187
1362
 
1188
1363
  def start_analysis(
1189
- self,
1190
- script_name,
1191
- version,
1192
- in_container_id=None,
1193
- analysis_name=None,
1194
- analysis_description=None,
1195
- ignore_warnings=False,
1196
- settings=None,
1197
- tags=None,
1198
- preferred_destination=None
1364
+ self,
1365
+ script_name,
1366
+ version,
1367
+ in_container_id=None,
1368
+ analysis_name=None,
1369
+ analysis_description=None,
1370
+ ignore_warnings=False,
1371
+ settings=None,
1372
+ tags=None,
1373
+ preferred_destination=None,
1199
1374
  ):
1200
1375
  """
1201
1376
  Starts an analysis on a subject.
@@ -1234,13 +1409,9 @@ class Project:
1234
1409
  logger = logging.getLogger(logger_name)
1235
1410
 
1236
1411
  if in_container_id is None and settings is None:
1237
- raise ValueError(
1238
- "Pass a value for either in_container_id or settings.")
1412
+ raise ValueError("Pass a value for either in_container_id or settings.")
1239
1413
 
1240
- post_data = {
1241
- "script_name": script_name,
1242
- "version": version
1243
- }
1414
+ post_data = {"script_name": script_name, "version": version}
1244
1415
 
1245
1416
  settings = settings or {}
1246
1417
 
@@ -1270,9 +1441,7 @@ class Project:
1270
1441
  post_data["preferred_destination"] = preferred_destination
1271
1442
 
1272
1443
  logger.debug(f"post_data = {post_data}")
1273
- return self.__handle_start_analysis(
1274
- post_data, ignore_warnings=ignore_warnings
1275
- )
1444
+ return self.__handle_start_analysis(post_data, ignore_warnings=ignore_warnings)
1276
1445
 
1277
1446
  def delete_analysis(self, analysis_id):
1278
1447
  """
@@ -1284,19 +1453,20 @@ class Project:
1284
1453
  logger = logging.getLogger(logger_name)
1285
1454
 
1286
1455
  try:
1287
- platform.parse_response(platform.post(
1288
- auth=self._account.auth,
1289
- endpoint="analysis_manager/delete_analysis",
1290
- data={"project_id": analysis_id}
1291
- ))
1456
+ platform.parse_response(
1457
+ platform.post(
1458
+ auth=self._account.auth,
1459
+ endpoint="analysis_manager/delete_analysis",
1460
+ data={"project_id": analysis_id},
1461
+ )
1462
+ )
1292
1463
  except errors.PlatformError as error:
1293
1464
  logger.error("Could not delete analysis: {}".format(error))
1294
1465
  return False
1295
1466
 
1296
1467
  return True
1297
1468
 
1298
- def __handle_start_analysis(self, post_data, ignore_warnings=False,
1299
- n_calls=0):
1469
+ def __handle_start_analysis(self, post_data, ignore_warnings=False, n_calls=0):
1300
1470
  """
1301
1471
  Handle the possible responses from the server after start_analysis.
1302
1472
  Sometimes we have to send a request again, and then check again the
@@ -1311,16 +1481,16 @@ class Project:
1311
1481
 
1312
1482
  logger = logging.getLogger(logger_name)
1313
1483
  if n_calls > call_limit:
1314
- logger.error(f"__handle_start_analysis_response called itself more\
1315
- than {n_calls} times: aborting.")
1484
+ logger.error(
1485
+ f"__handle_start_analysis_response called itself more\
1486
+ than {n_calls} times: aborting."
1487
+ )
1316
1488
  return None
1317
1489
 
1318
1490
  try:
1319
- response = platform.parse_response(platform.post(
1320
- self._account.auth,
1321
- "analysis_manager/analysis_registration",
1322
- data=post_data
1323
- ))
1491
+ response = platform.parse_response(
1492
+ platform.post(self._account.auth, "analysis_manager/analysis_registration", data=post_data)
1493
+ )
1324
1494
  logger.info(response["message"])
1325
1495
  return int(response["analysis_id"])
1326
1496
  except platform.ChooseDataError as choose_data:
@@ -1342,8 +1512,7 @@ class Project:
1342
1512
  chosen_files = {}
1343
1513
  for settings_key in choose_data.data_to_choose:
1344
1514
  chosen_files[settings_key] = {}
1345
- filters = choose_data.data_to_choose[
1346
- settings_key]["filters"]
1515
+ filters = choose_data.data_to_choose[settings_key]["filters"]
1347
1516
  for filter_key in filters:
1348
1517
  filter_data = filters[filter_key]
1349
1518
 
@@ -1355,35 +1524,24 @@ class Project:
1355
1524
  if filter_data["range"][0] != 0:
1356
1525
  number_of_files_to_select = filter_data["range"][0]
1357
1526
  elif filter_data["range"][1] != 0:
1358
- number_of_files_to_select = min(
1359
- filter_data["range"][1],
1360
- len(filter_data["files"])
1361
- )
1527
+ number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
1362
1528
  else:
1363
- number_of_files_to_select = len(
1364
- filter_data["files"]
1365
- )
1529
+ number_of_files_to_select = len(filter_data["files"])
1366
1530
 
1367
- files_selection = [ff["_id"] for ff in
1368
- filter_data["files"]
1369
- [:number_of_files_to_select]]
1370
- chosen_files[settings_key][filter_key] = \
1371
- files_selection
1531
+ files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
1532
+ chosen_files[settings_key][filter_key] = files_selection
1372
1533
 
1373
1534
  new_post["user_preference"] = json.dumps(chosen_files)
1374
1535
  else:
1375
1536
  if has_warning and not ignore_warnings:
1376
- logger.info("cancelling analysis due to warnings, " +
1377
- "set \"ignore_warnings\" to True to override")
1537
+ logger.info("cancelling analysis due to warnings, " + 'set "ignore_warnings" to True to override')
1378
1538
  new_post["cancel"] = "1"
1379
1539
  else:
1380
1540
  logger.info("suppressing warnings")
1381
1541
  new_post["user_preference"] = "{}"
1382
1542
  new_post["_mint_only_warning"] = "1"
1383
1543
 
1384
- return self.__handle_start_analysis(
1385
- new_post, ignore_warnings=ignore_warnings, n_calls=n_calls
1386
- )
1544
+ return self.__handle_start_analysis(new_post, ignore_warnings=ignore_warnings, n_calls=n_calls)
1387
1545
  except platform.ActionFailedError as e:
1388
1546
  logger.error(f"Unable to start the analysis: {e}")
1389
1547
  return None
@@ -1414,19 +1572,15 @@ class Project:
1414
1572
  logger = logging.getLogger(__name__)
1415
1573
  logger.info(f"Setting QC status to {status}: {comments}")
1416
1574
 
1417
- platform.parse_response(platform.post(
1418
- auth=self._account.auth,
1419
- endpoint="projectset_manager/set_qa_status",
1420
- data={
1421
- "item_ids": analysis_id,
1422
- "status": status.value,
1423
- "comments": comments,
1424
- "entity": "analysis"
1425
- }
1426
- ))
1575
+ platform.parse_response(
1576
+ platform.post(
1577
+ auth=self._account.auth,
1578
+ endpoint="projectset_manager/set_qa_status",
1579
+ data={"item_ids": analysis_id, "status": status.value, "comments": comments, "entity": "analysis"},
1580
+ )
1581
+ )
1427
1582
 
1428
- def get_qc_status(
1429
- self, patient_secret_name=None, ssid=None, analysis_id=None):
1583
+ def get_qc_status(self, patient_secret_name=None, ssid=None, analysis_id=None):
1430
1584
  """
1431
1585
  Gets the session QC status of a session. If the analysis_id is
1432
1586
  specified, it returns the QC of the
@@ -1436,17 +1590,15 @@ class Project:
1436
1590
  if patient_secret_name and ssid:
1437
1591
  session = self.get_subjects_metadata(
1438
1592
  search_criteria={
1439
- "pars_patient_secret_name": f"string;"
1440
- f"{patient_secret_name}",
1441
- "pars_ssid": f"integer;eq|{ssid}"
1593
+ "pars_patient_secret_name": f"string;" f"{patient_secret_name}",
1594
+ "pars_ssid": f"integer;eq|{ssid}",
1442
1595
  }
1443
1596
  )
1444
1597
  to_return = session["qa_status"], session["qa_comments"]
1445
1598
  elif analysis_id:
1446
1599
  try:
1447
1600
  to_return = [
1448
- analysis["qa_data"] for analysis in self.list_analysis()
1449
- if analysis["_id"] == analysis_id
1601
+ analysis["qa_data"] for analysis in self.list_analysis() if analysis["_id"] == analysis_id
1450
1602
  ][0]
1451
1603
  to_return = to_return["qa_status"], to_return["qa_comments"]
1452
1604
  except IndexError:
@@ -1457,22 +1609,21 @@ class Project:
1457
1609
  print(f"An error occurred: {e}")
1458
1610
  to_return = None
1459
1611
  else:
1460
- raise Exception(f"Must specify {patient_secret_name} and "
1461
- f"{ssid} or {analysis_id}.")
1612
+ raise Exception(f"Must specify {patient_secret_name} and {ssid} or {analysis_id}.")
1462
1613
  return to_return
1463
1614
 
1464
1615
  def start_multiple_analyses(
1465
- self,
1466
- script_name,
1467
- version,
1468
- n_times,
1469
- in_container_id=None,
1470
- analysis_name=None,
1471
- analysis_description=None,
1472
- ignore_warnings=False,
1473
- settings=None,
1474
- tags=None,
1475
- preferred_destination=None
1616
+ self,
1617
+ script_name,
1618
+ version,
1619
+ n_times,
1620
+ in_container_id=None,
1621
+ analysis_name=None,
1622
+ analysis_description=None,
1623
+ ignore_warnings=False,
1624
+ settings=None,
1625
+ tags=None,
1626
+ preferred_destination=None,
1476
1627
  ):
1477
1628
  """
1478
1629
  Starts multiple times the same analysis on a subject with the same
@@ -1513,9 +1664,7 @@ class Project:
1513
1664
  """
1514
1665
  logger = logging.getLogger(logger_name)
1515
1666
  for n in range(n_times):
1516
- logger.info(
1517
- f"Running tool {script_name}:{version} {n + 1}/{n_times}"
1518
- )
1667
+ logger.info(f"Running tool {script_name}:{version} {n + 1}/{n_times}")
1519
1668
  yield self.start_analysis(
1520
1669
  script_name,
1521
1670
  version,
@@ -1525,5 +1674,40 @@ class Project:
1525
1674
  ignore_warnings=ignore_warnings,
1526
1675
  settings=settings,
1527
1676
  tags=tags,
1528
- preferred_destination=preferred_destination
1677
+ preferred_destination=preferred_destination,
1529
1678
  )
1679
+
1680
+ def set_project_qa_rules(self, rules_file_path, guidance_text=""):
1681
+ """
1682
+ Logs in to the Qmenta platform, retrieves the project ID based on the project name,
1683
+ and updates the project's QA rules using the provided rules file.
1684
+
1685
+ Args:
1686
+ rules_file_path (str): The file path to the JSON file containing the QA rules.
1687
+ guidance_text (str): Description of the rules. Only visible for Platform admins.
1688
+
1689
+ Returns:
1690
+ bool: True if the rules were set successfully, False otherwise.
1691
+ """
1692
+ # Read the rules from the JSON file
1693
+ try:
1694
+ with open(rules_file_path, "r") as fr:
1695
+ rules = json.load(fr)
1696
+ except FileNotFoundError:
1697
+ print(f"ERROR: Rules file '{rules_file_path}' not found.")
1698
+ return False
1699
+
1700
+ # Update the project's QA rules
1701
+ res = platform.post(
1702
+ auth=self._account.auth,
1703
+ endpoint="projectset_manager/set_session_qa_requirements",
1704
+ data={"project_id": self._project_id, "rules": json.dumps(rules), "guidance_text": guidance_text},
1705
+ )
1706
+
1707
+ if res.json().get("success") == 1:
1708
+ print("Rules set up successfully!")
1709
+ return True
1710
+ else:
1711
+ print("ERROR setting the rules")
1712
+ print(res.json())
1713
+ return False