qmenta-client 1.1.dev1444__py3-none-any.whl → 1.1.dev1468__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/__init__.py +1 -1
- qmenta/client/Account.py +46 -52
- qmenta/client/File.py +12 -30
- qmenta/client/Project.py +221 -174
- qmenta/client/Subject.py +9 -12
- qmenta/client/__init__.py +1 -5
- qmenta/client/utils.py +2 -6
- {qmenta_client-1.1.dev1444.dist-info → qmenta_client-1.1.dev1468.dist-info}/METADATA +1 -1
- qmenta_client-1.1.dev1468.dist-info/RECORD +10 -0
- qmenta_client-1.1.dev1444.dist-info/RECORD +0 -10
- {qmenta_client-1.1.dev1444.dist-info → qmenta_client-1.1.dev1468.dist-info}/WHEEL +0 -0
qmenta/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__path__ = __import__(
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
qmenta/client/Account.py
CHANGED
|
@@ -9,7 +9,7 @@ from qmenta.core import errors
|
|
|
9
9
|
from .Project import Project
|
|
10
10
|
from .utils import load_json
|
|
11
11
|
|
|
12
|
-
logger_name =
|
|
12
|
+
logger_name = "qmenta.client"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Account:
|
|
@@ -35,9 +35,7 @@ class Account:
|
|
|
35
35
|
The Auth object used for communication with the platform
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
def __init__(self, username, password,
|
|
39
|
-
base_url="https://platform.qmenta.com",
|
|
40
|
-
verify_certificates=True):
|
|
38
|
+
def __init__(self, username, password, base_url="https://platform.qmenta.com", verify_certificates=True):
|
|
41
39
|
|
|
42
40
|
self._cookie = None
|
|
43
41
|
self.username = username
|
|
@@ -64,17 +62,14 @@ class Account:
|
|
|
64
62
|
"""
|
|
65
63
|
logger = logging.getLogger(logger_name)
|
|
66
64
|
try:
|
|
67
|
-
auth = platform.Auth.login(
|
|
68
|
-
self.username, self.password, base_url=self.baseurl,
|
|
69
|
-
ask_for_2fa_input=True
|
|
70
|
-
)
|
|
65
|
+
auth = platform.Auth.login(self.username, self.password, base_url=self.baseurl, ask_for_2fa_input=True)
|
|
71
66
|
except errors.PlatformError as e:
|
|
72
|
-
logger.error(
|
|
67
|
+
logger.error("Failed to log in: {}".format(e))
|
|
73
68
|
self.auth = None
|
|
74
69
|
raise
|
|
75
70
|
|
|
76
71
|
self.auth = auth
|
|
77
|
-
logger.info(
|
|
72
|
+
logger.info("Logged in as {}".format(self.username))
|
|
78
73
|
|
|
79
74
|
def logout(self):
|
|
80
75
|
"""
|
|
@@ -88,12 +83,12 @@ class Account:
|
|
|
88
83
|
|
|
89
84
|
logger = logging.getLogger(logger_name)
|
|
90
85
|
try:
|
|
91
|
-
platform.parse_response(platform.post(self.auth,
|
|
86
|
+
platform.parse_response(platform.post(self.auth, "logout"))
|
|
92
87
|
except errors.PlatformError as e:
|
|
93
|
-
logger.error(
|
|
88
|
+
logger.error("Logout was unsuccessful: {}".format(e))
|
|
94
89
|
raise
|
|
95
90
|
|
|
96
|
-
logger.info(
|
|
91
|
+
logger.info("Logged out successfully")
|
|
97
92
|
|
|
98
93
|
def get_project(self, project_id):
|
|
99
94
|
"""
|
|
@@ -114,14 +109,9 @@ class Account:
|
|
|
114
109
|
return Project(self, int(project_id))
|
|
115
110
|
elif type(project_id) is str:
|
|
116
111
|
projects = self.projects
|
|
117
|
-
projects_match = [
|
|
118
|
-
proj for proj in projects if proj['name'] == project_id
|
|
119
|
-
]
|
|
112
|
+
projects_match = [proj for proj in projects if proj["name"] == project_id]
|
|
120
113
|
if not projects_match:
|
|
121
|
-
raise Exception(
|
|
122
|
-
f"Project {project_id} does not exist or is "
|
|
123
|
-
f"not available for this user."
|
|
124
|
-
)
|
|
114
|
+
raise Exception(f"Project {project_id} does not exist or is not available for this user.")
|
|
125
115
|
return Project(self, int(projects_match[0]["id"]))
|
|
126
116
|
|
|
127
117
|
@property
|
|
@@ -137,11 +127,9 @@ class Account:
|
|
|
137
127
|
logger = logging.getLogger(logger_name)
|
|
138
128
|
|
|
139
129
|
try:
|
|
140
|
-
data = platform.parse_response(platform.post(
|
|
141
|
-
self.auth, 'projectset_manager/get_projectset_list'
|
|
142
|
-
))
|
|
130
|
+
data = platform.parse_response(platform.post(self.auth, "projectset_manager/get_projectset_list"))
|
|
143
131
|
except errors.PlatformError as e:
|
|
144
|
-
logger.error(
|
|
132
|
+
logger.error("Failed to get project list: {}".format(e))
|
|
145
133
|
raise
|
|
146
134
|
|
|
147
135
|
titles = []
|
|
@@ -149,8 +137,7 @@ class Account:
|
|
|
149
137
|
titles.append({"name": project["name"], "id": project["_id"]})
|
|
150
138
|
return titles
|
|
151
139
|
|
|
152
|
-
def add_project(self, project_abbreviation, project_name,
|
|
153
|
-
description="", users=None, from_date="", to_date=""):
|
|
140
|
+
def add_project(self, project_abbreviation, project_name, description="", users=None, from_date="", to_date=""):
|
|
154
141
|
"""
|
|
155
142
|
Add a new project to the user account.
|
|
156
143
|
|
|
@@ -183,17 +170,20 @@ class Account:
|
|
|
183
170
|
return False
|
|
184
171
|
|
|
185
172
|
try:
|
|
186
|
-
platform.parse_response(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
173
|
+
platform.parse_response(
|
|
174
|
+
platform.post(
|
|
175
|
+
self.auth,
|
|
176
|
+
"projectset_manager/upsert_project",
|
|
177
|
+
data={
|
|
178
|
+
"name": project_name,
|
|
179
|
+
"description": description,
|
|
180
|
+
"from_date": from_date,
|
|
181
|
+
"to_date": to_date,
|
|
182
|
+
"abbr": project_abbreviation,
|
|
183
|
+
"users": "|".join(users),
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
197
187
|
except errors.PlatformError as e:
|
|
198
188
|
logger.error(e)
|
|
199
189
|
return False
|
|
@@ -205,9 +195,15 @@ class Account:
|
|
|
205
195
|
logger.error("Project could note be created.")
|
|
206
196
|
return False
|
|
207
197
|
|
|
208
|
-
def _send_request(
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
def _send_request(
|
|
199
|
+
self,
|
|
200
|
+
path,
|
|
201
|
+
req_parameters=None,
|
|
202
|
+
req_headers=None,
|
|
203
|
+
stream=False,
|
|
204
|
+
return_raw_response=False,
|
|
205
|
+
response_timeout=900.0,
|
|
206
|
+
):
|
|
211
207
|
"""
|
|
212
208
|
TODO: not used and contains warnings, maybe we should delete it.
|
|
213
209
|
|
|
@@ -234,34 +230,33 @@ class Account:
|
|
|
234
230
|
"""
|
|
235
231
|
|
|
236
232
|
req_headers = req_headers or {}
|
|
237
|
-
req_url =
|
|
233
|
+
req_url = "/".join((self.baseurl, path))
|
|
238
234
|
if self._cookie is not None:
|
|
239
235
|
req_headers["Cookie"] = self._cookie
|
|
240
236
|
req_headers["Mint-Api-Call"] = "1"
|
|
241
237
|
|
|
242
238
|
logger = logging.getLogger(logger_name)
|
|
243
239
|
try:
|
|
244
|
-
if path ==
|
|
240
|
+
if path == "upload":
|
|
245
241
|
response = self.pool.request(
|
|
246
|
-
|
|
242
|
+
"POST",
|
|
247
243
|
req_url,
|
|
248
244
|
body=req_parameters,
|
|
249
245
|
headers=req_headers,
|
|
250
246
|
timeout=response_timeout,
|
|
251
|
-
preload_content=not stream
|
|
247
|
+
preload_content=not stream,
|
|
252
248
|
)
|
|
253
249
|
else:
|
|
254
250
|
response = self.pool.request(
|
|
255
|
-
|
|
251
|
+
"POST",
|
|
256
252
|
req_url,
|
|
257
253
|
req_parameters or {},
|
|
258
254
|
headers=req_headers,
|
|
259
255
|
timeout=response_timeout,
|
|
260
|
-
preload_content=not stream
|
|
256
|
+
preload_content=not stream,
|
|
261
257
|
)
|
|
262
258
|
if response.status >= 400:
|
|
263
|
-
raise HTTPError(
|
|
264
|
-
'STATUS {}: {}'.format(response.status, response.reason))
|
|
259
|
+
raise HTTPError("STATUS {}: {}".format(response.status, response.reason))
|
|
265
260
|
except Exception as e:
|
|
266
261
|
error = "Could not send request. ERROR: {0}".format(e)
|
|
267
262
|
logger.error(error)
|
|
@@ -283,8 +278,7 @@ class Account:
|
|
|
283
278
|
try:
|
|
284
279
|
parsed_content = load_json(response.data)
|
|
285
280
|
except Exception:
|
|
286
|
-
error = "Could not parse the response as JSON data: {}".format(
|
|
287
|
-
response.data)
|
|
281
|
+
error = "Could not parse the response as JSON data: {}".format(response.data)
|
|
288
282
|
logger.error(error)
|
|
289
283
|
raise
|
|
290
284
|
|
|
@@ -293,8 +287,8 @@ class Account:
|
|
|
293
287
|
error = parsed_content["error"] or "Unknown error"
|
|
294
288
|
logger.error(error)
|
|
295
289
|
raise Exception(error)
|
|
296
|
-
elif
|
|
297
|
-
error = parsed_content[
|
|
290
|
+
elif "success" in parsed_content and parsed_content["success"] == 3:
|
|
291
|
+
error = parsed_content["message"]
|
|
298
292
|
logger.error(error)
|
|
299
293
|
raise Exception(error)
|
|
300
294
|
return parsed_content
|
qmenta/client/File.py
CHANGED
|
@@ -32,14 +32,12 @@ class File:
|
|
|
32
32
|
`ff_container = File(project, "MRB", "1", "gre_field_mapping_3.zip")`
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
def __init__(self, project: qmenta.client.Project, subject_name: str,
|
|
36
|
-
ssid: str, file_name: str):
|
|
35
|
+
def __init__(self, project: qmenta.client.Project, subject_name: str, ssid: str, file_name: str):
|
|
37
36
|
self.file_name = file_name
|
|
38
37
|
self.project = project
|
|
39
38
|
self._container_id = None
|
|
40
39
|
for cont in self.project.list_input_containers():
|
|
41
|
-
if cont["patient_secret_name"] == subject_name and cont[
|
|
42
|
-
"ssid"] == ssid:
|
|
40
|
+
if cont["patient_secret_name"] == subject_name and cont["ssid"] == ssid:
|
|
43
41
|
self._container_id = cont["container_id"]
|
|
44
42
|
break
|
|
45
43
|
if self.file_name.endswith(".zip"):
|
|
@@ -52,8 +50,7 @@ class File:
|
|
|
52
50
|
def __str__(self):
|
|
53
51
|
return self.file_name
|
|
54
52
|
|
|
55
|
-
def pull_scan_sequence_info(self, specify_dicom_tags=None,
|
|
56
|
-
output_format: str = "csv"):
|
|
53
|
+
def pull_scan_sequence_info(self, specify_dicom_tags=None, output_format: str = "csv"):
|
|
57
54
|
"""
|
|
58
55
|
Pulling scan sequence information (e.g. series number, series type,
|
|
59
56
|
series description, number of files, file formats available)
|
|
@@ -77,13 +74,11 @@ class File:
|
|
|
77
74
|
if specify_dicom_tags is None:
|
|
78
75
|
specify_dicom_tags = list()
|
|
79
76
|
valid_output_formats = ["csv", "tsv", "json", "dict"]
|
|
80
|
-
assert output_format in valid_output_formats,
|
|
81
|
-
f"Output format not valid. Choose one of {valid_output_formats}"
|
|
77
|
+
assert output_format in valid_output_formats, f"Output format not valid. Choose one of {valid_output_formats}"
|
|
82
78
|
|
|
83
79
|
with TemporaryDirectory() as temp_dir:
|
|
84
80
|
local_file_name = mkstemp(dir=temp_dir) + ".zip"
|
|
85
|
-
self.project.download_file(self._container_id, self.file_name,
|
|
86
|
-
local_file_name, overwrite=False)
|
|
81
|
+
self.project.download_file(self._container_id, self.file_name, local_file_name, overwrite=False)
|
|
87
82
|
if self._file_type == "DICOM":
|
|
88
83
|
unzip_dicoms(local_file_name, temp_dir, exclude_members=None)
|
|
89
84
|
dicoms = glob.glob(os.path.join(temp_dir, "*"))
|
|
@@ -97,20 +92,11 @@ class File:
|
|
|
97
92
|
# only needed for one file, all have the same data.
|
|
98
93
|
for attribute in specify_dicom_tags:
|
|
99
94
|
# error handling of attributes send by the user
|
|
100
|
-
assert len(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
int), (
|
|
106
|
-
"Invalid type of DICOM Attribute"
|
|
107
|
-
)
|
|
108
|
-
output_data["dicom tag"].append(
|
|
109
|
-
open_file[attribute].tag)
|
|
110
|
-
output_data["attribute"].append(
|
|
111
|
-
open_file[attribute].name)
|
|
112
|
-
output_data["value"].append(
|
|
113
|
-
open_file[attribute].value)
|
|
95
|
+
assert len(attribute) == 2, "Invalid length of DICOM Attribute"
|
|
96
|
+
assert isinstance(attribute[0], int), "Invalid type of DICOM Attribute"
|
|
97
|
+
output_data["dicom tag"].append(open_file[attribute].tag)
|
|
98
|
+
output_data["attribute"].append(open_file[attribute].name)
|
|
99
|
+
output_data["value"].append(open_file[attribute].value)
|
|
114
100
|
except Exception as e:
|
|
115
101
|
print(str(e))
|
|
116
102
|
pass
|
|
@@ -120,14 +106,10 @@ class File:
|
|
|
120
106
|
output_data["value"].append(number_files)
|
|
121
107
|
|
|
122
108
|
if output_format == "csv":
|
|
123
|
-
pd.DataFrame(output_data).to_csv(
|
|
124
|
-
f"scan_sequence_info.{output_format}", index=False)
|
|
109
|
+
pd.DataFrame(output_data).to_csv(f"scan_sequence_info.{output_format}", index=False)
|
|
125
110
|
|
|
126
111
|
elif output_format == "tsv":
|
|
127
|
-
pd.DataFrame(output_data).to_csv(
|
|
128
|
-
f"scan_sequence_info.{output_format}", sep="\t",
|
|
129
|
-
index=False
|
|
130
|
-
)
|
|
112
|
+
pd.DataFrame(output_data).to_csv(f"scan_sequence_info.{output_format}", sep="\t", index=False)
|
|
131
113
|
|
|
132
114
|
elif output_format == "json":
|
|
133
115
|
with open(f"scan_sequence_info.{output_format}", "w") as fp:
|
qmenta/client/Project.py
CHANGED
|
@@ -43,8 +43,7 @@ def convert_qc_value_to_qcstatus(value):
|
|
|
43
43
|
elif value == "":
|
|
44
44
|
return QCStatus.UNDERTERMINED
|
|
45
45
|
else:
|
|
46
|
-
logger.error(f"The input value '{value}' cannot be converted "
|
|
47
|
-
f"to class QCStatus.")
|
|
46
|
+
logger.error(f"The input value '{value}' cannot be converted to class QCStatus.")
|
|
48
47
|
return False
|
|
49
48
|
|
|
50
49
|
|
|
@@ -76,6 +75,7 @@ class Project:
|
|
|
76
75
|
"""
|
|
77
76
|
|
|
78
77
|
""" Project Related Methods """
|
|
78
|
+
|
|
79
79
|
def __init__(self, account: Account, project_id, max_upload_retries=5):
|
|
80
80
|
# if project_id is a string (the name of the project), get the
|
|
81
81
|
# project id (int)
|
|
@@ -134,6 +134,7 @@ class Project:
|
|
|
134
134
|
return rep
|
|
135
135
|
|
|
136
136
|
""" Upload / Download Data Related Methods """
|
|
137
|
+
|
|
137
138
|
def _upload_chunk(
|
|
138
139
|
self,
|
|
139
140
|
data,
|
|
@@ -350,14 +351,14 @@ class Project:
|
|
|
350
351
|
retries_count += 1
|
|
351
352
|
time.sleep(retries_count * 5)
|
|
352
353
|
if retries_count > self.max_retries:
|
|
353
|
-
error_message = "Error Code: 416;
|
|
354
|
+
error_message = "Error Code: 416; Requested Range Not Satisfiable (NGINX)"
|
|
354
355
|
logger.error(error_message)
|
|
355
356
|
break
|
|
356
357
|
else:
|
|
357
358
|
retries_count += 1
|
|
358
359
|
time.sleep(retries_count * 5)
|
|
359
360
|
if retries_count > max_retries:
|
|
360
|
-
error_message = "Number of retries has been reached.
|
|
361
|
+
error_message = "Number of retries has been reached. Upload process stops here !"
|
|
361
362
|
logger.error(error_message)
|
|
362
363
|
break
|
|
363
364
|
|
|
@@ -451,14 +452,12 @@ class Project:
|
|
|
451
452
|
"""
|
|
452
453
|
logger = logging.getLogger(logger_name)
|
|
453
454
|
if not isinstance(file_name, str):
|
|
454
|
-
raise ValueError("The name of the file to download (file_name) "
|
|
455
|
-
"should be of type string.")
|
|
455
|
+
raise ValueError("The name of the file to download (file_name) should be of type string.")
|
|
456
456
|
if not isinstance(file_name, str):
|
|
457
|
-
raise ValueError("The name of the output file (local_filename) "
|
|
458
|
-
"should be of type string.")
|
|
457
|
+
raise ValueError("The name of the output file (local_filename) should be of type string.")
|
|
459
458
|
|
|
460
459
|
if file_name not in self.list_container_files(container_id):
|
|
461
|
-
msg = f'File "{file_name}" does not exist in container
|
|
460
|
+
msg = f'File "{file_name}" does not exist in container {container_id}'
|
|
462
461
|
logger.error(msg)
|
|
463
462
|
return False
|
|
464
463
|
|
|
@@ -479,7 +478,7 @@ class Project:
|
|
|
479
478
|
f.write(chunk)
|
|
480
479
|
f.flush()
|
|
481
480
|
|
|
482
|
-
logger.info(f"File {file_name} from container {container_id} saved to
|
|
481
|
+
logger.info(f"File {file_name} from container {container_id} saved to {local_filename}")
|
|
483
482
|
return True
|
|
484
483
|
|
|
485
484
|
def download_files(self, container_id, filenames, zip_name="files.zip", overwrite=False):
|
|
@@ -500,18 +499,14 @@ class Project:
|
|
|
500
499
|
logger = logging.getLogger(logger_name)
|
|
501
500
|
|
|
502
501
|
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.")
|
|
502
|
+
raise ValueError("The name of the files to download (filenames) should be of type string.")
|
|
505
503
|
if not isinstance(zip_name, str):
|
|
506
|
-
raise ValueError("The name of the output ZIP file (zip_name) "
|
|
507
|
-
"should be of type string.")
|
|
504
|
+
raise ValueError("The name of the output ZIP file (zip_name) should be of type string.")
|
|
508
505
|
|
|
509
506
|
files_not_in_container = list(filter(lambda f: f not in self.list_container_files(container_id), filenames))
|
|
510
507
|
|
|
511
508
|
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
|
-
)
|
|
509
|
+
msg = f"The following files are missing in container {container_id}: {', '.join(files_not_in_container)}"
|
|
515
510
|
logger.error(msg)
|
|
516
511
|
return False
|
|
517
512
|
|
|
@@ -575,6 +570,7 @@ class Project:
|
|
|
575
570
|
return True
|
|
576
571
|
|
|
577
572
|
""" Subject/Session Related Methods """
|
|
573
|
+
|
|
578
574
|
@property
|
|
579
575
|
def subjects(self):
|
|
580
576
|
"""
|
|
@@ -810,12 +806,12 @@ class Project:
|
|
|
810
806
|
|
|
811
807
|
"""
|
|
812
808
|
|
|
813
|
-
assert len(items) == 2, f"The number of elements in items
|
|
809
|
+
assert len(items) == 2, f"The number of elements in items '{len(items)}' should be equal to two."
|
|
814
810
|
assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
|
|
815
811
|
|
|
816
|
-
assert all(
|
|
817
|
-
|
|
818
|
-
)
|
|
812
|
+
assert all(
|
|
813
|
+
[key[:5] == "pars_" for key in search_criteria.keys()]
|
|
814
|
+
), f"All keys of the search_criteria dictionary '{search_criteria.keys()}' must start with 'pars_'."
|
|
819
815
|
|
|
820
816
|
for key, value in search_criteria.items():
|
|
821
817
|
if value.split(";")[0] in ["integer", "decimal"]:
|
|
@@ -869,7 +865,7 @@ class Project:
|
|
|
869
865
|
try:
|
|
870
866
|
patient_id = str(int(patient_id))
|
|
871
867
|
except ValueError:
|
|
872
|
-
raise ValueError(f"'patient_id': '{patient_id}' not valid.
|
|
868
|
+
raise ValueError(f"'patient_id': '{patient_id}' not valid. Must be convertible to int.")
|
|
873
869
|
|
|
874
870
|
assert isinstance(tags, list) and all(
|
|
875
871
|
isinstance(item, str) for item in tags
|
|
@@ -882,13 +878,13 @@ class Project:
|
|
|
882
878
|
try:
|
|
883
879
|
age_at_scan = str(int(age_at_scan)) if age_at_scan else None
|
|
884
880
|
except ValueError:
|
|
885
|
-
raise ValueError(f"age_at_scan: '{age_at_scan}' not valid.
|
|
881
|
+
raise ValueError(f"age_at_scan: '{age_at_scan}' not valid. Must be an integer.")
|
|
886
882
|
|
|
887
883
|
assert isinstance(metadata, dict), f"metadata: '{metadata}' should be a dictionary."
|
|
888
884
|
|
|
889
|
-
assert all("md_" == key[:3] for key in metadata.keys()) or all(
|
|
890
|
-
|
|
891
|
-
)
|
|
885
|
+
assert all("md_" == key[:3] for key in metadata.keys()) or all(
|
|
886
|
+
"md_" != key[:3] for key in metadata.keys()
|
|
887
|
+
), f"metadata: '{metadata}' must be a dictionary whose keys are either all starting with 'md_' or none."
|
|
892
888
|
|
|
893
889
|
metadata_keys = self.metadata_parameters.keys()
|
|
894
890
|
assert all(
|
|
@@ -1047,27 +1043,26 @@ class Project:
|
|
|
1047
1043
|
# them from the results.
|
|
1048
1044
|
subjects = list()
|
|
1049
1045
|
for subject in content:
|
|
1050
|
-
files = platform.parse_response(
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1046
|
+
files = platform.parse_response(
|
|
1047
|
+
platform.post(
|
|
1048
|
+
self._account.auth,
|
|
1049
|
+
"file_manager/get_container_files",
|
|
1050
|
+
data={"container_id": str(int(subject["container_id"]))},
|
|
1051
|
+
)
|
|
1052
|
+
)
|
|
1054
1053
|
|
|
1055
1054
|
for file in files["meta"]:
|
|
1056
|
-
if modality and
|
|
1057
|
-
modality != (file.get("metadata") or {}).get("modality"):
|
|
1055
|
+
if modality and modality != (file.get("metadata") or {}).get("modality"):
|
|
1058
1056
|
continue
|
|
1059
1057
|
if tags and not all([tag in file.get("tags") for tag in tags]):
|
|
1060
1058
|
continue
|
|
1061
1059
|
if dicoms:
|
|
1062
1060
|
result_values = list()
|
|
1063
1061
|
for key, dict_value in dicoms.items():
|
|
1064
|
-
f_value = ((file.get("metadata") or {})
|
|
1065
|
-
.get("info") or {}).get(key)
|
|
1062
|
+
f_value = ((file.get("metadata") or {}).get("info") or {}).get(key)
|
|
1066
1063
|
d_operator = dict_value["operation"]
|
|
1067
1064
|
d_value = dict_value["value"]
|
|
1068
|
-
result_values.append(
|
|
1069
|
-
self.__operation(d_value, d_operator, f_value)
|
|
1070
|
-
)
|
|
1065
|
+
result_values.append(self.__operation(d_value, d_operator, f_value))
|
|
1071
1066
|
|
|
1072
1067
|
if not all(result_values):
|
|
1073
1068
|
continue
|
|
@@ -1150,10 +1145,10 @@ class Project:
|
|
|
1150
1145
|
]
|
|
1151
1146
|
|
|
1152
1147
|
if not session_to_del:
|
|
1153
|
-
logger.error(f"Session {subject_name}/{session_id} could not be found
|
|
1148
|
+
logger.error(f"Session {subject_name}/{session_id} could not be found in this project.")
|
|
1154
1149
|
return False
|
|
1155
1150
|
elif len(session_to_del) > 1:
|
|
1156
|
-
raise RuntimeError("Multiple sessions with same Subject ID and Session ID.
|
|
1151
|
+
raise RuntimeError("Multiple sessions with same Subject ID and Session ID. Contact support.")
|
|
1157
1152
|
else:
|
|
1158
1153
|
logger.info("{}/{} found (id {})".format(subject_name, session_id, session_to_del[0]["_id"]))
|
|
1159
1154
|
|
|
@@ -1168,10 +1163,10 @@ class Project:
|
|
|
1168
1163
|
)
|
|
1169
1164
|
)
|
|
1170
1165
|
except errors.PlatformError:
|
|
1171
|
-
logger.error(f"Session \"{subject_name}/{session['ssid']}\" could
|
|
1166
|
+
logger.error(f"Session \"{subject_name}/{session['ssid']}\" could not be deleted.")
|
|
1172
1167
|
return False
|
|
1173
1168
|
|
|
1174
|
-
logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully
|
|
1169
|
+
logger.info(f"Session \"{subject_name}/{session['ssid']}\" successfully deleted.")
|
|
1175
1170
|
return True
|
|
1176
1171
|
|
|
1177
1172
|
def delete_session_by_patientid(self, patient_id):
|
|
@@ -1273,7 +1268,7 @@ class Project:
|
|
|
1273
1268
|
{"container_name", "container_id", "patient_secret_name", "ssid"}
|
|
1274
1269
|
"""
|
|
1275
1270
|
|
|
1276
|
-
assert len(items) == 2, f"The number of elements in items
|
|
1271
|
+
assert len(items) == 2, f"The number of elements in items '{len(items)}' should be equal to two."
|
|
1277
1272
|
assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
|
|
1278
1273
|
|
|
1279
1274
|
response = platform.parse_response(
|
|
@@ -1316,7 +1311,8 @@ class Project:
|
|
|
1316
1311
|
----------
|
|
1317
1312
|
search_condition : dict
|
|
1318
1313
|
- p_n: str or None Analysis name
|
|
1319
|
-
- type: str or None Type
|
|
1314
|
+
- type: str or None Type (analysis_code:analysis_version).
|
|
1315
|
+
- analysis_code: str or None Type
|
|
1320
1316
|
- from_d: str or None dd.mm.yyyy Date from
|
|
1321
1317
|
- to_d: str or None dd.mm.yyyy Date to
|
|
1322
1318
|
- qa_status: str or None pass/fail/nd QC status
|
|
@@ -1372,13 +1368,7 @@ class Project:
|
|
|
1372
1368
|
return False
|
|
1373
1369
|
return content["files"]
|
|
1374
1370
|
|
|
1375
|
-
def list_container_filter_files(
|
|
1376
|
-
self,
|
|
1377
|
-
container_id,
|
|
1378
|
-
modality="",
|
|
1379
|
-
metadata_info={},
|
|
1380
|
-
tags=[]
|
|
1381
|
-
):
|
|
1371
|
+
def list_container_filter_files(self, container_id, modality="", metadata_info={}, tags=[]):
|
|
1382
1372
|
"""
|
|
1383
1373
|
List the name of the files available inside a given container.
|
|
1384
1374
|
search condition example:
|
|
@@ -1414,23 +1404,12 @@ class Project:
|
|
|
1414
1404
|
if modality == "":
|
|
1415
1405
|
modality_bool = True
|
|
1416
1406
|
else:
|
|
1417
|
-
modality_bool = modality == metadata_file["metadata"].get(
|
|
1418
|
-
"modality"
|
|
1419
|
-
)
|
|
1407
|
+
modality_bool = modality == metadata_file["metadata"].get("modality")
|
|
1420
1408
|
for key in metadata_info.keys():
|
|
1421
|
-
meta_key = (
|
|
1422
|
-
(
|
|
1423
|
-
metadata_file.get("metadata") or {}
|
|
1424
|
-
).get("info") or {}).get(
|
|
1425
|
-
key
|
|
1426
|
-
)
|
|
1409
|
+
meta_key = ((metadata_file.get("metadata") or {}).get("info") or {}).get(key)
|
|
1427
1410
|
if meta_key is None:
|
|
1428
|
-
logging.getLogger(logger_name).warning(
|
|
1429
|
-
|
|
1430
|
-
)
|
|
1431
|
-
info_bool.append(
|
|
1432
|
-
metadata_info[key] == meta_key
|
|
1433
|
-
)
|
|
1411
|
+
logging.getLogger(logger_name).warning(f"{key} is not in file_info from file {file}")
|
|
1412
|
+
info_bool.append(metadata_info[key] == meta_key)
|
|
1434
1413
|
if all(tags_bool) and all(info_bool) and modality_bool:
|
|
1435
1414
|
selected_files.append(file)
|
|
1436
1415
|
return selected_files
|
|
@@ -1486,13 +1465,12 @@ class Project:
|
|
|
1486
1465
|
analysis_name_or_id = int(analysis_name_or_id)
|
|
1487
1466
|
else:
|
|
1488
1467
|
search_tag = "p_n"
|
|
1489
|
-
excluded_characters = ["\\", "[", "]", "(", ")",
|
|
1490
|
-
"{", "}", "+", "*"]
|
|
1468
|
+
excluded_characters = ["\\", "[", "]", "(", ")", "{", "}", "+", "*"]
|
|
1491
1469
|
excluded_bool = [character in analysis_name_or_id for character in excluded_characters]
|
|
1492
1470
|
if any(excluded_bool):
|
|
1493
1471
|
raise Exception(f"p_n does not allow characters {excluded_characters}")
|
|
1494
1472
|
else:
|
|
1495
|
-
raise Exception("The analysis identifier must be its name or an
|
|
1473
|
+
raise Exception("The analysis identifier must be its name or an integer")
|
|
1496
1474
|
|
|
1497
1475
|
search_condition = {
|
|
1498
1476
|
search_tag: analysis_name_or_id,
|
|
@@ -1502,7 +1480,7 @@ class Project:
|
|
|
1502
1480
|
)
|
|
1503
1481
|
|
|
1504
1482
|
if len(response) > 1:
|
|
1505
|
-
raise Exception(f"multiple analyses with name
|
|
1483
|
+
raise Exception(f"multiple analyses with name {analysis_name_or_id} found")
|
|
1506
1484
|
elif len(response) == 1:
|
|
1507
1485
|
return response[0]
|
|
1508
1486
|
else:
|
|
@@ -1530,7 +1508,8 @@ class Project:
|
|
|
1530
1508
|
----------
|
|
1531
1509
|
search_condition : dict
|
|
1532
1510
|
- p_n: str or None Analysis name
|
|
1533
|
-
- type: str or None Type
|
|
1511
|
+
- type: str or None Type (analysis_code:analysis_version).
|
|
1512
|
+
- analysis_code: str or None Type
|
|
1534
1513
|
- from_d: str or None dd.mm.yyyy Date from
|
|
1535
1514
|
- to_d: str or None dd.mm.yyyy Date to
|
|
1536
1515
|
- qa_status: str or None pass/fail/nd QC status
|
|
@@ -1550,11 +1529,12 @@ class Project:
|
|
|
1550
1529
|
dict
|
|
1551
1530
|
List of analysis, each a dictionary
|
|
1552
1531
|
"""
|
|
1553
|
-
assert len(items) == 2, f"The number of elements in items
|
|
1532
|
+
assert len(items) == 2, f"The number of elements in items '{len(items)}' should be equal to two."
|
|
1554
1533
|
assert all([isinstance(item, int) for item in items]), f"All items elements '{items}' should be integers."
|
|
1555
1534
|
search_keys = {
|
|
1556
1535
|
"p_n": str,
|
|
1557
1536
|
"type": str,
|
|
1537
|
+
"analysis_code": str,
|
|
1558
1538
|
"from_d": str,
|
|
1559
1539
|
"to_d": str,
|
|
1560
1540
|
"qa_status": str,
|
|
@@ -1567,12 +1547,11 @@ class Project:
|
|
|
1567
1547
|
}
|
|
1568
1548
|
for key in search_condition.keys():
|
|
1569
1549
|
if key not in search_keys.keys():
|
|
1570
|
-
raise Exception((f"This key '{key}' is not accepted by this
|
|
1550
|
+
raise Exception((f"This key '{key}' is not accepted by this search condition"))
|
|
1571
1551
|
if not isinstance(search_condition[key], search_keys[key]) and search_condition[key] is not None:
|
|
1572
|
-
raise Exception((f"The key {key} in the search condition
|
|
1552
|
+
raise Exception((f"The key {key} in the search condition is not type {search_keys[key]}"))
|
|
1573
1553
|
if "p_n" == key:
|
|
1574
|
-
excluded_characters = ["\\", "[", "]", "(", ")",
|
|
1575
|
-
"{", "}", "+", "*"]
|
|
1554
|
+
excluded_characters = ["\\", "[", "]", "(", ")", "{", "}", "+", "*"]
|
|
1576
1555
|
excluded_bool = [character in search_condition["p_n"] for character in excluded_characters]
|
|
1577
1556
|
if any(excluded_bool):
|
|
1578
1557
|
raise Exception(f"p_n does not allow characters {excluded_characters}")
|
|
@@ -1598,6 +1577,7 @@ class Project:
|
|
|
1598
1577
|
settings=None,
|
|
1599
1578
|
tags=None,
|
|
1600
1579
|
preferred_destination=None,
|
|
1580
|
+
ignore_file_selection=True,
|
|
1601
1581
|
):
|
|
1602
1582
|
"""
|
|
1603
1583
|
Starts an analysis on a subject.
|
|
@@ -1627,6 +1607,10 @@ class Project:
|
|
|
1627
1607
|
The tags of the analysis.
|
|
1628
1608
|
preferred_destination : str
|
|
1629
1609
|
The machine on which to run the analysis
|
|
1610
|
+
ignore_file_selection : Bool
|
|
1611
|
+
When the file filter of the analysis is satified by multiple files.
|
|
1612
|
+
False if you want to select manually the file to input to the
|
|
1613
|
+
analysis. True otherwise and the analysis will cancelled.
|
|
1630
1614
|
|
|
1631
1615
|
Returns
|
|
1632
1616
|
-------
|
|
@@ -1668,7 +1652,9 @@ class Project:
|
|
|
1668
1652
|
post_data["preferred_destination"] = preferred_destination
|
|
1669
1653
|
|
|
1670
1654
|
logger.debug(f"post_data = {post_data}")
|
|
1671
|
-
return self.__handle_start_analysis(
|
|
1655
|
+
return self.__handle_start_analysis(
|
|
1656
|
+
post_data, ignore_warnings=ignore_warnings, ignore_file_selection=ignore_file_selection
|
|
1657
|
+
)
|
|
1672
1658
|
|
|
1673
1659
|
def delete_analysis(self, analysis_id):
|
|
1674
1660
|
"""
|
|
@@ -1759,17 +1745,16 @@ class Project:
|
|
|
1759
1745
|
try:
|
|
1760
1746
|
analysis_id = str(int(analysis_id))
|
|
1761
1747
|
except ValueError:
|
|
1762
|
-
raise ValueError(f"'analysis_id' has to be an integer"
|
|
1763
|
-
f" not '{analysis_id}'.")
|
|
1748
|
+
raise ValueError(f"'analysis_id' has to be an integer not '{analysis_id}'.")
|
|
1764
1749
|
|
|
1765
1750
|
file_name = file_name if file_name else f"logs_{analysis_id}.txt"
|
|
1766
1751
|
try:
|
|
1767
1752
|
res = platform.post(
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1753
|
+
auth=self._account.auth,
|
|
1754
|
+
endpoint="analysis_manager/download_execution_file",
|
|
1755
|
+
data={"project_id": analysis_id, "file": f"logs_{analysis_id}"},
|
|
1756
|
+
timeout=1000,
|
|
1757
|
+
)
|
|
1773
1758
|
except Exception:
|
|
1774
1759
|
logger.error(f"Could not export the analysis log of '{analysis_id}'")
|
|
1775
1760
|
return False
|
|
@@ -1784,8 +1769,7 @@ class Project:
|
|
|
1784
1769
|
|
|
1785
1770
|
""" QC Status Related Methods """
|
|
1786
1771
|
|
|
1787
|
-
def set_qc_status_analysis(self, analysis_id,
|
|
1788
|
-
status=QCStatus.UNDERTERMINED, comments=""):
|
|
1772
|
+
def set_qc_status_analysis(self, analysis_id, status=QCStatus.UNDERTERMINED, comments=""):
|
|
1789
1773
|
"""
|
|
1790
1774
|
Changes the analysis QC status.
|
|
1791
1775
|
|
|
@@ -1814,25 +1798,27 @@ class Project:
|
|
|
1814
1798
|
try:
|
|
1815
1799
|
analysis_id = str(int(analysis_id))
|
|
1816
1800
|
except ValueError:
|
|
1817
|
-
raise ValueError(f"analysis_id: '{analysis_id}' not valid.
|
|
1801
|
+
raise ValueError(f"analysis_id: '{analysis_id}' not valid. Must be convertible to int.")
|
|
1818
1802
|
|
|
1819
1803
|
try:
|
|
1820
1804
|
platform.parse_response(
|
|
1821
1805
|
platform.post(
|
|
1822
1806
|
auth=self._account.auth,
|
|
1823
1807
|
endpoint="projectset_manager/set_qa_status",
|
|
1824
|
-
data={
|
|
1825
|
-
|
|
1808
|
+
data={
|
|
1809
|
+
"item_ids": analysis_id,
|
|
1810
|
+
"status": status.value,
|
|
1811
|
+
"comments": str(comments),
|
|
1812
|
+
"entity": "analysis",
|
|
1813
|
+
},
|
|
1826
1814
|
)
|
|
1827
1815
|
)
|
|
1828
1816
|
except Exception:
|
|
1829
|
-
logger.error(f"It was not possible to change the QC status of"
|
|
1830
|
-
f"Analysis ID: {analysis_id}")
|
|
1817
|
+
logger.error(f"It was not possible to change the QC status of Analysis ID: {analysis_id}")
|
|
1831
1818
|
return False
|
|
1832
1819
|
return True
|
|
1833
1820
|
|
|
1834
|
-
def set_qc_status_subject(self, patient_id,
|
|
1835
|
-
status=QCStatus.UNDERTERMINED, comments=""):
|
|
1821
|
+
def set_qc_status_subject(self, patient_id, status=QCStatus.UNDERTERMINED, comments=""):
|
|
1836
1822
|
"""
|
|
1837
1823
|
Changes the QC status of a Patient ID (equivalent to a
|
|
1838
1824
|
Subject ID/Session ID).
|
|
@@ -1861,20 +1847,23 @@ class Project:
|
|
|
1861
1847
|
try:
|
|
1862
1848
|
patient_id = str(int(patient_id))
|
|
1863
1849
|
except ValueError:
|
|
1864
|
-
raise ValueError(f"'patient_id': '{patient_id}' not valid.
|
|
1850
|
+
raise ValueError(f"'patient_id': '{patient_id}' not valid. Must be convertible to int.")
|
|
1865
1851
|
|
|
1866
1852
|
try:
|
|
1867
1853
|
platform.parse_response(
|
|
1868
1854
|
platform.post(
|
|
1869
1855
|
auth=self._account.auth,
|
|
1870
1856
|
endpoint="projectset_manager/set_qa_status",
|
|
1871
|
-
data={
|
|
1872
|
-
|
|
1857
|
+
data={
|
|
1858
|
+
"item_ids": patient_id,
|
|
1859
|
+
"status": status.value,
|
|
1860
|
+
"comments": str(comments),
|
|
1861
|
+
"entity": "patients",
|
|
1862
|
+
},
|
|
1873
1863
|
)
|
|
1874
1864
|
)
|
|
1875
1865
|
except Exception:
|
|
1876
|
-
logger.error(f"It was not possible to change the QC status of"
|
|
1877
|
-
f"Patient ID: {patient_id}")
|
|
1866
|
+
logger.error(f"It was not possible to change the QC status of Patient ID: {patient_id}")
|
|
1878
1867
|
return False
|
|
1879
1868
|
return True
|
|
1880
1869
|
|
|
@@ -1906,12 +1895,10 @@ class Project:
|
|
|
1906
1895
|
return False, False
|
|
1907
1896
|
except Exception:
|
|
1908
1897
|
# Handle other potential exceptions
|
|
1909
|
-
logging.error(f"It was not possible to extract the QC status"
|
|
1910
|
-
f"from Analysis ID: {analysis_id}")
|
|
1898
|
+
logging.error(f"It was not possible to extract the QC status from Analysis ID: {analysis_id}")
|
|
1911
1899
|
return False, False
|
|
1912
1900
|
|
|
1913
|
-
def get_qc_status_subject(self, patient_id=None, subject_name=None,
|
|
1914
|
-
ssid=None):
|
|
1901
|
+
def get_qc_status_subject(self, patient_id=None, subject_name=None, ssid=None):
|
|
1915
1902
|
"""
|
|
1916
1903
|
Gets the session QC status via the patient ID or the Subject ID
|
|
1917
1904
|
and the Session ID.
|
|
@@ -1939,13 +1926,11 @@ class Project:
|
|
|
1939
1926
|
try:
|
|
1940
1927
|
patient_id = int(patient_id)
|
|
1941
1928
|
except ValueError:
|
|
1942
|
-
raise ValueError(f"patient_id '{patient_id}' should be an "
|
|
1943
|
-
f"integer.")
|
|
1929
|
+
raise ValueError(f"patient_id '{patient_id}' should be an integer.")
|
|
1944
1930
|
sessions = self.get_subjects_metadata(search_criteria={})
|
|
1945
1931
|
session = [session for session in sessions if int(session["_id"]) == patient_id]
|
|
1946
1932
|
if len(session) < 1:
|
|
1947
|
-
logging.error(f"No session was found with Patient ID: "
|
|
1948
|
-
f"'{patient_id}'.")
|
|
1933
|
+
logging.error(f"No session was found with Patient ID: '{patient_id}'.")
|
|
1949
1934
|
return False, False
|
|
1950
1935
|
return convert_qc_value_to_qcstatus(session[0]["qa_status"]), session[0]["qa_comments"]
|
|
1951
1936
|
elif subject_name and ssid:
|
|
@@ -1956,15 +1941,14 @@ class Project:
|
|
|
1956
1941
|
}
|
|
1957
1942
|
)
|
|
1958
1943
|
if len(session) < 1:
|
|
1959
|
-
logging.error(f"No session was found with Subject ID: "
|
|
1960
|
-
f"'{subject_name}' and Session ID: '{ssid}'.")
|
|
1944
|
+
logging.error(f"No session was found with Subject ID: '{subject_name}' and Session ID: '{ssid}'.")
|
|
1961
1945
|
return False, False
|
|
1962
1946
|
return convert_qc_value_to_qcstatus(session[0]["qa_status"]), session[0]["qa_comments"]
|
|
1963
1947
|
else:
|
|
1964
|
-
raise ValueError("Either 'patient_id' or 'subject_name' and 'ssid'"
|
|
1965
|
-
" must not be empty.")
|
|
1948
|
+
raise ValueError("Either 'patient_id' or 'subject_name' and 'ssid' must not be empty.")
|
|
1966
1949
|
|
|
1967
1950
|
""" Protocol Adherence Related Methods """
|
|
1951
|
+
|
|
1968
1952
|
def set_project_pa_rules(self, rules_file_path, guidance_text=""):
|
|
1969
1953
|
"""
|
|
1970
1954
|
Updates the active project's protocol adherence rules using the
|
|
@@ -1989,20 +1973,20 @@ class Project:
|
|
|
1989
1973
|
with open(rules_file_path, "r") as fr:
|
|
1990
1974
|
rules = json.load(fr)
|
|
1991
1975
|
except FileNotFoundError:
|
|
1992
|
-
logger.error(f"Pprotocol adherence rule file '{rules_file_path}' "
|
|
1993
|
-
f"not found.")
|
|
1976
|
+
logger.error(f"Pprotocol adherence rule file '{rules_file_path}' not found.")
|
|
1994
1977
|
return False
|
|
1995
1978
|
|
|
1996
1979
|
# Update the project's QA rules
|
|
1997
|
-
res = platform.parse_response(
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
1980
|
+
res = platform.parse_response(
|
|
1981
|
+
platform.post(
|
|
1982
|
+
auth=self._account.auth,
|
|
1983
|
+
endpoint="projectset_manager/set_session_qa_requirements",
|
|
1984
|
+
data={"project_id": self._project_id, "rules": json.dumps(rules), "guidance_text": guidance_text},
|
|
1985
|
+
)
|
|
1986
|
+
)
|
|
2002
1987
|
|
|
2003
1988
|
if not res.get("success") == 1:
|
|
2004
|
-
logger.error("There was an error setting up the protocol "
|
|
2005
|
-
"adherence rules.")
|
|
1989
|
+
logger.error("There was an error setting up the protocol adherence rules.")
|
|
2006
1990
|
logger.error(platform.parse_response(res))
|
|
2007
1991
|
return False
|
|
2008
1992
|
|
|
@@ -2028,15 +2012,16 @@ class Project:
|
|
|
2028
2012
|
logger = logging.getLogger(logger_name)
|
|
2029
2013
|
|
|
2030
2014
|
# Update the project's QA rules
|
|
2031
|
-
res = platform.parse_response(
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2015
|
+
res = platform.parse_response(
|
|
2016
|
+
platform.post(
|
|
2017
|
+
auth=self._account.auth,
|
|
2018
|
+
endpoint="projectset_manager/get_session_qa_requirements",
|
|
2019
|
+
data={"project_id": self._project_id},
|
|
2020
|
+
)
|
|
2021
|
+
)
|
|
2036
2022
|
|
|
2037
2023
|
if "rules" not in res:
|
|
2038
|
-
logger.error(f"There was an error extracting the protocol "
|
|
2039
|
-
f"adherence rules from {self._project_name}.")
|
|
2024
|
+
logger.error(f"There was an error extracting the protocol adherence rules from {self._project_name}.")
|
|
2040
2025
|
logger.error(platform.parse_response(res))
|
|
2041
2026
|
return False
|
|
2042
2027
|
|
|
@@ -2048,14 +2033,14 @@ class Project:
|
|
|
2048
2033
|
with open(rules_file_path, "w") as fr:
|
|
2049
2034
|
json.dump(res["rules"], fr, indent=4)
|
|
2050
2035
|
except FileNotFoundError:
|
|
2051
|
-
logger.error(f"Protocol adherence rules could not be exported"
|
|
2052
|
-
f"to file: '{rules_file_path}'.")
|
|
2036
|
+
logger.error(f"Protocol adherence rules could not be exported to file: '{rules_file_path}'.")
|
|
2053
2037
|
return False
|
|
2054
2038
|
|
|
2055
2039
|
return res["guidance_text"]
|
|
2056
2040
|
|
|
2057
2041
|
""" Helper Methods """
|
|
2058
|
-
|
|
2042
|
+
|
|
2043
|
+
def __handle_start_analysis(self, post_data, ignore_warnings=False, ignore_file_selection=True, n_calls=0):
|
|
2059
2044
|
"""
|
|
2060
2045
|
Handle the possible responses from the server after start_analysis.
|
|
2061
2046
|
Sometimes we have to send a request again, and then check again the
|
|
@@ -2081,8 +2066,21 @@ class Project:
|
|
|
2081
2066
|
platform.post(self._account.auth, "analysis_manager/analysis_registration", data=post_data)
|
|
2082
2067
|
)
|
|
2083
2068
|
logger.info(response["message"])
|
|
2084
|
-
return int(response["analysis_id"])
|
|
2069
|
+
return int(response["analysis_id"]) if "analysis_id" in response else None
|
|
2070
|
+
|
|
2085
2071
|
except platform.ChooseDataError as choose_data:
|
|
2072
|
+
if ignore_file_selection:
|
|
2073
|
+
logger.error(
|
|
2074
|
+
"Existence of multiple files satisfying the input data "
|
|
2075
|
+
"requirements and parameter ignore_file_selection==True."
|
|
2076
|
+
)
|
|
2077
|
+
logger.error(
|
|
2078
|
+
f"Unable to start the analysis: "
|
|
2079
|
+
f"{post_data['script_name']}:"
|
|
2080
|
+
f"{post_data['version']} with input "
|
|
2081
|
+
f"{post_data['as_input']}."
|
|
2082
|
+
)
|
|
2083
|
+
return None
|
|
2086
2084
|
has_warning = False
|
|
2087
2085
|
|
|
2088
2086
|
# logging any warning that we have
|
|
@@ -2097,44 +2095,104 @@ class Project:
|
|
|
2097
2095
|
}
|
|
2098
2096
|
|
|
2099
2097
|
if choose_data.data_to_choose:
|
|
2100
|
-
|
|
2101
|
-
chosen_files = {}
|
|
2102
|
-
for settings_key in choose_data.data_to_choose:
|
|
2103
|
-
chosen_files[settings_key] = {}
|
|
2104
|
-
filters = choose_data.data_to_choose[settings_key]["filters"]
|
|
2105
|
-
for filter_key in filters:
|
|
2106
|
-
filter_data = filters[filter_key]
|
|
2107
|
-
|
|
2108
|
-
# skip the filters that did not pass
|
|
2109
|
-
if not filter_data["passed"]:
|
|
2110
|
-
continue
|
|
2111
|
-
|
|
2112
|
-
number_of_files_to_select = 1
|
|
2113
|
-
if filter_data["range"][0] != 0:
|
|
2114
|
-
number_of_files_to_select = filter_data["range"][0]
|
|
2115
|
-
elif filter_data["range"][1] != 0:
|
|
2116
|
-
number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
|
|
2117
|
-
else:
|
|
2118
|
-
number_of_files_to_select = len(filter_data["files"])
|
|
2119
|
-
|
|
2120
|
-
files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
|
|
2121
|
-
chosen_files[settings_key][filter_key] = files_selection
|
|
2122
|
-
|
|
2123
|
-
new_post["user_preference"] = json.dumps(chosen_files)
|
|
2098
|
+
self.__handle_manual_choose_data(new_post, choose_data)
|
|
2124
2099
|
else:
|
|
2125
2100
|
if has_warning and not ignore_warnings:
|
|
2126
|
-
logger.
|
|
2101
|
+
logger.error("Cancelling analysis due to warnings, set 'ignore_warnings' to True to override.")
|
|
2127
2102
|
new_post["cancel"] = "1"
|
|
2128
2103
|
else:
|
|
2129
2104
|
logger.info("suppressing warnings")
|
|
2130
2105
|
new_post["user_preference"] = "{}"
|
|
2131
2106
|
new_post["_mint_only_warning"] = "1"
|
|
2132
2107
|
|
|
2133
|
-
return self.__handle_start_analysis(new_post, ignore_warnings
|
|
2108
|
+
return self.__handle_start_analysis(new_post, ignore_warnings, ignore_file_selection, n_calls)
|
|
2134
2109
|
except platform.ActionFailedError as e:
|
|
2135
|
-
logger.error(f"Unable to start the analysis: {e}")
|
|
2110
|
+
logger.error(f"Unable to start the analysis: {e}.")
|
|
2136
2111
|
return None
|
|
2137
2112
|
|
|
2113
|
+
def __handle_manual_choose_data(self, post_data, choose_data):
|
|
2114
|
+
"""
|
|
2115
|
+
Handle the responses of the user when there is need to select a file
|
|
2116
|
+
to start the analysis.
|
|
2117
|
+
|
|
2118
|
+
At the moment only supports a manual selection via the command-line
|
|
2119
|
+
interface.
|
|
2120
|
+
|
|
2121
|
+
Parameters
|
|
2122
|
+
----------
|
|
2123
|
+
post_data : dict
|
|
2124
|
+
Current post_data dictionary. To be mofidied in-place.
|
|
2125
|
+
choose_data : platform.ChooseDataError
|
|
2126
|
+
Error raised when trying to start an analysis, but data has to be chosen.
|
|
2127
|
+
"""
|
|
2128
|
+
|
|
2129
|
+
logger = logging.getLogger(logger_name)
|
|
2130
|
+
logger.warning("Multiple inputs available. You have to select the desired file/s to continue.")
|
|
2131
|
+
# in case we have data to choose
|
|
2132
|
+
chosen_files = {}
|
|
2133
|
+
for settings_key in choose_data.data_to_choose:
|
|
2134
|
+
logger.warning(f"Type next the file/s for the input with ID: '{settings_key}'.")
|
|
2135
|
+
chosen_files[settings_key] = {}
|
|
2136
|
+
filters = choose_data.data_to_choose[settings_key]["filters"]
|
|
2137
|
+
for filter_key in filters:
|
|
2138
|
+
filter_data = filters[filter_key]
|
|
2139
|
+
|
|
2140
|
+
# skip the filters that did not pass
|
|
2141
|
+
if not filter_data["passed"]:
|
|
2142
|
+
continue
|
|
2143
|
+
elif post_data.get("cancel"):
|
|
2144
|
+
continue
|
|
2145
|
+
|
|
2146
|
+
number_of_files_to_select = 1
|
|
2147
|
+
if filter_data["range"][0] != 0:
|
|
2148
|
+
number_of_files_to_select = filter_data["range"][0]
|
|
2149
|
+
elif filter_data["range"][1] != 0:
|
|
2150
|
+
number_of_files_to_select = min(filter_data["range"][1], len(filter_data["files"]))
|
|
2151
|
+
else:
|
|
2152
|
+
number_of_files_to_select = len(filter_data["files"])
|
|
2153
|
+
|
|
2154
|
+
# If the files need to be selected automatically based
|
|
2155
|
+
# on their file information.
|
|
2156
|
+
# Need to apply some filtering criteria at this stage.
|
|
2157
|
+
# Based on the information provided by next methods.
|
|
2158
|
+
# files_info = list_container_files_metadata() or
|
|
2159
|
+
# list_container_filter_files()
|
|
2160
|
+
|
|
2161
|
+
if number_of_files_to_select != len(filter_data["files"]):
|
|
2162
|
+
logger.warning(
|
|
2163
|
+
f" · File filter name: '{filter_key}'. Type "
|
|
2164
|
+
f"{number_of_files_to_select} file"
|
|
2165
|
+
f"{'s (i.e., file1.zip, file2.zip, file3.zip)' if number_of_files_to_select >1 else ''}."
|
|
2166
|
+
)
|
|
2167
|
+
save_file_ids, select_file_filter = {}, ""
|
|
2168
|
+
for file_ in filter_data["files"]:
|
|
2169
|
+
select_file_filter += f" · File name: {file_['name']}\n"
|
|
2170
|
+
save_file_ids[file_["name"]] = file_["_id"]
|
|
2171
|
+
names = [el.strip() for el in input(select_file_filter).strip().split(",")]
|
|
2172
|
+
|
|
2173
|
+
if len(names) != number_of_files_to_select:
|
|
2174
|
+
logger.error("The number of files selected does not correspond to the number of needed files.")
|
|
2175
|
+
logger.error(
|
|
2176
|
+
f"Selected: {len(names)} vs. "
|
|
2177
|
+
f"Number of files to select: "
|
|
2178
|
+
f"{number_of_files_to_select}."
|
|
2179
|
+
)
|
|
2180
|
+
logger.error("Cancelling analysis.")
|
|
2181
|
+
post_data["cancel"] = "1"
|
|
2182
|
+
|
|
2183
|
+
elif any([name not in save_file_ids for name in names]):
|
|
2184
|
+
logger.error(f"Some selected file/s '{', '.join(names)}' do not exist. Cancelling analysis...")
|
|
2185
|
+
post_data["cancel"] = "1"
|
|
2186
|
+
else:
|
|
2187
|
+
chosen_files[settings_key][filter_key] = [save_file_ids[name] for name in names]
|
|
2188
|
+
|
|
2189
|
+
else:
|
|
2190
|
+
logger.warning("Setting all available files to be input to the analysis.")
|
|
2191
|
+
files_selection = [ff["_id"] for ff in filter_data["files"][:number_of_files_to_select]]
|
|
2192
|
+
chosen_files[settings_key][filter_key] = files_selection
|
|
2193
|
+
|
|
2194
|
+
post_data["user_preference"] = json.dumps(chosen_files)
|
|
2195
|
+
|
|
2138
2196
|
@staticmethod
|
|
2139
2197
|
def __get_modalities(files):
|
|
2140
2198
|
modalities = []
|
|
@@ -2284,9 +2342,9 @@ class Project:
|
|
|
2284
2342
|
if key == "pars_modalities":
|
|
2285
2343
|
modalities = value.split(";")[1].split(",")
|
|
2286
2344
|
if len(modalities) != 1:
|
|
2287
|
-
raise ValueError(
|
|
2288
|
-
|
|
2289
|
-
|
|
2345
|
+
raise ValueError(
|
|
2346
|
+
f"A file can only have one modality. Provided Modalities: {', '.join(modalities)}."
|
|
2347
|
+
)
|
|
2290
2348
|
modality = modalities[0]
|
|
2291
2349
|
elif key == "pars_tags":
|
|
2292
2350
|
tags = value.split(";")[1].split(",")
|
|
@@ -2294,27 +2352,16 @@ class Project:
|
|
|
2294
2352
|
d_tag = key.split("pars_[dicom]_")[1]
|
|
2295
2353
|
d_type = value.split(";")[0]
|
|
2296
2354
|
if d_type == "string":
|
|
2297
|
-
file_metadata[d_tag] = {
|
|
2298
|
-
"operation": "in",
|
|
2299
|
-
"value": value.replace(d_type + ";", "")
|
|
2300
|
-
}
|
|
2355
|
+
file_metadata[d_tag] = {"operation": "in", "value": value.replace(d_type + ";", "")}
|
|
2301
2356
|
elif d_type == "integer":
|
|
2302
2357
|
d_operator = value.split(";")[1].split("|")[0]
|
|
2303
2358
|
d_value = value.split(";")[1].split("|")[1]
|
|
2304
|
-
file_metadata[d_tag] = {
|
|
2305
|
-
"operation": d_operator,
|
|
2306
|
-
"value": int(d_value)}
|
|
2359
|
+
file_metadata[d_tag] = {"operation": d_operator, "value": int(d_value)}
|
|
2307
2360
|
elif d_type == "decimal":
|
|
2308
2361
|
d_operator = value.split(";")[1].split("|")[0]
|
|
2309
2362
|
d_value = value.split(";")[1].split("|")[1]
|
|
2310
|
-
file_metadata[d_tag] = {
|
|
2311
|
-
"operation": d_operator,
|
|
2312
|
-
"value": float(d_value)
|
|
2313
|
-
}
|
|
2363
|
+
file_metadata[d_tag] = {"operation": d_operator, "value": float(d_value)}
|
|
2314
2364
|
elif d_type == "list":
|
|
2315
2365
|
value.replace(d_type + ";", "")
|
|
2316
|
-
file_metadata[d_tag] = {
|
|
2317
|
-
"operation": "in-list",
|
|
2318
|
-
"value": value.replace(d_type + ";", "").split(";")
|
|
2319
|
-
}
|
|
2366
|
+
file_metadata[d_tag] = {"operation": "in-list", "value": value.replace(d_type + ";", "").split(";")}
|
|
2320
2367
|
return modality, tags, file_metadata
|
qmenta/client/Subject.py
CHANGED
|
@@ -30,8 +30,7 @@ class Subject:
|
|
|
30
30
|
self._all_data = None
|
|
31
31
|
|
|
32
32
|
def __repr__(self):
|
|
33
|
-
rep = "<Subject {} ({})>".format(self.name,
|
|
34
|
-
self.project or "No project")
|
|
33
|
+
rep = "<Subject {} ({})>".format(self.name, self.project or "No project")
|
|
35
34
|
return rep
|
|
36
35
|
|
|
37
36
|
@property
|
|
@@ -110,14 +109,13 @@ class Subject:
|
|
|
110
109
|
|
|
111
110
|
def get_all_data(self, cache=True):
|
|
112
111
|
if not cache or not self._all_data:
|
|
113
|
-
self._all_data = platform.parse_response(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"patient_id": self.subject_id,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
))
|
|
112
|
+
self._all_data = platform.parse_response(
|
|
113
|
+
platform.post(
|
|
114
|
+
auth=self.project._account.auth,
|
|
115
|
+
endpoint="patient_manager/get_patient_profile_data",
|
|
116
|
+
data={"patient_id": self.subject_id, "patient_secret_name": self.name},
|
|
117
|
+
)
|
|
118
|
+
)
|
|
121
119
|
return self._all_data
|
|
122
120
|
|
|
123
121
|
@property
|
|
@@ -242,8 +240,7 @@ class Subject:
|
|
|
242
240
|
List of dictionaries with the data of each analysis performed.
|
|
243
241
|
"""
|
|
244
242
|
all_analysis = self.project.list_analysis(limit=10000000)
|
|
245
|
-
return [a for a in all_analysis if
|
|
246
|
-
a["patient_secret_name"] == self._name]
|
|
243
|
+
return [a for a in all_analysis if a["patient_secret_name"] == self._name]
|
|
247
244
|
|
|
248
245
|
def upload_mri(self, path):
|
|
249
246
|
"""
|
qmenta/client/__init__.py
CHANGED
qmenta/client/utils.py
CHANGED
|
@@ -67,14 +67,10 @@ def unzip_dicoms(zip_path, output_folder, exclude_members=None):
|
|
|
67
67
|
if exclude_members is not None:
|
|
68
68
|
# Filter members according to the passed exclusion rules
|
|
69
69
|
for exclude_members_rule in exclude_members:
|
|
70
|
-
members = [
|
|
71
|
-
x for x in members if not re.match(exclude_members_rule, x)
|
|
72
|
-
]
|
|
70
|
+
members = [x for x in members if not re.match(exclude_members_rule, x)]
|
|
73
71
|
|
|
74
72
|
# Extract all files and add them to the list of extracted files
|
|
75
73
|
zfile.extractall(path=output_folder, members=members)
|
|
76
|
-
current_extracted_files = [
|
|
77
|
-
os.path.join(output_folder, fpath) for fpath in members
|
|
78
|
-
]
|
|
74
|
+
current_extracted_files = [os.path.join(output_folder, fpath) for fpath in members]
|
|
79
75
|
extracted_files.extend(current_extracted_files)
|
|
80
76
|
os.unlink(zip_path)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
qmenta/__init__.py,sha256=ED6jHcYiuYpr_0vjGz0zx2lrrmJT9sDJCzIljoDfmlM,65
|
|
2
|
+
qmenta/client/Account.py,sha256=7BOWHtRbHdfpBYQqv9v2m2Fag13pExZSxFsjDA7UsW0,9500
|
|
3
|
+
qmenta/client/File.py,sha256=iCrzrd7rIfjjW2AgMgUoK-ZF2wf-95wCcPKxKw6PGyg,4816
|
|
4
|
+
qmenta/client/Project.py,sha256=eIEmFcfh5dYjuHqGgfJfjvJNUYM8EEPFDwRidjjGOK8,86415
|
|
5
|
+
qmenta/client/Subject.py,sha256=b5sg9UFtn11bmPM-xFXP8aehOm_HGxnhgT7IPKbrZnE,8688
|
|
6
|
+
qmenta/client/__init__.py,sha256=Mtqe4zf8n3wuwMXSALENQgp5atQY5VcsyXWs2hjBs28,133
|
|
7
|
+
qmenta/client/utils.py,sha256=vWUAW0r9yDetdlwNo86sdzKn03FNGvwa7D9UtOA3TEc,2419
|
|
8
|
+
qmenta_client-1.1.dev1468.dist-info/METADATA,sha256=iVD5lU8ZbFqP5B6I8SdaN784WqN6nkx2MI3IG3lICOU,672
|
|
9
|
+
qmenta_client-1.1.dev1468.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
10
|
+
qmenta_client-1.1.dev1468.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
qmenta/__init__.py,sha256=jv2YF__bseklT3OWEzlqJ5qE24c4aWd5F4r0TTjOrWQ,65
|
|
2
|
-
qmenta/client/Account.py,sha256=S9D0-lmcBln2o3DwJfmLfu5G2wjE7gEJCIeJu89v0Is,9647
|
|
3
|
-
qmenta/client/File.py,sha256=ZgvSqejIosUt4uoX7opUnPnp5XGEaJNMRwFC0mQVB8k,5344
|
|
4
|
-
qmenta/client/Project.py,sha256=7MPUskUznuCSN7SOdNyIZckDcnBnJvlMnIoIbF3KvzE,83483
|
|
5
|
-
qmenta/client/Subject.py,sha256=lhxxVdQ6d-GNoQC8mrJwa4L1f44nJc4PcJtDspmKN7I,8756
|
|
6
|
-
qmenta/client/__init__.py,sha256=AjTojBhZeW5nl0i605KS8S1Gl5tPNc1hdzD47BGNfoI,147
|
|
7
|
-
qmenta/client/utils.py,sha256=5DK2T_HQprrCwLS0Ycm2CjseaYmAUKaJkJvYoW-Rqzc,2479
|
|
8
|
-
qmenta_client-1.1.dev1444.dist-info/METADATA,sha256=dNf9vlNtJKHYdXVgbu-rYvsHb5eQ7q_UVWz-ymM5Vb8,672
|
|
9
|
-
qmenta_client-1.1.dev1444.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
10
|
-
qmenta_client-1.1.dev1444.dist-info/RECORD,,
|
|
File without changes
|