jinko-sdk 0.3.4__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.
- jinko_sdk-0.3.4/LICENSE +21 -0
- jinko_sdk-0.3.4/PKG-INFO +59 -0
- jinko_sdk-0.3.4/README.md +41 -0
- jinko_sdk-0.3.4/crabbit/__init__.py +5 -0
- jinko_sdk-0.3.4/crabbit/__main__.py +31 -0
- jinko_sdk-0.3.4/crabbit/cli.py +330 -0
- jinko_sdk-0.3.4/crabbit/launcher.py +53 -0
- jinko_sdk-0.3.4/crabbit/utils.py +100 -0
- jinko_sdk-0.3.4/jinko_helpers/__init__.py +108 -0
- jinko_sdk-0.3.4/jinko_helpers/__version__.py +5 -0
- jinko_sdk-0.3.4/jinko_helpers/calibration.py +23 -0
- jinko_sdk-0.3.4/jinko_helpers/jinko_helpers.py +638 -0
- jinko_sdk-0.3.4/jinko_helpers/trial.py +103 -0
- jinko_sdk-0.3.4/jinko_stats/__init__.py +5 -0
- jinko_sdk-0.3.4/pyproject.toml +30 -0
jinko_sdk-0.3.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nova In Silico
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jinko_sdk-0.3.4/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: jinko-sdk
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: A Python package containing jinko_helpers, jinko_stats, and crabbit modules aimed to be used with Jinko.ai API
|
|
5
|
+
Author: SSE Team
|
|
6
|
+
Author-email: oss@novainsilico.ai
|
|
7
|
+
Requires-Python: >=3.10
|
|
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
|
+
Requires-Dist: black (>=24.4.2,<25.0.0)
|
|
13
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
14
|
+
Requires-Dist: requests (>=2.25.1,<3.0.0)
|
|
15
|
+
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Jinko API Helpers
|
|
19
|
+
|
|
20
|
+
Python helpers function to ease use of Jinko's API
|
|
21
|
+
|
|
22
|
+
Jinko is an innovative SaaS and CaaS platform focused on trial simulation and design optimization. The Jinko API offers programmatic access to a wide range of functionalities, facilitating integration with various tools and enhancing collaboration.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install git+https://github.com/novainsilico/jinko-api-helpers-python.git
|
|
28
|
+
|
|
29
|
+
# Or
|
|
30
|
+
|
|
31
|
+
poetry add git+https://github.com/novainsilico/jinko-api-helpers-python.git
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import jinko_helpers as jinko
|
|
37
|
+
|
|
38
|
+
# Connect to Jinko
|
|
39
|
+
jinko.initialize(
|
|
40
|
+
projectId='016140de-1753-4133-8cbf-e67d9a399ec1',
|
|
41
|
+
apiKey='50b5085e-3675-40c9-b65b-2aa8d0af101c'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Check authentication
|
|
45
|
+
response = jinko.makeRequest('/app/v1/auth/check')
|
|
46
|
+
|
|
47
|
+
# Make a few requests
|
|
48
|
+
projectItem = jinko.makeRequest(
|
|
49
|
+
'/app/v1/project-item/tr-EUsp-WjjI',
|
|
50
|
+
method='GET',
|
|
51
|
+
).json()
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Find out more at [Jinko Doc](https://doc.jinko.ai)
|
|
59
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Jinko API Helpers
|
|
2
|
+
|
|
3
|
+
Python helpers function to ease use of Jinko's API
|
|
4
|
+
|
|
5
|
+
Jinko is an innovative SaaS and CaaS platform focused on trial simulation and design optimization. The Jinko API offers programmatic access to a wide range of functionalities, facilitating integration with various tools and enhancing collaboration.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install git+https://github.com/novainsilico/jinko-api-helpers-python.git
|
|
11
|
+
|
|
12
|
+
# Or
|
|
13
|
+
|
|
14
|
+
poetry add git+https://github.com/novainsilico/jinko-api-helpers-python.git
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import jinko_helpers as jinko
|
|
20
|
+
|
|
21
|
+
# Connect to Jinko
|
|
22
|
+
jinko.initialize(
|
|
23
|
+
projectId='016140de-1753-4133-8cbf-e67d9a399ec1',
|
|
24
|
+
apiKey='50b5085e-3675-40c9-b65b-2aa8d0af101c'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Check authentication
|
|
28
|
+
response = jinko.makeRequest('/app/v1/auth/check')
|
|
29
|
+
|
|
30
|
+
# Make a few requests
|
|
31
|
+
projectItem = jinko.makeRequest(
|
|
32
|
+
'/app/v1/project-item/tr-EUsp-WjjI',
|
|
33
|
+
method='GET',
|
|
34
|
+
).json()
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Find out more at [Jinko Doc](https://doc.jinko.ai)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from crabbit import launcher
|
|
4
|
+
from crabbit.utils import bold_text
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_usage():
|
|
8
|
+
"""Show usage of the "crabbit" app."""
|
|
9
|
+
return (
|
|
10
|
+
bold_text("python -m crabbit {mode} {URL} {path}")
|
|
11
|
+
+ f"\n\nexample: {bold_text('python -m crabbit download https://jinko.ai/my-project-item my-project/download-folder')}"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# parse the command line arguments
|
|
16
|
+
parser = argparse.ArgumentParser(usage=get_usage())
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"mode",
|
|
19
|
+
choices=["download", "launch"],
|
|
20
|
+
help='The running mode of crabbit: currently only "download" mode is available.',
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("url", help="URL of the jinko project item or jinko folder.")
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"path",
|
|
25
|
+
help="Path to the local working folder of crabbit, e.g. folder for downloading the results of a trial.",
|
|
26
|
+
)
|
|
27
|
+
crab = launcher.CrabbitAppLauncher()
|
|
28
|
+
parser.parse_args(namespace=crab)
|
|
29
|
+
|
|
30
|
+
# run the app launcher
|
|
31
|
+
crab.run()
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Module containing the command-line apps of crabbit."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["CrabbitDownloader"]
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import zipfile
|
|
8
|
+
import io
|
|
9
|
+
import requests
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
import jinko_helpers as jinko
|
|
13
|
+
from crabbit.utils import bold_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CrabbitDownloader:
|
|
17
|
+
"""CLI app for running the crabbit "download" mode."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, project_item, output_path):
|
|
20
|
+
self.project_item = project_item
|
|
21
|
+
self.output_path = output_path
|
|
22
|
+
self.core_id = (
|
|
23
|
+
self.project_item["coreId"] if "coreId" in self.project_item else ""
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
self.pretty_patient_name = (
|
|
27
|
+
"CalibratedPatient" # nice name to be used in calibration visualization
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def run(self):
|
|
31
|
+
"""Main function of the download app."""
|
|
32
|
+
if (
|
|
33
|
+
not self.check_valid_item_type()
|
|
34
|
+
or not self.check_calib_status()
|
|
35
|
+
or not self.download_calib_inputs()
|
|
36
|
+
):
|
|
37
|
+
return
|
|
38
|
+
if self.project_item["type"] == "Calibration":
|
|
39
|
+
best_patient = self.find_best_calib_patient()
|
|
40
|
+
if best_patient is None:
|
|
41
|
+
return
|
|
42
|
+
self.download_calib_patient_timeseries(best_patient)
|
|
43
|
+
self.download_calib_patient_scalar_results(best_patient)
|
|
44
|
+
print(
|
|
45
|
+
bold_text("Done!"), f"To visualize: crabbit trialViz {self.output_path}"
|
|
46
|
+
)
|
|
47
|
+
else: # placeholder for future download types
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def check_valid_item_type(self):
|
|
51
|
+
"""Check whether the project item can be downloaded (currently only "Calibration" is supported) and get its CoreItemId."""
|
|
52
|
+
if (
|
|
53
|
+
"type" not in self.project_item
|
|
54
|
+
or self.project_item["type"] != "Calibration"
|
|
55
|
+
or not self.core_id
|
|
56
|
+
):
|
|
57
|
+
print(
|
|
58
|
+
'Currently "crabbit download" only supports the "Calibration" item type.'
|
|
59
|
+
)
|
|
60
|
+
return False
|
|
61
|
+
print(
|
|
62
|
+
bold_text(
|
|
63
|
+
'Note: for the "Calibration" item type, only the results of the "best patient", i.e. highest optimizationWeightedScore, will be downloaded.'
|
|
64
|
+
),
|
|
65
|
+
end="\n\n",
|
|
66
|
+
)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def check_calib_status(self):
|
|
70
|
+
"""Check whether the calibration can be downloaded depending on its status."""
|
|
71
|
+
status = jinko.get_calib_status(self.core_id)
|
|
72
|
+
if not status:
|
|
73
|
+
return False
|
|
74
|
+
elif status == "not_launched":
|
|
75
|
+
print("Error: calibration is not launched! (is it the correct version?)")
|
|
76
|
+
return False
|
|
77
|
+
elif status != "completed":
|
|
78
|
+
print("Warning: the status of the calibration is", status)
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def find_best_calib_patient(self):
|
|
82
|
+
"""Return the "patientNumber" of the best calibration patient, i.e. highest optimizationWeightedScore."""
|
|
83
|
+
print("Finding the ID of the best calib patient...")
|
|
84
|
+
response = jinko.make_request(
|
|
85
|
+
path=f"/core/v2/result_manager/calibration/sorted_patients",
|
|
86
|
+
method="POST",
|
|
87
|
+
json={
|
|
88
|
+
"calibId": {
|
|
89
|
+
"coreItemId": self.core_id["id"],
|
|
90
|
+
"snapshotId": self.core_id["snapshotId"],
|
|
91
|
+
},
|
|
92
|
+
"sortBy": "optimizationWeightedScore",
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
if not response.json():
|
|
96
|
+
print("Warning: best patient cannot be found! (is it the correct version?)")
|
|
97
|
+
return None
|
|
98
|
+
best_patient = response.json()[0]["patientNumber"]
|
|
99
|
+
print("Best patient is", best_patient, end="\n\n")
|
|
100
|
+
return best_patient
|
|
101
|
+
|
|
102
|
+
def download_calib_inputs(self):
|
|
103
|
+
"""Download calibration inputs (currently only scorings and data tables are downloaded)."""
|
|
104
|
+
csv_data = {}
|
|
105
|
+
json_data = []
|
|
106
|
+
try:
|
|
107
|
+
response = jinko.make_request(
|
|
108
|
+
path=f"/core/v2/calibration_manager/calibration/{self.core_id['id']}/snapshots/{self.core_id['snapshotId']}/bundle",
|
|
109
|
+
method="GET",
|
|
110
|
+
)
|
|
111
|
+
archive = zipfile.ZipFile(io.BytesIO(response.content))
|
|
112
|
+
for item in archive.namelist():
|
|
113
|
+
if item.startswith("data_tables"):
|
|
114
|
+
if not item.endswith(".csv"):
|
|
115
|
+
continue
|
|
116
|
+
csv_data[item.split("/")[1]] = pd.read_csv(
|
|
117
|
+
io.StringIO(archive.read(item).decode("utf-8")), sep=","
|
|
118
|
+
)
|
|
119
|
+
elif item.startswith("scorings"):
|
|
120
|
+
json_data.append(json.loads(archive.read(item).decode("utf-8")))
|
|
121
|
+
except requests.exceptions.HTTPError:
|
|
122
|
+
print(
|
|
123
|
+
"Error: failed to download calibration inputs (scorings and data tables)."
|
|
124
|
+
)
|
|
125
|
+
return False
|
|
126
|
+
assert (
|
|
127
|
+
json_data or csv_data
|
|
128
|
+
), "Something wrong happened (calibration without scoring nor data table)."
|
|
129
|
+
if json_data:
|
|
130
|
+
merged_json_scorings = {
|
|
131
|
+
"objectives": sum((item["objectives"] for item in json_data), [])
|
|
132
|
+
}
|
|
133
|
+
if merged_json_scorings["objectives"]:
|
|
134
|
+
json_path = os.path.join(self.output_path, "Scorings.json")
|
|
135
|
+
json.dump(merged_json_scorings, open(json_path, "w", encoding="utf-8"))
|
|
136
|
+
if csv_data:
|
|
137
|
+
try:
|
|
138
|
+
merged_csv_data = pd.concat(csv_data.values(), ignore_index=True)
|
|
139
|
+
# when data tables can be merged, save them in one single file
|
|
140
|
+
merged_csv_data.to_csv(
|
|
141
|
+
os.path.join(self.output_path, "ReferenceTimeSeries.csv"),
|
|
142
|
+
index=False,
|
|
143
|
+
)
|
|
144
|
+
except:
|
|
145
|
+
try:
|
|
146
|
+
# trim the data tables to the minimum columns then try merge again
|
|
147
|
+
trimmed_csv = []
|
|
148
|
+
mandatory_columns = [
|
|
149
|
+
"armScope",
|
|
150
|
+
"obsId",
|
|
151
|
+
"time",
|
|
152
|
+
"value",
|
|
153
|
+
"narrowRangeLowBound",
|
|
154
|
+
"narrowRangeHighBound",
|
|
155
|
+
"wideRangeLowBound",
|
|
156
|
+
"wideRangeHighBound",
|
|
157
|
+
]
|
|
158
|
+
for csv_name, csv_df in csv_data.items():
|
|
159
|
+
data_table_id = csv_name.split(".csv")[0]
|
|
160
|
+
sub_csv_df = csv_df.loc[:, mandatory_columns]
|
|
161
|
+
sub_csv_df["dataTableID"] = data_table_id
|
|
162
|
+
trimmed_csv.append(sub_csv_df)
|
|
163
|
+
merged_csv_data = pd.concat(trimmed_csv, ignore_index=True)
|
|
164
|
+
# when data tables can be merged, save them in one single file
|
|
165
|
+
merged_csv_data.to_csv(
|
|
166
|
+
os.path.join(self.output_path, "ReferenceTimeSeries.csv"),
|
|
167
|
+
index=False,
|
|
168
|
+
)
|
|
169
|
+
except:
|
|
170
|
+
# if still cannot merge, save the data table separately
|
|
171
|
+
for csv_name, csv_df in csv_data.items():
|
|
172
|
+
csv_df.to_csv(
|
|
173
|
+
os.path.join(self.output_path, csv_name), index=False
|
|
174
|
+
)
|
|
175
|
+
print("Downloaded calibration inputs (scorings and data tables).", end="\n\n")
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
def download_calib_patient_timeseries(self, patient_id):
|
|
179
|
+
"""Download one calibration patient's timeseries."""
|
|
180
|
+
print("Downloading the timeseries of the best calib patient...")
|
|
181
|
+
timeseries_path = os.path.join(self.output_path, "ModelResult")
|
|
182
|
+
os.mkdir(timeseries_path)
|
|
183
|
+
arms = []
|
|
184
|
+
try:
|
|
185
|
+
response = jinko.make_request(
|
|
186
|
+
path=f"/core/v2/result_manager/calibration/model_result",
|
|
187
|
+
method="POST",
|
|
188
|
+
json={
|
|
189
|
+
"calibId": {
|
|
190
|
+
"coreItemId": self.core_id["id"],
|
|
191
|
+
"snapshotId": self.core_id["snapshotId"],
|
|
192
|
+
},
|
|
193
|
+
"patientId": patient_id,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
for arm_item in response.json():
|
|
197
|
+
arm_name = arm_item["group"][0]["contents"]
|
|
198
|
+
arms.append(arm_name)
|
|
199
|
+
assert (
|
|
200
|
+
arm_item["indexes"]["patientNumber"] == patient_id
|
|
201
|
+
), "Something wrong happened (patient number mismatch between requests)!"
|
|
202
|
+
result_path = os.path.join(
|
|
203
|
+
timeseries_path, f"{self.pretty_patient_name}_{arm_name}.json"
|
|
204
|
+
)
|
|
205
|
+
json.dump(
|
|
206
|
+
{"res": arm_item["res"]}, open(result_path, "w", encoding="utf-8")
|
|
207
|
+
)
|
|
208
|
+
except (requests.exceptions.HTTPError, TypeError, KeyError):
|
|
209
|
+
print("Error: failed to download the timeseries.")
|
|
210
|
+
return
|
|
211
|
+
arm_count = len(arms)
|
|
212
|
+
print(
|
|
213
|
+
f'Successfully downloaded the timeseries of {arm_count} protocol arm{"s" if arm_count > 1 else ""}.',
|
|
214
|
+
end="\n\n",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def download_calib_patient_scalar_results(self, patient_id):
|
|
218
|
+
"""Download one calibration patient's scalar results (into scalar arrays, categorical arrays and scalar metadata)."""
|
|
219
|
+
print("Downloading the scalar results of the best calib patient...")
|
|
220
|
+
scalar_array_path = os.path.join(self.output_path, "ScalarArrays")
|
|
221
|
+
categorical_array_path = os.path.join(self.output_path, "CategoricalArrays")
|
|
222
|
+
metadata_path = os.path.join(self.output_path, "ScalarMetaData.json")
|
|
223
|
+
os.mkdir(scalar_array_path)
|
|
224
|
+
os.mkdir(categorical_array_path)
|
|
225
|
+
scalars, categoricals, scalars_cross, categoricals_cross = {}, {}, {}, {}
|
|
226
|
+
arms = set()
|
|
227
|
+
try:
|
|
228
|
+
response = jinko.make_request(
|
|
229
|
+
path=f"/core/v2/result_manager/calibration/scalar_result",
|
|
230
|
+
method="POST",
|
|
231
|
+
json={
|
|
232
|
+
"calibId": {
|
|
233
|
+
"coreItemId": self.core_id["id"],
|
|
234
|
+
"snapshotId": self.core_id["snapshotId"],
|
|
235
|
+
},
|
|
236
|
+
"patientId": patient_id,
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
response = response.json()
|
|
240
|
+
for (
|
|
241
|
+
response_subtype,
|
|
242
|
+
multi_field,
|
|
243
|
+
array_path,
|
|
244
|
+
metadata_dict,
|
|
245
|
+
cross_metadata_dict,
|
|
246
|
+
has_unit,
|
|
247
|
+
) in zip(
|
|
248
|
+
["outputs", "outputsCategorical"],
|
|
249
|
+
["scalarValues", "categoricalLevels"],
|
|
250
|
+
[scalar_array_path, categorical_array_path],
|
|
251
|
+
[scalars, categoricals],
|
|
252
|
+
[scalars_cross, categoricals_cross],
|
|
253
|
+
[True, False],
|
|
254
|
+
):
|
|
255
|
+
for arm_item in response[response_subtype]:
|
|
256
|
+
arm_name = arm_item["group"][0]["contents"]
|
|
257
|
+
is_cross = arm_name == "crossArms"
|
|
258
|
+
if not is_cross:
|
|
259
|
+
arms.add(arm_name)
|
|
260
|
+
assert (
|
|
261
|
+
arm_item["indexes"]["patientNumber"] == patient_id
|
|
262
|
+
), "Something wrong happened (patient number mismatch between requests)!"
|
|
263
|
+
scalar_array = []
|
|
264
|
+
for one_scalar in arm_item["res"]:
|
|
265
|
+
# fetch scalar metadata
|
|
266
|
+
scalar_id = one_scalar["id"]
|
|
267
|
+
scalar_unit = (
|
|
268
|
+
one_scalar["unit"] if "unit" in one_scalar else None
|
|
269
|
+
)
|
|
270
|
+
if is_cross:
|
|
271
|
+
if scalar_id not in cross_metadata_dict:
|
|
272
|
+
cross_metadata_dict[scalar_id] = {
|
|
273
|
+
"description": None,
|
|
274
|
+
"id": scalar_id,
|
|
275
|
+
"type": one_scalar["type"],
|
|
276
|
+
}
|
|
277
|
+
if has_unit:
|
|
278
|
+
cross_metadata_dict[scalar_id]["unit"] = scalar_unit
|
|
279
|
+
else:
|
|
280
|
+
if scalar_id not in metadata_dict:
|
|
281
|
+
metadata_dict[scalar_id] = {
|
|
282
|
+
"arms": set(),
|
|
283
|
+
"description": None,
|
|
284
|
+
"id": scalar_id,
|
|
285
|
+
"type": one_scalar["type"],
|
|
286
|
+
}
|
|
287
|
+
if has_unit:
|
|
288
|
+
metadata_dict[scalar_id]["unit"] = scalar_unit
|
|
289
|
+
metadata_dict[scalar_id]["arms"].add(arm_name)
|
|
290
|
+
|
|
291
|
+
# turn the scalar array record into multi-patient format
|
|
292
|
+
one_scalar[multi_field] = []
|
|
293
|
+
one_scalar["errors"] = []
|
|
294
|
+
if "value" in one_scalar:
|
|
295
|
+
one_scalar[multi_field] = [one_scalar["value"]]
|
|
296
|
+
del one_scalar["value"]
|
|
297
|
+
if "error" in one_scalar:
|
|
298
|
+
one_scalar["errors"] = [one_scalar["error"]]
|
|
299
|
+
del one_scalar["error"]
|
|
300
|
+
scalar_array.append(one_scalar)
|
|
301
|
+
|
|
302
|
+
# save the scalar array
|
|
303
|
+
result_path = os.path.join(array_path, f"{arm_name}.json")
|
|
304
|
+
json.dump(scalar_array, open(result_path, "w", encoding="utf-8"))
|
|
305
|
+
|
|
306
|
+
except (requests.exceptions.HTTPError, TypeError, KeyError):
|
|
307
|
+
print("Error: failed to download the scalar results.")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# save the scalar metadata
|
|
311
|
+
metadata_json = {"arms": list(arms), "patients": [self.pretty_patient_name]}
|
|
312
|
+
for metadata_dict, metadata_array in zip(
|
|
313
|
+
[scalars, categoricals, scalars_cross, categoricals_cross],
|
|
314
|
+
["scalars", "categoricals", "scalarsCrossArm", "categoricalsCrossArm"],
|
|
315
|
+
):
|
|
316
|
+
metadata_json[metadata_array] = []
|
|
317
|
+
# flatten the metadata dict (scalarID-indexed) into array
|
|
318
|
+
for scalar_id, one_scalar_metadata in metadata_dict.items():
|
|
319
|
+
if "arms" in one_scalar_metadata:
|
|
320
|
+
one_scalar_metadata["arms"] = list(one_scalar_metadata["arms"])
|
|
321
|
+
metadata_json[metadata_array].append(
|
|
322
|
+
{"id": scalar_id} | one_scalar_metadata
|
|
323
|
+
)
|
|
324
|
+
json.dump(metadata_json, open(metadata_path, "w", encoding="utf-8"))
|
|
325
|
+
|
|
326
|
+
arm_count = len(arms)
|
|
327
|
+
print(
|
|
328
|
+
f'Successfully downloaded the scalar results of {arm_count} protocol arm{"s" if arm_count > 1 else ""}.',
|
|
329
|
+
end="\n\n",
|
|
330
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Crabbit app launcher module."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["CrabbitAppLauncher"]
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
import jinko_helpers as jinko
|
|
9
|
+
import crabbit.cli as cli
|
|
10
|
+
from crabbit.utils import bold_text, clear_directory, get_sid_revision_from_url
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrabbitAppLauncher:
|
|
14
|
+
"""Crabbit app launcher, connecting argparse to cli apps (and gui apps in the future)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.mode = ""
|
|
18
|
+
self.url = ""
|
|
19
|
+
self.path = ""
|
|
20
|
+
|
|
21
|
+
def run(self):
|
|
22
|
+
self.path = os.path.abspath(self.path)
|
|
23
|
+
try:
|
|
24
|
+
jinko.initialize()
|
|
25
|
+
except:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if self.mode == "download":
|
|
29
|
+
project_item = self.check_project_item_url()
|
|
30
|
+
if project_item is None:
|
|
31
|
+
return
|
|
32
|
+
crab = cli.CrabbitDownloader(project_item, self.path)
|
|
33
|
+
print(
|
|
34
|
+
"Downloading jinko project item", self.url, "to", self.path, end="\n\n"
|
|
35
|
+
)
|
|
36
|
+
if clear_directory(self.path):
|
|
37
|
+
crab.run()
|
|
38
|
+
else:
|
|
39
|
+
print(f'The mode "{self.mode}" is still under development!')
|
|
40
|
+
|
|
41
|
+
def check_project_item_url(self):
|
|
42
|
+
"""Get the project item from URL or print a nice error message."""
|
|
43
|
+
message = f'{bold_text("Error:")} {self.url} is not a valid project item URL!'
|
|
44
|
+
sid, revision = get_sid_revision_from_url(self.url)
|
|
45
|
+
if sid is None:
|
|
46
|
+
print(message)
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
project_item = jinko.get_project_item(sid, revision)
|
|
50
|
+
except requests.exceptions.HTTPError:
|
|
51
|
+
print(message)
|
|
52
|
+
return None
|
|
53
|
+
return project_item
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Utility functions used in the crabbit package."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"get_sid_revision_from_url",
|
|
5
|
+
"bold_text",
|
|
6
|
+
"clear_directory",
|
|
7
|
+
"get_calib_status",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
import jinko_helpers as jinko
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_sid_revision_from_url(url):
|
|
19
|
+
"""Return the sid and revision number from a jinko URL."""
|
|
20
|
+
# start with a generic URL match
|
|
21
|
+
pattern = (
|
|
22
|
+
r"^"
|
|
23
|
+
r"((?P<schema>.+?)://)?"
|
|
24
|
+
r"(?P<host>.*?)"
|
|
25
|
+
r"(:(?P<port>\d+?))?"
|
|
26
|
+
r"(/(?P<path>.*?))?"
|
|
27
|
+
r"(?P<query>[?].*?)?"
|
|
28
|
+
r"$"
|
|
29
|
+
)
|
|
30
|
+
regex = re.compile(pattern)
|
|
31
|
+
match = regex.match(url)
|
|
32
|
+
if match is None:
|
|
33
|
+
return None, None
|
|
34
|
+
# sid should directly follow the base URL
|
|
35
|
+
path = match.groupdict()["path"]
|
|
36
|
+
if not path or "/" in path:
|
|
37
|
+
return None, None
|
|
38
|
+
sid = path
|
|
39
|
+
# revision number should be an integer
|
|
40
|
+
query = match.groupdict()["query"]
|
|
41
|
+
try:
|
|
42
|
+
revision = int(query.split("revision=")[1]) if query is not None else None
|
|
43
|
+
except ValueError:
|
|
44
|
+
revision = None
|
|
45
|
+
return sid, revision
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bold_text(text):
|
|
49
|
+
"""Return bold text to print in console application."""
|
|
50
|
+
return "\033[1m" + text + "\033[0m"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def clear_directory(directory):
|
|
54
|
+
"""Remove files and folders, so that the directory becomes empty (except for hidden files)."""
|
|
55
|
+
if not os.path.exists(directory):
|
|
56
|
+
print("(The folder does not exist; it will be created.)")
|
|
57
|
+
os.makedirs(directory, exist_ok=True)
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
old_files = []
|
|
61
|
+
old_dirs = []
|
|
62
|
+
try:
|
|
63
|
+
with os.scandir(directory) as it:
|
|
64
|
+
for entry in it:
|
|
65
|
+
if not entry.name.startswith(".") and entry.is_file():
|
|
66
|
+
old_files.append(entry)
|
|
67
|
+
elif entry.is_dir():
|
|
68
|
+
old_dirs.append(entry)
|
|
69
|
+
except NotADirectoryError:
|
|
70
|
+
print('Error: the output path is not a folder')
|
|
71
|
+
return False
|
|
72
|
+
if not old_files and not old_dirs:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
max_tries = 5
|
|
76
|
+
k = 0
|
|
77
|
+
while k < max_tries:
|
|
78
|
+
print(
|
|
79
|
+
"Folder already exists! Do you want to clean it up (existing content will be removed)? (y/n)",
|
|
80
|
+
end=" ",
|
|
81
|
+
)
|
|
82
|
+
answer = input()
|
|
83
|
+
if answer == "n":
|
|
84
|
+
return False
|
|
85
|
+
elif answer == "y":
|
|
86
|
+
try:
|
|
87
|
+
for entry in old_files:
|
|
88
|
+
os.remove(entry)
|
|
89
|
+
for entry in old_dirs:
|
|
90
|
+
shutil.rmtree(entry)
|
|
91
|
+
except:
|
|
92
|
+
print(
|
|
93
|
+
"Something wrong happened when cleaning the folder (maybe some files are locked by other application?)!"
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
k += 1
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|