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
@@ -1,17 +1,18 @@
1
1
  import os
2
2
  import re
3
3
  from pathlib import Path
4
- import yaml
4
+
5
5
  import rich.progress
6
+ import yaml
6
7
 
7
- from biolib.lfs.utils import get_iterable_zip_stream, get_files_and_size_of_directory
8
- from biolib.typing_utils import Optional, Set, TypedDict, Iterable
8
+ from biolib import api, utils
9
+ from biolib._internal.file_utils import get_files_and_size_of_directory, get_iterable_zip_stream
9
10
  from biolib.biolib_api_client import BiolibApiClient
10
- from biolib.biolib_docker_client import BiolibDockerClient
11
11
  from biolib.biolib_api_client.biolib_app_api import BiolibAppApi
12
+ from biolib.biolib_docker_client import BiolibDockerClient
12
13
  from biolib.biolib_errors import BioLibError
13
14
  from biolib.biolib_logging import logger
14
- from biolib import utils, api
15
+ from biolib.typing_utils import Iterable, Optional, Set, TypedDict
15
16
 
16
17
  REGEX_MARKDOWN_INLINE_IMAGE = re.compile(r'!\[(?P<alt>.*)\]\((?P<src>.*)\)')
17
18
 
@@ -38,9 +39,7 @@ def process_docker_status_updates(status_updates: Iterable[DockerStatusUpdate],
38
39
  progress_detail = update['progressDetail']
39
40
 
40
41
  if layer_id not in layer_id_to_task_id:
41
- layer_id_to_task_id[layer_id] = progress.add_task(
42
- description=f'[cyan]{action} layer {layer_id}'
43
- )
42
+ layer_id_to_task_id[layer_id] = progress.add_task(description=f'[cyan]{action} layer {layer_id}')
44
43
 
45
44
  if progress_detail and 'current' in progress_detail and 'total' in progress_detail:
46
45
  progress.update(
@@ -60,7 +59,7 @@ def process_docker_status_updates(status_updates: Iterable[DockerStatusUpdate],
60
59
 
61
60
 
62
61
  def set_app_version_as_active(
63
- app_version_uuid: str,
62
+ app_version_uuid: str,
64
63
  ):
65
64
  logger.debug(f'Setting app version {app_version_uuid} as active.')
66
65
  api.client.patch(
@@ -70,10 +69,10 @@ def set_app_version_as_active(
70
69
 
71
70
 
72
71
  def push_application(
73
- app_uri: str,
74
- app_path: str,
75
- app_version_to_copy_images_from: Optional[str],
76
- is_dev_version: Optional[bool],
72
+ app_uri: str,
73
+ app_path: str,
74
+ app_version_to_copy_images_from: Optional[str],
75
+ is_dev_version: Optional[bool],
77
76
  ):
78
77
  app_path_absolute = Path(app_path).resolve()
79
78
 
@@ -96,7 +95,7 @@ def push_application(
96
95
 
97
96
  input_files_maps_to_root = False
98
97
  try:
99
- with open(config_yml_path, mode='r') as config_yml_file:
98
+ with open(config_yml_path) as config_yml_file:
100
99
  config = yaml.safe_load(config_yml_file.read())
101
100
 
102
101
  license_file_relative_path = config.get('license_file', 'LICENSE')
@@ -109,7 +108,7 @@ def push_application(
109
108
  raise BioLibError(f'Could not find {description_file_relative_path}')
110
109
 
111
110
  zip_filters.add(description_file_relative_path)
112
- with open(description_file_absolute_path, mode='r') as description_file:
111
+ with open(description_file_absolute_path) as description_file:
113
112
  description_file_content = description_file.read()
114
113
 
115
114
  for _, img_src_path in re.findall(REGEX_MARKDOWN_INLINE_IMAGE, description_file_content):
@@ -171,8 +170,9 @@ def push_application(
171
170
  author=app['account_handle'],
172
171
  set_as_active=False,
173
172
  zip_binary=source_files_zip_bytes,
174
- app_version_id_to_copy_images_from=app_response['app_version']['public_id'] if app_version_to_copy_images_from
175
- else None
173
+ app_version_id_to_copy_images_from=app_response['app_version']['public_id']
174
+ if app_version_to_copy_images_from
175
+ else None,
176
176
  )
177
177
 
178
178
  # Don't push docker images if copying from another app version
@@ -180,18 +180,6 @@ def push_application(
180
180
  if not app_version_to_copy_images_from and docker_tags:
181
181
  logger.info('Found docker images to push.')
182
182
 
183
- try:
184
- yaml_file = open(f'{app_path}/.biolib/config.yml', 'r', encoding='utf-8')
185
-
186
- except Exception as error: # pylint: disable=broad-except
187
- raise BioLibError('Could not open the config file .biolib/config.yml') from error
188
-
189
- try:
190
- config_data = yaml.safe_load(yaml_file)
191
-
192
- except Exception as error: # pylint: disable=broad-except
193
- raise BioLibError('Could not parse .biolib/config.yml. Please make sure it is valid YAML') from error
194
-
195
183
  # Auth to be sent to proxy
196
184
  # The tokens are sent as "{access_token},{job_id}". We leave job_id blank on push.
197
185
  tokens = f'{BiolibApiClient.get().access_token},'
@@ -200,14 +188,12 @@ def push_application(
200
188
  docker_client = BiolibDockerClient.get_docker_client()
201
189
 
202
190
  for module_name, repo_and_tag in docker_tags.items():
203
- docker_image_definition = config_data['modules'][module_name]['image']
191
+ docker_image_definition = config['modules'][module_name]['image']
204
192
  repo, tag = repo_and_tag.split(':')
205
193
 
206
194
  if docker_image_definition.startswith('dockerhub://'):
207
195
  docker_image_name = docker_image_definition.replace('dockerhub://', 'docker.io/', 1)
208
- logger.info(
209
- f'Pulling image {docker_image_name} defined on module {module_name} from Dockerhub.'
210
- )
196
+ logger.info(f'Pulling image {docker_image_name} defined on module {module_name} from Dockerhub.')
211
197
  dockerhub_repo, dockerhub_tag = docker_image_name.split(':')
212
198
  pull_status_updates: Iterable[DockerStatusUpdate] = docker_client.api.pull(
213
199
  decode=True,
@@ -238,7 +224,7 @@ def push_application(
238
224
 
239
225
  process_docker_status_updates(push_status_updates, action='Pushing')
240
226
 
241
- except Exception as exception: # pylint: disable=broad-except
227
+ except Exception as exception:
242
228
  raise BioLibError(f'Failed to tag and push image {docker_image_name}.') from exception
243
229
 
244
230
  logger.info(f'Successfully pushed {docker_image_name}')
@@ -249,10 +235,9 @@ def push_application(
249
235
  data={'set_as_active': not is_dev_version},
250
236
  )
251
237
 
252
- sematic_version = \
253
- f"{new_app_version_json['major']}.{new_app_version_json['minor']}.{new_app_version_json['patch']}"
238
+ sematic_version = f"{new_app_version_json['major']}.{new_app_version_json['minor']}.{new_app_version_json['patch']}"
254
239
  logger.info(
255
240
  f"Successfully pushed new {'development ' if is_dev_version else ''}version {sematic_version} of {app_uri}."
256
241
  )
257
242
 
258
- return {"app_uri": app_uri, "sematic_version": sematic_version}
243
+ return {'app_uri': app_uri, 'sematic_version': sematic_version}
@@ -0,0 +1,19 @@
1
+ from biolib.typing_utils import TypedDict
2
+
3
+
4
+ class RuntimeJobDataDict(TypedDict):
5
+ version: str
6
+ job_requested_machine: str
7
+ job_uuid: str
8
+ job_auth_token: str
9
+ app_uri: str
10
+
11
+
12
+ class BioLibRuntimeError(Exception):
13
+ pass
14
+
15
+
16
+ class BioLibRuntimeNotRecognizedError(BioLibRuntimeError):
17
+ def __init__(self, message='The runtime is not recognized as a BioLib app'):
18
+ self.message = message
19
+ super().__init__(self.message)
@@ -0,0 +1,4 @@
1
+ from .app import * # noqa: F403
2
+ from .data_record import * # noqa: F403
3
+ from .experiment import * # noqa: F403
4
+ from .resource import * # noqa: F403
@@ -0,0 +1,9 @@
1
+ from .typing import TypedDict
2
+
3
+
4
+ class AppSlimDict(TypedDict):
5
+ pass
6
+
7
+
8
+ class AppDetailedDict(AppSlimDict):
9
+ pass
@@ -0,0 +1,40 @@
1
+ from .typing import Dict, List, Literal, Optional, TypedDict, Union
2
+
3
+
4
+ class SqliteV1ForeignKey(TypedDict):
5
+ table: str
6
+ column: str
7
+
8
+
9
+ class SqliteV1Column(TypedDict):
10
+ type: Literal['INTEGER', 'REAL', 'TEXT', 'JSON'] # noqa:F821
11
+ nullable: Optional[bool]
12
+ foreign_key: Optional[SqliteV1ForeignKey]
13
+ json_schema: Optional[Dict]
14
+
15
+
16
+ class SqliteV1Table(TypedDict):
17
+ columns: Dict[str, SqliteV1Column]
18
+
19
+
20
+ class SqliteV1DatabaseSchema(TypedDict):
21
+ tables: Dict[str, SqliteV1Table]
22
+
23
+
24
+ class DataRecordValidationRuleDict(TypedDict):
25
+ path: str
26
+ type: str
27
+ rule: Union[SqliteV1DatabaseSchema]
28
+
29
+
30
+ class DataRecordTypeDict(TypedDict):
31
+ name: str
32
+ validation_rules: List[DataRecordValidationRuleDict]
33
+
34
+
35
+ class DataRecordSlimDict(TypedDict):
36
+ pass
37
+
38
+
39
+ class DataRecordDetailedDict(DataRecordSlimDict):
40
+ type: Optional[DataRecordTypeDict]
@@ -0,0 +1,10 @@
1
+ from .typing import TypedDict
2
+
3
+
4
+ class ExperimentSlimDict(TypedDict):
5
+ job_count: int
6
+ job_running_count: int
7
+
8
+
9
+ class ExperimentDetailedDict(ExperimentSlimDict):
10
+ pass
@@ -0,0 +1,14 @@
1
+ from .app import AppSlimDict
2
+ from .data_record import DataRecordSlimDict
3
+ from .experiment import ExperimentSlimDict
4
+ from .typing import Optional, TypedDict
5
+
6
+
7
+ class ResourceDict(TypedDict):
8
+ uuid: str
9
+ uri: str
10
+ name: str
11
+ created_at: str
12
+ app: Optional[AppSlimDict]
13
+ data_record: Optional[DataRecordSlimDict]
14
+ experiment: Optional[ExperimentSlimDict]
@@ -0,0 +1,7 @@
1
+ import sys
2
+
3
+ # import and expose everything from the typing module
4
+ from typing import * # noqa:F403 pylint: disable=wildcard-import, unused-wildcard-import
5
+
6
+ if sys.version_info < (3, 8): # noqa: UP036
7
+ from typing_extensions import Literal, TypedDict # pylint: disable=unused-import
@@ -0,0 +1,18 @@
1
+ import time
2
+ import uuid
3
+
4
+
5
+ def open_browser_window_from_notebook(url_to_open: str) -> None:
6
+ try:
7
+ from IPython.display import ( # type:ignore # pylint: disable=import-error, import-outside-toplevel
8
+ Javascript,
9
+ display,
10
+ update_display,
11
+ )
12
+ except ImportError as error:
13
+ raise Exception('Unexpected environment. This function can only be called from a notebook.') from error
14
+
15
+ display_id = str(uuid.uuid4())
16
+ display(Javascript(f'window.open("{url_to_open}");'), display_id=display_id)
17
+ time.sleep(1)
18
+ update_display(Javascript(''), display_id=display_id)
@@ -0,0 +1,80 @@
1
+ import json
2
+ import re
3
+ from typing import Optional
4
+
5
+ from biolib import api
6
+ from biolib._internal.runtime import BioLibRuntimeError, BioLibRuntimeNotRecognizedError, RuntimeJobDataDict
7
+ from biolib.typing_utils import cast
8
+
9
+
10
+ class Runtime:
11
+ _job_data: Optional[RuntimeJobDataDict] = None
12
+
13
+ @staticmethod
14
+ def check_is_environment_biolib_app() -> bool:
15
+ return bool(Runtime._try_to_get_job_data())
16
+
17
+ @staticmethod
18
+ def get_job_id() -> str:
19
+ return Runtime._get_job_data()['job_uuid']
20
+
21
+ @staticmethod
22
+ def get_job_auth_token() -> str:
23
+ return Runtime._get_job_data()['job_auth_token']
24
+
25
+ @staticmethod
26
+ def get_job_requested_machine() -> str:
27
+ return Runtime._get_job_data()['job_requested_machine']
28
+
29
+ @staticmethod
30
+ def get_app_uri() -> str:
31
+ return Runtime._get_job_data()['app_uri']
32
+
33
+ @staticmethod
34
+ def get_secret(secret_name: str) -> bytes:
35
+ assert re.match(
36
+ '^[a-zA-Z0-9_-]*$', secret_name
37
+ ), 'Secret name can only contain alphanumeric characters and dashes or underscores '
38
+ try:
39
+ with open(f'/biolib/secrets/{secret_name}', 'rb') as file:
40
+ return file.read()
41
+ except BaseException as error:
42
+ raise BioLibRuntimeError(f'Unable to get system secret: {secret_name}') from error
43
+
44
+ @staticmethod
45
+ def set_main_result_prefix(result_prefix: str) -> None:
46
+ job_data = Runtime._get_job_data()
47
+ api.client.patch(
48
+ data={'result_name_prefix': result_prefix},
49
+ headers={'Job-Auth-Token': job_data['job_auth_token']},
50
+ path=f"/jobs/{job_data['job_uuid']}/main_result/",
51
+ )
52
+
53
+ @staticmethod
54
+ def create_result_note(note: str) -> None:
55
+ job_id = Runtime.get_job_id()
56
+ # Note: Authentication is added by app caller proxy in compute node
57
+ api.client.post(data={'note': note}, path=f'/jobs/{job_id}/notes/')
58
+
59
+ @staticmethod
60
+ def _try_to_get_job_data() -> Optional[RuntimeJobDataDict]:
61
+ if not Runtime._job_data:
62
+ try:
63
+ with open('/biolib/secrets/biolib_system_secret') as file:
64
+ job_data: RuntimeJobDataDict = json.load(file)
65
+ except BaseException:
66
+ return None
67
+
68
+ if not job_data['version'].startswith('1.'):
69
+ raise BioLibRuntimeError(f"Unexpected system secret version {job_data['version']} expected 1.x.x")
70
+
71
+ Runtime._job_data = job_data
72
+
73
+ return cast(RuntimeJobDataDict, Runtime._job_data)
74
+
75
+ @staticmethod
76
+ def _get_job_data() -> RuntimeJobDataDict:
77
+ job_data = Runtime._try_to_get_job_data()
78
+ if not job_data:
79
+ raise BioLibRuntimeNotRecognizedError() from None
80
+ return job_data
biolib/api/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from biolib._internal.http_client import HttpClient
2
+
2
3
  from .client import ApiClient as _ApiClient
3
4
 
4
5
  _client = _ApiClient()
biolib/api/client.py CHANGED
@@ -1,8 +1,10 @@
1
- from urllib.parse import urljoin, urlencode
1
+ from urllib.parse import urlencode, urljoin
2
2
 
3
- from biolib.typing_utils import Dict, Optional, Union
3
+ import importlib_metadata
4
+
5
+ from biolib._internal.http_client import HttpClient, HttpResponse
4
6
  from biolib.biolib_api_client import BiolibApiClient as DeprecatedApiClient
5
- from biolib._internal.http_client import HttpResponse, HttpClient
7
+ from biolib.typing_utils import Dict, Optional, Union, cast
6
8
 
7
9
  OptionalHeaders = Union[
8
10
  Optional[Dict[str, str]],
@@ -10,14 +12,24 @@ OptionalHeaders = Union[
10
12
  ]
11
13
 
12
14
 
15
+ def _get_biolib_package_version() -> str:
16
+ # try fetching version, if it fails (usually when in dev), add default
17
+ try:
18
+ return cast(str, importlib_metadata.version('pybiolib'))
19
+ except importlib_metadata.PackageNotFoundError:
20
+ return '0.0.0'
21
+
22
+
13
23
  class ApiClient(HttpClient):
24
+ _biolib_package_version: str = _get_biolib_package_version()
25
+
14
26
  def get(
15
- self,
16
- path: str,
17
- params: Optional[Dict[str, Union[str, int]]] = None,
18
- headers: OptionalHeaders = None,
19
- authenticate: bool = True,
20
- retries: int = 10,
27
+ self,
28
+ path: str,
29
+ params: Optional[Dict[str, Union[str, int]]] = None,
30
+ headers: OptionalHeaders = None,
31
+ authenticate: bool = True,
32
+ retries: int = 10,
21
33
  ) -> HttpResponse:
22
34
  return self.request(
23
35
  headers=self._get_headers(opt_headers=headers, authenticate=authenticate),
@@ -27,12 +39,12 @@ class ApiClient(HttpClient):
27
39
  )
28
40
 
29
41
  def post(
30
- self,
31
- path: str,
32
- data: Optional[Union[Dict, bytes]] = None,
33
- headers: OptionalHeaders = None,
34
- authenticate: bool = True,
35
- retries: int = 5,
42
+ self,
43
+ path: str,
44
+ data: Optional[Union[Dict, bytes]] = None,
45
+ headers: OptionalHeaders = None,
46
+ authenticate: bool = True,
47
+ retries: int = 50, # TODO: reduce this back to 5 when timeout errors have been solved
36
48
  ) -> HttpResponse:
37
49
  return self.request(
38
50
  data=data,
@@ -42,13 +54,20 @@ class ApiClient(HttpClient):
42
54
  url=self._get_absolute_url(path=path, query_params=None),
43
55
  )
44
56
 
45
- def patch(self, path: str, data: Dict, headers: OptionalHeaders = None, retries: int = 5) -> HttpResponse:
57
+ def patch(
58
+ self,
59
+ path: str,
60
+ data: Dict,
61
+ headers: OptionalHeaders = None,
62
+ retries: int = 5,
63
+ params: Optional[Dict[str, Union[str, int]]] = None,
64
+ ) -> HttpResponse:
46
65
  return self.request(
47
66
  data=data,
48
67
  headers=self._get_headers(opt_headers=headers, authenticate=True),
49
68
  method='PATCH',
50
69
  retries=retries,
51
- url=self._get_absolute_url(path=path, query_params=None),
70
+ url=self._get_absolute_url(path=path, query_params=params),
52
71
  )
53
72
 
54
73
  @staticmethod
@@ -67,6 +86,9 @@ class ApiClient(HttpClient):
67
86
  if access_token and authenticate:
68
87
  headers['Authorization'] = f'Bearer {access_token}'
69
88
 
89
+ headers['client-type'] = 'biolib-python'
90
+ headers['client-version'] = ApiClient._biolib_package_version
91
+
70
92
  return headers
71
93
 
72
94
  @staticmethod
biolib/app/app.py CHANGED
@@ -1,29 +1,26 @@
1
- import os
2
1
  import io
3
- import random
4
2
  import json
3
+ import os
4
+ import random
5
5
  import string
6
-
7
6
  from pathlib import Path
7
+
8
8
  from biolib import utils
9
- from biolib.compute_node.job_worker.job_storage import JobStorage
10
- from biolib.compute_node.job_worker.job_worker import JobWorker
11
- from biolib.experiments.experiment import Experiment
12
- from biolib.jobs import Job
13
- from biolib.typing_utils import Optional, cast
14
- from biolib.biolib_api_client import CreatedJobDict, JobState
15
- from biolib.jobs.types import JobDict
9
+ from biolib.biolib_api_client import JobState
16
10
  from biolib.biolib_api_client.app_types import App, AppVersion
17
- from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
18
11
  from biolib.biolib_api_client.biolib_app_api import BiolibAppApi
12
+ from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
19
13
  from biolib.biolib_binary_format import ModuleInput
20
14
  from biolib.biolib_errors import BioLibError
21
15
  from biolib.biolib_logging import logger
16
+ from biolib.compute_node.job_worker.job_worker import JobWorker
17
+ from biolib.experiments.experiment import Experiment
18
+ from biolib.jobs import Job
19
+ from biolib.typing_utils import Optional
22
20
  from biolib.utils.app_uri import parse_app_uri
23
21
 
24
22
 
25
23
  class BioLibApp:
26
-
27
24
  def __init__(self, uri: str):
28
25
  app_response = BiolibAppApi.get_by_uri(uri)
29
26
  self._app: App = app_response['app']
@@ -48,17 +45,18 @@ class BioLibApp:
48
45
  return self._app_version
49
46
 
50
47
  def cli(
51
- self,
52
- args=None,
53
- stdin=None,
54
- files=None,
55
- override_command=False,
56
- machine='',
57
- blocking: bool = True,
58
- experiment_id: Optional[str] = None,
59
- result_prefix: Optional[str] = None,
60
- timeout: Optional[int] = None,
61
- notify: bool = False,
48
+ self,
49
+ args=None,
50
+ stdin=None,
51
+ files=None,
52
+ override_command=False,
53
+ machine='',
54
+ blocking: bool = True,
55
+ experiment_id: Optional[str] = None,
56
+ result_prefix: Optional[str] = None,
57
+ timeout: Optional[int] = None,
58
+ notify: bool = False,
59
+ machine_count: Optional[int] = None,
62
60
  ) -> Job:
63
61
  if not experiment_id:
64
62
  experiment = Experiment.get_experiment_in_context()
@@ -78,7 +76,9 @@ class BioLibApp:
78
76
 
79
77
  return self._run_locally(module_input_serialized)
80
78
 
81
- job = self._start_in_cloud(
79
+ job = Job._start_job_in_cloud( # pylint: disable=protected-access
80
+ app_uri=self._app_uri,
81
+ app_version_uuid=self._app_version['public_id'],
82
82
  experiment_id=experiment_id,
83
83
  machine=machine,
84
84
  module_input_serialized=module_input_serialized,
@@ -86,6 +86,7 @@ class BioLibApp:
86
86
  override_command=override_command,
87
87
  result_prefix=result_prefix,
88
88
  timeout=timeout,
89
+ requested_machine_count=machine_count,
89
90
  )
90
91
  if blocking:
91
92
  # TODO: Deprecate utils.STREAM_STDOUT and always stream logs by simply calling job.stream_logs()
@@ -93,8 +94,8 @@ class BioLibApp:
93
94
  utils.STREAM_STDOUT = True
94
95
 
95
96
  enable_print = bool(
96
- utils.STREAM_STDOUT and
97
- (self._app_version.get('main_output_file') or self._app_version.get('stdout_render_type') == 'text')
97
+ utils.STREAM_STDOUT
98
+ and (self._app_version.get('main_output_file') or self._app_version.get('stdout_render_type') == 'text')
98
99
  )
99
100
  job._stream_logs(enable_print=enable_print) # pylint: disable=protected-access
100
101
 
@@ -108,11 +109,11 @@ class BioLibApp:
108
109
  self.cli()
109
110
 
110
111
  else:
111
- raise BioLibError('''
112
+ raise BioLibError("""
112
113
  Calling an app directly with app() is currently being reworked.
113
114
  To use the previous functionality, please call app.cli() instead.
114
115
  Example: "app.cli('--help')"
115
- ''')
116
+ """)
116
117
 
117
118
  @staticmethod
118
119
  def _get_serialized_module_input(args=None, stdin=None, files=None) -> bytes:
@@ -142,9 +143,9 @@ Example: "app.cli('--help')"
142
143
  args[idx] = Path(arg).name
143
144
 
144
145
  # support --myarg=file.txt
145
- elif os.path.isfile(arg.split("=")[-1]) or os.path.isdir(arg.split("=")[-1]):
146
- files.append(arg.split("=")[-1])
147
- args[idx] = arg.split("=")[0] + '=' + Path(arg.split("=")[-1]).name
146
+ elif os.path.isfile(arg.split('=')[-1]) or os.path.isdir(arg.split('=')[-1]):
147
+ files.append(arg.split('=')[-1])
148
+ args[idx] = arg.split('=')[0] + '=' + Path(arg.split('=')[-1]).name
148
149
  else:
149
150
  pass # a normal string arg was given
150
151
  else:
@@ -154,7 +155,7 @@ Example: "app.cli('--help')"
154
155
  elif isinstance(arg, io.BytesIO):
155
156
  file_data = arg.getvalue()
156
157
  else:
157
- raise Exception(f"Unexpected type of argument: {arg}")
158
+ raise Exception(f'Unexpected type of argument: {arg}')
158
159
  files_dict[f'/{tmp_filename}'] = file_data
159
160
  args[idx] = tmp_filename
160
161
 
@@ -192,48 +193,10 @@ Example: "app.cli('--help')"
192
193
  )
193
194
  return module_input_serialized
194
195
 
195
- def _start_in_cloud(
196
- self,
197
- module_input_serialized: bytes,
198
- override_command: bool = False,
199
- machine: Optional[str] = None,
200
- experiment_id: Optional[str] = None,
201
- result_prefix: Optional[str] = None,
202
- timeout: Optional[int] = None,
203
- notify: bool = False,
204
- ) -> Job:
205
- if len(module_input_serialized) < 500_000:
206
- _job_dict = BiolibJobApi.create_job_with_data(
207
- app_resource_name_prefix=parse_app_uri(self._app_uri)['resource_name_prefix'],
208
- app_version_uuid=self._app_version['public_id'],
209
- arguments_override_command=override_command,
210
- experiment_uuid=experiment_id,
211
- module_input_serialized=module_input_serialized,
212
- notify=notify,
213
- requested_machine=machine,
214
- requested_timeout_seconds=timeout,
215
- result_name_prefix=result_prefix,
216
- )
217
- return Job(cast(JobDict, _job_dict))
218
-
219
- job_dict: CreatedJobDict = BiolibJobApi.create(
220
- app_resource_name_prefix=parse_app_uri(self._app_uri)['resource_name_prefix'],
221
- app_version_id=self._app_version['public_id'],
222
- experiment_uuid=experiment_id,
223
- machine=machine,
224
- notify=notify,
225
- override_command=override_command,
226
- timeout=timeout,
227
- )
228
- JobStorage.upload_module_input(job=job_dict, module_input_serialized=module_input_serialized)
229
- cloud_job = BiolibJobApi.create_cloud_job(job_id=job_dict['public_id'], result_name_prefix=result_prefix)
230
- logger.debug(f"Cloud: Job created with id {cloud_job['public_id']}")
231
- return Job(cast(JobDict, job_dict))
232
-
233
196
  def _run_locally(self, module_input_serialized: bytes) -> Job:
234
197
  job_dict = BiolibJobApi.create(
235
198
  app_version_id=self._app_version['public_id'],
236
- app_resource_name_prefix=parse_app_uri(self._app_uri)['resource_name_prefix']
199
+ app_resource_name_prefix=parse_app_uri(self._app_uri)['resource_name_prefix'],
237
200
  )
238
201
  job = Job(job_dict)
239
202
 
@@ -263,7 +226,12 @@ Example: "app.cli('--help')"
263
226
  if not key.startswith('--'):
264
227
  key = f'--{key}'
265
228
 
266
- args.extend([key, value])
229
+ args.append(key)
230
+ if isinstance(value, list):
231
+ # TODO: only do this if argument key is of type file list
232
+ args.extend(value)
233
+ else:
234
+ args.append(value)
267
235
 
268
236
  return self.cli(args, **biolib_kwargs)
269
237