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.
Files changed (262) hide show
  1. biolib/__init__.py +357 -11
  2. biolib/_data_record/data_record.py +380 -0
  3. biolib/_index/__init__.py +0 -0
  4. biolib/_index/index.py +55 -0
  5. biolib/_index/query_result.py +103 -0
  6. biolib/_internal/__init__.py +0 -0
  7. biolib/_internal/add_copilot_prompts.py +58 -0
  8. biolib/_internal/add_gui_files.py +81 -0
  9. biolib/_internal/data_record/__init__.py +1 -0
  10. biolib/_internal/data_record/data_record.py +85 -0
  11. biolib/_internal/data_record/push_data.py +116 -0
  12. biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
  13. biolib/_internal/errors.py +5 -0
  14. biolib/_internal/file_utils.py +125 -0
  15. biolib/_internal/fuse_mount/__init__.py +1 -0
  16. biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
  17. biolib/_internal/http_client.py +159 -0
  18. biolib/_internal/lfs/__init__.py +1 -0
  19. biolib/_internal/lfs/cache.py +51 -0
  20. biolib/_internal/libs/__init__.py +1 -0
  21. biolib/_internal/libs/fusepy/__init__.py +1257 -0
  22. biolib/_internal/push_application.py +488 -0
  23. biolib/_internal/runtime.py +22 -0
  24. biolib/_internal/string_utils.py +13 -0
  25. biolib/_internal/templates/__init__.py +1 -0
  26. biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
  27. biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
  28. biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
  29. biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
  30. biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
  31. biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
  32. biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
  33. biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
  34. biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
  35. biolib/_internal/templates/gitignore_template/.gitignore +10 -0
  36. biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
  37. biolib/_internal/templates/gui_template/App.tsx +53 -0
  38. biolib/_internal/templates/gui_template/Dockerfile +27 -0
  39. biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
  40. biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
  41. biolib/_internal/templates/gui_template/index.css +5 -0
  42. biolib/_internal/templates/gui_template/index.html +13 -0
  43. biolib/_internal/templates/gui_template/index.tsx +10 -0
  44. biolib/_internal/templates/gui_template/package.json +27 -0
  45. biolib/_internal/templates/gui_template/tsconfig.json +24 -0
  46. biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
  47. biolib/_internal/templates/gui_template/vite.config.mts +10 -0
  48. biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
  49. biolib/_internal/templates/init_template/Dockerfile +14 -0
  50. biolib/_internal/templates/init_template/requirements.txt +1 -0
  51. biolib/_internal/templates/init_template/run.py +12 -0
  52. biolib/_internal/templates/init_template/run.sh +4 -0
  53. biolib/_internal/templates/templates.py +25 -0
  54. biolib/_internal/tree_utils.py +106 -0
  55. biolib/_internal/utils/__init__.py +65 -0
  56. biolib/_internal/utils/auth.py +46 -0
  57. biolib/_internal/utils/job_url.py +33 -0
  58. biolib/_internal/utils/multinode.py +263 -0
  59. biolib/_runtime/runtime.py +157 -0
  60. biolib/_session/session.py +44 -0
  61. biolib/_shared/__init__.py +0 -0
  62. biolib/_shared/types/__init__.py +74 -0
  63. biolib/_shared/types/account.py +12 -0
  64. biolib/_shared/types/account_member.py +8 -0
  65. biolib/_shared/types/app.py +9 -0
  66. biolib/_shared/types/data_record.py +40 -0
  67. biolib/_shared/types/experiment.py +32 -0
  68. biolib/_shared/types/file_node.py +17 -0
  69. biolib/_shared/types/push.py +6 -0
  70. biolib/_shared/types/resource.py +37 -0
  71. biolib/_shared/types/resource_deploy_key.py +11 -0
  72. biolib/_shared/types/resource_permission.py +14 -0
  73. biolib/_shared/types/resource_version.py +19 -0
  74. biolib/_shared/types/result.py +14 -0
  75. biolib/_shared/types/typing.py +10 -0
  76. biolib/_shared/types/user.py +19 -0
  77. biolib/_shared/utils/__init__.py +7 -0
  78. biolib/_shared/utils/resource_uri.py +75 -0
  79. biolib/api/__init__.py +6 -0
  80. biolib/api/client.py +168 -0
  81. biolib/app/app.py +252 -49
  82. biolib/app/search_apps.py +45 -0
  83. biolib/biolib_api_client/api_client.py +126 -31
  84. biolib/biolib_api_client/app_types.py +24 -4
  85. biolib/biolib_api_client/auth.py +31 -8
  86. biolib/biolib_api_client/biolib_app_api.py +147 -52
  87. biolib/biolib_api_client/biolib_job_api.py +161 -141
  88. biolib/biolib_api_client/job_types.py +21 -5
  89. biolib/biolib_api_client/lfs_types.py +7 -23
  90. biolib/biolib_api_client/user_state.py +56 -0
  91. biolib/biolib_binary_format/__init__.py +1 -4
  92. biolib/biolib_binary_format/file_in_container.py +105 -0
  93. biolib/biolib_binary_format/module_input.py +24 -7
  94. biolib/biolib_binary_format/module_output_v2.py +149 -0
  95. biolib/biolib_binary_format/remote_endpoints.py +34 -0
  96. biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
  97. biolib/biolib_binary_format/saved_job.py +3 -2
  98. biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
  99. biolib/biolib_binary_format/system_status_update.py +3 -2
  100. biolib/biolib_binary_format/utils.py +175 -0
  101. biolib/biolib_docker_client/__init__.py +11 -2
  102. biolib/biolib_errors.py +36 -0
  103. biolib/biolib_logging.py +27 -10
  104. biolib/cli/__init__.py +38 -0
  105. biolib/cli/auth.py +46 -0
  106. biolib/cli/data_record.py +164 -0
  107. biolib/cli/index.py +32 -0
  108. biolib/cli/init.py +421 -0
  109. biolib/cli/lfs.py +101 -0
  110. biolib/cli/push.py +50 -0
  111. biolib/cli/run.py +63 -0
  112. biolib/cli/runtime.py +14 -0
  113. biolib/cli/sdk.py +16 -0
  114. biolib/cli/start.py +56 -0
  115. biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
  116. biolib/compute_node/job_worker/cache_state.py +66 -88
  117. biolib/compute_node/job_worker/cache_types.py +1 -6
  118. biolib/compute_node/job_worker/docker_image_cache.py +112 -37
  119. biolib/compute_node/job_worker/executors/__init__.py +0 -3
  120. biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
  121. biolib/compute_node/job_worker/executors/docker_types.py +9 -1
  122. biolib/compute_node/job_worker/executors/types.py +19 -9
  123. biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
  124. biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
  125. biolib/compute_node/job_worker/job_storage.py +108 -0
  126. biolib/compute_node/job_worker/job_worker.py +397 -212
  127. biolib/compute_node/job_worker/large_file_system.py +87 -38
  128. biolib/compute_node/job_worker/network_alloc.py +99 -0
  129. biolib/compute_node/job_worker/network_buffer.py +240 -0
  130. biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
  131. biolib/compute_node/job_worker/utils.py +9 -24
  132. biolib/compute_node/remote_host_proxy.py +400 -98
  133. biolib/compute_node/utils.py +31 -9
  134. biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
  135. biolib/compute_node/webserver/proxy_utils.py +28 -0
  136. biolib/compute_node/webserver/webserver.py +130 -44
  137. biolib/compute_node/webserver/webserver_types.py +2 -6
  138. biolib/compute_node/webserver/webserver_utils.py +77 -12
  139. biolib/compute_node/webserver/worker_thread.py +183 -42
  140. biolib/experiments/__init__.py +0 -0
  141. biolib/experiments/experiment.py +356 -0
  142. biolib/jobs/__init__.py +1 -0
  143. biolib/jobs/job.py +741 -0
  144. biolib/jobs/job_result.py +185 -0
  145. biolib/jobs/types.py +50 -0
  146. biolib/py.typed +0 -0
  147. biolib/runtime/__init__.py +14 -0
  148. biolib/sdk/__init__.py +91 -0
  149. biolib/tables.py +34 -0
  150. biolib/typing_utils.py +2 -7
  151. biolib/user/__init__.py +1 -0
  152. biolib/user/sign_in.py +54 -0
  153. biolib/utils/__init__.py +162 -0
  154. biolib/utils/cache_state.py +94 -0
  155. biolib/utils/multipart_uploader.py +194 -0
  156. biolib/utils/seq_util.py +150 -0
  157. biolib/utils/zip/remote_zip.py +640 -0
  158. pybiolib-1.2.1890.dist-info/METADATA +41 -0
  159. pybiolib-1.2.1890.dist-info/RECORD +177 -0
  160. {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
  161. pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
  162. README.md +0 -17
  163. biolib/app/app_result.py +0 -68
  164. biolib/app/utils.py +0 -62
  165. biolib/biolib-js/0-biolib.worker.js +0 -1
  166. biolib/biolib-js/1-biolib.worker.js +0 -1
  167. biolib/biolib-js/2-biolib.worker.js +0 -1
  168. biolib/biolib-js/3-biolib.worker.js +0 -1
  169. biolib/biolib-js/4-biolib.worker.js +0 -1
  170. biolib/biolib-js/5-biolib.worker.js +0 -1
  171. biolib/biolib-js/6-biolib.worker.js +0 -1
  172. biolib/biolib-js/index.html +0 -10
  173. biolib/biolib-js/main-biolib.js +0 -1
  174. biolib/biolib_api_client/biolib_account_api.py +0 -21
  175. biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
  176. biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
  177. biolib/biolib_binary_format/module_output.py +0 -58
  178. biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
  179. biolib/biolib_push.py +0 -114
  180. biolib/cli.py +0 -203
  181. biolib/cli_utils.py +0 -273
  182. biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
  183. biolib/compute_node/enclave/__init__.py +0 -2
  184. biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
  185. biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
  186. biolib/compute_node/job_worker/executors/base_executor.py +0 -18
  187. biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
  188. biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
  189. biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
  190. biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
  191. biolib/lfs.py +0 -196
  192. biolib/pyppeteer/.circleci/config.yml +0 -100
  193. biolib/pyppeteer/.coveragerc +0 -3
  194. biolib/pyppeteer/.gitignore +0 -89
  195. biolib/pyppeteer/.pre-commit-config.yaml +0 -28
  196. biolib/pyppeteer/CHANGES.md +0 -253
  197. biolib/pyppeteer/CONTRIBUTING.md +0 -26
  198. biolib/pyppeteer/LICENSE +0 -12
  199. biolib/pyppeteer/README.md +0 -137
  200. biolib/pyppeteer/docs/Makefile +0 -177
  201. biolib/pyppeteer/docs/_static/custom.css +0 -28
  202. biolib/pyppeteer/docs/_templates/layout.html +0 -10
  203. biolib/pyppeteer/docs/changes.md +0 -1
  204. biolib/pyppeteer/docs/conf.py +0 -299
  205. biolib/pyppeteer/docs/index.md +0 -21
  206. biolib/pyppeteer/docs/make.bat +0 -242
  207. biolib/pyppeteer/docs/reference.md +0 -211
  208. biolib/pyppeteer/docs/server.py +0 -60
  209. biolib/pyppeteer/poetry.lock +0 -1699
  210. biolib/pyppeteer/pyppeteer/__init__.py +0 -135
  211. biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
  212. biolib/pyppeteer/pyppeteer/browser.py +0 -401
  213. biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
  214. biolib/pyppeteer/pyppeteer/command.py +0 -22
  215. biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
  216. biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
  217. biolib/pyppeteer/pyppeteer/coverage.py +0 -346
  218. biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
  219. biolib/pyppeteer/pyppeteer/dialog.py +0 -79
  220. biolib/pyppeteer/pyppeteer/domworld.py +0 -597
  221. biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
  222. biolib/pyppeteer/pyppeteer/errors.py +0 -48
  223. biolib/pyppeteer/pyppeteer/events.py +0 -63
  224. biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
  225. biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
  226. biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
  227. biolib/pyppeteer/pyppeteer/helpers.py +0 -245
  228. biolib/pyppeteer/pyppeteer/input.py +0 -371
  229. biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
  230. biolib/pyppeteer/pyppeteer/launcher.py +0 -683
  231. biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
  232. biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
  233. biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
  234. biolib/pyppeteer/pyppeteer/multimap.py +0 -82
  235. biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
  236. biolib/pyppeteer/pyppeteer/options.py +0 -8
  237. biolib/pyppeteer/pyppeteer/page.py +0 -1728
  238. biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
  239. biolib/pyppeteer/pyppeteer/target.py +0 -147
  240. biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
  241. biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
  242. biolib/pyppeteer/pyppeteer/tracing.py +0 -93
  243. biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
  244. biolib/pyppeteer/pyppeteer/util.py +0 -18
  245. biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
  246. biolib/pyppeteer/pyppeteer/worker.py +0 -101
  247. biolib/pyppeteer/pyproject.toml +0 -97
  248. biolib/pyppeteer/spell.txt +0 -137
  249. biolib/pyppeteer/tox.ini +0 -72
  250. biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
  251. biolib/start_cli.py +0 -7
  252. biolib/utils.py +0 -47
  253. biolib/validators/validate_app_version.py +0 -183
  254. biolib/validators/validate_argument.py +0 -134
  255. biolib/validators/validate_module.py +0 -323
  256. biolib/validators/validate_zip_file.py +0 -40
  257. biolib/validators/validator_utils.py +0 -103
  258. pybiolib-0.2.951.dist-info/LICENSE +0 -21
  259. pybiolib-0.2.951.dist-info/METADATA +0 -61
  260. pybiolib-0.2.951.dist-info/RECORD +0 -153
  261. pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
  262. /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
- # 15 min (900s) is the maximum supported by AWS load balancers
14
- BiolibDockerClient.docker_client = docker.from_env(timeout=900)
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='%(asctime)s | %(levelname)s : %(message)s', level=logging.INFO, stream=sys.stdout)
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
- super(_BioLibLogger, self).setLevel(level)
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._acquireLock() # type: ignore # pylint: disable=protected-access
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('biolib')
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._releaseLock() # type: ignore # pylint: disable=protected-access
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 logger
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)