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