qmenta-sdk-lib 2.0.1.dev3472__tar.gz
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_sdk_lib-2.0.1.dev3472/PKG-INFO +18 -0
- qmenta_sdk_lib-2.0.1.dev3472/pyproject.toml +28 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/__init__.py +1 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/__init__.py +1 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/bids/__init__.py +0 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/bids/make_entrypoint.py +24 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/bids/wrapper.py +255 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/client.py +116 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/communication.py +296 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/context.py +1696 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/directory_utils.py +18 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/executor.py +137 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/init.py +147 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/local/__init__.py +0 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/local/client.py +27 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/local/context.py +654 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/local/executor.py +120 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/local/parse_settings.py +88 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/log_capture.py +76 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/make_entrypoint.py +39 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/__init__.py +0 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/context.py +42 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/file_filter.py +101 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/inputs.py +286 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/integration_ui.py +385 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/integration_workflow_ui.py +485 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/launch_gui.py +162 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/make_files.py +117 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/modalities.py +39 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/outputs.py +395 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/run_test_docker.py +216 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/templates_tool_maker/Dockerfile_schema +26 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/templates_tool_maker/description_schema +1 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/templates_tool_maker/qmenta.png +0 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/templates_tool_maker/test_tool_schema +75 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/templates_tool_maker/tool_schema +203 -0
- qmenta_sdk_lib-2.0.1.dev3472/python/qmenta/sdk/tool_maker/tool_maker.py +872 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: qmenta-sdk-lib
|
|
3
|
+
Version: 2.0.1.dev3472
|
|
4
|
+
Summary: QMENTA SDK for tool development.
|
|
5
|
+
Author: QMENTA
|
|
6
|
+
Maintainer: Marc Ramos
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: PySimpleGui (>=1.0)
|
|
14
|
+
Requires-Dist: enum-compat (>=0.0.1)
|
|
15
|
+
Requires-Dist: future (>=0.18.2)
|
|
16
|
+
Requires-Dist: humanfriendly (>=1.0)
|
|
17
|
+
Requires-Dist: qmenta-client (>=1.0)
|
|
18
|
+
Requires-Dist: requests (>=1.0)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "qmenta-sdk-lib"
|
|
3
|
+
version = "2.0.1.dev3472"
|
|
4
|
+
description = "QMENTA SDK for tool development."
|
|
5
|
+
authors = ["QMENTA"]
|
|
6
|
+
packages = [
|
|
7
|
+
{include = "qmenta", from = "python"}
|
|
8
|
+
]
|
|
9
|
+
maintainers = ["Marc Ramos", "Vicente Ferrer", "Tim Peeters"]
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
qmenta-client = ">=1.0"
|
|
13
|
+
enum-compat = ">=0.0.1"
|
|
14
|
+
future = ">=0.18.2"
|
|
15
|
+
requests = ">=1.0"
|
|
16
|
+
humanfriendly = ">=1.0"
|
|
17
|
+
PySimpleGui = ">=1.0"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.test]
|
|
20
|
+
optional = true
|
|
21
|
+
[tool.poetry.group.test.dependencies]
|
|
22
|
+
pytest = ">=8.2.0"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
where = ["templates_tool_maker"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
mypkg = ["*_schema*", "*.png"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "{version}" # This is filled by CI at build time (also used for documentation)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from argparse import ArgumentParser
|
|
3
|
+
|
|
4
|
+
entry_point_contents = """#!/bin/bash -eu
|
|
5
|
+
# Add your configuration here:
|
|
6
|
+
# ...
|
|
7
|
+
|
|
8
|
+
# Tool start:
|
|
9
|
+
exec {executable} -m qmenta.sdk.executor "$@"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_entrypoint():
|
|
14
|
+
|
|
15
|
+
parser = ArgumentParser()
|
|
16
|
+
parser.add_argument("target")
|
|
17
|
+
options = parser.parse_args()
|
|
18
|
+
|
|
19
|
+
with open(options.target, "w") as fp:
|
|
20
|
+
fp.write(entry_point_contents.format(executable=sys.executable))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
make_entrypoint()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from subprocess import check_output, CalledProcessError, STDOUT
|
|
6
|
+
|
|
7
|
+
from qmenta.sdk.directory_utils import mkdirs, TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tag_files(files):
|
|
11
|
+
# TODO: improve tagging
|
|
12
|
+
tfiles = []
|
|
13
|
+
|
|
14
|
+
for f in files:
|
|
15
|
+
tags = []
|
|
16
|
+
|
|
17
|
+
# Modality
|
|
18
|
+
if "T1w" in f:
|
|
19
|
+
tags.append("m:T1")
|
|
20
|
+
elif "T2w" in f:
|
|
21
|
+
tags.append("m:T2")
|
|
22
|
+
|
|
23
|
+
# Tags
|
|
24
|
+
if "brainmask" in f:
|
|
25
|
+
tags.append("mask")
|
|
26
|
+
elif "labels" in f:
|
|
27
|
+
tags.append("labels")
|
|
28
|
+
if "tissue_labels" in f:
|
|
29
|
+
tags.append("tissue_segmentation")
|
|
30
|
+
elif "biasfield" in f:
|
|
31
|
+
tags.append("bf")
|
|
32
|
+
elif "restore" in f:
|
|
33
|
+
tags.append("bfc")
|
|
34
|
+
elif "anat2" in f or "2anat" in f:
|
|
35
|
+
tags.append("warp")
|
|
36
|
+
|
|
37
|
+
tfiles.append((f, tags))
|
|
38
|
+
|
|
39
|
+
return tfiles
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def generate_file_path(original_name, sub_name, modality_name, cat_dir):
|
|
43
|
+
"""
|
|
44
|
+
Returns a valid BIDS name (full path) for the given file name.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
original_name : str
|
|
49
|
+
Original name of the file.
|
|
50
|
+
sub_name : str
|
|
51
|
+
QMENTA platform subject name (SUB-XXX_...).
|
|
52
|
+
modality_name : str
|
|
53
|
+
Modality string in the BIDS format (may be part of the name).
|
|
54
|
+
cat_dir : str
|
|
55
|
+
Path to the directory where the file will be stored.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
str
|
|
60
|
+
The full BIDS-compatible path where the file should be downloaded.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Try to detect if the file name is already in BIDS format
|
|
64
|
+
if original_name.startswith("sub-") and modality_name in original_name:
|
|
65
|
+
|
|
66
|
+
# Use the original name (replacing the subject ID)
|
|
67
|
+
target_name = "_".join([sub_name] + original_name.split("_")[1:])
|
|
68
|
+
|
|
69
|
+
else:
|
|
70
|
+
|
|
71
|
+
# Automatically generate a BIDS compatible name for the file
|
|
72
|
+
extension = "." + ".".join(original_name.split(".")[1:])
|
|
73
|
+
if extension in [
|
|
74
|
+
".bval",
|
|
75
|
+
".bvec",
|
|
76
|
+
]: # In BIDS, bval and bvec use dwi as modality
|
|
77
|
+
target_name = "{}_dwi{}".format(sub_name, extension)
|
|
78
|
+
else:
|
|
79
|
+
target_name = "{}_{}{}".format(sub_name, modality_name, extension)
|
|
80
|
+
|
|
81
|
+
return os.path.join(cat_dir, target_name)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def format_input_data(context, subject, session, path):
|
|
85
|
+
"""
|
|
86
|
+
Format input data followings the BIDS specification: http://bids.neuroimaging.io/bids_spec.pdf
|
|
87
|
+
|
|
88
|
+
:param context: AnalysisContext of the running analysis
|
|
89
|
+
:param subject: Subject name (from analysis_data)
|
|
90
|
+
:param session: Session ID (from analysis_data)
|
|
91
|
+
:param path: Destination directory where the files will be placed
|
|
92
|
+
|
|
93
|
+
TODO: support group analyses (multiple input containers).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
logger = logging.getLogger(__name__)
|
|
97
|
+
|
|
98
|
+
dataset_description = {"Name": "QMENTA_ANALYSIS", "BIDSVersion": "1.1.0"}
|
|
99
|
+
|
|
100
|
+
# The input container specification must include file filters following the bids_ff_names nomenclature
|
|
101
|
+
bids_ff_names = {
|
|
102
|
+
"anat": [
|
|
103
|
+
"T1w",
|
|
104
|
+
"T2w",
|
|
105
|
+
"T1rho",
|
|
106
|
+
"T1map",
|
|
107
|
+
"T2map",
|
|
108
|
+
"T2star",
|
|
109
|
+
"FLAIR",
|
|
110
|
+
"FLASH",
|
|
111
|
+
"PD",
|
|
112
|
+
"PDmap",
|
|
113
|
+
"PDT2",
|
|
114
|
+
"inplaneT1",
|
|
115
|
+
"inplaneT2",
|
|
116
|
+
"angio",
|
|
117
|
+
],
|
|
118
|
+
"func": ["bold", "events", "physio", "sbref"],
|
|
119
|
+
"dwi": ["dwi", "bval", "bvec"],
|
|
120
|
+
"fmap": ["phasediff, magnitude1"],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
supported_extensions = [".nii", ".nii.gz", ".bval", ".bvec", ".tsv"]
|
|
124
|
+
|
|
125
|
+
# BIDS spec does not allow underscores or dashes
|
|
126
|
+
sub_name = "sub-{}".format(subject.replace("-", "").replace("_", ""))
|
|
127
|
+
sub_path = os.path.join(path, sub_name)
|
|
128
|
+
mkdirs(sub_path)
|
|
129
|
+
|
|
130
|
+
with open(os.path.join(path, "dataset_description.json"), "w") as fp:
|
|
131
|
+
json.dump(dataset_description, fp)
|
|
132
|
+
|
|
133
|
+
for category in list(bids_ff_names.keys()):
|
|
134
|
+
for modality_name in bids_ff_names[category]:
|
|
135
|
+
try:
|
|
136
|
+
file_handlers = context.get_files(
|
|
137
|
+
"input",
|
|
138
|
+
file_filter_condition_name="c_{}".format(modality_name),
|
|
139
|
+
)
|
|
140
|
+
except: # noqa: E731,E123,E722
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
for fh in file_handlers:
|
|
144
|
+
|
|
145
|
+
# NIFTI IS A MUST FOR BIDS
|
|
146
|
+
if any(
|
|
147
|
+
[fh.name.endswith(ext) for ext in supported_extensions]
|
|
148
|
+
):
|
|
149
|
+
cat_dir = os.path.join(sub_path, category)
|
|
150
|
+
mkdirs(cat_dir)
|
|
151
|
+
|
|
152
|
+
target_path = generate_file_path(
|
|
153
|
+
fh.name, subject, modality_name, cat_dir
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if os.path.isfile(target_path):
|
|
157
|
+
logger.warning(
|
|
158
|
+
"Multiple files with same name ({}). Only the first will be used".format(
|
|
159
|
+
target_path
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
with TemporaryDirectory() as tmp_dir:
|
|
165
|
+
download_path = fh.download(tmp_dir)
|
|
166
|
+
shutil.move(download_path, target_path)
|
|
167
|
+
logger.info("File: {}".format(target_path))
|
|
168
|
+
|
|
169
|
+
metadata = fh.get_file_info()
|
|
170
|
+
if metadata:
|
|
171
|
+
metadata_target_name = "{}_{}{}".format(
|
|
172
|
+
sub_name, modality_name, ".json"
|
|
173
|
+
)
|
|
174
|
+
metadata_target_path = os.path.join(
|
|
175
|
+
cat_dir, metadata_target_name
|
|
176
|
+
)
|
|
177
|
+
with open(metadata_target_path, "w") as fp:
|
|
178
|
+
json.dump(metadata, fp)
|
|
179
|
+
else:
|
|
180
|
+
logger.warning(
|
|
181
|
+
"File {} extension not supported ({})".format(
|
|
182
|
+
fh.name, supported_extensions
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def format_settings(settings):
|
|
188
|
+
# Bypass from JSON schema
|
|
189
|
+
# See docs.qmenta.com for more information
|
|
190
|
+
|
|
191
|
+
# FIXME: Better define inputs, for now, we'll skip common settings
|
|
192
|
+
ignore_param_list = ["input", "age_months"]
|
|
193
|
+
|
|
194
|
+
options = []
|
|
195
|
+
for param in settings:
|
|
196
|
+
if param not in ignore_param_list:
|
|
197
|
+
options += ["--" + param, str(settings[param])]
|
|
198
|
+
return options
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def run(context):
|
|
202
|
+
logger = logging.getLogger(__name__)
|
|
203
|
+
|
|
204
|
+
# Get information from the platform such as patient name, user_id, ssid...
|
|
205
|
+
analysis_data = context.fetch_analysis_data()
|
|
206
|
+
context.set_progress(message="Preparing data...")
|
|
207
|
+
|
|
208
|
+
# Create in/out dirs
|
|
209
|
+
input_path = "/analysis/input/"
|
|
210
|
+
output_path = "/analysis/output/"
|
|
211
|
+
mkdirs(input_path)
|
|
212
|
+
mkdirs(output_path)
|
|
213
|
+
|
|
214
|
+
# Download data from the platform and store following the BIDS specification
|
|
215
|
+
format_input_data(
|
|
216
|
+
context,
|
|
217
|
+
subject=analysis_data["patient_secret_name"],
|
|
218
|
+
session=analysis_data["ssid"],
|
|
219
|
+
path=input_path,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Select type of analysis (TODO: add support for group analysis)
|
|
223
|
+
analysis_type = "participant"
|
|
224
|
+
|
|
225
|
+
# Prepare a list of arguments from the settings
|
|
226
|
+
options = format_settings(context.get_settings())
|
|
227
|
+
|
|
228
|
+
# Run the original BIDS-APP entrypoint
|
|
229
|
+
app_path = os.environ["BIDS_APP_ENTRYPOINT"]
|
|
230
|
+
context.set_progress(
|
|
231
|
+
message="Running {}".format(os.path.basename(app_path))
|
|
232
|
+
)
|
|
233
|
+
try:
|
|
234
|
+
args = [app_path, input_path, output_path, analysis_type] + options
|
|
235
|
+
logger.info("Calling: {}".format(" ".join(args)))
|
|
236
|
+
ret = check_output(args, stderr=STDOUT)
|
|
237
|
+
logger.info(ret)
|
|
238
|
+
except CalledProcessError as e:
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
"Program exited with error: {}\nOutput:\n{}".format(e, e.output)
|
|
241
|
+
)
|
|
242
|
+
except Exception:
|
|
243
|
+
raise
|
|
244
|
+
|
|
245
|
+
# Upload output files
|
|
246
|
+
context.set_progress(message="Saving output...")
|
|
247
|
+
output_files = []
|
|
248
|
+
for dirpath, _, filenames in os.walk(output_path):
|
|
249
|
+
for f in filenames:
|
|
250
|
+
file_path = os.path.abspath(os.path.join(dirpath, f))
|
|
251
|
+
output_files.append(file_path)
|
|
252
|
+
|
|
253
|
+
for f, tags in tag_files(output_files):
|
|
254
|
+
output_container_path = os.path.relpath(f, output_path)
|
|
255
|
+
context.upload_file(f, output_container_path, tags=tags)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from qmenta.sdk.context import NoFilesError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalysisState(Enum):
|
|
11
|
+
RUNNING = "running"
|
|
12
|
+
COMPLETED = "completed"
|
|
13
|
+
EXCEPTION = "exception"
|
|
14
|
+
NO_FILES = "no_files"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExecClient(object):
|
|
18
|
+
def __init__(self, analysis_id, comm, context, log_path):
|
|
19
|
+
self.__analysis_id = analysis_id
|
|
20
|
+
self.__comm = comm
|
|
21
|
+
self.__context = context
|
|
22
|
+
self.__log_path = log_path
|
|
23
|
+
|
|
24
|
+
def __call__(self, user_script_path):
|
|
25
|
+
"""
|
|
26
|
+
Run an analysis provided by the user
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
user_script_path : str
|
|
31
|
+
Should look like 'path.to.module:object', where 'object' should be an importable analysis tool class
|
|
32
|
+
or a function taking one 'context' argument.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
logger.info("Launching: {}".format(user_script_path))
|
|
37
|
+
|
|
38
|
+
analysis_state = None
|
|
39
|
+
try:
|
|
40
|
+
# noinspection PyTypeChecker
|
|
41
|
+
self.set_state(AnalysisState.RUNNING, log_path=self.__log_path)
|
|
42
|
+
class_or_func = load_user_analysis(user_script_path)
|
|
43
|
+
self.__context.fetch_analysis_data()
|
|
44
|
+
|
|
45
|
+
# run the analysis
|
|
46
|
+
# may allow classes to be used later
|
|
47
|
+
assert inspect.isfunction(class_or_func)
|
|
48
|
+
self.__context.set_progress(value=0, message="Running")
|
|
49
|
+
class_or_func(self.__context)
|
|
50
|
+
except NoFilesError:
|
|
51
|
+
logger.warning("No files found")
|
|
52
|
+
analysis_state = AnalysisState.NO_FILES
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.exception(e)
|
|
55
|
+
self.__context.set_progress(message="Error")
|
|
56
|
+
analysis_state = AnalysisState.EXCEPTION
|
|
57
|
+
raise e
|
|
58
|
+
else:
|
|
59
|
+
self.__context.set_progress(message="Completed", value=100)
|
|
60
|
+
analysis_state = AnalysisState.COMPLETED
|
|
61
|
+
finally:
|
|
62
|
+
self.upload_log()
|
|
63
|
+
# noinspection PyTypeChecker
|
|
64
|
+
self.set_state(analysis_state)
|
|
65
|
+
|
|
66
|
+
def set_state(self, state, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
Set analysis state
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
state : AnalysisState
|
|
73
|
+
One of AnalysisState.RUNNING, AnalysisState.COMPLETED, AnalysisState.EXCEPTION or AnalysisState.NO_FILES
|
|
74
|
+
kwargs : dict
|
|
75
|
+
"""
|
|
76
|
+
assert isinstance(
|
|
77
|
+
state, AnalysisState
|
|
78
|
+
), "'state' is {}, not an 'AnalysisState'".format(state)
|
|
79
|
+
data_to_send = {
|
|
80
|
+
"analysis_id": self.__analysis_id,
|
|
81
|
+
"state": state.value,
|
|
82
|
+
}
|
|
83
|
+
data_to_send.update(kwargs)
|
|
84
|
+
logging.getLogger(__name__).info(
|
|
85
|
+
"State = {state!r} for analysis {analysis_id}".format(
|
|
86
|
+
**data_to_send
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
self.__comm.send_request(
|
|
90
|
+
"analysis_manager/set_analysis_state", data_to_send
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def upload_log(self):
|
|
94
|
+
|
|
95
|
+
data_to_send = {"analysis_id": self.__analysis_id}
|
|
96
|
+
log_contents = ""
|
|
97
|
+
try:
|
|
98
|
+
with open(self.__log_path, "r") as log_file:
|
|
99
|
+
log_contents = log_file.read()
|
|
100
|
+
except IOError:
|
|
101
|
+
log_contents = "Log file error ({})".format(self.__log_path)
|
|
102
|
+
finally:
|
|
103
|
+
# Send file in one single request
|
|
104
|
+
# FIXME Implement chuncked upload
|
|
105
|
+
self.__comm.send_files(
|
|
106
|
+
"analysis_manager/upload_log",
|
|
107
|
+
files={"file": ("exec.log", log_contents)},
|
|
108
|
+
req_data=data_to_send,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_user_analysis(user_script_path):
|
|
113
|
+
module_name, class_or_func_name = user_script_path.split(":")
|
|
114
|
+
user_module = importlib.import_module(module_name)
|
|
115
|
+
class_or_func = getattr(user_module, class_or_func_name)
|
|
116
|
+
return class_or_func
|