qmenta-client 1.1.dev1295__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,6 +17,7 @@ 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):
@@ -67,6 +68,138 @@ def check_upload_file(file_path):
67
68
  return True
68
69
 
69
70
 
71
+ def operation(reference_value, operator, input_value):
72
+ """
73
+ The method performs an operation by comparing the two input values.
74
+ The Operation is applied to the Input Value in comparison to the Reference
75
+ Value.
76
+
77
+ Parameters
78
+ ----------
79
+ reference_value : str, list, or int
80
+ Reference value.
81
+ operator : str
82
+ Operation.
83
+ input_value : str, list, or int
84
+ Input value.
85
+
86
+ Returns
87
+ -------
88
+ bool
89
+ True if the operation is satisfied, False otherwise.
90
+ """
91
+ if input_value is None or input_value == "":
92
+ return False
93
+
94
+ if operator == "in":
95
+ return reference_value in input_value
96
+
97
+ elif operator == "in-list":
98
+ return all([el in input_value for el in reference_value])
99
+
100
+ elif operator == "eq":
101
+ return input_value == reference_value
102
+
103
+ elif operator == "gt":
104
+ return input_value > reference_value
105
+
106
+ elif operator == "gte":
107
+ return input_value >= reference_value
108
+
109
+ elif operator == "lt":
110
+ return input_value < reference_value
111
+
112
+ elif operator == "lte":
113
+ return input_value <= reference_value
114
+ else:
115
+ return False
116
+
117
+
118
+ def wrap_search_criteria(search_criteria={}):
119
+ """
120
+ Wraps the conditions specified within the Search Criteria in order for
121
+ other methods to handle it easily. The conditions are grouped only into
122
+ three groups: Modality, Tags and the File Metadata (if DICOM it corresponds
123
+ to the DICOM information), and each of them is output in a different
124
+ variable.
125
+
126
+ Parameters
127
+ ----------
128
+ search_criteria : dict
129
+ Each element is a string and is built using the formatting
130
+ "KEYTYPE;VALUE", or "KEYTYPE;OPERATOR|VALUE".
131
+
132
+ Full list of keys avaiable for the dictionary:
133
+
134
+ search_criteria = {
135
+ "pars_patient_secret_name": "string;SUBJECTID",
136
+ "pars_ssid": "integer;OPERATOR|SSID",
137
+ "pars_modalities": "string;MODALITY",
138
+ "pars_tags": "tags;TAGS",
139
+ "pars_age_at_scan": "integer;OPERATOR|AGE_AT_SCAN",
140
+ "pars_[dicom]_KEY": "KEYTYPE;KEYVALUE",
141
+ "pars_PROJECTMETADATA": "METADATATYPE;METADATAVALUE",
142
+ }
143
+
144
+ See documentation for a complete definition.
145
+
146
+ Returns
147
+ -------
148
+ modality : str
149
+ String containing the modality of the search criteria extracted from
150
+ 'pars_modalities'
151
+
152
+ tags : list of str
153
+ List of strings containing the tags of the search criteria extracted
154
+ 'from pars_tags'
155
+
156
+ file_metadata : Dict
157
+ Dictionary containing the file metadata of 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
+
70
203
  class QCStatus(Enum):
71
204
  """
72
205
  Enum with the following options:
@@ -162,8 +295,8 @@ class Project:
162
295
 
163
296
  def get_subjects_metadata(self, search_criteria={}, items=(0, 9999)):
164
297
  """
165
- List all subjects data from the selected project that meet the defined
166
- search criteria.
298
+ List all Subject ID/Session ID from the selected project that meet the
299
+ defined search criteria at a session level.
167
300
 
168
301
  Parameters
169
302
  ----------
@@ -198,10 +331,12 @@ class Project:
198
331
 
199
332
  "pars_modalities": Applies the search to the file 'Modalities'
200
333
  available within each Subject ID.
201
- 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.
202
336
  "pars_tags": Applies the search to the file 'Tags' available within
203
337
  each Subject ID and to the subject-level 'Tags'.
204
- 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.
205
340
  "pars_age_at_scan": Applies the search to the 'age_at_scan' metadata
206
341
  field.
207
342
  AGE_AT_SCAN is an integer.
@@ -211,11 +346,15 @@ class Project:
211
346
  'file_m["metadata"]["info"].keys()'.
212
347
  KEYTYPE is the type of the KEY. One of:
213
348
  - integer
349
+ - decimal
214
350
  - string
215
351
  - list
216
352
 
217
- if 'integer' you must also include an OPERATOR
353
+ if 'integer' or 'decimal' you must also include an OPERATOR
218
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.
219
358
  KEYVALUE is the expected value of the KEY.
220
359
  "pars_[dicom]_PROJECTMETADATA": Applies to the metadata defined
221
360
  within the 'Metadata Manager' of the project.
@@ -244,6 +383,7 @@ class Project:
244
383
  "pars_tags": "tags;flair",
245
384
  "pars_[dicom]_Manufacturer": "string;ge",
246
385
  "pars_[dicom]_FlipAngle": "integer;gt|5",
386
+ "pars_[dicom]_ImageType": "list;PRIMARY;SECONDARY",
247
387
  }
248
388
 
249
389
  Note the search criteria applies to all the files included within a
@@ -251,8 +391,9 @@ class Project:
251
391
  are applied to the same files. In example 2) above, it means that
252
392
  any Subject ID/Session ID that has a file classified with a 'T1'
253
393
  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.
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.
256
397
 
257
398
  Returns
258
399
  -------
@@ -268,12 +409,11 @@ class Project:
268
409
  f"All keys of the search_criteria dictionary " f"'{search_criteria.keys()}' must start with 'pars_'."
269
410
  )
270
411
 
271
- operator_list = ["eq", "ne", "gt", "gte", "lt", "lte"]
272
412
  for key, value in search_criteria.items():
273
413
  if value.split(";")[0] in ["integer", "decimal"]:
274
- assert value.split(";")[1].split("|")[0] in operator_list, (
414
+ assert value.split(";")[1].split("|")[0] in OPERATOR_LIST, (
275
415
  f"Search criteria of type '{value.split(';')[0]}' must "
276
- f"include an operator ({', '.join(operator_list)})."
416
+ f"include an operator ({', '.join(OPERATOR_LIST)})."
277
417
  )
278
418
 
279
419
  content = platform.parse_response(
@@ -286,6 +426,160 @@ class Project:
286
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
  -------
@@ -345,49 +643,6 @@ class Project:
345
643
  return None
346
644
  return data["fields"]
347
645
 
348
- def add_metadata_parameter(self, title, param_id=None, param_type="string", visible=False):
349
- """
350
- Add a metadata parameter to the project.
351
-
352
- Parameters
353
- ----------
354
- title : str
355
- Identifier of this new parameter
356
- param_id : str
357
- Title of this new parameter
358
- param_type : str
359
- Type of the parameter. One of:
360
- "integer", "date", "string", "list", "decimal"
361
- visible : bool
362
- whether the parameter will be visible in the table of patients
363
-
364
- Returns
365
- -------
366
- bool
367
- True if parameter was correctly added, False otherwise.
368
- """
369
- # use param_id equal to title if param_id is not provided
370
- param_id = param_id or title
371
-
372
- param_properties = [title, param_id, param_type, str(int(visible))]
373
-
374
- post_data = {"add": "|".join(param_properties), "edit": "", "delete": ""}
375
-
376
- logger = logging.getLogger(logger_name)
377
- try:
378
- answer = platform.parse_response(
379
- platform.post(self._account.auth, "patient_manager/save_metadata_changes", data=post_data)
380
- )
381
- except errors.PlatformError:
382
- answer = {}
383
-
384
- if title not in answer:
385
- logger.error(f"Could not add new parameter: {title}")
386
- return False
387
-
388
- logger.info("New parameter added:", title, param_properties)
389
- return True
390
-
391
646
  def get_analysis(self, analysis_name_or_id):
392
647
  if isinstance(analysis_name_or_id, int):
393
648
  search_tag = "id"
@@ -788,38 +1043,6 @@ class Project:
788
1043
  return int(user["_id"])
789
1044
  return False
790
1045
 
791
- def add_subject(self, subject):
792
- """
793
- Add a subject to the project.
794
-
795
- Parameters
796
- ----------
797
- subject : Subject
798
- Instance of Subject representing the subject to add.
799
-
800
- Returns
801
- -------
802
- bool
803
- True if correctly added, False otherwise
804
- """
805
- logger = logging.getLogger(logger_name)
806
- if self.check_subject_name(subject.name):
807
- logger.error(f"Subject with name {subject.name} already exists in " f"project!")
808
- return False
809
-
810
- try:
811
- platform.parse_response(
812
- platform.post(self._account.auth, "patient_manager/upsert_patient", data={"secret_name": subject.name})
813
- )
814
- except errors.PlatformError:
815
- logger.error(f"Subject {subject.name} could not be created.")
816
- return False
817
-
818
- subject.subject_id = self.get_subject_id(subject.name)
819
- subject.project = self
820
- logger.info("Subject {0} was successfully created".format(subject.name))
821
- return True
822
-
823
1046
  def change_subject_metadata(self, patient_id, subject_name, ssid, tags, age_at_scan, metadata):
824
1047
  """
825
1048
  Change the Subject ID, Session ID, Tags, Age at Scan and Metadata of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qmenta-client
3
- Version: 1.1.dev1295
3
+ Version: 1.1.dev1311
4
4
  Summary: Python client lib to interact with the QMENTA platform.
5
5
  Author: QMENTA
6
6
  Author-email: dev@qmenta.com
@@ -1,10 +1,10 @@
1
1
  qmenta/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
2
2
  qmenta/client/Account.py,sha256=MEljEy9cmg2uP2FG1d3TZAgfj66EE2_3PQAZ9rvpCXY,9647
3
3
  qmenta/client/File.py,sha256=ZgvSqejIosUt4uoX7opUnPnp5XGEaJNMRwFC0mQVB8k,5344
4
- qmenta/client/Project.py,sha256=XXwsd2QKk_p61Mg2ahXxS9MHl1KENDPeNTsooQoXNkE,58955
4
+ qmenta/client/Project.py,sha256=u2fGizvjr3uU9476m_tV0padhlKxqcSR7i2xp07jrps,68234
5
5
  qmenta/client/Subject.py,sha256=lhxxVdQ6d-GNoQC8mrJwa4L1f44nJc4PcJtDspmKN7I,8756
6
6
  qmenta/client/__init__.py,sha256=AjTojBhZeW5nl0i605KS8S1Gl5tPNc1hdzD47BGNfoI,147
7
7
  qmenta/client/utils.py,sha256=5DK2T_HQprrCwLS0Ycm2CjseaYmAUKaJkJvYoW-Rqzc,2479
8
- qmenta_client-1.1.dev1295.dist-info/METADATA,sha256=G87kykIBmTLL0ebfHvfT0J-0as4u-HCWPzpdDnTiuds,672
9
- qmenta_client-1.1.dev1295.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
10
- qmenta_client-1.1.dev1295.dist-info/RECORD,,
8
+ qmenta_client-1.1.dev1311.dist-info/METADATA,sha256=7Zesw_sWgPMQYv9ByAllGzdmTCCQcQo4u-o4Q2EHyso,672
9
+ qmenta_client-1.1.dev1311.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
10
+ qmenta_client-1.1.dev1311.dist-info/RECORD,,