pybiolib 0.2.951__py3-none-any.whl → 1.2.1890__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. biolib/__init__.py +357 -11
  2. biolib/_data_record/data_record.py +380 -0
  3. biolib/_index/__init__.py +0 -0
  4. biolib/_index/index.py +55 -0
  5. biolib/_index/query_result.py +103 -0
  6. biolib/_internal/__init__.py +0 -0
  7. biolib/_internal/add_copilot_prompts.py +58 -0
  8. biolib/_internal/add_gui_files.py +81 -0
  9. biolib/_internal/data_record/__init__.py +1 -0
  10. biolib/_internal/data_record/data_record.py +85 -0
  11. biolib/_internal/data_record/push_data.py +116 -0
  12. biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
  13. biolib/_internal/errors.py +5 -0
  14. biolib/_internal/file_utils.py +125 -0
  15. biolib/_internal/fuse_mount/__init__.py +1 -0
  16. biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
  17. biolib/_internal/http_client.py +159 -0
  18. biolib/_internal/lfs/__init__.py +1 -0
  19. biolib/_internal/lfs/cache.py +51 -0
  20. biolib/_internal/libs/__init__.py +1 -0
  21. biolib/_internal/libs/fusepy/__init__.py +1257 -0
  22. biolib/_internal/push_application.py +488 -0
  23. biolib/_internal/runtime.py +22 -0
  24. biolib/_internal/string_utils.py +13 -0
  25. biolib/_internal/templates/__init__.py +1 -0
  26. biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
  27. biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
  28. biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
  29. biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
  30. biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
  31. biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
  32. biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
  33. biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
  34. biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
  35. biolib/_internal/templates/gitignore_template/.gitignore +10 -0
  36. biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
  37. biolib/_internal/templates/gui_template/App.tsx +53 -0
  38. biolib/_internal/templates/gui_template/Dockerfile +27 -0
  39. biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
  40. biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
  41. biolib/_internal/templates/gui_template/index.css +5 -0
  42. biolib/_internal/templates/gui_template/index.html +13 -0
  43. biolib/_internal/templates/gui_template/index.tsx +10 -0
  44. biolib/_internal/templates/gui_template/package.json +27 -0
  45. biolib/_internal/templates/gui_template/tsconfig.json +24 -0
  46. biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
  47. biolib/_internal/templates/gui_template/vite.config.mts +10 -0
  48. biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
  49. biolib/_internal/templates/init_template/Dockerfile +14 -0
  50. biolib/_internal/templates/init_template/requirements.txt +1 -0
  51. biolib/_internal/templates/init_template/run.py +12 -0
  52. biolib/_internal/templates/init_template/run.sh +4 -0
  53. biolib/_internal/templates/templates.py +25 -0
  54. biolib/_internal/tree_utils.py +106 -0
  55. biolib/_internal/utils/__init__.py +65 -0
  56. biolib/_internal/utils/auth.py +46 -0
  57. biolib/_internal/utils/job_url.py +33 -0
  58. biolib/_internal/utils/multinode.py +263 -0
  59. biolib/_runtime/runtime.py +157 -0
  60. biolib/_session/session.py +44 -0
  61. biolib/_shared/__init__.py +0 -0
  62. biolib/_shared/types/__init__.py +74 -0
  63. biolib/_shared/types/account.py +12 -0
  64. biolib/_shared/types/account_member.py +8 -0
  65. biolib/_shared/types/app.py +9 -0
  66. biolib/_shared/types/data_record.py +40 -0
  67. biolib/_shared/types/experiment.py +32 -0
  68. biolib/_shared/types/file_node.py +17 -0
  69. biolib/_shared/types/push.py +6 -0
  70. biolib/_shared/types/resource.py +37 -0
  71. biolib/_shared/types/resource_deploy_key.py +11 -0
  72. biolib/_shared/types/resource_permission.py +14 -0
  73. biolib/_shared/types/resource_version.py +19 -0
  74. biolib/_shared/types/result.py +14 -0
  75. biolib/_shared/types/typing.py +10 -0
  76. biolib/_shared/types/user.py +19 -0
  77. biolib/_shared/utils/__init__.py +7 -0
  78. biolib/_shared/utils/resource_uri.py +75 -0
  79. biolib/api/__init__.py +6 -0
  80. biolib/api/client.py +168 -0
  81. biolib/app/app.py +252 -49
  82. biolib/app/search_apps.py +45 -0
  83. biolib/biolib_api_client/api_client.py +126 -31
  84. biolib/biolib_api_client/app_types.py +24 -4
  85. biolib/biolib_api_client/auth.py +31 -8
  86. biolib/biolib_api_client/biolib_app_api.py +147 -52
  87. biolib/biolib_api_client/biolib_job_api.py +161 -141
  88. biolib/biolib_api_client/job_types.py +21 -5
  89. biolib/biolib_api_client/lfs_types.py +7 -23
  90. biolib/biolib_api_client/user_state.py +56 -0
  91. biolib/biolib_binary_format/__init__.py +1 -4
  92. biolib/biolib_binary_format/file_in_container.py +105 -0
  93. biolib/biolib_binary_format/module_input.py +24 -7
  94. biolib/biolib_binary_format/module_output_v2.py +149 -0
  95. biolib/biolib_binary_format/remote_endpoints.py +34 -0
  96. biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
  97. biolib/biolib_binary_format/saved_job.py +3 -2
  98. biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
  99. biolib/biolib_binary_format/system_status_update.py +3 -2
  100. biolib/biolib_binary_format/utils.py +175 -0
  101. biolib/biolib_docker_client/__init__.py +11 -2
  102. biolib/biolib_errors.py +36 -0
  103. biolib/biolib_logging.py +27 -10
  104. biolib/cli/__init__.py +38 -0
  105. biolib/cli/auth.py +46 -0
  106. biolib/cli/data_record.py +164 -0
  107. biolib/cli/index.py +32 -0
  108. biolib/cli/init.py +421 -0
  109. biolib/cli/lfs.py +101 -0
  110. biolib/cli/push.py +50 -0
  111. biolib/cli/run.py +63 -0
  112. biolib/cli/runtime.py +14 -0
  113. biolib/cli/sdk.py +16 -0
  114. biolib/cli/start.py +56 -0
  115. biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
  116. biolib/compute_node/job_worker/cache_state.py +66 -88
  117. biolib/compute_node/job_worker/cache_types.py +1 -6
  118. biolib/compute_node/job_worker/docker_image_cache.py +112 -37
  119. biolib/compute_node/job_worker/executors/__init__.py +0 -3
  120. biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
  121. biolib/compute_node/job_worker/executors/docker_types.py +9 -1
  122. biolib/compute_node/job_worker/executors/types.py +19 -9
  123. biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
  124. biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
  125. biolib/compute_node/job_worker/job_storage.py +108 -0
  126. biolib/compute_node/job_worker/job_worker.py +397 -212
  127. biolib/compute_node/job_worker/large_file_system.py +87 -38
  128. biolib/compute_node/job_worker/network_alloc.py +99 -0
  129. biolib/compute_node/job_worker/network_buffer.py +240 -0
  130. biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
  131. biolib/compute_node/job_worker/utils.py +9 -24
  132. biolib/compute_node/remote_host_proxy.py +400 -98
  133. biolib/compute_node/utils.py +31 -9
  134. biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
  135. biolib/compute_node/webserver/proxy_utils.py +28 -0
  136. biolib/compute_node/webserver/webserver.py +130 -44
  137. biolib/compute_node/webserver/webserver_types.py +2 -6
  138. biolib/compute_node/webserver/webserver_utils.py +77 -12
  139. biolib/compute_node/webserver/worker_thread.py +183 -42
  140. biolib/experiments/__init__.py +0 -0
  141. biolib/experiments/experiment.py +356 -0
  142. biolib/jobs/__init__.py +1 -0
  143. biolib/jobs/job.py +741 -0
  144. biolib/jobs/job_result.py +185 -0
  145. biolib/jobs/types.py +50 -0
  146. biolib/py.typed +0 -0
  147. biolib/runtime/__init__.py +14 -0
  148. biolib/sdk/__init__.py +91 -0
  149. biolib/tables.py +34 -0
  150. biolib/typing_utils.py +2 -7
  151. biolib/user/__init__.py +1 -0
  152. biolib/user/sign_in.py +54 -0
  153. biolib/utils/__init__.py +162 -0
  154. biolib/utils/cache_state.py +94 -0
  155. biolib/utils/multipart_uploader.py +194 -0
  156. biolib/utils/seq_util.py +150 -0
  157. biolib/utils/zip/remote_zip.py +640 -0
  158. pybiolib-1.2.1890.dist-info/METADATA +41 -0
  159. pybiolib-1.2.1890.dist-info/RECORD +177 -0
  160. {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
  161. pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
  162. README.md +0 -17
  163. biolib/app/app_result.py +0 -68
  164. biolib/app/utils.py +0 -62
  165. biolib/biolib-js/0-biolib.worker.js +0 -1
  166. biolib/biolib-js/1-biolib.worker.js +0 -1
  167. biolib/biolib-js/2-biolib.worker.js +0 -1
  168. biolib/biolib-js/3-biolib.worker.js +0 -1
  169. biolib/biolib-js/4-biolib.worker.js +0 -1
  170. biolib/biolib-js/5-biolib.worker.js +0 -1
  171. biolib/biolib-js/6-biolib.worker.js +0 -1
  172. biolib/biolib-js/index.html +0 -10
  173. biolib/biolib-js/main-biolib.js +0 -1
  174. biolib/biolib_api_client/biolib_account_api.py +0 -21
  175. biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
  176. biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
  177. biolib/biolib_binary_format/module_output.py +0 -58
  178. biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
  179. biolib/biolib_push.py +0 -114
  180. biolib/cli.py +0 -203
  181. biolib/cli_utils.py +0 -273
  182. biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
  183. biolib/compute_node/enclave/__init__.py +0 -2
  184. biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
  185. biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
  186. biolib/compute_node/job_worker/executors/base_executor.py +0 -18
  187. biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
  188. biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
  189. biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
  190. biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
  191. biolib/lfs.py +0 -196
  192. biolib/pyppeteer/.circleci/config.yml +0 -100
  193. biolib/pyppeteer/.coveragerc +0 -3
  194. biolib/pyppeteer/.gitignore +0 -89
  195. biolib/pyppeteer/.pre-commit-config.yaml +0 -28
  196. biolib/pyppeteer/CHANGES.md +0 -253
  197. biolib/pyppeteer/CONTRIBUTING.md +0 -26
  198. biolib/pyppeteer/LICENSE +0 -12
  199. biolib/pyppeteer/README.md +0 -137
  200. biolib/pyppeteer/docs/Makefile +0 -177
  201. biolib/pyppeteer/docs/_static/custom.css +0 -28
  202. biolib/pyppeteer/docs/_templates/layout.html +0 -10
  203. biolib/pyppeteer/docs/changes.md +0 -1
  204. biolib/pyppeteer/docs/conf.py +0 -299
  205. biolib/pyppeteer/docs/index.md +0 -21
  206. biolib/pyppeteer/docs/make.bat +0 -242
  207. biolib/pyppeteer/docs/reference.md +0 -211
  208. biolib/pyppeteer/docs/server.py +0 -60
  209. biolib/pyppeteer/poetry.lock +0 -1699
  210. biolib/pyppeteer/pyppeteer/__init__.py +0 -135
  211. biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
  212. biolib/pyppeteer/pyppeteer/browser.py +0 -401
  213. biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
  214. biolib/pyppeteer/pyppeteer/command.py +0 -22
  215. biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
  216. biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
  217. biolib/pyppeteer/pyppeteer/coverage.py +0 -346
  218. biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
  219. biolib/pyppeteer/pyppeteer/dialog.py +0 -79
  220. biolib/pyppeteer/pyppeteer/domworld.py +0 -597
  221. biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
  222. biolib/pyppeteer/pyppeteer/errors.py +0 -48
  223. biolib/pyppeteer/pyppeteer/events.py +0 -63
  224. biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
  225. biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
  226. biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
  227. biolib/pyppeteer/pyppeteer/helpers.py +0 -245
  228. biolib/pyppeteer/pyppeteer/input.py +0 -371
  229. biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
  230. biolib/pyppeteer/pyppeteer/launcher.py +0 -683
  231. biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
  232. biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
  233. biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
  234. biolib/pyppeteer/pyppeteer/multimap.py +0 -82
  235. biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
  236. biolib/pyppeteer/pyppeteer/options.py +0 -8
  237. biolib/pyppeteer/pyppeteer/page.py +0 -1728
  238. biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
  239. biolib/pyppeteer/pyppeteer/target.py +0 -147
  240. biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
  241. biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
  242. biolib/pyppeteer/pyppeteer/tracing.py +0 -93
  243. biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
  244. biolib/pyppeteer/pyppeteer/util.py +0 -18
  245. biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
  246. biolib/pyppeteer/pyppeteer/worker.py +0 -101
  247. biolib/pyppeteer/pyproject.toml +0 -97
  248. biolib/pyppeteer/spell.txt +0 -137
  249. biolib/pyppeteer/tox.ini +0 -72
  250. biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
  251. biolib/start_cli.py +0 -7
  252. biolib/utils.py +0 -47
  253. biolib/validators/validate_app_version.py +0 -183
  254. biolib/validators/validate_argument.py +0 -134
  255. biolib/validators/validate_module.py +0 -323
  256. biolib/validators/validate_zip_file.py +0 -40
  257. biolib/validators/validator_utils.py +0 -103
  258. pybiolib-0.2.951.dist-info/LICENSE +0 -21
  259. pybiolib-0.2.951.dist-info/METADATA +0 -61
  260. pybiolib-0.2.951.dist-info/RECORD +0 -153
  261. pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
  262. /LICENSE → /pybiolib-1.2.1890.dist-info/licenses/LICENSE +0 -0
biolib/app/app.py CHANGED
@@ -1,29 +1,74 @@
1
+ import copy
2
+ import io
3
+ import json
1
4
  import os
5
+ import posixpath
6
+ import random
7
+ import string
8
+ from pathlib import Path
2
9
 
3
- from biolib.app.utils import run_job
10
+ from biolib import utils
11
+ from biolib._internal.file_utils import path_to_renamed_path
12
+ from biolib._runtime.runtime import Runtime
13
+ from biolib._shared.utils import parse_resource_uri
14
+ from biolib.api.client import ApiClient
4
15
  from biolib.biolib_api_client import JobState
5
16
  from biolib.biolib_api_client.app_types import App, AppVersion
6
- from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
7
- from biolib.app.app_result import AppResult
8
17
  from biolib.biolib_api_client.biolib_app_api import BiolibAppApi
18
+ from biolib.biolib_api_client.biolib_job_api import BiolibJobApi
9
19
  from biolib.biolib_binary_format import ModuleInput
10
- from biolib.biolib_errors import BioLibError
20
+ from biolib.biolib_errors import BioLibError, JobResultNonZeroExitCodeError
11
21
  from biolib.biolib_logging import logger
22
+ from biolib.compute_node.job_worker.job_worker import JobWorker
23
+ from biolib.experiments.experiment import Experiment
24
+ from biolib.jobs.job import Result
25
+ from biolib.typing_utils import Dict, Optional
26
+
27
+
28
+ class JsonStringIO(io.StringIO):
29
+ pass
12
30
 
13
31
 
14
32
  class BioLibApp:
33
+ def __init__(
34
+ self,
35
+ uri: str,
36
+ _api_client: Optional[ApiClient] = None,
37
+ suppress_version_warning: bool = False,
38
+ _experiment: Optional[str] = None,
39
+ ):
40
+ self._api_client: Optional[ApiClient] = _api_client
41
+ self._experiment = _experiment
42
+ self._input_uri = uri
43
+ self._parsed_input_uri = parse_resource_uri(uri)
15
44
 
16
- def __init__(self, uri: str):
17
- app_response = BiolibAppApi.get_by_uri(uri)
45
+ app_response = BiolibAppApi.get_by_uri(uri=uri, api_client=self._api_client)
18
46
  self._app: App = app_response['app']
19
47
  self._app_uri = app_response['app_uri']
20
48
  self._app_version: AppVersion = app_response['app_version']
21
49
 
22
- logger.info(f'Loaded project {self._app_uri}')
50
+ if not suppress_version_warning:
51
+ if self._parsed_input_uri['version'] is None:
52
+ if Runtime.check_is_environment_biolib_app():
53
+ logger.warning(
54
+ f"No version specified in URI '{uri}'. This will use the default version, "
55
+ f'which may change behaviour over time. Consider locking down the exact version, '
56
+ f"e.g. '{uri}:1.2.3'"
57
+ )
58
+
59
+ if self._parsed_input_uri['tag']:
60
+ semantic_version = f"{self._app_version['major']}.{self._app_version['minor']}.{self._app_version['patch']}"
61
+ logger.info(f'Loaded {self._input_uri} (resolved to {semantic_version})')
62
+ else:
63
+ logger.info(f'Loaded {self._app_uri}')
23
64
 
24
65
  def __str__(self) -> str:
25
66
  return self._app_uri
26
67
 
68
+ @property
69
+ def uri(self) -> str:
70
+ return self._app_uri
71
+
27
72
  @property
28
73
  def uuid(self) -> str:
29
74
  return self._app['public_id']
@@ -32,50 +77,98 @@ class BioLibApp:
32
77
  def version(self) -> AppVersion:
33
78
  return self._app_version
34
79
 
35
- def cli(self, args=None, stdin=None, files=None):
80
+ def cli(
81
+ self,
82
+ args=None,
83
+ stdin=None,
84
+ files=None,
85
+ override_command=False,
86
+ machine='',
87
+ blocking: bool = True,
88
+ experiment_id: Optional[str] = None,
89
+ result_prefix: Optional[str] = None,
90
+ timeout: Optional[int] = None,
91
+ notify: bool = False,
92
+ max_workers: Optional[int] = None,
93
+ experiment: Optional[str] = None,
94
+ temporary_client_secrets: Optional[Dict[str, str]] = None,
95
+ check: bool = False,
96
+ stream_logs: bool = False,
97
+ ) -> Result:
98
+ if experiment_id and experiment:
99
+ raise ValueError('Only one of experiment_id and experiment can be specified')
100
+
101
+ if check and not blocking:
102
+ raise ValueError('The argument "check" cannot be True when blocking is False')
103
+
104
+ if not experiment_id:
105
+ experiment_to_use = experiment if experiment is not None else self._experiment
106
+ experiment_instance: Optional[Experiment]
107
+ if experiment_to_use:
108
+ experiment_instance = Experiment(experiment_to_use, _api_client=self._api_client)
109
+ else:
110
+ experiment_instance = Experiment.get_experiment_in_context()
111
+ experiment_id = experiment_instance.uuid if experiment_instance else None
112
+
36
113
  module_input_serialized = self._get_serialized_module_input(args, stdin, files)
37
114
 
38
- job = BiolibJobApi.create(self._app_version['public_id'])
39
- BiolibJobApi.update_state(job['public_id'], JobState.IN_PROGRESS.value)
115
+ if machine == 'local':
116
+ raise BioLibError('Running applications locally with machine="local" is no longer supported.')
40
117
 
41
- try:
42
- module_output = run_job(job, module_input_serialized)
43
- try:
44
- BiolibJobApi.update_state(job_id=job['public_id'], state=JobState.COMPLETED.value)
45
- except Exception as error: # pylint: disable=broad-except
46
- logger.warning(f'Could not update job state to completed:\n{error}')
47
-
48
- return AppResult(
49
- exitcode=module_output['exit_code'],
50
- stderr=module_output['stderr'],
51
- stdout=module_output['stdout'],
52
- files=module_output['files']
118
+ job = Result._start_job_in_cloud( # pylint: disable=protected-access
119
+ app_uri=self._app_uri,
120
+ app_version_uuid=self._app_version['public_id'],
121
+ experiment_id=experiment_id,
122
+ machine=machine,
123
+ module_input_serialized=module_input_serialized,
124
+ notify=notify,
125
+ override_command=override_command,
126
+ result_prefix=result_prefix,
127
+ timeout=timeout,
128
+ requested_machine_count=max_workers,
129
+ temporary_client_secrets=temporary_client_secrets,
130
+ api_client=self._api_client,
131
+ )
132
+ if utils.IS_RUNNING_IN_NOTEBOOK:
133
+ logger.info(f'View the result in your browser at: {utils.BIOLIB_BASE_URL}/results/{job.id}/')
134
+ if blocking:
135
+ # TODO: Deprecate utils.STREAM_STDOUT and always stream logs by simply calling job.stream_logs()
136
+ if utils.IS_RUNNING_IN_NOTEBOOK:
137
+ utils.STREAM_STDOUT = True
138
+
139
+ enable_print = bool(
140
+ (utils.STREAM_STDOUT or stream_logs)
141
+ and (self._app_version.get('main_output_file') or self._app_version.get('stdout_render_type') == 'text')
53
142
  )
143
+ job._stream_logs(enable_print=enable_print) # pylint: disable=protected-access
144
+
145
+ if check:
146
+ exit_code = job.get_exit_code()
147
+ if exit_code != 0:
148
+ raise JobResultNonZeroExitCodeError(exit_code)
54
149
 
55
- except BioLibError as exception:
56
- logger.error(f'Compute failed with: {exception.message}')
57
- try:
58
- BiolibJobApi.update_state(job_id=job['public_id'], state=JobState.FAILED.value)
59
- except Exception as error: # pylint: disable=broad-except
60
- logger.warning(f'Could not update job state to failed:\n{error}')
150
+ return job
61
151
 
62
- raise exception
152
+ def exec(self, args=None, stdin=None, files=None, machine=''):
153
+ return self.cli(args, stdin, files, override_command=True, machine=machine)
63
154
 
64
155
  def __call__(self, *args, **kwargs):
65
156
  if not args and not kwargs:
66
157
  self.cli()
67
158
 
68
159
  else:
69
- raise BioLibError('''
160
+ raise BioLibError("""
70
161
  Calling an app directly with app() is currently being reworked.
71
- To use the previous functionality, please call app.cli() instead.
162
+ To use the previous functionality, please call app.cli() instead.
72
163
  Example: "app.cli('--help')"
73
- ''')
164
+ """)
74
165
 
75
166
  @staticmethod
76
167
  def _get_serialized_module_input(args=None, stdin=None, files=None) -> bytes:
77
168
  if args is None:
78
169
  args = []
170
+ else:
171
+ args = copy.copy(args)
79
172
 
80
173
  if stdin is None:
81
174
  stdin = b''
@@ -91,26 +184,136 @@ Example: "app.cli('--help')"
91
184
 
92
185
  if files is None:
93
186
  files = []
94
- for idx, arg in enumerate(args):
95
- if os.path.isfile(arg):
96
- files.append(arg)
97
- args[idx] = arg.split('/')[-1]
98
187
 
99
- cwd = os.getcwd()
100
188
  files_dict = {}
189
+ if isinstance(files, list):
190
+ for file_path in files:
191
+ path = Path(file_path)
192
+ if path.is_dir():
193
+ renamed_dir = path_to_renamed_path(file_path)
194
+ for filename in path.rglob('*'):
195
+ if filename.is_dir():
196
+ continue
197
+ with open(filename, 'rb') as f:
198
+ relative_to_dir = filename.resolve().relative_to(path.resolve())
199
+ files_dict[posixpath.join(renamed_dir, relative_to_dir.as_posix())] = f.read()
200
+ else:
201
+ with open(path, 'rb') as f:
202
+ files_dict[path_to_renamed_path(str(path))] = f.read()
203
+ elif isinstance(files, dict):
204
+ files_dict = {}
205
+ for key, value in files.items():
206
+ if '//' in key:
207
+ raise BioLibError(f"File path '{key}' contains double slashes which are not allowed")
208
+ if not key.startswith('/'):
209
+ key = '/' + key
210
+ files_dict[key] = value
211
+ else:
212
+ raise Exception('The given files input must be list or dict or None')
101
213
 
102
- for file in files:
103
- path = file
104
- if not file.startswith('/'):
105
- # make path absolute
106
- path = cwd + '/' + file
107
-
108
- arg_split = path.split('/')
109
- file = open(path, 'rb')
110
- path = '/' + arg_split[-1]
214
+ for idx, arg in enumerate(args):
215
+ if isinstance(arg, str):
216
+ if os.path.isfile(arg) or os.path.isdir(arg):
217
+ if os.path.isfile(arg):
218
+ with open(arg, 'rb') as f:
219
+ files_dict[path_to_renamed_path(arg)] = f.read()
220
+ elif os.path.isdir(arg):
221
+ path = Path(arg)
222
+ renamed_dir = path_to_renamed_path(arg)
223
+ for filename in path.rglob('*'):
224
+ if filename.is_dir():
225
+ continue
226
+ with open(filename, 'rb') as f:
227
+ relative_to_dir = filename.resolve().relative_to(path.resolve())
228
+ files_dict[posixpath.join(renamed_dir, relative_to_dir.as_posix())] = f.read()
229
+ args[idx] = path_to_renamed_path(arg, prefix_with_slash=False)
111
230
 
112
- files_dict[path] = file.read()
113
- file.close()
231
+ # support --myarg=file.txt
232
+ elif os.path.isfile(arg.split('=')[-1]) or os.path.isdir(arg.split('=')[-1]):
233
+ file_path = arg.split('=')[-1]
234
+ if os.path.isfile(file_path):
235
+ with open(file_path, 'rb') as f:
236
+ files_dict[path_to_renamed_path(file_path)] = f.read()
237
+ elif os.path.isdir(file_path):
238
+ path = Path(file_path)
239
+ renamed_dir = path_to_renamed_path(file_path)
240
+ for filename in path.rglob('*'):
241
+ if filename.is_dir():
242
+ continue
243
+ with open(filename, 'rb') as f:
244
+ relative_to_dir = filename.resolve().relative_to(path.resolve())
245
+ files_dict[posixpath.join(renamed_dir, relative_to_dir.as_posix())] = f.read()
246
+ args[idx] = arg.split('=')[0] + '=' + path_to_renamed_path(file_path, prefix_with_slash=False)
247
+ else:
248
+ pass # a normal string arg was given
249
+ else:
250
+ tmp_filename = f'input_{"".join(random.choices(string.ascii_letters + string.digits, k=7))}'
251
+ if isinstance(arg, JsonStringIO):
252
+ file_data = arg.getvalue().encode()
253
+ tmp_filename += '.json'
254
+ elif isinstance(arg, io.StringIO):
255
+ file_data = arg.getvalue().encode()
256
+ elif isinstance(arg, io.BytesIO):
257
+ file_data = arg.getvalue()
258
+ else:
259
+ raise Exception(f'Unexpected type of argument: {arg}')
260
+ files_dict[f'/{tmp_filename}'] = file_data
261
+ args[idx] = tmp_filename
114
262
 
115
- module_input_serialized: bytes = ModuleInput().serialize(stdin=stdin, arguments=args, files=files_dict)
263
+ module_input_serialized: bytes = ModuleInput().serialize(
264
+ stdin=stdin,
265
+ arguments=args,
266
+ files=files_dict,
267
+ )
116
268
  return module_input_serialized
269
+
270
+ def _run_locally(self, module_input_serialized: bytes) -> Result:
271
+ job_dict = BiolibJobApi.create(
272
+ app_version_id=self._app_version['public_id'],
273
+ app_resource_name_prefix=parse_resource_uri(self._app_uri)['resource_prefix'],
274
+ )
275
+ job = Result(job_dict)
276
+
277
+ try:
278
+ BiolibJobApi.update_state(job.id, JobState.IN_PROGRESS)
279
+ module_output = JobWorker().run_job_locally(job_dict, module_input_serialized)
280
+ job._set_result_module_output(module_output) # pylint: disable=protected-access
281
+ BiolibJobApi.update_state(job.id, JobState.COMPLETED)
282
+ except BaseException as error:
283
+ BiolibJobApi.update_state(job.id, JobState.FAILED)
284
+ raise error
285
+
286
+ return job
287
+
288
+ def run(self, **kwargs) -> Result:
289
+ args = []
290
+ biolib_kwargs = {}
291
+ for key, value in kwargs.items():
292
+ if key.startswith('biolib_'):
293
+ biolib_kwarg_key = key.replace('biolib_', '')
294
+ biolib_kwargs[biolib_kwarg_key] = value
295
+ continue
296
+
297
+ if isinstance(value, dict):
298
+ value = JsonStringIO(json.dumps(value))
299
+ elif isinstance(value, (int, float)): # Cast numeric values to strings
300
+ value = str(value)
301
+
302
+ if not key.startswith('--'):
303
+ key = f'--{key}'
304
+
305
+ args.append(key)
306
+ if isinstance(value, list):
307
+ # TODO: only do this if argument key is of type file list
308
+ args.extend(value)
309
+ else:
310
+ args.append(value)
311
+
312
+ # Set check=True by default if not explicitly provided and not in non-blocking mode
313
+ if 'check' not in biolib_kwargs and biolib_kwargs.get('blocking', True) is not False:
314
+ biolib_kwargs['check'] = True
315
+
316
+ return self.cli(args, **biolib_kwargs)
317
+
318
+ def start(self, **kwargs) -> Result:
319
+ return self.run(biolib_blocking=False, **kwargs)
@@ -0,0 +1,45 @@
1
+ # TODO: Fix ignore of type
2
+ # type: ignore
3
+ from biolib import api, utils
4
+ from biolib.typing_utils import Optional, List
5
+
6
+
7
+ def search_apps(
8
+ search_query: Optional[str] = None,
9
+ team: Optional[str] = None,
10
+ count: int = 100,
11
+ ) -> List[str]:
12
+ query_exceeded_page_size = False
13
+ params = {
14
+ 'page_size': count,
15
+ }
16
+ if team:
17
+ if not team.startswith('@'):
18
+ team = '@biolib.com/' + team
19
+ params['account_handle'] = team
20
+
21
+ if search_query:
22
+ params['search'] = search_query
23
+
24
+ apps_json = api.client.get(path='/apps/', params=params).json()
25
+ if apps_json['count'] > count:
26
+ query_exceeded_page_size = True
27
+
28
+ apps = [app['resource_uri'] for app in apps_json['results']]
29
+
30
+ if not utils.BASE_URL_IS_PUBLIC_BIOLIB and (not team or team.lower().startswith('@biolib.com')):
31
+ # Also get federated apps if running on enterprise deployment
32
+ public_biolib_apps_json = api.client.get(
33
+ authenticate=False,
34
+ path='https://biolib.com/api/apps/',
35
+ params=params,
36
+ ).json()
37
+ if public_biolib_apps_json['count'] > count:
38
+ query_exceeded_page_size = True
39
+
40
+ apps.extend([f"@biolib.com/{app['resource_uri']}" for app in public_biolib_apps_json['results']])
41
+
42
+ if query_exceeded_page_size:
43
+ print(f'Search results exceeded {count}, use the argument "count" to increase the amount of results returned')
44
+
45
+ return apps
@@ -1,46 +1,130 @@
1
1
  import os
2
+ from datetime import datetime, timezone
2
3
  from json.decoder import JSONDecodeError
3
4
 
4
- import requests
5
- from biolib.typing_utils import Optional
5
+ from biolib._internal.http_client import HttpClient
6
+ from biolib._internal.utils.auth import decode_jwt_without_checking_signature
7
+ from biolib._runtime.runtime import Runtime
6
8
  from biolib.biolib_errors import BioLibError
7
- from biolib.biolib_logging import logger
9
+ from biolib.biolib_logging import logger, logger_no_user_data
10
+ from biolib.typing_utils import Optional, TypedDict
11
+
12
+ from .user_state import UserState
13
+
14
+
15
+ class UserTokens(TypedDict):
16
+ access: str
17
+ refresh: str
8
18
 
9
19
 
10
20
  class _ApiClient:
11
21
  def __init__(self, base_url: str, access_token: Optional[str] = None):
12
22
  self.base_url: str = base_url
23
+ self.access_token: Optional[str] = access_token # TODO: Deprecate passing access_token in constructor
13
24
  self.refresh_token: Optional[str] = None
14
- self.access_token: Optional[str] = access_token
25
+ self.resource_deploy_key: Optional[str] = None
26
+
27
+ self._user_state = UserState()
28
+ self._sign_in_attempted: bool = False
15
29
 
16
30
  @property
17
31
  def is_signed_in(self) -> bool:
18
- return self.refresh_token is not None and self.access_token is not None
32
+ return bool(self.refresh_token or self.resource_deploy_key)
19
33
 
20
- def login(self, api_token: Optional[str], exit_on_failure=False):
21
- if not api_token:
22
- if exit_on_failure:
23
- raise BioLibError('Error: Attempted login, but BIOLIB_TOKEN was not set, exiting...')
24
- else:
25
- logger.debug('Attempted login, but BIOLIB_TOKEN was not set, so continuing without logging in')
34
+ def set_user_tokens(self, user_tokens: UserTokens) -> None:
35
+ with self._user_state as user_state:
36
+ user_state['refresh_token'] = user_tokens['refresh']
37
+
38
+ self.access_token = user_tokens['access']
39
+ self.refresh_token = user_tokens['refresh']
40
+
41
+ def sign_out(self) -> None:
42
+ api_token = os.getenv('BIOLIB_TOKEN', default=None)
43
+ if api_token:
44
+ print('To sign out unset the environment variable "BIOLIB_TOKEN"')
45
+
46
+ self.access_token = None
47
+ self.refresh_token = None
48
+
49
+ with self._user_state as user_state:
50
+ user_state['refresh_token'] = None
51
+
52
+ def refresh_access_token(self) -> None:
53
+ if not self.is_signed_in or self.resource_deploy_key:
54
+ return
55
+
56
+ if self.access_token:
57
+ decoded_token = decode_jwt_without_checking_signature(self.access_token)
58
+ if datetime.now(tz=timezone.utc).timestamp() < decoded_token['payload']['exp'] - 60: # 60 second buffer
59
+ # Token has not expired yet
26
60
  return
27
61
 
28
- response = requests.post(
29
- f'{self.base_url}/api/user/api_tokens/exchange/',
30
- json={'token': api_token},
31
- )
62
+ # TODO: Implement nicer error handling
32
63
  try:
33
- json_response = response.json()
64
+ response = HttpClient.request(
65
+ method='POST',
66
+ url=f'{self.base_url}/api/user/token/refresh/',
67
+ data={'refresh': self.refresh_token},
68
+ )
69
+ except Exception as exception:
70
+ logger.error('Sign in with refresh token failed')
71
+ raise exception
72
+
73
+ try:
74
+ response_dict = response.json()
34
75
  except JSONDecodeError as error:
35
76
  logger.error('Could not decode response from server as JSON:')
36
77
  raise BioLibError(response.text) from error
37
- if not response.ok:
38
- logger.error('Login with API token failed:')
39
- raise BioLibError(json_response['detail'])
78
+
79
+ self.access_token = response_dict['access']
80
+
81
+ def attempt_sign_in(self) -> None:
82
+ if not self._sign_in_attempted:
83
+ self._attempt_sign_in()
84
+ self._sign_in_attempted = True
85
+
86
+ def _attempt_sign_in(self) -> None:
87
+ api_token = os.getenv('BIOLIB_TOKEN', default=None)
88
+
89
+ if api_token:
90
+ if api_token.startswith('bld_'):
91
+ self.resource_deploy_key = api_token
92
+ else:
93
+ self.sign_in_with_api_token(api_token)
40
94
  else:
41
- self.refresh_token = json_response['refresh_token']
42
- self.access_token = json_response['access_token']
43
- logger.info('Successfully authenticated')
95
+ with self._user_state as user_state:
96
+ refresh_token_from_state = user_state['refresh_token']
97
+
98
+ # TODO: Handle expired refresh token
99
+ if refresh_token_from_state:
100
+ logger_no_user_data.debug('ApiClient: Signing in with refresh token from user state...')
101
+ self.refresh_token = refresh_token_from_state
102
+ try:
103
+ self.refresh_access_token()
104
+ except Exception: # pylint: disable=broad-except
105
+ self.refresh_token = None
106
+ with self._user_state as user_state:
107
+ user_state['refresh_token'] = None
108
+
109
+ def sign_in_with_api_token(self, api_token: str) -> None:
110
+ logger_no_user_data.debug('ApiClient: Signing in with BIOLIB_TOKEN...')
111
+ try:
112
+ response = HttpClient.request(
113
+ method='POST',
114
+ url=f'{self.base_url}/api/user/api_tokens/exchange/',
115
+ data={'token': api_token},
116
+ )
117
+ except Exception as exception:
118
+ logger.error('Sign in with API token failed')
119
+ raise exception
120
+ try:
121
+ json_response = response.json()
122
+ except JSONDecodeError as error:
123
+ logger.error('Could not decode response from server as JSON')
124
+ raise BioLibError(response.text) from error
125
+
126
+ self.access_token = json_response['access_token']
127
+ self.refresh_token = json_response['refresh_token']
44
128
 
45
129
 
46
130
  class BiolibApiClient:
@@ -48,23 +132,34 @@ class BiolibApiClient:
48
132
 
49
133
  @staticmethod
50
134
  def initialize(base_url: str, access_token: Optional[str] = None):
51
- BiolibApiClient.api_client = _ApiClient(base_url, access_token)
135
+ BiolibApiClient.api_client = _ApiClient(base_url=base_url, access_token=access_token)
52
136
 
53
137
  @staticmethod
54
- def get() -> _ApiClient:
138
+ def get(attempt_sign_in: bool = True) -> _ApiClient:
55
139
  api_client = BiolibApiClient.api_client
56
- if api_client is not None:
57
- biolib_token = os.getenv('BIOLIB_TOKEN', default=None)
58
- if biolib_token is not None and not api_client.is_signed_in:
59
- api_client.login(api_token=biolib_token, exit_on_failure=True)
140
+ if api_client:
141
+ if attempt_sign_in:
142
+ api_client.attempt_sign_in()
60
143
  return api_client
144
+
145
+ raise BioLibError('Attempted to use uninitialized API client')
146
+
147
+ @staticmethod
148
+ def refresh_auth_token():
149
+ api_client = BiolibApiClient.get()
150
+ api_client.refresh_access_token()
151
+
152
+ @staticmethod
153
+ def is_reauthentication_needed() -> bool:
154
+ api_client = BiolibApiClient.get()
155
+ if not api_client.is_signed_in and not Runtime.check_is_environment_biolib_app():
156
+ return True
61
157
  else:
62
- raise BioLibError('Attempted to use uninitialized API client')
158
+ return False
63
159
 
64
160
  @staticmethod
65
161
  def assert_is_signed_in(authenticated_action_description: str) -> None:
66
- api_client = BiolibApiClient.get()
67
- if not api_client.is_signed_in:
162
+ if BiolibApiClient.is_reauthentication_needed():
68
163
  raise BioLibError(
69
164
  f'You must be signed in to {authenticated_action_description}. '
70
165
  f'Please set the environment variable "BIOLIB_TOKEN"'
@@ -1,7 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
- from biolib.typing_utils import TypedDict, List, Optional, Dict, Literal
4
3
  from biolib.biolib_api_client.common_types import SemanticVersion
4
+ from biolib.typing_utils import Dict, List, Literal, Optional, TypedDict
5
5
 
6
6
 
7
7
  class AppVersionSlim(SemanticVersion):
@@ -14,6 +14,9 @@ class AppVersion(AppVersionSlim):
14
14
  description: str
15
15
  is_runnable_by_user: bool
16
16
  source_code_license: str
17
+ stdout_render_type: Literal['text', 'markdown']
18
+ main_output_file: Optional[str]
19
+ app_uri: str
17
20
 
18
21
 
19
22
  class App(TypedDict):
@@ -28,6 +31,8 @@ class App(TypedDict):
28
31
  name: str
29
32
  public_id: str
30
33
  state: str
34
+ resource_uri: str
35
+ type: str
31
36
 
32
37
 
33
38
  class AppGetResponse(TypedDict):
@@ -57,34 +62,49 @@ class FilesMapping(TypedDict):
57
62
 
58
63
 
59
64
  class LargeFileSystemMapping(TypedDict):
60
- uuid: str
65
+ presigned_download_url: str
61
66
  size_bytes: int
62
67
  to_path: str
68
+ uuid: str
63
69
 
64
70
 
65
- class Module(TypedDict):
71
+ class _Module(TypedDict):
66
72
  command: str
67
73
  environment: Literal['biolib-app', 'biolib-custom', 'biolib-ecr']
68
74
  image_uri: str
75
+ absolute_image_uri: str
69
76
  estimated_image_size_bytes: Optional[int]
70
77
  input_files_mappings: List[FilesMapping]
71
78
  large_file_systems: List[LargeFileSystemMapping]
72
79
  name: str
73
80
  output_files_mappings: List[FilesMapping]
81
+ ports: List[int]
74
82
  source_files_mappings: List[FilesMapping]
75
83
  working_directory: str
76
84
 
77
85
 
86
+ # type optional keys with total=False
87
+ class Module(_Module, total=False):
88
+ secrets: Dict[str, str]
89
+
90
+
78
91
  class _AppVersionOnJob(TypedDict):
92
+ created_at: str
79
93
  client_side_executable_zip: Optional[str]
80
- consumes_stdin: bool
81
94
  is_runnable_by_user: bool
82
95
  public_id: str
83
96
  remote_hosts: List[RemoteHost]
84
97
  settings: List[Dict]
85
98
  stdout_render_type: Literal['text', 'markdown']
99
+ main_output_file: Optional[str]
100
+
101
+
102
+ class AppOnJob(TypedDict):
103
+ allow_client_side_execution: bool
104
+ state: Literal['public', 'draft']
86
105
 
87
106
 
88
107
  # type optional keys with total=False
89
108
  class AppVersionOnJob(_AppVersionOnJob, total=False):
109
+ app: AppOnJob
90
110
  modules: List[Module]