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 +607 -432
- {qmenta_client-1.1.dev1289.dist-info → qmenta_client-1.1.dev1311.dist-info}/METADATA +1 -1
- {qmenta_client-1.1.dev1289.dist-info → qmenta_client-1.1.dev1311.dist-info}/RECORD +4 -4
- {qmenta_client-1.1.dev1289.dist-info → qmenta_client-1.1.dev1311.dist-info}/WHEEL +0 -0
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(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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',
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
279
|
-
f"Search criteria of type '{value.split(';')[0]}' must "
|
|
280
|
-
f"include an operator ({', '.join(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
327
|
-
This function returns all these parameters and its
|
|
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(
|
|
412
|
-
self._account.auth, "analysis_manager/get_analysis_list",
|
|
413
|
-
|
|
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
|
-
|
|
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(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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(
|
|
620
|
-
|
|
621
|
-
|
|
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(
|
|
650
|
-
|
|
651
|
-
|
|
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(
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
"
|
|
705
|
-
"filename": filename,
|
|
706
|
-
|
|
707
|
-
|
|
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 =
|
|
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(
|
|
744
|
-
|
|
745
|
-
|
|
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
|
|
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 = (
|
|
781
|
-
|
|
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
|
|
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(
|
|
792
|
-
|
|
793
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
924
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
f"
|
|
933
|
-
f"
|
|
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(
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
"
|
|
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(
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
"
|
|
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(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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",
|
|
1127
|
-
|
|
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(
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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,
|
|
1424
|
+
data,
|
|
1425
|
+
bytes_range,
|
|
1426
|
+
bytes_to_send,
|
|
1427
|
+
session_id,
|
|
1428
|
+
dispstr,
|
|
1258
1429
|
last_chunk,
|
|
1259
|
-
name,
|
|
1260
|
-
|
|
1261
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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(
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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(
|
|
1547
|
-
|
|
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(
|
|
1552
|
-
self._account.auth,
|
|
1553
|
-
|
|
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
|
-
|
|
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(
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
"item_ids": analysis_id,
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|