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.
@@ -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.
@@ -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,5 @@
1
+ """
2
+ crabbit package initialization.
3
+
4
+ This package provides utilities for biomodelers for interacting with the Jinko API.
5
+ """
@@ -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
+