pybiolib 1.1.1747__py3-none-any.whl → 1.1.2193__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 +18 -5
- biolib/_data_record/data_record.py +278 -0
- biolib/_internal/data_record/__init__.py +1 -0
- biolib/_internal/data_record/data_record.py +97 -0
- biolib/_internal/data_record/remote_storage_endpoint.py +38 -0
- biolib/_internal/file_utils.py +77 -0
- biolib/_internal/fuse_mount/__init__.py +1 -0
- biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
- biolib/_internal/http_client.py +42 -23
- biolib/_internal/lfs/__init__.py +1 -0
- biolib/_internal/libs/__init__.py +1 -0
- biolib/_internal/libs/fusepy/__init__.py +1257 -0
- biolib/_internal/push_application.py +22 -37
- biolib/_internal/runtime.py +19 -0
- biolib/_internal/types/__init__.py +4 -0
- biolib/_internal/types/app.py +9 -0
- biolib/_internal/types/data_record.py +40 -0
- biolib/_internal/types/experiment.py +10 -0
- biolib/_internal/types/resource.py +14 -0
- biolib/_internal/types/typing.py +7 -0
- biolib/_internal/utils/__init__.py +18 -0
- biolib/_runtime/runtime.py +80 -0
- biolib/api/__init__.py +1 -0
- biolib/api/client.py +39 -17
- biolib/app/app.py +40 -72
- biolib/app/search_apps.py +8 -12
- biolib/biolib_api_client/api_client.py +22 -10
- biolib/biolib_api_client/app_types.py +2 -1
- biolib/biolib_api_client/biolib_app_api.py +1 -1
- biolib/biolib_api_client/biolib_job_api.py +6 -0
- biolib/biolib_api_client/job_types.py +4 -4
- biolib/biolib_api_client/lfs_types.py +8 -2
- biolib/biolib_binary_format/remote_endpoints.py +12 -10
- biolib/biolib_binary_format/utils.py +41 -4
- biolib/cli/__init__.py +6 -2
- biolib/cli/auth.py +58 -0
- biolib/cli/data_record.py +80 -0
- biolib/cli/download_container.py +3 -1
- biolib/cli/init.py +1 -0
- biolib/cli/lfs.py +45 -11
- biolib/cli/push.py +1 -1
- biolib/cli/run.py +3 -2
- biolib/cli/start.py +1 -0
- biolib/compute_node/cloud_utils/cloud_utils.py +15 -18
- biolib/compute_node/job_worker/cache_state.py +1 -1
- biolib/compute_node/job_worker/executors/docker_executor.py +134 -114
- biolib/compute_node/job_worker/job_storage.py +3 -4
- biolib/compute_node/job_worker/job_worker.py +31 -15
- biolib/compute_node/remote_host_proxy.py +75 -70
- biolib/compute_node/webserver/webserver_types.py +0 -1
- biolib/experiments/experiment.py +75 -44
- biolib/jobs/job.py +125 -47
- biolib/jobs/job_result.py +46 -21
- biolib/jobs/types.py +1 -1
- biolib/runtime/__init__.py +14 -1
- biolib/sdk/__init__.py +29 -5
- biolib/typing_utils.py +2 -7
- biolib/user/sign_in.py +10 -14
- biolib/utils/__init__.py +1 -1
- biolib/utils/app_uri.py +11 -4
- biolib/utils/cache_state.py +2 -2
- biolib/utils/seq_util.py +38 -30
- {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/METADATA +1 -1
- pybiolib-1.1.2193.dist-info/RECORD +123 -0
- {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/WHEEL +1 -1
- biolib/biolib_api_client/biolib_account_api.py +0 -8
- biolib/biolib_api_client/biolib_large_file_system_api.py +0 -34
- biolib/experiments/types.py +0 -9
- biolib/lfs/__init__.py +0 -6
- biolib/lfs/utils.py +0 -237
- biolib/runtime/results.py +0 -20
- pybiolib-1.1.1747.dist-info/RECORD +0 -108
- /biolib/{lfs → _internal/lfs}/cache.py +0 -0
- {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/LICENSE +0 -0
- {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/entry_points.txt +0 -0
@@ -97,7 +97,11 @@ class JobWorker:
|
|
97
97
|
if socket_port:
|
98
98
|
self._connect_to_parent()
|
99
99
|
|
100
|
-
def _handle_exit_gracefully(
|
100
|
+
def _handle_exit_gracefully(
|
101
|
+
self,
|
102
|
+
signum: int,
|
103
|
+
frame: Optional[FrameType], # pylint: disable=unused-argument
|
104
|
+
) -> None:
|
101
105
|
job_id = self._root_job_wrapper["job"]["public_id"] if self._root_job_wrapper else None
|
102
106
|
logger_no_user_data.debug(
|
103
107
|
f'_JobWorker ({job_id}) got exit signal {signal.Signals(signum).name}' # pylint: disable=no-member
|
@@ -129,7 +133,8 @@ class JobWorker:
|
|
129
133
|
).start()
|
130
134
|
|
131
135
|
try:
|
132
|
-
|
136
|
+
module_input_path = os.path.join(self.job_temporary_dir, JobStorage.module_input_file_name)
|
137
|
+
JobStorage.download_module_input(job=job, path=module_input_path)
|
133
138
|
except StorageDownloadFailed:
|
134
139
|
# Expect module input to be handled in a separate ModuleInput package
|
135
140
|
self._legacy_input_wait_timeout_thread = JobLegacyInputWaitTimeout(
|
@@ -143,7 +148,7 @@ class JobWorker:
|
|
143
148
|
raise error
|
144
149
|
|
145
150
|
try:
|
146
|
-
self._run_root_job(
|
151
|
+
self._run_root_job(module_input_path)
|
147
152
|
|
148
153
|
# This error occurs when trying to access the container after the job worker has cleaned it up.
|
149
154
|
# In that case stop the computation.
|
@@ -161,7 +166,9 @@ class JobWorker:
|
|
161
166
|
self._legacy_input_wait_timeout_thread.stop()
|
162
167
|
|
163
168
|
try:
|
164
|
-
self.
|
169
|
+
module_input_path = os.path.join(self.job_temporary_dir, JobStorage.module_input_file_name)
|
170
|
+
open(module_input_path, 'wb').write(package)
|
171
|
+
self._run_root_job(module_input_path)
|
165
172
|
|
166
173
|
# This error occurs when trying to access the container after the job worker has cleaned it up.
|
167
174
|
# In that case stop the computation.
|
@@ -307,7 +314,7 @@ class JobWorker:
|
|
307
314
|
self._public_network,
|
308
315
|
self._internal_network,
|
309
316
|
job_id,
|
310
|
-
ports
|
317
|
+
ports,
|
311
318
|
)
|
312
319
|
remote_host_proxy.start()
|
313
320
|
self._remote_host_proxies.append(remote_host_proxy)
|
@@ -325,15 +332,15 @@ class JobWorker:
|
|
325
332
|
def _run_app_version(
|
326
333
|
self,
|
327
334
|
app_version_id: str,
|
328
|
-
|
335
|
+
module_input_path: str,
|
329
336
|
caller_job: CreatedJobDict,
|
330
337
|
main_module_output_path: str,
|
331
338
|
) -> None:
|
332
339
|
job: CreatedJobDict = BiolibJobApi.create(app_version_id, caller_job=caller_job['public_id'])
|
333
340
|
self._jobs[job['public_id']] = job
|
334
|
-
self._run_job(job,
|
341
|
+
self._run_job(job, module_input_path, main_module_output_path)
|
335
342
|
|
336
|
-
def _run_job(self, job: CreatedJobDict,
|
343
|
+
def _run_job(self, job: CreatedJobDict, module_input_path: str, main_module_output_path: str) -> None:
|
337
344
|
job_uuid = job['public_id']
|
338
345
|
logger_no_user_data.info(f'Job "{job_uuid}" running...')
|
339
346
|
if self._root_job_wrapper is None:
|
@@ -400,7 +407,7 @@ class JobWorker:
|
|
400
407
|
send_system_exception=self.send_system_exception,
|
401
408
|
send_stdout_and_stderr=self.send_stdout_and_stderr,
|
402
409
|
),
|
403
|
-
|
410
|
+
module_input_path,
|
404
411
|
main_module_output_path,
|
405
412
|
)
|
406
413
|
|
@@ -411,15 +418,20 @@ class JobWorker:
|
|
411
418
|
def _run_module(
|
412
419
|
self,
|
413
420
|
options: LocalExecutorOptions,
|
414
|
-
|
421
|
+
module_input_path: str,
|
415
422
|
module_output_path: str,
|
416
423
|
) -> None:
|
417
424
|
module = options['module']
|
418
425
|
job_id = options['job']['public_id']
|
419
426
|
logger_no_user_data.debug(f'Job "{job_id}" running module "{module["name"]}"...')
|
427
|
+
|
420
428
|
executor_instance: DockerExecutor
|
421
429
|
if module['environment'] == ModuleEnvironment.BIOLIB_APP.value:
|
430
|
+
if not self.job_temporary_dir:
|
431
|
+
raise BioLibError('Undefined job_temporary_dir')
|
422
432
|
logger_no_user_data.debug(f'Job "{job_id}" starting child job...')
|
433
|
+
with open(module_input_path,'rb') as fp:
|
434
|
+
module_input_serialized = fp.read()
|
423
435
|
module_input = ModuleInput(module_input_serialized).deserialize()
|
424
436
|
module_input_with_runtime_zip = self._add_runtime_zip_and_command_to_module_input(options, module_input)
|
425
437
|
module_input_with_runtime_zip_serialized = ModuleInput().serialize(
|
@@ -427,9 +439,11 @@ class JobWorker:
|
|
427
439
|
arguments=module_input_with_runtime_zip['arguments'],
|
428
440
|
files=module_input_with_runtime_zip['files'],
|
429
441
|
)
|
442
|
+
module_input_path_new = os.path.join(self.job_temporary_dir, "runtime." + JobStorage.module_input_file_name)
|
443
|
+
open(module_input_path_new, 'wb').write(module_input_with_runtime_zip_serialized)
|
430
444
|
return self._run_app_version(
|
431
445
|
module['image_uri'],
|
432
|
-
|
446
|
+
module_input_path_new,
|
433
447
|
options['job'],
|
434
448
|
module_output_path,
|
435
449
|
)
|
@@ -455,7 +469,7 @@ class JobWorker:
|
|
455
469
|
# Log memory and disk before pulling and executing module
|
456
470
|
log_disk_and_memory_usage_info()
|
457
471
|
|
458
|
-
executor_instance.execute_module(
|
472
|
+
executor_instance.execute_module(module_input_path, module_output_path)
|
459
473
|
|
460
474
|
def _connect_to_parent(self):
|
461
475
|
try:
|
@@ -581,7 +595,7 @@ class JobWorker:
|
|
581
595
|
may_contain_user_data=False
|
582
596
|
) from exception
|
583
597
|
|
584
|
-
def _run_root_job(self,
|
598
|
+
def _run_root_job(self, module_input_path: str) -> str:
|
585
599
|
# Make typechecker happy
|
586
600
|
if not self._root_job_wrapper or not self.job_temporary_dir:
|
587
601
|
raise BioLibError('Undefined job_wrapper or job_temporary_dir')
|
@@ -589,7 +603,7 @@ class JobWorker:
|
|
589
603
|
main_module_output_path = os.path.join(self.job_temporary_dir, JobStorage.module_output_file_name)
|
590
604
|
self._run_job(
|
591
605
|
job=self._root_job_wrapper['job'],
|
592
|
-
|
606
|
+
module_input_path=module_input_path,
|
593
607
|
main_module_output_path=main_module_output_path,
|
594
608
|
)
|
595
609
|
self._send_status_update(StatusUpdate(progress=94, log_message='Computation finished'))
|
@@ -608,7 +622,9 @@ class JobWorker:
|
|
608
622
|
job_temporary_dir=job_temporary_dir,
|
609
623
|
)
|
610
624
|
self._start_network_and_remote_host_proxies(job_dict)
|
611
|
-
|
625
|
+
module_input_path = os.path.join(self.job_temporary_dir, JobStorage.module_input_file_name)
|
626
|
+
open(module_input_path, 'wb').write(module_input_serialized)
|
627
|
+
module_output_path = self._run_root_job(module_input_path)
|
612
628
|
with open(module_output_path, mode='rb') as module_output_file:
|
613
629
|
module_output_serialized = module_output_file.read()
|
614
630
|
return ModuleOutputV2(InMemoryIndexableBuffer(module_output_serialized))
|
@@ -1,21 +1,21 @@
|
|
1
1
|
import io
|
2
|
-
import tarfile
|
3
2
|
import subprocess
|
3
|
+
import tarfile
|
4
4
|
import time
|
5
|
+
from urllib.parse import urlparse
|
5
6
|
|
6
|
-
from docker.models.containers import Container # type: ignore
|
7
7
|
from docker.errors import ImageNotFound # type: ignore
|
8
|
+
from docker.models.containers import Container # type: ignore
|
8
9
|
from docker.models.images import Image # type: ignore
|
9
10
|
from docker.models.networks import Network # type: ignore
|
10
11
|
|
11
12
|
from biolib import utils
|
12
|
-
from biolib.
|
13
|
-
from biolib.compute_node.cloud_utils import CloudUtils
|
14
|
-
from biolib.typing_utils import Optional, List
|
15
|
-
from biolib.biolib_api_client import RemoteHost
|
13
|
+
from biolib.biolib_api_client import BiolibApiClient, RemoteHost
|
16
14
|
from biolib.biolib_docker_client import BiolibDockerClient
|
15
|
+
from biolib.biolib_errors import BioLibError
|
17
16
|
from biolib.biolib_logging import logger_no_user_data
|
18
|
-
from biolib.
|
17
|
+
from biolib.compute_node.cloud_utils import CloudUtils
|
18
|
+
from biolib.typing_utils import List, Optional
|
19
19
|
|
20
20
|
|
21
21
|
# Prepare for remote hosts with specified port
|
@@ -24,29 +24,23 @@ class RemoteHostExtended(RemoteHost):
|
|
24
24
|
|
25
25
|
|
26
26
|
class RemoteHostProxy:
|
27
|
-
|
28
27
|
def __init__(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
self,
|
29
|
+
remote_host: RemoteHost,
|
30
|
+
public_network: Network,
|
31
|
+
internal_network: Optional[Network],
|
32
|
+
job_id: str,
|
33
|
+
ports: List[int],
|
35
34
|
):
|
36
35
|
self.is_app_caller_proxy = remote_host['hostname'] == 'AppCallerProxy'
|
37
|
-
|
38
|
-
# Default to port 443 for now until backend serves remote_hosts with port specified
|
39
|
-
self._remote_host: RemoteHostExtended = RemoteHostExtended(
|
40
|
-
hostname=remote_host['hostname'],
|
41
|
-
ports=ports
|
42
|
-
)
|
36
|
+
self._remote_host: RemoteHostExtended = RemoteHostExtended(hostname=remote_host['hostname'], ports=ports)
|
43
37
|
self._public_network: Network = public_network
|
44
38
|
self._internal_network: Optional[Network] = internal_network
|
45
39
|
|
46
40
|
if not job_id:
|
47
41
|
raise Exception('RemoteHostProxy missing argument "job_id"')
|
48
42
|
|
49
|
-
self._name = f
|
43
|
+
self._name = f'biolib-remote-host-proxy-{job_id}-{self.hostname}'
|
50
44
|
self._job_uuid = job_id
|
51
45
|
self._container: Optional[Container] = None
|
52
46
|
self._enclave_traffic_forwarder_processes: List[subprocess.Popen] = []
|
@@ -152,32 +146,21 @@ class RemoteHostProxy:
|
|
152
146
|
raise Exception('RemoteHostProxy container not defined when attempting to write NGINX config')
|
153
147
|
|
154
148
|
docker = BiolibDockerClient.get_docker_client()
|
155
|
-
|
149
|
+
upstream_hostname = urlparse(BiolibApiClient.get().base_url).hostname
|
156
150
|
if self.is_app_caller_proxy:
|
151
|
+
if not utils.IS_RUNNING_IN_CLOUD:
|
152
|
+
raise BioLibError('Calling apps inside apps is not supported in local compute environment')
|
153
|
+
|
157
154
|
logger_no_user_data.debug(f'Job "{self._job_uuid}" writing config for and starting App Caller Proxy...')
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
if base_url in ('https://biolib.com', 'https://staging-elb.biolib.com'):
|
162
|
-
cloud_base_url = 'https://biolibcloud.com'
|
163
|
-
else:
|
164
|
-
raise BioLibError('Calling apps inside apps is not supported in local compute environment')
|
165
|
-
|
166
|
-
if utils.IS_RUNNING_IN_CLOUD:
|
167
|
-
config = CloudUtils.get_webserver_config()
|
168
|
-
s3_results_bucket_name = config['s3_general_storage_bucket_name'] # pylint: disable=unsubscriptable-object
|
169
|
-
s3_results_base_url = f'https://{s3_results_bucket_name}.s3.amazonaws.com'
|
170
|
-
else:
|
171
|
-
if base_url in ('https://biolib.com', 'https://staging-elb.biolib.com'):
|
172
|
-
s3_results_base_url = 'https://biolib-cloud-api.s3.amazonaws.com'
|
173
|
-
else:
|
174
|
-
raise BioLibError("Calling apps inside apps locally is only supported on biolib.com")
|
155
|
+
config = CloudUtils.get_webserver_config()
|
156
|
+
compute_node_uuid = config['compute_node_info']['public_id']
|
157
|
+
compute_node_auth_token = config['compute_node_info']['auth_token']
|
175
158
|
|
176
159
|
# TODO: Get access_token from new API class instead
|
177
160
|
access_token = BiolibApiClient.get().access_token
|
178
161
|
bearer_token = f'Bearer {access_token}' if access_token else ''
|
179
162
|
|
180
|
-
nginx_config = f
|
163
|
+
nginx_config = f"""
|
181
164
|
events {{
|
182
165
|
worker_connections 1024;
|
183
166
|
}}
|
@@ -194,11 +177,6 @@ http {{
|
|
194
177
|
default "";
|
195
178
|
}}
|
196
179
|
|
197
|
-
map $request_method $bearer_token_on_patch {{
|
198
|
-
PATCH "{bearer_token}";
|
199
|
-
default "";
|
200
|
-
}}
|
201
|
-
|
202
180
|
map $request_method $bearer_token_on_patch_and_get {{
|
203
181
|
PATCH "{bearer_token}";
|
204
182
|
GET "{bearer_token}";
|
@@ -207,10 +185,11 @@ http {{
|
|
207
185
|
|
208
186
|
server {{
|
209
187
|
listen 80;
|
210
|
-
resolver 127.0.0.11 valid=30s;
|
188
|
+
resolver 127.0.0.11 ipv6=off valid=30s;
|
189
|
+
set $upstream_hostname {upstream_hostname};
|
211
190
|
|
212
191
|
location ~* "^/api/jobs/cloud/(?<job_id>[a-z0-9-]{{36}})/status/$" {{
|
213
|
-
proxy_pass
|
192
|
+
proxy_pass https://$upstream_hostname/api/jobs/cloud/$job_id/status/;
|
214
193
|
proxy_set_header authorization $bearer_token_on_get;
|
215
194
|
proxy_set_header cookie "";
|
216
195
|
proxy_ssl_server_name on;
|
@@ -218,35 +197,35 @@ http {{
|
|
218
197
|
|
219
198
|
location ~* "^/api/jobs/cloud/$" {{
|
220
199
|
# Note: Using $1 here as URI part from regex must be used for proxy_pass
|
221
|
-
proxy_pass
|
200
|
+
proxy_pass https://$upstream_hostname/api/jobs/cloud/$1;
|
222
201
|
proxy_set_header authorization $bearer_token_on_post;
|
223
202
|
proxy_set_header cookie "";
|
224
203
|
proxy_ssl_server_name on;
|
225
204
|
}}
|
226
205
|
|
227
206
|
location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/start_upload/$" {{
|
228
|
-
proxy_pass
|
207
|
+
proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/start_upload/;
|
229
208
|
proxy_set_header authorization "";
|
230
209
|
proxy_set_header cookie "";
|
231
210
|
proxy_ssl_server_name on;
|
232
211
|
}}
|
233
212
|
|
234
213
|
location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/presigned_upload_url/$" {{
|
235
|
-
proxy_pass
|
214
|
+
proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/presigned_upload_url/$is_args$args;
|
236
215
|
proxy_set_header authorization "";
|
237
216
|
proxy_set_header cookie "";
|
238
217
|
proxy_ssl_server_name on;
|
239
218
|
}}
|
240
219
|
|
241
220
|
location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/storage/input/complete_upload/$" {{
|
242
|
-
proxy_pass
|
221
|
+
proxy_pass https://$upstream_hostname/api/jobs/$job_id/storage/input/complete_upload/;
|
243
222
|
proxy_set_header authorization "";
|
244
223
|
proxy_set_header cookie "";
|
245
224
|
proxy_ssl_server_name on;
|
246
225
|
}}
|
247
226
|
|
248
227
|
location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/main_result/$" {{
|
249
|
-
proxy_pass
|
228
|
+
proxy_pass https://$upstream_hostname/api/jobs/$job_id/main_result/;
|
250
229
|
proxy_set_header authorization "";
|
251
230
|
proxy_set_header cookie "";
|
252
231
|
proxy_pass_request_headers on;
|
@@ -254,7 +233,7 @@ http {{
|
|
254
233
|
}}
|
255
234
|
|
256
235
|
location ~* "^/api/jobs/(?<job_id>[a-z0-9-]{{36}})/$" {{
|
257
|
-
proxy_pass
|
236
|
+
proxy_pass https://$upstream_hostname/api/jobs/$job_id/;
|
258
237
|
proxy_set_header authorization $bearer_token_on_patch_and_get;
|
259
238
|
proxy_set_header caller-job-uuid "{self._job_uuid}";
|
260
239
|
proxy_set_header cookie "";
|
@@ -263,7 +242,7 @@ http {{
|
|
263
242
|
|
264
243
|
location ~* "^/api/jobs/create_job_with_data/$" {{
|
265
244
|
# Note: Using $1 here as URI part from regex must be used for proxy_pass
|
266
|
-
proxy_pass
|
245
|
+
proxy_pass https://$upstream_hostname/api/jobs/create_job_with_data/$1;
|
267
246
|
proxy_set_header authorization $bearer_token_on_post;
|
268
247
|
proxy_set_header caller-job-uuid "{self._job_uuid}";
|
269
248
|
proxy_set_header cookie "";
|
@@ -272,58 +251,84 @@ http {{
|
|
272
251
|
|
273
252
|
location ~* "^/api/jobs/$" {{
|
274
253
|
# Note: Using $1 here as URI part from regex must be used for proxy_pass
|
275
|
-
proxy_pass
|
254
|
+
proxy_pass https://$upstream_hostname/api/jobs/$1;
|
276
255
|
proxy_set_header authorization $bearer_token_on_post;
|
277
256
|
proxy_set_header caller-job-uuid "{self._job_uuid}";
|
278
257
|
proxy_set_header cookie "";
|
279
258
|
proxy_ssl_server_name on;
|
280
259
|
}}
|
281
260
|
|
282
|
-
location
|
283
|
-
proxy_pass
|
261
|
+
location ~ "^/api/jobs/{self._job_uuid}/notes/$" {{
|
262
|
+
# Note: Using $1 here as URI part from regex must be used for proxy_pass
|
263
|
+
proxy_pass https://$upstream_hostname/api/jobs/{self._job_uuid}/notes/$1;
|
264
|
+
proxy_set_header authorization "";
|
265
|
+
proxy_set_header job-auth-token "";
|
266
|
+
proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
|
267
|
+
proxy_set_header compute-node-uuid "{compute_node_uuid}";
|
268
|
+
proxy_set_header cookie "";
|
269
|
+
proxy_ssl_server_name on;
|
270
|
+
}}
|
271
|
+
|
272
|
+
location /api/lfs/ {{
|
273
|
+
proxy_pass https://$upstream_hostname/api/lfs/;
|
284
274
|
proxy_set_header authorization "";
|
275
|
+
proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
|
276
|
+
proxy_set_header job-uuid "{self._job_uuid}";
|
285
277
|
proxy_set_header cookie "";
|
286
278
|
proxy_ssl_server_name on;
|
287
279
|
}}
|
288
280
|
|
289
|
-
location /
|
290
|
-
proxy_pass
|
281
|
+
location /api/app/ {{
|
282
|
+
proxy_pass https://$upstream_hostname/api/app/;
|
291
283
|
proxy_set_header authorization "";
|
284
|
+
proxy_set_header compute-node-auth-token "{compute_node_auth_token}";
|
285
|
+
proxy_set_header job-uuid "{self._job_uuid}";
|
292
286
|
proxy_set_header cookie "";
|
293
287
|
proxy_ssl_server_name on;
|
294
288
|
}}
|
295
289
|
|
296
|
-
location /
|
297
|
-
proxy_pass
|
290
|
+
location /api/ {{
|
291
|
+
proxy_pass https://$upstream_hostname/api/;
|
298
292
|
proxy_set_header authorization "";
|
299
293
|
proxy_set_header cookie "";
|
300
294
|
proxy_ssl_server_name on;
|
301
295
|
}}
|
302
296
|
|
303
297
|
location /proxy/storage/job-storage/ {{
|
304
|
-
proxy_pass
|
298
|
+
proxy_pass https://$upstream_hostname/proxy/storage/job-storage/;
|
299
|
+
proxy_set_header authorization "";
|
300
|
+
proxy_set_header cookie "";
|
301
|
+
proxy_ssl_server_name on;
|
302
|
+
}}
|
303
|
+
|
304
|
+
location /proxy/storage/lfs/versions/ {{
|
305
|
+
proxy_pass https://$upstream_hostname/proxy/storage/lfs/versions/;
|
305
306
|
proxy_set_header authorization "";
|
306
307
|
proxy_set_header cookie "";
|
307
308
|
proxy_ssl_server_name on;
|
308
309
|
}}
|
309
310
|
|
310
311
|
location /proxy/cloud/ {{
|
311
|
-
proxy_pass
|
312
|
+
proxy_pass https://$upstream_hostname/proxy/cloud/;
|
312
313
|
proxy_set_header authorization "";
|
313
314
|
proxy_set_header cookie "";
|
314
315
|
proxy_ssl_server_name on;
|
315
316
|
}}
|
317
|
+
|
318
|
+
location / {{
|
319
|
+
return 404 "Not found";
|
320
|
+
}}
|
316
321
|
}}
|
317
322
|
}}
|
318
|
-
|
323
|
+
"""
|
319
324
|
else:
|
320
|
-
nginx_config =
|
325
|
+
nginx_config = """
|
321
326
|
events {}
|
322
327
|
error_log /dev/stdout info;
|
323
328
|
stream {
|
324
|
-
resolver 127.0.0.11 valid=30s;
|
329
|
+
resolver 127.0.0.11 valid=30s;"""
|
325
330
|
for idx, upstream_server_port in enumerate(upstream_server_ports):
|
326
|
-
nginx_config += f
|
331
|
+
nginx_config += f"""
|
327
332
|
map "" $upstream_{idx} {{
|
328
333
|
default {upstream_server_name}:{upstream_server_port};
|
329
334
|
}}
|
@@ -336,11 +341,11 @@ stream {
|
|
336
341
|
server {{
|
337
342
|
listen {self._remote_host['ports'][idx]} udp;
|
338
343
|
proxy_pass $upstream_{idx};
|
339
|
-
}}
|
344
|
+
}}"""
|
340
345
|
|
341
|
-
nginx_config +=
|
346
|
+
nginx_config += """
|
342
347
|
}
|
343
|
-
|
348
|
+
"""
|
344
349
|
|
345
350
|
nginx_config_bytes = nginx_config.encode()
|
346
351
|
tarfile_in_memory = io.BytesIO()
|
biolib/experiments/experiment.py
CHANGED
@@ -1,30 +1,29 @@
|
|
1
1
|
import time
|
2
2
|
from collections import OrderedDict
|
3
3
|
|
4
|
-
|
5
|
-
from biolib.jobs.types import JobsPaginatedResponse
|
6
|
-
from biolib.typing_utils import List, Optional
|
7
|
-
|
4
|
+
import biolib._internal.types as _types
|
8
5
|
from biolib import api
|
9
|
-
from biolib.
|
6
|
+
from biolib.biolib_errors import BioLibError
|
10
7
|
from biolib.jobs.job import Job
|
11
|
-
from biolib.
|
12
|
-
|
8
|
+
from biolib.jobs.types import JobsPaginatedResponse
|
13
9
|
from biolib.tables import BioLibTable
|
10
|
+
from biolib.typing_utils import Dict, List, Optional, Union
|
14
11
|
|
15
12
|
|
16
13
|
class Experiment:
|
17
14
|
_BIOLIB_EXPERIMENTS: List['Experiment'] = []
|
18
15
|
|
19
16
|
# Columns to print in table when showing Job
|
20
|
-
_table_columns_to_row_map = OrderedDict(
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
17
|
+
_table_columns_to_row_map = OrderedDict(
|
18
|
+
{
|
19
|
+
'Name': {'key': 'name', 'params': {}},
|
20
|
+
'Job Count': {'key': 'job_count', 'params': {}},
|
21
|
+
'Created At': {'key': 'created_at', 'params': {}},
|
22
|
+
}
|
23
|
+
)
|
25
24
|
|
26
|
-
def __init__(self,
|
27
|
-
self.
|
25
|
+
def __init__(self, uri: str, _resource_dict: Optional[_types.ResourceDict] = None):
|
26
|
+
self._resource_dict: _types.ResourceDict = _resource_dict or self._get_or_create_resource_dict(uri)
|
28
27
|
|
29
28
|
def __enter__(self):
|
30
29
|
Experiment._BIOLIB_EXPERIMENTS.append(self)
|
@@ -33,18 +32,29 @@ class Experiment:
|
|
33
32
|
Experiment._BIOLIB_EXPERIMENTS.pop()
|
34
33
|
|
35
34
|
def __str__(self):
|
36
|
-
return f'Experiment: {self.
|
35
|
+
return f'Experiment: {self.uri}'
|
37
36
|
|
38
37
|
def __repr__(self):
|
39
|
-
return f'Experiment: {self.
|
38
|
+
return f'Experiment: {self.uri}'
|
40
39
|
|
41
40
|
@property
|
42
41
|
def uuid(self) -> str:
|
43
|
-
return self.
|
42
|
+
return self._resource_dict['uuid']
|
44
43
|
|
45
44
|
@property
|
46
45
|
def name(self) -> str:
|
47
|
-
return self.
|
46
|
+
return self._resource_dict['name']
|
47
|
+
|
48
|
+
@property
|
49
|
+
def uri(self) -> str:
|
50
|
+
return self._resource_dict['uri']
|
51
|
+
|
52
|
+
@property
|
53
|
+
def _experiment_dict(self) -> _types.ExperimentSlimDict:
|
54
|
+
if not self._resource_dict['experiment']:
|
55
|
+
raise ValueError(f'Resource {self.uri} is not an Experiment')
|
56
|
+
|
57
|
+
return self._resource_dict['experiment']
|
48
58
|
|
49
59
|
@staticmethod
|
50
60
|
def get_experiment_in_context() -> Optional['Experiment']:
|
@@ -55,32 +65,46 @@ class Experiment:
|
|
55
65
|
# Prints a table listing info about experiments accessible to the user
|
56
66
|
@staticmethod
|
57
67
|
def show_experiments(count: int = 25) -> None:
|
58
|
-
experiment_dicts = api.client.get(
|
59
|
-
path='/experiments/',
|
60
|
-
params={
|
61
|
-
'page_size': str(count)
|
62
|
-
}
|
63
|
-
).json()['results']
|
68
|
+
experiment_dicts = api.client.get(path='/experiments/', params={'page_size': str(count)}).json()['results']
|
64
69
|
BioLibTable(
|
65
70
|
columns_to_row_map=Experiment._table_columns_to_row_map,
|
66
71
|
rows=experiment_dicts,
|
67
|
-
title='Experiments'
|
72
|
+
title='Experiments',
|
68
73
|
).print_table()
|
69
74
|
|
75
|
+
@staticmethod
|
76
|
+
def get_by_uri(uri: str) -> 'Experiment':
|
77
|
+
query_param_key = 'uri' if '/' in uri else 'name'
|
78
|
+
resource_dict: _types.ResourceDict = api.client.get('/resource/', params={query_param_key: uri}).json()
|
79
|
+
if not resource_dict['experiment']:
|
80
|
+
raise ValueError(f'Resource {uri} is not an experiment')
|
81
|
+
|
82
|
+
return Experiment(uri=resource_dict['uri'], _resource_dict=resource_dict)
|
83
|
+
|
70
84
|
def wait(self) -> None:
|
71
|
-
self.
|
85
|
+
self._refetch()
|
72
86
|
while self._experiment_dict['job_running_count'] > 0:
|
73
87
|
print(f"Waiting for {self._experiment_dict['job_running_count']} jobs to finish", end='\r')
|
74
88
|
time.sleep(5)
|
75
|
-
self.
|
89
|
+
self._refetch()
|
76
90
|
|
77
91
|
print(f'All jobs of experiment {self.name} have finished')
|
78
92
|
|
79
93
|
def add_job(self, job_id: str) -> None:
|
80
|
-
api.client.patch(
|
81
|
-
|
82
|
-
|
83
|
-
|
94
|
+
api.client.patch(path=f'/jobs/{job_id}/', data={'experiment_uuid': self.uuid})
|
95
|
+
|
96
|
+
def mount_files(self, mount_path: str) -> None:
|
97
|
+
try:
|
98
|
+
# Only attempt to import FUSE dependencies when strictly necessary
|
99
|
+
from biolib._internal.fuse_mount import ( # pylint: disable=import-outside-toplevel
|
100
|
+
ExperimentFuseMount as _ExperimentFuseMount,
|
101
|
+
)
|
102
|
+
except ImportError as error:
|
103
|
+
raise ImportError(
|
104
|
+
'Failed to import FUSE mounting utils. Please ensure FUSE is installed on your system.'
|
105
|
+
) from error
|
106
|
+
|
107
|
+
_ExperimentFuseMount.mount_experiment(experiment=self, mount_path=mount_path)
|
84
108
|
|
85
109
|
def export_job_list(self, export_format='dicts'):
|
86
110
|
valid_formats = ('dicts', 'dataframe')
|
@@ -98,7 +122,7 @@ class Experiment:
|
|
98
122
|
raise ImportError(
|
99
123
|
'Pandas must be installed to use this method. '
|
100
124
|
'Alternatively, use .get_jobs() to get a list of job objects.'
|
101
|
-
|
125
|
+
) from error
|
102
126
|
|
103
127
|
jobs_df = pd.DataFrame.from_dict(job_dict_list)
|
104
128
|
jobs_df.started_at = pd.to_datetime(jobs_df.started_at)
|
@@ -125,7 +149,7 @@ class Experiment:
|
|
125
149
|
BioLibTable(
|
126
150
|
columns_to_row_map=Job.table_columns_to_row_map,
|
127
151
|
rows=[job._job_dict for job in jobs], # pylint: disable=protected-access
|
128
|
-
title=f'Jobs in experiment: "{self.name}"'
|
152
|
+
title=f'Jobs in experiment: "{self.name}"',
|
129
153
|
).print_table()
|
130
154
|
|
131
155
|
def get_jobs(self, status: Optional[str] = None) -> List[Job]:
|
@@ -147,15 +171,22 @@ class Experiment:
|
|
147
171
|
|
148
172
|
return jobs
|
149
173
|
|
150
|
-
def
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
174
|
+
def rename(self, destination: str) -> None:
|
175
|
+
api.client.patch(f'/resources/{self.uuid}/', data={'uri': destination})
|
176
|
+
self._refetch()
|
177
|
+
|
178
|
+
@staticmethod
|
179
|
+
def _get_resource_dict_by_uuid(uuid: str) -> _types.ResourceDict:
|
180
|
+
resource_dict: _types.ResourceDict = api.client.get(f'/resources/{uuid}/').json()
|
181
|
+
if not resource_dict['experiment']:
|
182
|
+
raise ValueError('Resource from URI is not an experiment')
|
183
|
+
|
184
|
+
return resource_dict
|
185
|
+
|
186
|
+
@staticmethod
|
187
|
+
def _get_or_create_resource_dict(uri: str) -> _types.ResourceDict:
|
188
|
+
response_dict = api.client.post(path='/experiments/', data={'uri' if '/' in uri else 'name': uri}).json()
|
189
|
+
return Experiment._get_resource_dict_by_uuid(uuid=response_dict['uuid'])
|
159
190
|
|
160
|
-
def
|
161
|
-
self.
|
191
|
+
def _refetch(self) -> None:
|
192
|
+
self._resource_dict = self._get_resource_dict_by_uuid(uuid=self._resource_dict['uuid'])
|