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.
Files changed (75) hide show
  1. biolib/__init__.py +18 -5
  2. biolib/_data_record/data_record.py +278 -0
  3. biolib/_internal/data_record/__init__.py +1 -0
  4. biolib/_internal/data_record/data_record.py +97 -0
  5. biolib/_internal/data_record/remote_storage_endpoint.py +38 -0
  6. biolib/_internal/file_utils.py +77 -0
  7. biolib/_internal/fuse_mount/__init__.py +1 -0
  8. biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
  9. biolib/_internal/http_client.py +42 -23
  10. biolib/_internal/lfs/__init__.py +1 -0
  11. biolib/_internal/libs/__init__.py +1 -0
  12. biolib/_internal/libs/fusepy/__init__.py +1257 -0
  13. biolib/_internal/push_application.py +22 -37
  14. biolib/_internal/runtime.py +19 -0
  15. biolib/_internal/types/__init__.py +4 -0
  16. biolib/_internal/types/app.py +9 -0
  17. biolib/_internal/types/data_record.py +40 -0
  18. biolib/_internal/types/experiment.py +10 -0
  19. biolib/_internal/types/resource.py +14 -0
  20. biolib/_internal/types/typing.py +7 -0
  21. biolib/_internal/utils/__init__.py +18 -0
  22. biolib/_runtime/runtime.py +80 -0
  23. biolib/api/__init__.py +1 -0
  24. biolib/api/client.py +39 -17
  25. biolib/app/app.py +40 -72
  26. biolib/app/search_apps.py +8 -12
  27. biolib/biolib_api_client/api_client.py +22 -10
  28. biolib/biolib_api_client/app_types.py +2 -1
  29. biolib/biolib_api_client/biolib_app_api.py +1 -1
  30. biolib/biolib_api_client/biolib_job_api.py +6 -0
  31. biolib/biolib_api_client/job_types.py +4 -4
  32. biolib/biolib_api_client/lfs_types.py +8 -2
  33. biolib/biolib_binary_format/remote_endpoints.py +12 -10
  34. biolib/biolib_binary_format/utils.py +41 -4
  35. biolib/cli/__init__.py +6 -2
  36. biolib/cli/auth.py +58 -0
  37. biolib/cli/data_record.py +80 -0
  38. biolib/cli/download_container.py +3 -1
  39. biolib/cli/init.py +1 -0
  40. biolib/cli/lfs.py +45 -11
  41. biolib/cli/push.py +1 -1
  42. biolib/cli/run.py +3 -2
  43. biolib/cli/start.py +1 -0
  44. biolib/compute_node/cloud_utils/cloud_utils.py +15 -18
  45. biolib/compute_node/job_worker/cache_state.py +1 -1
  46. biolib/compute_node/job_worker/executors/docker_executor.py +134 -114
  47. biolib/compute_node/job_worker/job_storage.py +3 -4
  48. biolib/compute_node/job_worker/job_worker.py +31 -15
  49. biolib/compute_node/remote_host_proxy.py +75 -70
  50. biolib/compute_node/webserver/webserver_types.py +0 -1
  51. biolib/experiments/experiment.py +75 -44
  52. biolib/jobs/job.py +125 -47
  53. biolib/jobs/job_result.py +46 -21
  54. biolib/jobs/types.py +1 -1
  55. biolib/runtime/__init__.py +14 -1
  56. biolib/sdk/__init__.py +29 -5
  57. biolib/typing_utils.py +2 -7
  58. biolib/user/sign_in.py +10 -14
  59. biolib/utils/__init__.py +1 -1
  60. biolib/utils/app_uri.py +11 -4
  61. biolib/utils/cache_state.py +2 -2
  62. biolib/utils/seq_util.py +38 -30
  63. {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/METADATA +1 -1
  64. pybiolib-1.1.2193.dist-info/RECORD +123 -0
  65. {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/WHEEL +1 -1
  66. biolib/biolib_api_client/biolib_account_api.py +0 -8
  67. biolib/biolib_api_client/biolib_large_file_system_api.py +0 -34
  68. biolib/experiments/types.py +0 -9
  69. biolib/lfs/__init__.py +0 -6
  70. biolib/lfs/utils.py +0 -237
  71. biolib/runtime/results.py +0 -20
  72. pybiolib-1.1.1747.dist-info/RECORD +0 -108
  73. /biolib/{lfs → _internal/lfs}/cache.py +0 -0
  74. {pybiolib-1.1.1747.dist-info → pybiolib-1.1.2193.dist-info}/LICENSE +0 -0
  75. {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(self, signum: int, frame: FrameType) -> None: # pylint: disable=unused-argument
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
- module_input_serialized = JobStorage.get_module_input(job=job)
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(module_input_serialized)
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._run_root_job(package)
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
- module_input_serialized: bytes,
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, module_input_serialized, main_module_output_path)
341
+ self._run_job(job, module_input_path, main_module_output_path)
335
342
 
336
- def _run_job(self, job: CreatedJobDict, module_input_serialized: bytes, main_module_output_path: str) -> None:
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
- module_input_serialized,
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
- module_input_serialized: bytes,
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
- module_input_with_runtime_zip_serialized,
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(module_input_serialized, module_output_path)
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, module_input_serialized: bytes) -> str:
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
- module_input_serialized=module_input_serialized,
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
- module_output_path = self._run_root_job(module_input_serialized)
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.biolib_errors import BioLibError
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.biolib_api_client import BiolibApiClient
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
- self,
30
- remote_host: RemoteHost,
31
- public_network: Network,
32
- internal_network: Optional[Network],
33
- job_id: str,
34
- ports: List[int]
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"biolib-remote-host-proxy-{job_id}-{self.hostname}"
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
- base_url = BiolibApiClient.get().base_url
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
- if utils.BIOLIB_CLOUD_BASE_URL:
159
- cloud_base_url = utils.BIOLIB_CLOUD_BASE_URL
160
- else:
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 {base_url}/api/jobs/cloud/$job_id/status/;
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 {base_url}/api/jobs/cloud/$1;
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 {base_url}/api/jobs/$job_id/storage/input/start_upload/;
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 {base_url}/api/jobs/$job_id/storage/input/presigned_upload_url/$is_args$args;
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 {base_url}/api/jobs/$job_id/storage/input/complete_upload/;
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 {base_url}/api/jobs/$job_id/main_result/;
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 {base_url}/api/jobs/$job_id/;
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 {base_url}/api/jobs/create_job_with_data/$1;
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 {base_url}/api/jobs/$1;
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 /api/ {{
283
- proxy_pass {base_url}/api/;
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 /cloud-proxy/ {{
290
- proxy_pass {cloud_base_url}/cloud-proxy/;
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 /job-storage/ {{
297
- proxy_pass {s3_results_base_url}/job-storage/;
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 {cloud_base_url}/proxy/storage/job-storage/;
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 {cloud_base_url}/proxy/cloud/;
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()
@@ -16,5 +16,4 @@ class WebserverConfig(TypedDict):
16
16
  base_url: str
17
17
  compute_node_info: ComputeNodeInfo
18
18
  is_dev: bool
19
- s3_general_storage_bucket_name: str
20
19
  shutdown_times: ShutdownTimes
@@ -1,30 +1,29 @@
1
1
  import time
2
2
  from collections import OrderedDict
3
3
 
4
- from biolib.biolib_errors import BioLibError
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.experiments.types import ExperimentDict
6
+ from biolib.biolib_errors import BioLibError
10
7
  from biolib.jobs.job import Job
11
- from biolib.typing_utils import Dict, Union
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
- 'Name': {'key': 'name', 'params': {}},
22
- 'Job Count': {'key': 'job_count', 'params': {}},
23
- 'Created At': {'key': 'created_at', 'params': {}}
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, name: str):
27
- self._experiment_dict: ExperimentDict = self._create_in_backend_or_get_experiment_dict(name)
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.name}'
35
+ return f'Experiment: {self.uri}'
37
36
 
38
37
  def __repr__(self):
39
- return f'Experiment: {self.name}'
38
+ return f'Experiment: {self.uri}'
40
39
 
41
40
  @property
42
41
  def uuid(self) -> str:
43
- return self._experiment_dict['uuid']
42
+ return self._resource_dict['uuid']
44
43
 
45
44
  @property
46
45
  def name(self) -> str:
47
- return self._experiment_dict['name']
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._refetch_experiment_dict()
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._refetch_experiment_dict()
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
- path=f'/jobs/{job_id}/',
82
- data={'experiment_uuid': self.uuid}
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
- ) from error
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 _create_in_backend_or_get_experiment_dict(self, name: str) -> ExperimentDict:
151
- # This endpoint returns experiment dict if already created
152
- experiment_dict: ExperimentDict = api.client.post(
153
- path='/experiments/',
154
- data={
155
- 'name': name
156
- }
157
- ).json()
158
- return experiment_dict
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 _refetch_experiment_dict(self) -> None:
161
- self._experiment_dict = api.client.get(path=f'/experiments/{self.uuid}/').json()
191
+ def _refetch(self) -> None:
192
+ self._resource_dict = self._get_resource_dict_by_uuid(uuid=self._resource_dict['uuid'])