pybiolib 0.2.951__py3-none-any.whl → 1.2.1890__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.
- biolib/__init__.py +357 -11
- biolib/_data_record/data_record.py +380 -0
- biolib/_index/__init__.py +0 -0
- biolib/_index/index.py +55 -0
- biolib/_index/query_result.py +103 -0
- biolib/_internal/__init__.py +0 -0
- biolib/_internal/add_copilot_prompts.py +58 -0
- biolib/_internal/add_gui_files.py +81 -0
- biolib/_internal/data_record/__init__.py +1 -0
- biolib/_internal/data_record/data_record.py +85 -0
- biolib/_internal/data_record/push_data.py +116 -0
- biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
- biolib/_internal/errors.py +5 -0
- biolib/_internal/file_utils.py +125 -0
- biolib/_internal/fuse_mount/__init__.py +1 -0
- biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
- biolib/_internal/http_client.py +159 -0
- biolib/_internal/lfs/__init__.py +1 -0
- biolib/_internal/lfs/cache.py +51 -0
- biolib/_internal/libs/__init__.py +1 -0
- biolib/_internal/libs/fusepy/__init__.py +1257 -0
- biolib/_internal/push_application.py +488 -0
- biolib/_internal/runtime.py +22 -0
- biolib/_internal/string_utils.py +13 -0
- biolib/_internal/templates/__init__.py +1 -0
- biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
- biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
- biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
- biolib/_internal/templates/gitignore_template/.gitignore +10 -0
- biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
- biolib/_internal/templates/gui_template/App.tsx +53 -0
- biolib/_internal/templates/gui_template/Dockerfile +27 -0
- biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
- biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
- biolib/_internal/templates/gui_template/index.css +5 -0
- biolib/_internal/templates/gui_template/index.html +13 -0
- biolib/_internal/templates/gui_template/index.tsx +10 -0
- biolib/_internal/templates/gui_template/package.json +27 -0
- biolib/_internal/templates/gui_template/tsconfig.json +24 -0
- biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
- biolib/_internal/templates/gui_template/vite.config.mts +10 -0
- biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
- biolib/_internal/templates/init_template/Dockerfile +14 -0
- biolib/_internal/templates/init_template/requirements.txt +1 -0
- biolib/_internal/templates/init_template/run.py +12 -0
- biolib/_internal/templates/init_template/run.sh +4 -0
- biolib/_internal/templates/templates.py +25 -0
- biolib/_internal/tree_utils.py +106 -0
- biolib/_internal/utils/__init__.py +65 -0
- biolib/_internal/utils/auth.py +46 -0
- biolib/_internal/utils/job_url.py +33 -0
- biolib/_internal/utils/multinode.py +263 -0
- biolib/_runtime/runtime.py +157 -0
- biolib/_session/session.py +44 -0
- biolib/_shared/__init__.py +0 -0
- biolib/_shared/types/__init__.py +74 -0
- biolib/_shared/types/account.py +12 -0
- biolib/_shared/types/account_member.py +8 -0
- biolib/_shared/types/app.py +9 -0
- biolib/_shared/types/data_record.py +40 -0
- biolib/_shared/types/experiment.py +32 -0
- biolib/_shared/types/file_node.py +17 -0
- biolib/_shared/types/push.py +6 -0
- biolib/_shared/types/resource.py +37 -0
- biolib/_shared/types/resource_deploy_key.py +11 -0
- biolib/_shared/types/resource_permission.py +14 -0
- biolib/_shared/types/resource_version.py +19 -0
- biolib/_shared/types/result.py +14 -0
- biolib/_shared/types/typing.py +10 -0
- biolib/_shared/types/user.py +19 -0
- biolib/_shared/utils/__init__.py +7 -0
- biolib/_shared/utils/resource_uri.py +75 -0
- biolib/api/__init__.py +6 -0
- biolib/api/client.py +168 -0
- biolib/app/app.py +252 -49
- biolib/app/search_apps.py +45 -0
- biolib/biolib_api_client/api_client.py +126 -31
- biolib/biolib_api_client/app_types.py +24 -4
- biolib/biolib_api_client/auth.py +31 -8
- biolib/biolib_api_client/biolib_app_api.py +147 -52
- biolib/biolib_api_client/biolib_job_api.py +161 -141
- biolib/biolib_api_client/job_types.py +21 -5
- biolib/biolib_api_client/lfs_types.py +7 -23
- biolib/biolib_api_client/user_state.py +56 -0
- biolib/biolib_binary_format/__init__.py +1 -4
- biolib/biolib_binary_format/file_in_container.py +105 -0
- biolib/biolib_binary_format/module_input.py +24 -7
- biolib/biolib_binary_format/module_output_v2.py +149 -0
- biolib/biolib_binary_format/remote_endpoints.py +34 -0
- biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
- biolib/biolib_binary_format/saved_job.py +3 -2
- biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
- biolib/biolib_binary_format/system_status_update.py +3 -2
- biolib/biolib_binary_format/utils.py +175 -0
- biolib/biolib_docker_client/__init__.py +11 -2
- biolib/biolib_errors.py +36 -0
- biolib/biolib_logging.py +27 -10
- biolib/cli/__init__.py +38 -0
- biolib/cli/auth.py +46 -0
- biolib/cli/data_record.py +164 -0
- biolib/cli/index.py +32 -0
- biolib/cli/init.py +421 -0
- biolib/cli/lfs.py +101 -0
- biolib/cli/push.py +50 -0
- biolib/cli/run.py +63 -0
- biolib/cli/runtime.py +14 -0
- biolib/cli/sdk.py +16 -0
- biolib/cli/start.py +56 -0
- biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
- biolib/compute_node/job_worker/cache_state.py +66 -88
- biolib/compute_node/job_worker/cache_types.py +1 -6
- biolib/compute_node/job_worker/docker_image_cache.py +112 -37
- biolib/compute_node/job_worker/executors/__init__.py +0 -3
- biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
- biolib/compute_node/job_worker/executors/docker_types.py +9 -1
- biolib/compute_node/job_worker/executors/types.py +19 -9
- biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
- biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
- biolib/compute_node/job_worker/job_storage.py +108 -0
- biolib/compute_node/job_worker/job_worker.py +397 -212
- biolib/compute_node/job_worker/large_file_system.py +87 -38
- biolib/compute_node/job_worker/network_alloc.py +99 -0
- biolib/compute_node/job_worker/network_buffer.py +240 -0
- biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
- biolib/compute_node/job_worker/utils.py +9 -24
- biolib/compute_node/remote_host_proxy.py +400 -98
- biolib/compute_node/utils.py +31 -9
- biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
- biolib/compute_node/webserver/proxy_utils.py +28 -0
- biolib/compute_node/webserver/webserver.py +130 -44
- biolib/compute_node/webserver/webserver_types.py +2 -6
- biolib/compute_node/webserver/webserver_utils.py +77 -12
- biolib/compute_node/webserver/worker_thread.py +183 -42
- biolib/experiments/__init__.py +0 -0
- biolib/experiments/experiment.py +356 -0
- biolib/jobs/__init__.py +1 -0
- biolib/jobs/job.py +741 -0
- biolib/jobs/job_result.py +185 -0
- biolib/jobs/types.py +50 -0
- biolib/py.typed +0 -0
- biolib/runtime/__init__.py +14 -0
- biolib/sdk/__init__.py +91 -0
- biolib/tables.py +34 -0
- biolib/typing_utils.py +2 -7
- biolib/user/__init__.py +1 -0
- biolib/user/sign_in.py +54 -0
- biolib/utils/__init__.py +162 -0
- biolib/utils/cache_state.py +94 -0
- biolib/utils/multipart_uploader.py +194 -0
- biolib/utils/seq_util.py +150 -0
- biolib/utils/zip/remote_zip.py +640 -0
- pybiolib-1.2.1890.dist-info/METADATA +41 -0
- pybiolib-1.2.1890.dist-info/RECORD +177 -0
- {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
- pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
- README.md +0 -17
- biolib/app/app_result.py +0 -68
- biolib/app/utils.py +0 -62
- biolib/biolib-js/0-biolib.worker.js +0 -1
- biolib/biolib-js/1-biolib.worker.js +0 -1
- biolib/biolib-js/2-biolib.worker.js +0 -1
- biolib/biolib-js/3-biolib.worker.js +0 -1
- biolib/biolib-js/4-biolib.worker.js +0 -1
- biolib/biolib-js/5-biolib.worker.js +0 -1
- biolib/biolib-js/6-biolib.worker.js +0 -1
- biolib/biolib-js/index.html +0 -10
- biolib/biolib-js/main-biolib.js +0 -1
- biolib/biolib_api_client/biolib_account_api.py +0 -21
- biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
- biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
- biolib/biolib_binary_format/module_output.py +0 -58
- biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
- biolib/biolib_push.py +0 -114
- biolib/cli.py +0 -203
- biolib/cli_utils.py +0 -273
- biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
- biolib/compute_node/enclave/__init__.py +0 -2
- biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
- biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
- biolib/compute_node/job_worker/executors/base_executor.py +0 -18
- biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
- biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
- biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
- biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
- biolib/lfs.py +0 -196
- biolib/pyppeteer/.circleci/config.yml +0 -100
- biolib/pyppeteer/.coveragerc +0 -3
- biolib/pyppeteer/.gitignore +0 -89
- biolib/pyppeteer/.pre-commit-config.yaml +0 -28
- biolib/pyppeteer/CHANGES.md +0 -253
- biolib/pyppeteer/CONTRIBUTING.md +0 -26
- biolib/pyppeteer/LICENSE +0 -12
- biolib/pyppeteer/README.md +0 -137
- biolib/pyppeteer/docs/Makefile +0 -177
- biolib/pyppeteer/docs/_static/custom.css +0 -28
- biolib/pyppeteer/docs/_templates/layout.html +0 -10
- biolib/pyppeteer/docs/changes.md +0 -1
- biolib/pyppeteer/docs/conf.py +0 -299
- biolib/pyppeteer/docs/index.md +0 -21
- biolib/pyppeteer/docs/make.bat +0 -242
- biolib/pyppeteer/docs/reference.md +0 -211
- biolib/pyppeteer/docs/server.py +0 -60
- biolib/pyppeteer/poetry.lock +0 -1699
- biolib/pyppeteer/pyppeteer/__init__.py +0 -135
- biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
- biolib/pyppeteer/pyppeteer/browser.py +0 -401
- biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
- biolib/pyppeteer/pyppeteer/command.py +0 -22
- biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
- biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
- biolib/pyppeteer/pyppeteer/coverage.py +0 -346
- biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
- biolib/pyppeteer/pyppeteer/dialog.py +0 -79
- biolib/pyppeteer/pyppeteer/domworld.py +0 -597
- biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
- biolib/pyppeteer/pyppeteer/errors.py +0 -48
- biolib/pyppeteer/pyppeteer/events.py +0 -63
- biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
- biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
- biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
- biolib/pyppeteer/pyppeteer/helpers.py +0 -245
- biolib/pyppeteer/pyppeteer/input.py +0 -371
- biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
- biolib/pyppeteer/pyppeteer/launcher.py +0 -683
- biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
- biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
- biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
- biolib/pyppeteer/pyppeteer/multimap.py +0 -82
- biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
- biolib/pyppeteer/pyppeteer/options.py +0 -8
- biolib/pyppeteer/pyppeteer/page.py +0 -1728
- biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
- biolib/pyppeteer/pyppeteer/target.py +0 -147
- biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
- biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
- biolib/pyppeteer/pyppeteer/tracing.py +0 -93
- biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
- biolib/pyppeteer/pyppeteer/util.py +0 -18
- biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
- biolib/pyppeteer/pyppeteer/worker.py +0 -101
- biolib/pyppeteer/pyproject.toml +0 -97
- biolib/pyppeteer/spell.txt +0 -137
- biolib/pyppeteer/tox.ini +0 -72
- biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
- biolib/start_cli.py +0 -7
- biolib/utils.py +0 -47
- biolib/validators/validate_app_version.py +0 -183
- biolib/validators/validate_argument.py +0 -134
- biolib/validators/validate_module.py +0 -323
- biolib/validators/validate_zip_file.py +0 -40
- biolib/validators/validator_utils.py +0 -103
- pybiolib-0.2.951.dist-info/LICENSE +0 -21
- pybiolib-0.2.951.dist-info/METADATA +0 -61
- pybiolib-0.2.951.dist-info/RECORD +0 -153
- pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
- /LICENSE → /pybiolib-1.2.1890.dist-info/licenses/LICENSE +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import math
|
|
3
|
+
import multiprocessing
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from multiprocessing.pool import ThreadPool
|
|
6
|
+
from typing import Callable, Optional
|
|
7
|
+
|
|
8
|
+
from biolib._internal.http_client import HttpClient
|
|
9
|
+
from biolib.typing_utils import Iterator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteEndpoint(ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def get_remote_url(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IndexableBuffer(ABC):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.pointer = 0
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def get_data(self, start: int, length: int) -> bytes:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def get_data_as_string(self, start: int, length: int) -> str:
|
|
27
|
+
return self.get_data(start=start, length=length).decode()
|
|
28
|
+
|
|
29
|
+
def get_data_as_int(self, start: int, length: int) -> int:
|
|
30
|
+
return int.from_bytes(bytes=self.get_data(start=start, length=length), byteorder='big')
|
|
31
|
+
|
|
32
|
+
def get_data_with_pointer(self, length: int) -> bytes:
|
|
33
|
+
data = self.get_data(start=self.pointer, length=length)
|
|
34
|
+
self.pointer += length
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
def get_data_with_pointer_as_int(self, length: int) -> int:
|
|
38
|
+
data = self.get_data_as_int(start=self.pointer, length=length)
|
|
39
|
+
self.pointer += length
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
def get_data_with_pointer_as_string(self, length: int) -> str:
|
|
43
|
+
data = self.get_data_as_string(start=self.pointer, length=length)
|
|
44
|
+
self.pointer += length
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LocalFileIndexableBuffer(IndexableBuffer):
|
|
49
|
+
def __init__(self, filename: str):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self._filehandle = open(filename, 'rb')
|
|
52
|
+
|
|
53
|
+
def get_data(self, start: int, length: int) -> bytes:
|
|
54
|
+
if length < 0:
|
|
55
|
+
raise Exception('get_data length must be positive')
|
|
56
|
+
|
|
57
|
+
if length == 0:
|
|
58
|
+
return bytes(0)
|
|
59
|
+
|
|
60
|
+
self._filehandle.seek(start)
|
|
61
|
+
data: bytes = self._filehandle.read(length)
|
|
62
|
+
|
|
63
|
+
if len(data) != length:
|
|
64
|
+
raise Exception(f'get_data got response of unexpected length. Got {len(data)} expected {length}.')
|
|
65
|
+
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RemoteIndexableBuffer(IndexableBuffer):
|
|
70
|
+
def __init__(self, endpoint: RemoteEndpoint):
|
|
71
|
+
super().__init__()
|
|
72
|
+
self._endpoint = endpoint
|
|
73
|
+
|
|
74
|
+
def get_data(self, start: int, length: int) -> bytes:
|
|
75
|
+
if length < 0:
|
|
76
|
+
raise Exception('get_data length must be positive')
|
|
77
|
+
|
|
78
|
+
if length == 0:
|
|
79
|
+
return bytes(0)
|
|
80
|
+
|
|
81
|
+
end = start + length - 1
|
|
82
|
+
response = HttpClient.request(
|
|
83
|
+
url=self._endpoint.get_remote_url(),
|
|
84
|
+
headers={'range': f'bytes={start}-{end}'},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
data: bytes = response.content
|
|
88
|
+
if len(data) != length:
|
|
89
|
+
raise Exception(f'get_data got response of unexpected length. Got {len(data)} expected {length}.')
|
|
90
|
+
|
|
91
|
+
return data
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class InMemoryIndexableBuffer(IndexableBuffer):
|
|
95
|
+
def __init__(self, data: bytes):
|
|
96
|
+
super().__init__()
|
|
97
|
+
self._buffer = data
|
|
98
|
+
self._length_bytes = len(data)
|
|
99
|
+
|
|
100
|
+
def get_data(self, start: int, length: int) -> bytes:
|
|
101
|
+
end = start + length
|
|
102
|
+
return self._buffer[start:end]
|
|
103
|
+
|
|
104
|
+
def __len__(self):
|
|
105
|
+
return self._length_bytes
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class LazyLoadedFile:
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
path: str,
|
|
112
|
+
buffer: IndexableBuffer,
|
|
113
|
+
start: Optional[int],
|
|
114
|
+
length: int,
|
|
115
|
+
start_func: Optional[Callable[[], int]] = None,
|
|
116
|
+
):
|
|
117
|
+
self._path = path
|
|
118
|
+
self._buffer = buffer
|
|
119
|
+
self._start = start
|
|
120
|
+
self._start_func = start_func
|
|
121
|
+
self._length = length
|
|
122
|
+
|
|
123
|
+
def __repr__(self) -> str:
|
|
124
|
+
return f'File "{self._path}" with size of {self._length} bytes'
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def path(self) -> str:
|
|
128
|
+
return self._path
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def name(self) -> str:
|
|
132
|
+
return self._path.split('/')[-1]
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def start(self) -> int:
|
|
136
|
+
if self._start is None:
|
|
137
|
+
assert self._start_func is not None, 'No start function or start value'
|
|
138
|
+
self._start = self._start_func()
|
|
139
|
+
|
|
140
|
+
return self._start
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def length(self) -> int:
|
|
144
|
+
return self._length
|
|
145
|
+
|
|
146
|
+
def get_file_handle(self) -> io.BufferedIOBase:
|
|
147
|
+
return io.BytesIO(self.get_data())
|
|
148
|
+
|
|
149
|
+
def get_data(self, start=0, length=None) -> bytes:
|
|
150
|
+
start_offset = start + self.start
|
|
151
|
+
# make sure length doesn't go outside file boundaries
|
|
152
|
+
length_to_end_of_file = max(self._length - start, 0)
|
|
153
|
+
length_to_request = length_to_end_of_file if length is None else min(length, length_to_end_of_file)
|
|
154
|
+
return self._buffer.get_data(start=start_offset, length=length_to_request)
|
|
155
|
+
|
|
156
|
+
def get_data_iterator(self) -> Iterator[bytes]:
|
|
157
|
+
if self._length == 0:
|
|
158
|
+
yield b''
|
|
159
|
+
else:
|
|
160
|
+
chunk_size = 50_000_000
|
|
161
|
+
chunk_count = math.ceil(self._length / chunk_size)
|
|
162
|
+
chunk_indices_iterator = range(chunk_count - 1)
|
|
163
|
+
|
|
164
|
+
def get_chunk(chunk_index: int) -> bytes:
|
|
165
|
+
return self._buffer.get_data(start=self.start + chunk_index * chunk_size, length=chunk_size)
|
|
166
|
+
|
|
167
|
+
if chunk_count > 1:
|
|
168
|
+
with ThreadPool(processes=min(16, chunk_count, multiprocessing.cpu_count() - 1)) as pool:
|
|
169
|
+
yield from pool.imap(func=get_chunk, iterable=chunk_indices_iterator)
|
|
170
|
+
|
|
171
|
+
data_already_yielded = (chunk_count - 1) * chunk_size
|
|
172
|
+
yield self._buffer.get_data(
|
|
173
|
+
start=self.start + data_already_yielded,
|
|
174
|
+
length=self._length - data_already_yielded,
|
|
175
|
+
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
import docker # type: ignore
|
|
2
4
|
|
|
3
5
|
|
|
@@ -8,10 +10,17 @@ class BiolibDockerClient:
|
|
|
8
10
|
def get_docker_client():
|
|
9
11
|
if BiolibDockerClient.docker_client is None:
|
|
10
12
|
try:
|
|
13
|
+
# Fixes: https://github.com/docker/docker-py/issues/2433
|
|
14
|
+
if os.environ.get('DOCKER_CERT_PATH'):
|
|
15
|
+
request_ca_bundle_env = os.environ.pop('REQUESTS_CA_BUNDLE', None)
|
|
16
|
+
|
|
11
17
|
# the final step of docker push can take a long time,
|
|
12
18
|
# so set a long timeout for operations performed by the docker client
|
|
13
|
-
#
|
|
14
|
-
BiolibDockerClient.docker_client = docker.from_env(timeout=
|
|
19
|
+
# ~66 min (4000s) is the maximum supported by AWS load balancers
|
|
20
|
+
BiolibDockerClient.docker_client = docker.from_env(timeout=4000)
|
|
21
|
+
|
|
22
|
+
if os.environ.get('DOCKER_CERT_PATH'):
|
|
23
|
+
os.environ['REQUESTS_CA_BUNDLE'] = request_ca_bundle_env
|
|
15
24
|
# Run a docker command to see if docker engine is running
|
|
16
25
|
BiolibDockerClient.docker_client.info()
|
|
17
26
|
except Exception as exception:
|
biolib/biolib_errors.py
CHANGED
|
@@ -7,6 +7,9 @@ class BioLibError(Exception):
|
|
|
7
7
|
super().__init__(message)
|
|
8
8
|
self.message = message
|
|
9
9
|
|
|
10
|
+
class ValidationError(BioLibError):
|
|
11
|
+
def __init__(self, message: Optional[str] = None):
|
|
12
|
+
super().__init__(message=message or 'Invalid input.')
|
|
10
13
|
|
|
11
14
|
class NotFound(BioLibError):
|
|
12
15
|
|
|
@@ -16,3 +19,36 @@ class NotFound(BioLibError):
|
|
|
16
19
|
|
|
17
20
|
class DockerContainerNotFoundDuringExecutionException(Exception):
|
|
18
21
|
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RetryLimitException(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StorageDownloadFailed(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CloudJobFinishedError(Exception):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class JobResultError(BioLibError):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class JobResultNotFound(JobResultError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class JobResultPermissionError(JobResultError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JobResultNonZeroExitCodeError(JobResultError):
|
|
49
|
+
"""Raised when an app returns a non-zero exit code and check=True."""
|
|
50
|
+
def __init__(self, exit_code: int, message: Optional[str] = None):
|
|
51
|
+
self.exit_code = exit_code
|
|
52
|
+
super().__init__(
|
|
53
|
+
message or f'App returned non-zero exit code: {exit_code}'
|
|
54
|
+
)
|
biolib/biolib_logging.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import sys
|
|
3
2
|
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
_DEFAULT_LOGGER_FORMAT = '%(asctime)s | %(levelname)s : %(message)s'
|
|
4
6
|
|
|
5
7
|
# define global logging format
|
|
6
|
-
logging.basicConfig(format=
|
|
8
|
+
logging.basicConfig(format=_DEFAULT_LOGGER_FORMAT, level=logging.INFO, stream=sys.stdout)
|
|
7
9
|
|
|
8
10
|
# define extra log levels
|
|
9
11
|
TRACE = 5
|
|
@@ -12,7 +14,6 @@ logging.addLevelName(TRACE, 'TRACE')
|
|
|
12
14
|
|
|
13
15
|
# note: Logger classes should never be instantiated directly
|
|
14
16
|
class _BioLibLogger(logging.Logger):
|
|
15
|
-
|
|
16
17
|
def __init__(self, name: str, level=logging.INFO):
|
|
17
18
|
super(_BioLibLogger, self).__init__(name=name, level=level)
|
|
18
19
|
|
|
@@ -25,7 +26,8 @@ class _BioLibLogger(logging.Logger):
|
|
|
25
26
|
|
|
26
27
|
def setLevel(self, level) -> None:
|
|
27
28
|
try:
|
|
28
|
-
|
|
29
|
+
normalized_level = level.upper() if isinstance(level, str) else level
|
|
30
|
+
super(_BioLibLogger, self).setLevel(normalized_level)
|
|
29
31
|
except ValueError:
|
|
30
32
|
raise Exception(f'Unknown log level "{level}"') from None
|
|
31
33
|
|
|
@@ -39,20 +41,35 @@ class _BioLibLogger(logging.Logger):
|
|
|
39
41
|
global_root_logger.setLevel(self.level)
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def _get_biolib_logger_instance() -> _BioLibLogger:
|
|
44
|
+
def _get_biolib_logger_instance(name: str) -> _BioLibLogger:
|
|
43
45
|
# for thread safety use the global lock of logging
|
|
44
|
-
logging.
|
|
46
|
+
logging._lock.acquire() # type: ignore # pylint: disable=protected-access
|
|
47
|
+
|
|
45
48
|
original_logger_class = logging.getLoggerClass()
|
|
46
49
|
try:
|
|
47
50
|
# change logger class temporarily to get instance of _BioLibLogger
|
|
48
51
|
logging.setLoggerClass(_BioLibLogger)
|
|
49
|
-
biolib_logger = logging.getLogger(
|
|
52
|
+
biolib_logger = logging.getLogger(name=name)
|
|
50
53
|
# change the logger class back to original so we do not interfere with other libraries
|
|
51
54
|
logging.setLoggerClass(original_logger_class)
|
|
52
55
|
return biolib_logger # type: ignore
|
|
53
56
|
finally:
|
|
54
|
-
logging.
|
|
57
|
+
logging._lock.release() # type: ignore # pylint: disable=protected-access
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_no_user_data_logger() -> _BioLibLogger:
|
|
61
|
+
_logger_no_user_data = _get_biolib_logger_instance(name='biolib_no_user_data')
|
|
62
|
+
|
|
63
|
+
# TODO: Simplify by refactoring to env BIOLIB_ENVIRONMENT_IS_CLOUD: boolean
|
|
64
|
+
if os.getenv('BIOLIB_CLOUD_ENVIRONMENT', '').lower() == 'non-enclave':
|
|
65
|
+
handler = logging.FileHandler(filename='/biolib/logs/biolib_no_user_data.log')
|
|
66
|
+
formatter = logging.Formatter(_DEFAULT_LOGGER_FORMAT)
|
|
67
|
+
handler.setFormatter(formatter)
|
|
68
|
+
_logger_no_user_data.addHandler(handler)
|
|
69
|
+
|
|
70
|
+
return _logger_no_user_data
|
|
55
71
|
|
|
56
72
|
|
|
57
|
-
# expose
|
|
58
|
-
logger = _get_biolib_logger_instance()
|
|
73
|
+
# expose loggers
|
|
74
|
+
logger = _get_biolib_logger_instance(name='biolib')
|
|
75
|
+
logger_no_user_data = _get_no_user_data_logger()
|
biolib/cli/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from biolib import utils
|
|
7
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
8
|
+
from biolib.cli import auth, data_record, index, init, lfs, push, run, runtime, sdk, start
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.version_option(version=utils.BIOLIB_PACKAGE_VERSION, prog_name='pybiolib')
|
|
12
|
+
@click.group(context_settings=dict(help_option_names=['-h', '--help']))
|
|
13
|
+
def cli() -> None:
|
|
14
|
+
logger_no_user_data.debug(f'pybiolib {utils.BIOLIB_PACKAGE_VERSION}')
|
|
15
|
+
logger_no_user_data.debug(f'Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')
|
|
16
|
+
utils.STREAM_STDOUT = True
|
|
17
|
+
|
|
18
|
+
# set more restrictive default log level for CLI
|
|
19
|
+
logger.configure(default_log_level=logging.WARNING)
|
|
20
|
+
logger_no_user_data.configure(default_log_level=logging.WARNING)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
cli.add_command(auth.login)
|
|
24
|
+
cli.add_command(auth.logout)
|
|
25
|
+
cli.add_command(auth.whoami)
|
|
26
|
+
cli.add_command(init.init)
|
|
27
|
+
cli.add_command(lfs.lfs)
|
|
28
|
+
cli.add_command(push.push)
|
|
29
|
+
cli.add_command(run.run)
|
|
30
|
+
cli.add_command(runtime.runtime)
|
|
31
|
+
cli.add_command(start.start)
|
|
32
|
+
cli.add_command(data_record.data_record)
|
|
33
|
+
cli.add_command(index.index)
|
|
34
|
+
cli.add_command(sdk.sdk)
|
|
35
|
+
|
|
36
|
+
# allow this script to be called without poetry in dev e.g. by an IDE debugger
|
|
37
|
+
if utils.IS_DEV and __name__ == '__main__':
|
|
38
|
+
cli()
|
biolib/cli/auth.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from biolib import api
|
|
7
|
+
from biolib.biolib_api_client.api_client import BiolibApiClient
|
|
8
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
9
|
+
from biolib.user import sign_in, sign_out
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command(help='Login your to BioLib account with web browser')
|
|
13
|
+
@click.option(
|
|
14
|
+
'-w',
|
|
15
|
+
is_flag=True,
|
|
16
|
+
default=False,
|
|
17
|
+
required=False,
|
|
18
|
+
type=bool,
|
|
19
|
+
help='Automatically open the login page in the default web browser',
|
|
20
|
+
)
|
|
21
|
+
def login(w: bool) -> None: # pylint: disable=invalid-name
|
|
22
|
+
logger.configure(default_log_level=logging.INFO)
|
|
23
|
+
logger_no_user_data.configure(default_log_level=logging.INFO)
|
|
24
|
+
sign_in(open_in_default_browser=w)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.command(help='Logout of your BioLib account')
|
|
28
|
+
def logout() -> None:
|
|
29
|
+
logger.configure(default_log_level=logging.INFO)
|
|
30
|
+
logger_no_user_data.configure(default_log_level=logging.INFO)
|
|
31
|
+
sign_out()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command(help='Prints out the full name of the user logged in')
|
|
35
|
+
def whoami() -> None:
|
|
36
|
+
client = BiolibApiClient.get()
|
|
37
|
+
if client.is_signed_in:
|
|
38
|
+
response = api.client.get(path='/users/me/')
|
|
39
|
+
user_dict = response.json()
|
|
40
|
+
email = user_dict['email']
|
|
41
|
+
display_name = user_dict['account']['display_name']
|
|
42
|
+
|
|
43
|
+
print(f'Name: {display_name}\nEmail: {email}\nLogged into: {client.base_url}')
|
|
44
|
+
else:
|
|
45
|
+
print('Not logged in', file=sys.stderr)
|
|
46
|
+
exit(1)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import rich.progress
|
|
9
|
+
|
|
10
|
+
from biolib._data_record.data_record import DataRecord
|
|
11
|
+
from biolib.biolib_api_client import BiolibApiClient
|
|
12
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
13
|
+
from biolib.typing_utils import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(help='Data Records')
|
|
17
|
+
def data_record() -> None:
|
|
18
|
+
logger.configure(default_log_level=logging.INFO)
|
|
19
|
+
logger_no_user_data.configure(default_log_level=logging.INFO)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@data_record.command(help='Create a Data Record')
|
|
23
|
+
@click.argument('uri', required=True)
|
|
24
|
+
@click.option('--data-path', required=True, type=click.Path(exists=True))
|
|
25
|
+
@click.option('--record-type', required=False, type=str, default=None)
|
|
26
|
+
def create(uri: str, data_path: str, record_type: Optional[str]) -> None:
|
|
27
|
+
DataRecord.create(destination=uri, data_path=data_path, record_type=record_type)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@data_record.command(help='Update a Data Record')
|
|
31
|
+
@click.argument('uri', required=True)
|
|
32
|
+
@click.option('--data-path', required=True, type=click.Path(exists=True))
|
|
33
|
+
@click.option('--chunk-size', default=None, required=False, type=click.INT, help='The size of each chunk (In MB)')
|
|
34
|
+
def update(uri: str, data_path: str, chunk_size: Optional[int]) -> None:
|
|
35
|
+
DataRecord.get_by_uri(uri=uri).update(data_path=data_path, chunk_size_in_mb=chunk_size)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@data_record.command(help='Download files from a Data Record')
|
|
39
|
+
@click.argument('uri', required=True)
|
|
40
|
+
@click.option('--file', required=False, type=str)
|
|
41
|
+
@click.option('--path-filter', required=False, type=str, hide_input=True)
|
|
42
|
+
def download(uri: str, file: Optional[str], path_filter: Optional[str]) -> None:
|
|
43
|
+
record = DataRecord.get_by_uri(uri=uri)
|
|
44
|
+
if file is not None:
|
|
45
|
+
try:
|
|
46
|
+
file_obj = [file_obj for file_obj in record.list_files() if file_obj.path == file][0]
|
|
47
|
+
except IndexError:
|
|
48
|
+
raise Exception('File not found in data record') from None
|
|
49
|
+
|
|
50
|
+
assert not os.path.exists(file_obj.name), 'File already exists in current directory'
|
|
51
|
+
with open(file_obj.name, 'wb') as file_handle:
|
|
52
|
+
file_handle.write(file_obj.get_data())
|
|
53
|
+
|
|
54
|
+
else:
|
|
55
|
+
assert not os.path.exists(record.name), f'Directory with name {record.name} already exists in current directory'
|
|
56
|
+
record.save_files(output_dir=record.name, path_filter=path_filter)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@data_record.command(help='Describe a Data Record')
|
|
60
|
+
@click.argument('uri', required=True)
|
|
61
|
+
@click.option('--json', 'output_as_json', is_flag=True, default=False, required=False, help='Format output as JSON')
|
|
62
|
+
def describe(uri: str, output_as_json: bool) -> None:
|
|
63
|
+
BiolibApiClient.assert_is_signed_in(authenticated_action_description='get Data Record description')
|
|
64
|
+
record = DataRecord.get_by_uri(uri)
|
|
65
|
+
files_info: List[Dict] = []
|
|
66
|
+
total_size_in_bytes = 0
|
|
67
|
+
for file in record.list_files():
|
|
68
|
+
files_info.append({'path': file.path, 'size_bytes': file.length})
|
|
69
|
+
total_size_in_bytes += file.length
|
|
70
|
+
|
|
71
|
+
if output_as_json:
|
|
72
|
+
print(
|
|
73
|
+
json.dumps(
|
|
74
|
+
obj={'uri': record.uri, 'size_bytes': total_size_in_bytes, 'files': files_info},
|
|
75
|
+
indent=4,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
print(f'Data Record {record.uri}\ntotal {total_size_in_bytes} bytes\n')
|
|
80
|
+
print('size bytes path')
|
|
81
|
+
for file_info in files_info:
|
|
82
|
+
size_string = str(file_info['size_bytes'])
|
|
83
|
+
leading_space_string = ' ' * (10 - len(size_string))
|
|
84
|
+
print(f"{leading_space_string}{size_string} {file_info['path']}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@data_record.command(help='Delete a Data Record')
|
|
88
|
+
@click.argument('uri', required=True)
|
|
89
|
+
def delete(uri: str) -> None:
|
|
90
|
+
record = DataRecord.get_by_uri(uri=uri)
|
|
91
|
+
|
|
92
|
+
print(f'You are about to delete the data record: {record.uri}')
|
|
93
|
+
print('This action cannot be undone.')
|
|
94
|
+
|
|
95
|
+
confirmation = input(f'To confirm deletion, please type the data record name "{record.name}": ')
|
|
96
|
+
if confirmation != record.name:
|
|
97
|
+
print('Data record name does not match. Deletion cancelled.')
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
record.delete()
|
|
101
|
+
print(f'Data record {record.uri} has been deleted.')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _clone_data_record_with_progress(
|
|
105
|
+
source_record: DataRecord,
|
|
106
|
+
dest_record: DataRecord,
|
|
107
|
+
) -> None:
|
|
108
|
+
# pylint: disable=protected-access
|
|
109
|
+
total_size_in_bytes = source_record._get_zip_size_bytes()
|
|
110
|
+
# pylint: enable=protected-access
|
|
111
|
+
|
|
112
|
+
if total_size_in_bytes == 0:
|
|
113
|
+
logger.info('Source data record has no data to clone')
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if sys.stdout.isatty():
|
|
117
|
+
with rich.progress.Progress(
|
|
118
|
+
rich.progress.TextColumn('[bold blue]{task.description}'),
|
|
119
|
+
rich.progress.BarColumn(),
|
|
120
|
+
rich.progress.TaskProgressColumn(),
|
|
121
|
+
rich.progress.TimeRemainingColumn(),
|
|
122
|
+
rich.progress.TransferSpeedColumn(),
|
|
123
|
+
) as progress:
|
|
124
|
+
task_id = progress.add_task('Cloning data record', total=total_size_in_bytes)
|
|
125
|
+
|
|
126
|
+
def on_progress(bytes_uploaded: int, _total_bytes: int) -> None:
|
|
127
|
+
progress.update(task_id, completed=bytes_uploaded)
|
|
128
|
+
|
|
129
|
+
DataRecord.clone(source=source_record, destination=dest_record, on_progress=on_progress)
|
|
130
|
+
else:
|
|
131
|
+
logger.info(f'Cloning ~{round(total_size_in_bytes / 10**6)}mb of data')
|
|
132
|
+
DataRecord.clone(source=source_record, destination=dest_record)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_or_create_destination_record(destination_uri: str) -> Optional[DataRecord]:
|
|
136
|
+
try:
|
|
137
|
+
return DataRecord.get_by_uri(uri=destination_uri)
|
|
138
|
+
except Exception:
|
|
139
|
+
print(f'Destination data record "{destination_uri}" does not exist.')
|
|
140
|
+
confirmation = input('Would you like to create it? [y/N]: ')
|
|
141
|
+
if confirmation.lower() != 'y':
|
|
142
|
+
print('Clone cancelled.')
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
return DataRecord.create(destination=destination_uri)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@data_record.command(help='Clone a Data Record to another location')
|
|
149
|
+
@click.argument('source_uri', required=True)
|
|
150
|
+
@click.argument('destination_uri', required=True)
|
|
151
|
+
def clone(source_uri: str, destination_uri: str) -> None:
|
|
152
|
+
BiolibApiClient.assert_is_signed_in(authenticated_action_description='clone a Data Record')
|
|
153
|
+
|
|
154
|
+
logger.info(f'Fetching source data record: {source_uri}')
|
|
155
|
+
source_record = DataRecord.get_by_uri(uri=source_uri)
|
|
156
|
+
|
|
157
|
+
logger.info(f'Checking destination data record: {destination_uri}')
|
|
158
|
+
dest_record = _get_or_create_destination_record(destination_uri)
|
|
159
|
+
if dest_record is None:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
logger.info(f'Cloning from {source_record.uri} to {dest_record.uri}...')
|
|
163
|
+
_clone_data_record_with_progress(source_record=source_record, dest_record=dest_record)
|
|
164
|
+
logger.info('Clone completed successfully.')
|
biolib/cli/index.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from biolib._index.index import Index
|
|
8
|
+
from biolib.biolib_errors import BioLibError
|
|
9
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(help='Manage Indexes')
|
|
13
|
+
def index() -> None:
|
|
14
|
+
logger.configure(default_log_level=logging.INFO)
|
|
15
|
+
logger_no_user_data.configure(default_log_level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@index.command(help='Create an Index')
|
|
19
|
+
@click.argument('uri', required=True)
|
|
20
|
+
@click.option('--config-path', required=True, type=click.Path(exists=True), help='Path to JSON config file')
|
|
21
|
+
def create(uri: str, config_path: str) -> None:
|
|
22
|
+
try:
|
|
23
|
+
Index.create_from_config_file(uri=uri, config_path=config_path)
|
|
24
|
+
except json.JSONDecodeError as error:
|
|
25
|
+
print(f'Error: Invalid JSON in config file: {error}', file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
except BioLibError as error:
|
|
28
|
+
print(f'Error creating index: {error.message}', file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
except Exception as error:
|
|
31
|
+
print(f'Error reading config file: {error}', file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|