pybiolib 1.2.1056__py3-none-any.whl → 1.2.1642__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.
Potentially problematic release.
This version of pybiolib might be problematic. Click here for more details.
- biolib/__init__.py +33 -10
- biolib/_data_record/data_record.py +24 -11
- biolib/_index/__init__.py +0 -0
- biolib/_index/index.py +51 -0
- biolib/_index/types.py +7 -0
- biolib/_internal/data_record/data_record.py +1 -1
- biolib/_internal/data_record/push_data.py +1 -1
- biolib/_internal/data_record/remote_storage_endpoint.py +3 -3
- biolib/_internal/file_utils.py +7 -4
- biolib/_internal/index/__init__.py +1 -0
- biolib/_internal/index/index.py +18 -0
- biolib/_internal/lfs/cache.py +4 -2
- biolib/_internal/push_application.py +89 -23
- biolib/_internal/runtime.py +2 -0
- biolib/_internal/templates/gui_template/App.tsx +38 -2
- biolib/_internal/templates/gui_template/Dockerfile +2 -0
- biolib/_internal/templates/gui_template/biolib-sdk.ts +37 -0
- biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
- biolib/_internal/templates/gui_template/package.json +1 -0
- biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +49 -0
- biolib/_internal/templates/gui_template/vite.config.mts +2 -1
- biolib/_internal/templates/init_template/.github/workflows/biolib.yml +6 -1
- biolib/_internal/templates/init_template/Dockerfile +2 -0
- biolib/_internal/utils/__init__.py +25 -0
- biolib/_internal/utils/job_url.py +33 -0
- biolib/_runtime/runtime.py +9 -0
- biolib/_session/session.py +7 -5
- biolib/_shared/__init__.py +0 -0
- biolib/_shared/types/__init__.py +69 -0
- biolib/_shared/types/resource.py +17 -0
- biolib/_shared/types/resource_deploy_key.py +11 -0
- biolib/{_internal → _shared}/types/resource_permission.py +1 -1
- biolib/_shared/utils/__init__.py +7 -0
- biolib/_shared/utils/resource_uri.py +75 -0
- biolib/api/client.py +1 -1
- biolib/app/app.py +56 -23
- biolib/biolib_api_client/app_types.py +1 -6
- biolib/biolib_api_client/biolib_app_api.py +17 -0
- biolib/biolib_binary_format/module_input.py +8 -0
- biolib/biolib_binary_format/remote_endpoints.py +3 -3
- biolib/biolib_binary_format/remote_stream_seeker.py +39 -25
- biolib/cli/__init__.py +2 -1
- biolib/cli/data_record.py +17 -0
- biolib/cli/index.py +32 -0
- biolib/cli/lfs.py +1 -1
- biolib/cli/start.py +14 -1
- biolib/compute_node/job_worker/executors/docker_executor.py +31 -9
- biolib/compute_node/job_worker/executors/docker_types.py +1 -1
- biolib/compute_node/job_worker/executors/types.py +6 -5
- biolib/compute_node/job_worker/job_worker.py +149 -93
- biolib/compute_node/job_worker/large_file_system.py +2 -6
- 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 +2 -2
- biolib/compute_node/remote_host_proxy.py +125 -67
- biolib/compute_node/utils.py +2 -0
- biolib/compute_node/webserver/compute_node_results_proxy.py +188 -0
- biolib/compute_node/webserver/proxy_utils.py +28 -0
- biolib/compute_node/webserver/webserver.py +64 -19
- biolib/experiments/experiment.py +98 -16
- biolib/jobs/job.py +119 -29
- biolib/jobs/job_result.py +70 -33
- biolib/jobs/types.py +1 -0
- biolib/sdk/__init__.py +17 -2
- biolib/typing_utils.py +1 -1
- biolib/utils/cache_state.py +2 -2
- biolib/utils/seq_util.py +1 -1
- {pybiolib-1.2.1056.dist-info → pybiolib-1.2.1642.dist-info}/METADATA +4 -2
- {pybiolib-1.2.1056.dist-info → pybiolib-1.2.1642.dist-info}/RECORD +84 -66
- {pybiolib-1.2.1056.dist-info → pybiolib-1.2.1642.dist-info}/WHEEL +1 -1
- biolib/_internal/types/__init__.py +0 -6
- biolib/utils/app_uri.py +0 -57
- /biolib/{_internal → _shared}/types/account.py +0 -0
- /biolib/{_internal → _shared}/types/account_member.py +0 -0
- /biolib/{_internal → _shared}/types/app.py +0 -0
- /biolib/{_internal → _shared}/types/data_record.py +0 -0
- /biolib/{_internal → _shared}/types/experiment.py +0 -0
- /biolib/{_internal → _shared}/types/file_node.py +0 -0
- /biolib/{_internal → _shared}/types/push.py +0 -0
- /biolib/{_internal/types/resource.py → _shared/types/resource_types.py} +0 -0
- /biolib/{_internal → _shared}/types/resource_version.py +0 -0
- /biolib/{_internal → _shared}/types/result.py +0 -0
- /biolib/{_internal → _shared}/types/typing.py +0 -0
- /biolib/{_internal → _shared}/types/user.py +0 -0
- {pybiolib-1.2.1056.dist-info → pybiolib-1.2.1642.dist-info}/entry_points.txt +0 -0
- {pybiolib-1.2.1056.dist-info → pybiolib-1.2.1642.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import tarfile
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from docker.models.containers import Container
|
|
8
|
+
|
|
9
|
+
from biolib.biolib_docker_client import BiolibDockerClient
|
|
10
|
+
from biolib.biolib_errors import BioLibError
|
|
11
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
12
|
+
from biolib.compute_node.utils import BIOLIB_PROXY_NETWORK_NAME
|
|
13
|
+
from biolib.compute_node.webserver.proxy_utils import get_biolib_nginx_proxy_image
|
|
14
|
+
from biolib.typing_utils import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LogStreamingThread(threading.Thread):
|
|
18
|
+
def __init__(self, container, container_name: str):
|
|
19
|
+
super().__init__(daemon=True)
|
|
20
|
+
self._container = container
|
|
21
|
+
self._container_name = container_name
|
|
22
|
+
self._stop_event = threading.Event()
|
|
23
|
+
|
|
24
|
+
def run(self) -> None:
|
|
25
|
+
try:
|
|
26
|
+
logger_no_user_data.debug(f'Starting log streaming for container "{self._container_name}"')
|
|
27
|
+
log_stream = self._container.logs(follow=True, stream=True)
|
|
28
|
+
for log_line in log_stream:
|
|
29
|
+
if self._stop_event.is_set():
|
|
30
|
+
break
|
|
31
|
+
if log_line:
|
|
32
|
+
logger.debug(f'ComputeNodeResultsProxy | {log_line.decode("utf-8", errors="replace").rstrip()}')
|
|
33
|
+
except Exception as error:
|
|
34
|
+
logger_no_user_data.debug(f'Log streaming for container "{self._container_name}" ended: {error}')
|
|
35
|
+
|
|
36
|
+
def stop(self) -> None:
|
|
37
|
+
self._stop_event.set()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ComputeNodeResultsProxy:
|
|
41
|
+
_instance: Optional['ComputeNodeResultsProxy'] = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, tls_pem_certificate_path: str, tls_pem_key_path: str):
|
|
44
|
+
assert tls_pem_certificate_path, 'tls_pem_certificate_path is required'
|
|
45
|
+
assert tls_pem_key_path, 'tls_pem_key_path is required'
|
|
46
|
+
self._name = 'biolib-compute-node-results-proxy'
|
|
47
|
+
self._container: Optional[Container] = None
|
|
48
|
+
self._docker = BiolibDockerClient().get_docker_client()
|
|
49
|
+
self._tls_pem_certificate_path = tls_pem_certificate_path
|
|
50
|
+
self._tls_pem_key_path = tls_pem_key_path
|
|
51
|
+
self._log_streaming_thread: Optional[LogStreamingThread] = None
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def start_proxy(tls_pem_certificate_path: str, tls_pem_key_path: str) -> None:
|
|
55
|
+
abs_cert_path = os.path.abspath(tls_pem_certificate_path)
|
|
56
|
+
abs_key_path = os.path.abspath(tls_pem_key_path)
|
|
57
|
+
assert os.path.exists(abs_cert_path), f'TLS certificate file does not exist: {abs_cert_path}'
|
|
58
|
+
assert os.path.exists(abs_key_path), f'TLS key file does not exist: {abs_key_path}'
|
|
59
|
+
|
|
60
|
+
if ComputeNodeResultsProxy._instance is None:
|
|
61
|
+
logger_no_user_data.debug(
|
|
62
|
+
f'Creating ComputeNodeResultsProxy instance with cert: {abs_cert_path}, key: {abs_key_path}'
|
|
63
|
+
)
|
|
64
|
+
ComputeNodeResultsProxy._instance = ComputeNodeResultsProxy(abs_cert_path, abs_key_path)
|
|
65
|
+
ComputeNodeResultsProxy._instance._start() # pylint: disable=protected-access
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def stop_proxy() -> None:
|
|
69
|
+
if ComputeNodeResultsProxy._instance is not None:
|
|
70
|
+
ComputeNodeResultsProxy._instance._terminate() # pylint: disable=protected-access
|
|
71
|
+
ComputeNodeResultsProxy._instance = None
|
|
72
|
+
|
|
73
|
+
def _start(self) -> None:
|
|
74
|
+
docker = BiolibDockerClient.get_docker_client()
|
|
75
|
+
|
|
76
|
+
for index in range(3):
|
|
77
|
+
logger_no_user_data.debug(
|
|
78
|
+
f'Attempt {index} at creating ComputeNodeResultsProxy container "{self._name}"...'
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
self._container = docker.containers.create(
|
|
82
|
+
detach=True,
|
|
83
|
+
image=get_biolib_nginx_proxy_image(),
|
|
84
|
+
name=self._name,
|
|
85
|
+
network=BIOLIB_PROXY_NETWORK_NAME,
|
|
86
|
+
ports={'443/tcp': 20443},
|
|
87
|
+
volumes={
|
|
88
|
+
self._tls_pem_certificate_path: {'bind': '/etc/ssl/certs/certificate.pem', 'mode': 'ro'},
|
|
89
|
+
self._tls_pem_key_path: {'bind': '/etc/ssl/private/private_key.pem', 'mode': 'ro'},
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
break
|
|
93
|
+
except Exception as error:
|
|
94
|
+
logger_no_user_data.exception(f'Failed to create container "{self._name}" hit error: {error}')
|
|
95
|
+
|
|
96
|
+
logger_no_user_data.debug('Sleeping before re-trying container creation...')
|
|
97
|
+
time.sleep(3)
|
|
98
|
+
|
|
99
|
+
if not self._container or not self._container.id:
|
|
100
|
+
raise BioLibError(f'Exceeded re-try limit for creating container {self._name}')
|
|
101
|
+
|
|
102
|
+
self._write_nginx_config_to_container()
|
|
103
|
+
self._container.start()
|
|
104
|
+
|
|
105
|
+
logger_no_user_data.debug(f'Waiting for container "{self._name}" to be ready...')
|
|
106
|
+
proxy_is_ready = False
|
|
107
|
+
for retry_count in range(1, 5):
|
|
108
|
+
time.sleep(0.5 * retry_count)
|
|
109
|
+
container_logs = self._container.logs()
|
|
110
|
+
if b'ready for start up\n' in container_logs or b'start worker process ' in container_logs:
|
|
111
|
+
proxy_is_ready = True
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if not proxy_is_ready:
|
|
115
|
+
logger_no_user_data.error('ComputeNodeResultsProxy did not start properly.')
|
|
116
|
+
self._terminate()
|
|
117
|
+
raise Exception('ComputeNodeResultsProxy did not start properly')
|
|
118
|
+
|
|
119
|
+
self._container.reload()
|
|
120
|
+
logger.debug(f'ComputeNodeResultsProxy container "{self._name}" started with ID: {self._container.id}')
|
|
121
|
+
|
|
122
|
+
self._log_streaming_thread = LogStreamingThread(self._container, self._name)
|
|
123
|
+
self._log_streaming_thread.start()
|
|
124
|
+
logger_no_user_data.debug(f'Started log streaming for container "{self._name}"')
|
|
125
|
+
|
|
126
|
+
def _terminate(self):
|
|
127
|
+
logger_no_user_data.debug(f'Terminating ComputeNodeResultsProxy container "{self._name}"')
|
|
128
|
+
if self._log_streaming_thread:
|
|
129
|
+
self._log_streaming_thread.stop()
|
|
130
|
+
self._log_streaming_thread = None
|
|
131
|
+
|
|
132
|
+
logger.debug(f'Docker container removal temporarily disabled for debugging purposes (container "{self._name}")')
|
|
133
|
+
# TODO: Figure if we need to remove the container or keep it for debugging purposes
|
|
134
|
+
# if self._container:
|
|
135
|
+
# self._container.remove(force=True)
|
|
136
|
+
|
|
137
|
+
def _write_nginx_config_to_container(self) -> None:
|
|
138
|
+
if not self._container:
|
|
139
|
+
raise Exception('ComputeNodeResultsProxy container not defined when attempting to write NGINX config')
|
|
140
|
+
|
|
141
|
+
docker = BiolibDockerClient.get_docker_client()
|
|
142
|
+
|
|
143
|
+
nginx_config = """
|
|
144
|
+
events {
|
|
145
|
+
worker_connections 1024;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
http {
|
|
149
|
+
client_max_body_size 0;
|
|
150
|
+
resolver 127.0.0.11 ipv6=off valid=30s;
|
|
151
|
+
|
|
152
|
+
server {
|
|
153
|
+
listen 443 ssl http2 default_server;
|
|
154
|
+
|
|
155
|
+
ssl_certificate /etc/ssl/certs/certificate.pem;
|
|
156
|
+
ssl_certificate_key /etc/ssl/private/private_key.pem;
|
|
157
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
158
|
+
|
|
159
|
+
location / {
|
|
160
|
+
if ($http_biolib_result_uuid = "") {
|
|
161
|
+
return 400 "Missing biolib-result-uuid header";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ($http_biolib_result_port = "") {
|
|
165
|
+
return 400 "Missing biolib-result-port header";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
set $target_host "biolib-app-caller-proxy-$http_biolib_result_uuid";
|
|
169
|
+
proxy_pass http://$target_host:1080$request_uri;
|
|
170
|
+
proxy_set_header biolib-result-uuid $http_biolib_result_uuid;
|
|
171
|
+
proxy_set_header biolib-result-port $http_biolib_result_port;
|
|
172
|
+
proxy_pass_request_headers on;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
nginx_config_bytes = nginx_config.encode()
|
|
179
|
+
tarfile_in_memory = io.BytesIO()
|
|
180
|
+
with tarfile.open(fileobj=tarfile_in_memory, mode='w:gz') as tar:
|
|
181
|
+
info = tarfile.TarInfo('/nginx.conf')
|
|
182
|
+
info.size = len(nginx_config_bytes)
|
|
183
|
+
tar.addfile(info, io.BytesIO(nginx_config_bytes))
|
|
184
|
+
|
|
185
|
+
tarfile_bytes = tarfile_in_memory.getvalue()
|
|
186
|
+
tarfile_in_memory.close()
|
|
187
|
+
logger_no_user_data.debug('Writing NGINX configuration to ComputeNodeResultsProxy container')
|
|
188
|
+
docker.api.put_archive(self._container.id, '/etc/nginx', tarfile_bytes)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from docker.errors import ImageNotFound
|
|
2
|
+
from docker.models.images import Image
|
|
3
|
+
|
|
4
|
+
from biolib import utils
|
|
5
|
+
from biolib.biolib_docker_client import BiolibDockerClient
|
|
6
|
+
from biolib.biolib_logging import logger_no_user_data
|
|
7
|
+
from biolib.typing_utils import cast
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_biolib_nginx_proxy_image() -> Image:
|
|
11
|
+
docker = BiolibDockerClient().get_docker_client()
|
|
12
|
+
|
|
13
|
+
if utils.IS_RUNNING_IN_CLOUD:
|
|
14
|
+
try:
|
|
15
|
+
logger_no_user_data.debug('Getting local Docker image for nginx proxy')
|
|
16
|
+
return cast(Image, docker.images.get('biolib-remote-host-proxy:latest'))
|
|
17
|
+
except ImageNotFound:
|
|
18
|
+
logger_no_user_data.debug(
|
|
19
|
+
'Local Docker image for nginx proxy not available. Falling back to public image...'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
public_image_uri = 'public.ecr.aws/h5y4b3l1/biolib-remote-host-proxy:latest'
|
|
23
|
+
try:
|
|
24
|
+
logger_no_user_data.debug('Getting public Docker image for nginx proxy')
|
|
25
|
+
return cast(Image, docker.images.get(public_image_uri))
|
|
26
|
+
except ImageNotFound:
|
|
27
|
+
logger_no_user_data.debug('Pulling public Docker image for nginx proxy')
|
|
28
|
+
return cast(Image, docker.images.pull(public_image_uri))
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# pylint: disable=unsubscriptable-object
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import os
|
|
5
|
-
import time
|
|
6
6
|
import tempfile
|
|
7
|
-
import
|
|
8
|
-
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from docker.models.networks import Network # type: ignore
|
|
10
|
+
from flask import Flask, Response, jsonify, request
|
|
9
11
|
|
|
10
12
|
from biolib import utils
|
|
11
13
|
from biolib.biolib_api_client import BiolibApiClient
|
|
12
14
|
from biolib.biolib_binary_format import SavedJob
|
|
13
|
-
from biolib.
|
|
15
|
+
from biolib.biolib_docker_client import BiolibDockerClient
|
|
16
|
+
from biolib.biolib_logging import TRACE, logger, logger_no_user_data
|
|
14
17
|
from biolib.compute_node.cloud_utils.cloud_utils import CloudUtils
|
|
18
|
+
from biolib.compute_node.utils import BIOLIB_PROXY_NETWORK_NAME
|
|
19
|
+
from biolib.compute_node.webserver import webserver_utils
|
|
20
|
+
from biolib.compute_node.webserver.compute_node_results_proxy import ComputeNodeResultsProxy
|
|
15
21
|
from biolib.compute_node.webserver.gunicorn_flask_application import GunicornFlaskApplication
|
|
16
|
-
from biolib.biolib_logging import logger, TRACE
|
|
17
22
|
from biolib.compute_node.webserver.webserver_utils import get_job_compute_state_or_404
|
|
18
23
|
from biolib.typing_utils import Optional
|
|
19
24
|
|
|
@@ -102,15 +107,14 @@ def status(job_id):
|
|
|
102
107
|
response_data['is_completed'] = compute_state['is_completed']
|
|
103
108
|
|
|
104
109
|
if current_status['stdout_and_stderr_packages_b64']:
|
|
105
|
-
compute_state['streamed_logs_packages_b64'] =
|
|
106
|
-
|
|
110
|
+
compute_state['streamed_logs_packages_b64'] = (
|
|
111
|
+
compute_state['streamed_logs_packages_b64'] + current_status['stdout_and_stderr_packages_b64']
|
|
112
|
+
)
|
|
107
113
|
|
|
108
114
|
compute_state['status']['stdout_and_stderr_packages_b64'] = []
|
|
109
115
|
|
|
110
116
|
if current_status['status_updates']:
|
|
111
|
-
compute_state['previous_status_updates'].extend(
|
|
112
|
-
current_status['status_updates']
|
|
113
|
-
)
|
|
117
|
+
compute_state['previous_status_updates'].extend(current_status['status_updates'])
|
|
114
118
|
compute_state['status']['status_updates'] = []
|
|
115
119
|
|
|
116
120
|
if return_full_logs:
|
|
@@ -132,21 +136,37 @@ def send_package_to_compute_process(job_id, package_bytes):
|
|
|
132
136
|
|
|
133
137
|
|
|
134
138
|
def start_webserver(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
host: str,
|
|
140
|
+
port: int,
|
|
141
|
+
tls_pem_certificate_path: Optional[str] = None,
|
|
142
|
+
tls_pem_key_path: Optional[str] = None,
|
|
139
143
|
) -> None:
|
|
140
144
|
def worker_exit(server, worker): # pylint: disable=unused-argument
|
|
141
|
-
active_compute_states =
|
|
142
|
-
webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT.values()) + webserver_utils.UNASSIGNED_COMPUTE_PROCESSES
|
|
145
|
+
active_compute_states = (
|
|
146
|
+
list(webserver_utils.JOB_ID_TO_COMPUTE_STATE_DICT.values()) + webserver_utils.UNASSIGNED_COMPUTE_PROCESSES
|
|
147
|
+
)
|
|
143
148
|
logger.debug(f'Sending terminate signal to {len(active_compute_states)} compute processes')
|
|
144
149
|
if active_compute_states:
|
|
145
150
|
for compute_state in active_compute_states:
|
|
146
151
|
if compute_state['worker_thread']:
|
|
147
152
|
compute_state['worker_thread'].terminate()
|
|
148
153
|
time.sleep(2)
|
|
149
|
-
|
|
154
|
+
|
|
155
|
+
if utils.IS_RUNNING_IN_CLOUD:
|
|
156
|
+
try:
|
|
157
|
+
logger_no_user_data.debug('Stopping ComputeNodeResultsProxy...')
|
|
158
|
+
ComputeNodeResultsProxy.stop_proxy()
|
|
159
|
+
except BaseException:
|
|
160
|
+
logger_no_user_data.exception('Failed to stop ComputeNodeResultsProxy')
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
logger_no_user_data.debug(f'Removing Docker network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
164
|
+
docker_client = BiolibDockerClient.get_docker_client()
|
|
165
|
+
biolib_proxy_network: Network = docker_client.networks.get(BIOLIB_PROXY_NETWORK_NAME)
|
|
166
|
+
biolib_proxy_network.remove()
|
|
167
|
+
logger_no_user_data.debug(f'Successfully removed Docker network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
168
|
+
except BaseException:
|
|
169
|
+
logger_no_user_data.exception(f'Failed to clean up network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
150
170
|
|
|
151
171
|
def post_fork(server, worker): # pylint: disable=unused-argument
|
|
152
172
|
logger.info('Started compute node')
|
|
@@ -157,8 +177,33 @@ def start_webserver(
|
|
|
157
177
|
utils.IS_DEV = config['is_dev']
|
|
158
178
|
BiolibApiClient.initialize(config['base_url'])
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
biolib_proxy_network: Optional[Network] = None
|
|
181
|
+
try:
|
|
182
|
+
logger_no_user_data.debug(f'Creating Docker network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
183
|
+
docker_client = BiolibDockerClient.get_docker_client()
|
|
184
|
+
biolib_proxy_network = docker_client.networks.create(
|
|
185
|
+
name=BIOLIB_PROXY_NETWORK_NAME,
|
|
186
|
+
internal=False,
|
|
187
|
+
driver='bridge',
|
|
188
|
+
)
|
|
189
|
+
logger_no_user_data.debug(f'Successfully created Docker network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
190
|
+
except BaseException:
|
|
191
|
+
logger_no_user_data.exception(f'Failed to create Docker network {BIOLIB_PROXY_NETWORK_NAME}')
|
|
192
|
+
|
|
193
|
+
if biolib_proxy_network:
|
|
194
|
+
try:
|
|
195
|
+
logger_no_user_data.debug('Starting ComputeNodeResultsProxy...')
|
|
196
|
+
ComputeNodeResultsProxy.start_proxy(tls_pem_certificate_path, tls_pem_key_path)
|
|
197
|
+
except BaseException:
|
|
198
|
+
logger_no_user_data.exception('Failed to start ComputeNodeResultsProxy')
|
|
199
|
+
|
|
200
|
+
CloudUtils.initialize()
|
|
201
|
+
webserver_utils.start_auto_shutdown_timer()
|
|
202
|
+
else:
|
|
203
|
+
logger_no_user_data.error(
|
|
204
|
+
f'Docker network {BIOLIB_PROXY_NETWORK_NAME} was not created, shutting down...'
|
|
205
|
+
)
|
|
206
|
+
CloudUtils.deregister_and_shutdown()
|
|
162
207
|
|
|
163
208
|
if logger.level == TRACE:
|
|
164
209
|
gunicorn_log_level_name = 'DEBUG'
|
biolib/experiments/experiment.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from collections import OrderedDict
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from biolib import api
|
|
5
|
-
from biolib._internal.types.experiment import DeprecatedExperimentDict, ExperimentDict
|
|
6
|
-
from biolib._internal.types.resource import ResourceDetailedDict
|
|
7
6
|
from biolib._internal.utils import open_browser_window_from_notebook
|
|
7
|
+
from biolib._shared.types import DeprecatedExperimentDict, ExperimentDict, ResourceDetailedDict
|
|
8
|
+
from biolib.api.client import ApiClient
|
|
8
9
|
from biolib.biolib_api_client import BiolibApiClient
|
|
9
10
|
from biolib.biolib_errors import BioLibError
|
|
10
11
|
from biolib.jobs.job import Job
|
|
12
|
+
from biolib.jobs.job_result import PathFilter
|
|
11
13
|
from biolib.jobs.types import JobsPaginatedResponse
|
|
12
14
|
from biolib.tables import BioLibTable
|
|
13
15
|
from biolib.typing_utils import Dict, List, Optional, Union
|
|
@@ -26,7 +28,13 @@ class Experiment:
|
|
|
26
28
|
}
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
uri: str,
|
|
34
|
+
_resource_dict: Optional[ResourceDetailedDict] = None,
|
|
35
|
+
_api_client: Optional[ApiClient] = None,
|
|
36
|
+
):
|
|
37
|
+
self._api_client = _api_client or api.client
|
|
30
38
|
self._resource_dict: ResourceDetailedDict = _resource_dict or self._get_or_create_resource_dict(uri)
|
|
31
39
|
|
|
32
40
|
def __enter__(self):
|
|
@@ -111,7 +119,7 @@ class Experiment:
|
|
|
111
119
|
job_id = job
|
|
112
120
|
elif job is None and job_id is None:
|
|
113
121
|
raise BioLibError('A job ID or job object must be provided to add job')
|
|
114
|
-
|
|
122
|
+
self._api_client.post(
|
|
115
123
|
path=f'/experiments/{self.uuid}/jobs/',
|
|
116
124
|
data={'job_uuid': job_id},
|
|
117
125
|
)
|
|
@@ -124,7 +132,7 @@ class Experiment:
|
|
|
124
132
|
else:
|
|
125
133
|
raise BioLibError('A job ID or job object must be provided to remove job')
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
self._api_client.delete(path=f'/experiments/{self.uuid}/jobs/{job_id}/')
|
|
128
136
|
|
|
129
137
|
def mount_files(self, mount_path: str) -> None:
|
|
130
138
|
try:
|
|
@@ -173,7 +181,7 @@ class Experiment:
|
|
|
173
181
|
|
|
174
182
|
# Prints a table listing info about the jobs in this experiment
|
|
175
183
|
def show_jobs(self) -> None:
|
|
176
|
-
response: JobsPaginatedResponse =
|
|
184
|
+
response: JobsPaginatedResponse = self._api_client.get(
|
|
177
185
|
path=f'/experiments/{self.uuid}/jobs/',
|
|
178
186
|
params=dict(page_size=10),
|
|
179
187
|
).json()
|
|
@@ -195,11 +203,13 @@ class Experiment:
|
|
|
195
203
|
if status:
|
|
196
204
|
params['status'] = status
|
|
197
205
|
|
|
198
|
-
response: JobsPaginatedResponse =
|
|
206
|
+
response: JobsPaginatedResponse = self._api_client.get(url, params=params).json()
|
|
199
207
|
jobs: List[Job] = [Job(job_dict) for job_dict in response['results']]
|
|
200
208
|
|
|
201
209
|
for page_number in range(2, response['page_count'] + 1):
|
|
202
|
-
page_response: JobsPaginatedResponse =
|
|
210
|
+
page_response: JobsPaginatedResponse = self._api_client.get(
|
|
211
|
+
url, params=dict(**params, page=page_number)
|
|
212
|
+
).json()
|
|
203
213
|
jobs.extend([Job(job_dict) for job_dict in page_response['results']])
|
|
204
214
|
|
|
205
215
|
return jobs
|
|
@@ -223,22 +233,94 @@ class Experiment:
|
|
|
223
233
|
"""
|
|
224
234
|
return self.get_jobs(status=status)
|
|
225
235
|
|
|
236
|
+
def save_completed_results(
|
|
237
|
+
self,
|
|
238
|
+
output_dir: Optional[str] = None,
|
|
239
|
+
path_filter: Optional[PathFilter] = None,
|
|
240
|
+
skip_file_if_exists: bool = False,
|
|
241
|
+
overwrite: bool = False,
|
|
242
|
+
) -> None:
|
|
243
|
+
r"""Save all completed results in this experiment to local folders.
|
|
244
|
+
|
|
245
|
+
Creates a folder structure with the experiment name as the root directory,
|
|
246
|
+
containing a subfolder for each completed result. Only results with
|
|
247
|
+
'completed' status will be saved.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
output_dir (str, optional): Base directory where the experiment folder
|
|
251
|
+
will be created. If None, uses the current working directory.
|
|
252
|
+
path_filter (PathFilter, optional): Filter to select which files in the results to save.
|
|
253
|
+
Can be a glob pattern string or a callable function.
|
|
254
|
+
skip_file_if_exists (bool, optional): Whether to skip files that already exist
|
|
255
|
+
locally instead of raising an error. Defaults to False.
|
|
256
|
+
overwrite (bool, optional): Whether to overwrite existing files.
|
|
257
|
+
Defaults to False.
|
|
258
|
+
|
|
259
|
+
Example::
|
|
260
|
+
|
|
261
|
+
>>> # Save all completed results to current directory
|
|
262
|
+
>>> experiment.save_completed_results()
|
|
263
|
+
>>> # This creates: ./experiment_name/result_1/, ./experiment_name/result_2/, etc.
|
|
264
|
+
|
|
265
|
+
>>> # Save to specific directory
|
|
266
|
+
>>> experiment.save_completed_results(output_dir="/path/to/save")
|
|
267
|
+
>>> # This creates: /path/to/save/experiment_name/result_1/, etc.
|
|
268
|
+
"""
|
|
269
|
+
base_dir = Path(output_dir) if output_dir else Path.cwd()
|
|
270
|
+
|
|
271
|
+
if base_dir == Path('/'):
|
|
272
|
+
raise BioLibError("Cannot save experiment results to root directory '/'")
|
|
273
|
+
|
|
274
|
+
experiment_folder = base_dir / self.name
|
|
275
|
+
experiment_folder.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
|
|
277
|
+
completed_results: List[Job] = []
|
|
278
|
+
failed_results = False
|
|
279
|
+
print('Getting experiment status...')
|
|
280
|
+
for result in self.get_results():
|
|
281
|
+
if result.get_status() == 'completed':
|
|
282
|
+
completed_results.append(result)
|
|
283
|
+
elif result.get_status() != 'in_progress':
|
|
284
|
+
failed_results = True
|
|
285
|
+
|
|
286
|
+
if failed_results:
|
|
287
|
+
print(
|
|
288
|
+
'WARNING: Found failed or cancelled results in the experiment. '
|
|
289
|
+
'Please verify you have all your results, and consider removing the failed ones.'
|
|
290
|
+
)
|
|
291
|
+
if not completed_results:
|
|
292
|
+
print(f"No completed results found in experiment '{self.name}'")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
print(f"Saving {len(completed_results)} completed results from experiment '{self.name}' to {experiment_folder}")
|
|
296
|
+
|
|
297
|
+
for result in completed_results:
|
|
298
|
+
result_name = result.get_name()
|
|
299
|
+
result_folder = experiment_folder / result_name
|
|
300
|
+
|
|
301
|
+
result_folder.mkdir(parents=True, exist_ok=True)
|
|
302
|
+
|
|
303
|
+
result.save_files(
|
|
304
|
+
output_dir=str(result_folder),
|
|
305
|
+
path_filter=path_filter,
|
|
306
|
+
skip_file_if_exists=skip_file_if_exists,
|
|
307
|
+
overwrite=overwrite,
|
|
308
|
+
)
|
|
309
|
+
|
|
226
310
|
def rename(self, destination: str) -> None:
|
|
227
|
-
|
|
311
|
+
self._api_client.patch(f'/resources/{self.uuid}/', data={'uri': destination})
|
|
228
312
|
self._refetch()
|
|
229
313
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
resource_dict: ResourceDetailedDict = api.client.get(f'/resources/{uuid}/').json()
|
|
314
|
+
def _get_resource_dict_by_uuid(self, uuid: str) -> ResourceDetailedDict:
|
|
315
|
+
resource_dict: ResourceDetailedDict = self._api_client.get(f'/resources/{uuid}/').json()
|
|
233
316
|
if not resource_dict['experiment']:
|
|
234
317
|
raise ValueError('Resource from URI is not an experiment')
|
|
235
318
|
|
|
236
319
|
return resource_dict
|
|
237
320
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return Experiment._get_resource_dict_by_uuid(uuid=response_dict['uuid'])
|
|
321
|
+
def _get_or_create_resource_dict(self, uri: str) -> ResourceDetailedDict:
|
|
322
|
+
response_dict = self._api_client.post(path='/experiments/', data={'uri' if '/' in uri else 'name': uri}).json()
|
|
323
|
+
return self._get_resource_dict_by_uuid(uuid=response_dict['uuid'])
|
|
242
324
|
|
|
243
325
|
def _refetch(self) -> None:
|
|
244
326
|
self._resource_dict = self._get_resource_dict_by_uuid(uuid=self._resource_dict['uuid'])
|