qmenta-sdk-lib 2.0.1.dev3450__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.
Files changed (37) hide show
  1. qmenta_sdk_lib-2.0.1.dev3450/PKG-INFO +16 -0
  2. qmenta_sdk_lib-2.0.1.dev3450/pyproject.toml +39 -0
  3. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/__init__.py +1 -0
  4. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/__init__.py +1 -0
  5. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/bids/__init__.py +0 -0
  6. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/bids/make_entrypoint.py +24 -0
  7. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/bids/wrapper.py +228 -0
  8. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/client.py +103 -0
  9. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/communication.py +273 -0
  10. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/context.py +1323 -0
  11. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/directory_utils.py +18 -0
  12. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/executor.py +119 -0
  13. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/init.py +118 -0
  14. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/local/__init__.py +0 -0
  15. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/local/client.py +23 -0
  16. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/local/context.py +555 -0
  17. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/local/executor.py +95 -0
  18. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/local/parse_settings.py +68 -0
  19. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/log_capture.py +76 -0
  20. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/make_entrypoint.py +34 -0
  21. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/__init__.py +0 -0
  22. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/context.py +34 -0
  23. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/file_filter.py +88 -0
  24. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/inputs.py +257 -0
  25. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/integration_ui.py +300 -0
  26. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/integration_workflow_ui.py +423 -0
  27. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/launch_gui.py +135 -0
  28. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/make_files.py +109 -0
  29. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/modalities.py +39 -0
  30. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/outputs.py +379 -0
  31. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/run_test_docker.py +191 -0
  32. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/templates_tool_maker/Dockerfile_schema +26 -0
  33. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/templates_tool_maker/description_schema +1 -0
  34. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/templates_tool_maker/qmenta.png +0 -0
  35. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/templates_tool_maker/test_tool_schema +75 -0
  36. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/templates_tool_maker/tool_schema +203 -0
  37. qmenta_sdk_lib-2.0.1.dev3450/python/qmenta/sdk/tool_maker/tool_maker.py +756 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: qmenta_sdk_lib
3
+ Version: 2.0.1.dev3450
4
+ Summary: QMENTA SDK for tool development.
5
+ Author: QMENTA
6
+ Requires-Python: >=3.10,<4.0
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: PySimpleGui (>=1.0)
12
+ Requires-Dist: enum-compat (>=0.0.1)
13
+ Requires-Dist: future (>=0.18.2)
14
+ Requires-Dist: humanfriendly (>=1.0)
15
+ Requires-Dist: qmenta-client (>=1.0)
16
+ Requires-Dist: requests (>=1.0)
@@ -0,0 +1,39 @@
1
+ [tool.poetry]
2
+ name = "qmenta_sdk_lib"
3
+ version = "2.0.1.dev3450"
4
+ description = "QMENTA SDK for tool development."
5
+ authors = ["QMENTA"]
6
+ packages = [
7
+ {include = "qmenta", from = "python"}
8
+ ]
9
+
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.dev-dependencies]
20
+
21
+ [build-system]
22
+ requires = ["poetry-core>=1.0.0"]
23
+ build-backend = "poetry.core.masonry.api"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["templates_tool_maker"]
27
+
28
+ [tool.setuptools.package-data]
29
+ mypkg = ["*_schema*", "*.png"]
30
+
31
+ [project]
32
+ authors = [
33
+ {name = "Tim Peeters", email = "tim@qmenta.com"}
34
+ ]
35
+ maintainers = [
36
+ {name = "Marc Ramos", email = "marc@qmenta.com"},
37
+ {name = "Vicente Ferrer", email = "vicente.ferrer@qmenta.com"}
38
+ ]
39
+ readme = {file = "README.md", content-type = "text/markdown"}
@@ -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)
@@ -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,228 @@
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 [".bval", ".bvec"]: # In BIDS, bval and bvec use dwi as modality
74
+ target_name = "{}_dwi{}".format(sub_name, extension)
75
+ else:
76
+ target_name = "{}_{}{}".format(sub_name, modality_name, extension)
77
+
78
+ return os.path.join(cat_dir, target_name)
79
+
80
+
81
+ def format_input_data(context, subject, session, path):
82
+ """
83
+ Format input data followings the BIDS specification: http://bids.neuroimaging.io/bids_spec.pdf
84
+
85
+ :param context: AnalysisContext of the running analysis
86
+ :param subject: Subject name (from analysis_data)
87
+ :param session: Session ID (from analysis_data)
88
+ :param path: Destination directory where the files will be placed
89
+
90
+ TODO: support group analyses (multiple input containers).
91
+ """
92
+
93
+ logger = logging.getLogger(__name__)
94
+
95
+ dataset_description = {"Name": "QMENTA_ANALYSIS", "BIDSVersion": "1.1.0"}
96
+
97
+ # The input container specification must include file filters following the bids_ff_names nomenclature
98
+ bids_ff_names = {
99
+ "anat": [
100
+ "T1w",
101
+ "T2w",
102
+ "T1rho",
103
+ "T1map",
104
+ "T2map",
105
+ "T2star",
106
+ "FLAIR",
107
+ "FLASH",
108
+ "PD",
109
+ "PDmap",
110
+ "PDT2",
111
+ "inplaneT1",
112
+ "inplaneT2",
113
+ "angio",
114
+ ],
115
+ "func": ["bold", "events", "physio", "sbref"],
116
+ "dwi": ["dwi", "bval", "bvec"],
117
+ "fmap": ["phasediff, magnitude1"],
118
+ }
119
+
120
+ supported_extensions = [".nii", ".nii.gz", ".bval", ".bvec", ".tsv"]
121
+
122
+ # BIDS spec does not allow underscores or dashes
123
+ sub_name = "sub-{}".format(subject.replace("-", "").replace("_", ""))
124
+ sub_path = os.path.join(path, sub_name)
125
+ mkdirs(sub_path)
126
+
127
+ with open(os.path.join(path, "dataset_description.json"), "w") as fp:
128
+ json.dump(dataset_description, fp)
129
+
130
+ for category in list(bids_ff_names.keys()):
131
+ for modality_name in bids_ff_names[category]:
132
+ try:
133
+ file_handlers = context.get_files("input", file_filter_condition_name="c_{}".format(modality_name))
134
+ except: # noqa: E731,E123,E722
135
+ continue
136
+
137
+ for fh in file_handlers:
138
+
139
+ # NIFTI IS A MUST FOR BIDS
140
+ if any([fh.name.endswith(ext) for ext in supported_extensions]):
141
+ cat_dir = os.path.join(sub_path, category)
142
+ mkdirs(cat_dir)
143
+
144
+ target_path = generate_file_path(fh.name, subject, modality_name, cat_dir)
145
+
146
+ if os.path.isfile(target_path):
147
+ logger.warning(
148
+ "Multiple files with same name ({}). Only the first will be used".format(target_path)
149
+ )
150
+ break
151
+
152
+ with TemporaryDirectory() as tmp_dir:
153
+ download_path = fh.download(tmp_dir)
154
+ shutil.move(download_path, target_path)
155
+ logger.info("File: {}".format(target_path))
156
+
157
+ metadata = fh.get_file_info()
158
+ if metadata:
159
+ metadata_target_name = "{}_{}{}".format(sub_name, modality_name, ".json")
160
+ metadata_target_path = os.path.join(cat_dir, metadata_target_name)
161
+ with open(metadata_target_path, "w") as fp:
162
+ json.dump(metadata, fp)
163
+ else:
164
+ logger.warning("File {} extension not supported ({})".format(fh.name, supported_extensions))
165
+
166
+
167
+ def format_settings(settings):
168
+ # Bypass from JSON schema
169
+ # See docs.qmenta.com for more information
170
+
171
+ # FIXME: Better define inputs, for now, we'll skip common settings
172
+ ignore_param_list = ["input", "age_months"]
173
+
174
+ options = []
175
+ for param in settings:
176
+ if param not in ignore_param_list:
177
+ options += ["--" + param, str(settings[param])]
178
+ return options
179
+
180
+
181
+ def run(context):
182
+ logger = logging.getLogger(__name__)
183
+
184
+ # Get information from the platform such as patient name, user_id, ssid...
185
+ analysis_data = context.fetch_analysis_data()
186
+ context.set_progress(message="Preparing data...")
187
+
188
+ # Create in/out dirs
189
+ input_path = "/analysis/input/"
190
+ output_path = "/analysis/output/"
191
+ mkdirs(input_path)
192
+ mkdirs(output_path)
193
+
194
+ # Download data from the platform and store following the BIDS specification
195
+ format_input_data(
196
+ context, subject=analysis_data["patient_secret_name"], session=analysis_data["ssid"], path=input_path
197
+ )
198
+
199
+ # Select type of analysis (TODO: add support for group analysis)
200
+ analysis_type = "participant"
201
+
202
+ # Prepare a list of arguments from the settings
203
+ options = format_settings(context.get_settings())
204
+
205
+ # Run the original BIDS-APP entrypoint
206
+ app_path = os.environ["BIDS_APP_ENTRYPOINT"]
207
+ context.set_progress(message="Running {}".format(os.path.basename(app_path)))
208
+ try:
209
+ args = [app_path, input_path, output_path, analysis_type] + options
210
+ logger.info("Calling: {}".format(" ".join(args)))
211
+ ret = check_output(args, stderr=STDOUT)
212
+ logger.info(ret)
213
+ except CalledProcessError as e:
214
+ raise RuntimeError("Program exited with error: {}\nOutput:\n{}".format(e, e.output))
215
+ except Exception:
216
+ raise
217
+
218
+ # Upload output files
219
+ context.set_progress(message="Saving output...")
220
+ output_files = []
221
+ for dirpath, _, filenames in os.walk(output_path):
222
+ for f in filenames:
223
+ file_path = os.path.abspath(os.path.join(dirpath, f))
224
+ output_files.append(file_path)
225
+
226
+ for f, tags in tag_files(output_files):
227
+ output_container_path = os.path.relpath(f, output_path)
228
+ context.upload_file(f, output_container_path, tags=tags)
@@ -0,0 +1,103 @@
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(state, AnalysisState), "'state' is {}, not an 'AnalysisState'".format(state)
77
+ data_to_send = {"analysis_id": self.__analysis_id, "state": state.value}
78
+ data_to_send.update(kwargs)
79
+ logging.getLogger(__name__).info("State = {state!r} for analysis {analysis_id}".format(**data_to_send))
80
+ self.__comm.send_request("analysis_manager/set_analysis_state", data_to_send)
81
+
82
+ def upload_log(self):
83
+
84
+ data_to_send = {"analysis_id": self.__analysis_id}
85
+ log_contents = ""
86
+ try:
87
+ with open(self.__log_path, "r") as log_file:
88
+ log_contents = log_file.read()
89
+ except IOError:
90
+ log_contents = "Log file error ({})".format(self.__log_path)
91
+ finally:
92
+ # Send file in one single request
93
+ # FIXME Implement chuncked upload
94
+ self.__comm.send_files(
95
+ "analysis_manager/upload_log", files={"file": ("exec.log", log_contents)}, req_data=data_to_send
96
+ )
97
+
98
+
99
+ def load_user_analysis(user_script_path):
100
+ module_name, class_or_func_name = user_script_path.split(":")
101
+ user_module = importlib.import_module(module_name)
102
+ class_or_func = getattr(user_module, class_or_func_name)
103
+ return class_or_func
@@ -0,0 +1,273 @@
1
+ import json
2
+ import logging
3
+ import time
4
+
5
+ import requests
6
+
7
+ try:
8
+ import urllib.parse as urlparse
9
+ except ImportError:
10
+ import urlparse
11
+
12
+
13
+ class CommunicationObject(object):
14
+ def __init__(self, url, project_id, token, timeout=900.0, chunk_size=256, verify=True, critical=False):
15
+ """
16
+ Communications with the platform
17
+
18
+ Parameters
19
+ ----------
20
+ url : str
21
+ Platform URL
22
+ project_id : int
23
+ Unique project identifier
24
+ token : str
25
+ Unique token that authenticates the communication
26
+ timeout : float or tuple, optional
27
+ How many seconds to wait for the server to send data before giving up, as a float, or a
28
+ (connect timeout, read timeout) tuple. Defaults to
29
+ chunk_size : int
30
+ Size of chunks in bytes (1MB = 1024KB, 1KB = 1024B)
31
+ verify : bool, optional
32
+ Controls whether the server's TLS certificate is verified.
33
+ Defaults to True.
34
+ critical: bool, optional
35
+ Overrides total_retry seting in requests by making themm fail after the first error if set.
36
+ """
37
+ self.timeout = timeout
38
+ self.chunk_size = chunk_size
39
+
40
+ self.__req_url = url
41
+ self.__project_id = project_id
42
+ self.__token = token
43
+ self.__verify = verify
44
+ self.__critical = critical
45
+
46
+ self.__session = requests.Session()
47
+
48
+ def __enter__(self):
49
+ return self
50
+
51
+ def __exit__(self, exc_type, exc_value, traceback):
52
+ ret = self.send_request("logout")
53
+ if not ret["success"]:
54
+ # Not being able to logout is a security issue and must be a fatal error
55
+ raise RuntimeError("Unable to logout")
56
+
57
+ def send_request(
58
+ self,
59
+ access_point=None,
60
+ req_data=None,
61
+ req_headers=None,
62
+ return_json=True,
63
+ timeout=None,
64
+ total_retry=5,
65
+ stream=False,
66
+ url=None,
67
+ method="POST",
68
+ **kwargs
69
+ ):
70
+ """
71
+ Send a request to the platform to the given API endpoint.
72
+
73
+ Parameters
74
+ ----------
75
+ access_point : str, optional
76
+ API endpoint. Will be joined with the request URL provided at construction time to create the
77
+ full ("absolute") url
78
+ req_data : dict or str, optional
79
+ Request data
80
+ req_headers : dict, optional
81
+ Request headers
82
+ return_json : bool, optional
83
+ Whether the response should be a JSON object or a requests.Response instance.
84
+ Defaults to True.
85
+ timeout : float, optional
86
+ Timeout in seconds for this request.
87
+ Defaults to the timeout set at construction time.
88
+ total_retry : int, optional
89
+ Number of connection retries.
90
+ Defaults to 5.
91
+ stream : bool, optional
92
+ If False, the response content will be immediately downloaded.
93
+ url : str, optional
94
+ When no access_point is provided, a full url must be provided through this argument
95
+ kwargs : dict
96
+ Additional arguments to be passed to `requests.Session.post`
97
+ method: str
98
+ HTTP verb (for instance POST or GET)
99
+
100
+ Returns
101
+ -------
102
+ requests.Response or dict
103
+ JSON response encoded in a dictionary if return_json is True (default),
104
+ otherwise requests.Response object.
105
+ """
106
+ logger = logging.getLogger(__name__)
107
+
108
+ # Default parameters
109
+ req_data = req_data or dict()
110
+ req_headers = req_headers or dict()
111
+ timeout = timeout or self.timeout
112
+
113
+ if access_point:
114
+ # Add authentication for the platform
115
+ req_headers["Cookie"] = "AUTH_COOKIE={}; MINT_AP={}".format(self.__token, self.__project_id)
116
+ req_headers["Mint-Api-Call"] = "1"
117
+ url = urlparse.urljoin(self.__req_url, access_point)
118
+
119
+ retry_counter = 0
120
+ while True:
121
+ try:
122
+ response = self.__session.request(
123
+ method=method,
124
+ url=url,
125
+ data=req_data,
126
+ headers=req_headers,
127
+ timeout=timeout,
128
+ verify=self.__verify,
129
+ stream=stream,
130
+ **kwargs
131
+ )
132
+
133
+ # log & raise error if we get a HTTP error code
134
+ if not response.ok:
135
+ logger.debug("{!r} HTTP response: {!r}".format(url, response.text))
136
+ response.raise_for_status()
137
+
138
+ except (requests.HTTPError, requests.Timeout, requests.ConnectionError):
139
+ retry_counter += 1
140
+ if self.__critical or total_retry == retry_counter:
141
+ raise
142
+ time.sleep(retry_counter * 5)
143
+
144
+ else:
145
+ return json.loads(response.text) if return_json else response
146
+
147
+ def send_files(
148
+ self,
149
+ access_point,
150
+ files,
151
+ req_data=None,
152
+ req_headers=None,
153
+ return_json=True,
154
+ timeout=None,
155
+ total_retry=5,
156
+ stream=False,
157
+ **kwargs
158
+ ):
159
+ """
160
+ Send a POST request to the platform to the given API endpoint.
161
+
162
+ Parameters
163
+ ----------
164
+ access_point : str
165
+ API endpoint. Will be joined with the request URL provided at construction time to create the
166
+ full ("absolute") url
167
+ files : dict
168
+ Request files
169
+ req_data : dict or str, optional
170
+ Request data
171
+ req_headers : dict, optional
172
+ Request headers
173
+ return_json : bool, optional
174
+ Whether the response should be a JSON object or a requests.Response instance.
175
+ Defaults to True.
176
+ timeout : float, optional
177
+ Timeout in seconds for this request.
178
+ Defaults to the timeout set at construction time.
179
+ total_retry : int, optional
180
+ Number of connection retries.
181
+ Defaults to 5.
182
+ stream : bool, optional
183
+ If False, the response content will be immediately downloaded.
184
+ kwargs : dict
185
+ Additional arguments to be passed to `requests.Session.post`
186
+
187
+ Returns
188
+ -------
189
+ requests.Response or dict
190
+ JSON response encoded in a dictionary if return_json is True (default),
191
+ otherwise requests.Response object.
192
+ """
193
+ logger = logging.getLogger(__name__)
194
+
195
+ # Default parameters
196
+ req_data = req_data or dict()
197
+ req_headers = req_headers or dict()
198
+ timeout = timeout or self.timeout
199
+
200
+ # Add authentication for the platform
201
+ req_headers["Cookie"] = "AUTH_COOKIE={}; MINT_AP={}".format(self.__token, self.__project_id)
202
+ req_headers["Mint-Api-Call"] = "1"
203
+
204
+ retry_counter = 0
205
+ while True:
206
+ try:
207
+ url = urlparse.urljoin(self.__req_url, access_point)
208
+ response = self.__session.post(
209
+ url,
210
+ files=files,
211
+ data=req_data,
212
+ headers=req_headers,
213
+ timeout=timeout,
214
+ verify=self.__verify,
215
+ stream=stream,
216
+ **kwargs
217
+ )
218
+
219
+ # Raise error if we get a HTTP error code
220
+ response.raise_for_status()
221
+ result = json.loads(response.text) if return_json else response
222
+ logger.info("{!r} HTTP response: {!r}".format(url, result))
223
+ return result
224
+
225
+ except (requests.HTTPError, requests.Timeout, requests.ConnectionError):
226
+ retry_counter += 1
227
+ if self.__critical or total_retry == retry_counter:
228
+ raise
229
+ time.sleep(retry_counter * 5)
230
+
231
+
232
+ class DummyCommunicationObject(object):
233
+ def __init__(self, url, chunk_size=1, mock_response=None):
234
+ self.__req_url = url
235
+ self.chunk_size = chunk_size
236
+ self.mock_response = mock_response
237
+ self.sent_contents = None
238
+
239
+ def send_request(
240
+ self,
241
+ access_point=None,
242
+ req_data=None,
243
+ req_headers=None,
244
+ return_json=True,
245
+ timeout=None,
246
+ total_retry=5,
247
+ stream=False,
248
+ url=None,
249
+ method="POST",
250
+ **kwargs
251
+ ):
252
+ self.sent_contents = req_data
253
+ if stream and method == "GET":
254
+ return DummmyResponse()
255
+ elif self.mock_response is None or stream:
256
+ return DummmyResponse()
257
+ else:
258
+ return self.mock_response
259
+
260
+
261
+ class DummmySession(object):
262
+
263
+ def get(self, url, stream=False):
264
+ if stream:
265
+ return DummmyResponse()
266
+ else:
267
+ return "{}"
268
+
269
+
270
+ class DummmyResponse(object):
271
+
272
+ def iter_content(self, *args, **kwargs):
273
+ return []