pybiolib 1.2.883__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.
- biolib/__init__.py +33 -10
- biolib/_data_record/data_record.py +220 -126
- biolib/_index/index.py +55 -0
- biolib/_index/query_result.py +103 -0
- biolib/_internal/add_copilot_prompts.py +24 -11
- biolib/_internal/add_gui_files.py +81 -0
- biolib/_internal/data_record/__init__.py +1 -1
- biolib/_internal/data_record/data_record.py +1 -18
- biolib/_internal/data_record/push_data.py +65 -16
- biolib/_internal/data_record/remote_storage_endpoint.py +18 -13
- biolib/_internal/file_utils.py +48 -0
- biolib/_internal/lfs/cache.py +4 -2
- biolib/_internal/push_application.py +95 -24
- biolib/_internal/runtime.py +2 -0
- biolib/_internal/string_utils.py +13 -0
- biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/style-general.instructions.md +5 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
- biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
- biolib/_internal/templates/{init_template → github_workflow_template}/.github/workflows/biolib.yml +7 -2
- biolib/_internal/templates/gitignore_template/.gitignore +10 -0
- biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
- biolib/_internal/templates/gui_template/App.tsx +53 -0
- biolib/_internal/templates/gui_template/Dockerfile +27 -0
- biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
- biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
- biolib/_internal/templates/gui_template/index.css +5 -0
- biolib/_internal/templates/gui_template/index.html +13 -0
- biolib/_internal/templates/gui_template/index.tsx +10 -0
- biolib/_internal/templates/gui_template/package.json +27 -0
- biolib/_internal/templates/gui_template/tsconfig.json +24 -0
- biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
- biolib/_internal/templates/gui_template/vite.config.mts +10 -0
- biolib/_internal/templates/init_template/.biolib/config.yml +1 -0
- biolib/_internal/templates/init_template/Dockerfile +5 -1
- biolib/_internal/templates/init_template/run.py +6 -15
- biolib/_internal/templates/init_template/run.sh +1 -0
- biolib/_internal/templates/templates.py +21 -1
- biolib/_internal/utils/__init__.py +47 -0
- biolib/_internal/utils/auth.py +46 -0
- biolib/_internal/utils/job_url.py +33 -0
- biolib/_internal/utils/multinode.py +12 -14
- biolib/_runtime/runtime.py +15 -2
- biolib/_session/session.py +7 -5
- biolib/_shared/__init__.py +0 -0
- biolib/_shared/types/__init__.py +74 -0
- biolib/_shared/types/account.py +12 -0
- biolib/_shared/types/account_member.py +8 -0
- biolib/{_internal → _shared}/types/experiment.py +1 -0
- biolib/_shared/types/resource.py +37 -0
- biolib/_shared/types/resource_deploy_key.py +11 -0
- biolib/{_internal → _shared}/types/resource_version.py +8 -2
- biolib/_shared/types/user.py +19 -0
- biolib/_shared/utils/__init__.py +7 -0
- biolib/_shared/utils/resource_uri.py +75 -0
- biolib/api/client.py +5 -48
- biolib/app/app.py +97 -55
- biolib/biolib_api_client/api_client.py +3 -47
- biolib/biolib_api_client/app_types.py +1 -1
- biolib/biolib_api_client/biolib_app_api.py +31 -6
- biolib/biolib_api_client/biolib_job_api.py +1 -1
- biolib/biolib_api_client/user_state.py +34 -2
- biolib/biolib_binary_format/module_input.py +8 -0
- biolib/biolib_binary_format/remote_endpoints.py +3 -3
- biolib/biolib_binary_format/remote_stream_seeker.py +39 -25
- biolib/biolib_logging.py +1 -1
- biolib/cli/__init__.py +2 -2
- biolib/cli/auth.py +4 -16
- biolib/cli/data_record.py +82 -0
- biolib/cli/index.py +32 -0
- biolib/cli/init.py +393 -71
- biolib/cli/lfs.py +1 -1
- biolib/cli/run.py +9 -6
- biolib/cli/start.py +14 -1
- biolib/compute_node/job_worker/executors/docker_executor.py +31 -9
- biolib/compute_node/job_worker/executors/docker_types.py +1 -1
- biolib/compute_node/job_worker/executors/types.py +6 -5
- biolib/compute_node/job_worker/job_storage.py +2 -1
- biolib/compute_node/job_worker/job_worker.py +155 -90
- biolib/compute_node/job_worker/large_file_system.py +2 -6
- biolib/compute_node/job_worker/network_alloc.py +99 -0
- biolib/compute_node/job_worker/network_buffer.py +240 -0
- biolib/compute_node/job_worker/utilization_reporter_thread.py +2 -2
- biolib/compute_node/remote_host_proxy.py +163 -79
- biolib/compute_node/utils.py +2 -0
- biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
- biolib/compute_node/webserver/proxy_utils.py +28 -0
- biolib/compute_node/webserver/webserver.py +64 -19
- biolib/experiments/experiment.py +111 -16
- biolib/jobs/job.py +128 -31
- biolib/jobs/job_result.py +74 -34
- biolib/jobs/types.py +1 -0
- biolib/sdk/__init__.py +28 -3
- biolib/typing_utils.py +1 -1
- biolib/utils/cache_state.py +8 -5
- biolib/utils/multipart_uploader.py +24 -18
- biolib/utils/seq_util.py +1 -1
- pybiolib-1.2.1890.dist-info/METADATA +41 -0
- pybiolib-1.2.1890.dist-info/RECORD +177 -0
- {pybiolib-1.2.883.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
- pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
- biolib/_internal/llm_instructions/.github/instructions/style-react-ts.instructions.md +0 -22
- biolib/_internal/templates/init_template/.gitignore +0 -2
- biolib/_internal/types/__init__.py +0 -6
- biolib/_internal/types/resource.py +0 -18
- biolib/biolib_download_container.py +0 -38
- biolib/cli/download_container.py +0 -14
- biolib/utils/app_uri.py +0 -57
- pybiolib-1.2.883.dist-info/METADATA +0 -50
- pybiolib-1.2.883.dist-info/RECORD +0 -148
- pybiolib-1.2.883.dist-info/entry_points.txt +0 -3
- /biolib/{_internal/llm_instructions → _index}/__init__.py +0 -0
- /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/general-app-knowledge.instructions.md +0 -0
- /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/style-python.instructions.md +0 -0
- /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/prompts/biolib_app_inputs.prompt.md +0 -0
- /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/prompts/biolib_run_apps.prompt.md +0 -0
- /biolib/{_internal → _shared}/types/app.py +0 -0
- /biolib/{_internal → _shared}/types/data_record.py +0 -0
- /biolib/{_internal → _shared}/types/file_node.py +0 -0
- /biolib/{_internal → _shared}/types/push.py +0 -0
- /biolib/{_internal → _shared}/types/resource_permission.py +0 -0
- /biolib/{_internal → _shared}/types/result.py +0 -0
- /biolib/{_internal → _shared}/types/typing.py +0 -0
- {pybiolib-1.2.883.dist-info → pybiolib-1.2.1890.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
import biolib.utils
|
|
5
|
+
from biolib.typing_utils import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_result_id_or_url(result_id_or_url: str, default_token: Optional[str] = None) -> Tuple[str, Optional[str]]:
|
|
9
|
+
result_id_or_url = result_id_or_url.strip()
|
|
10
|
+
|
|
11
|
+
if '/' not in result_id_or_url:
|
|
12
|
+
return (result_id_or_url, default_token)
|
|
13
|
+
|
|
14
|
+
if not result_id_or_url.startswith('http://') and not result_id_or_url.startswith('https://'):
|
|
15
|
+
result_id_or_url = 'https://' + result_id_or_url
|
|
16
|
+
|
|
17
|
+
parsed_url = urlparse(result_id_or_url)
|
|
18
|
+
|
|
19
|
+
if biolib.utils.BIOLIB_BASE_URL:
|
|
20
|
+
expected_base = urlparse(biolib.utils.BIOLIB_BASE_URL)
|
|
21
|
+
if parsed_url.scheme != expected_base.scheme or parsed_url.netloc != expected_base.netloc:
|
|
22
|
+
raise ValueError(f'URL must start with {biolib.utils.BIOLIB_BASE_URL}, got: {result_id_or_url}')
|
|
23
|
+
|
|
24
|
+
pattern = r'/results?/(?P<uuid>[a-f0-9-]+)/?(?:\?token=(?P<token>[^&]+))?'
|
|
25
|
+
match = re.search(pattern, result_id_or_url, re.IGNORECASE)
|
|
26
|
+
|
|
27
|
+
if not match:
|
|
28
|
+
raise ValueError(f'URL must be in format <base_url>/results/<UUID>/?token=<token>, got: {result_id_or_url}')
|
|
29
|
+
|
|
30
|
+
uuid = match.group('uuid')
|
|
31
|
+
token = match.group('token') or default_token
|
|
32
|
+
|
|
33
|
+
return (uuid, token)
|
|
@@ -86,25 +86,23 @@ def fasta_batch_records(records, work_per_batch_min, work_per_residue=1, verbose
|
|
|
86
86
|
|
|
87
87
|
batches = []
|
|
88
88
|
batch = []
|
|
89
|
-
|
|
90
|
-
total_work_units = 0
|
|
89
|
+
current_longest_seq_len = 0
|
|
91
90
|
for record in records:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Calculate work units
|
|
96
|
-
seq = record.sequence
|
|
97
|
-
sequence_work_units = len(seq) * work_per_residue
|
|
91
|
+
seq_len = len(record.sequence)
|
|
92
|
+
potential_longest_seq_len = max(current_longest_seq_len, seq_len)
|
|
98
93
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
total_work_units += sequence_work_units
|
|
94
|
+
# Calculate work units if we were to add this record
|
|
95
|
+
potential_work_units = potential_longest_seq_len * work_per_residue * (len(batch) + 1)
|
|
102
96
|
|
|
103
|
-
|
|
104
|
-
if current_work_units >= work_per_batch_min:
|
|
97
|
+
if potential_work_units >= work_per_batch_min and len(batch) > 0:
|
|
105
98
|
batches.append(batch)
|
|
106
99
|
batch = []
|
|
107
|
-
|
|
100
|
+
current_longest_seq_len = 0
|
|
101
|
+
potential_longest_seq_len = seq_len
|
|
102
|
+
|
|
103
|
+
# Add to batch
|
|
104
|
+
batch.append(record)
|
|
105
|
+
current_longest_seq_len = potential_longest_seq_len
|
|
108
106
|
|
|
109
107
|
# Append last batch if present
|
|
110
108
|
if batch:
|
biolib/_runtime/runtime.py
CHANGED
|
@@ -30,13 +30,26 @@ class Runtime:
|
|
|
30
30
|
return Runtime._get_job_data()['job_auth_token']
|
|
31
31
|
|
|
32
32
|
@staticmethod
|
|
33
|
-
def get_job_requested_machine() -> str:
|
|
34
|
-
|
|
33
|
+
def get_job_requested_machine() -> Optional[str]:
|
|
34
|
+
job_data = Runtime._get_job_data()
|
|
35
|
+
job_requested_machine = job_data.get('job_requested_machine')
|
|
36
|
+
if not job_requested_machine:
|
|
37
|
+
return None
|
|
38
|
+
return job_requested_machine
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def is_spot_machine_requested() -> bool:
|
|
42
|
+
job_data = Runtime._get_job_data()
|
|
43
|
+
return job_data.get('job_requested_machine_spot', False)
|
|
35
44
|
|
|
36
45
|
@staticmethod
|
|
37
46
|
def get_app_uri() -> str:
|
|
38
47
|
return Runtime._get_job_data()['app_uri']
|
|
39
48
|
|
|
49
|
+
@staticmethod
|
|
50
|
+
def get_max_workers() -> int:
|
|
51
|
+
return Runtime._get_job_data()['job_reserved_machines']
|
|
52
|
+
|
|
40
53
|
@staticmethod
|
|
41
54
|
def get_secret(secret_name: str) -> bytes:
|
|
42
55
|
assert re.match(
|
biolib/_session/session.py
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
from biolib import utils
|
|
2
|
-
from biolib.
|
|
2
|
+
from biolib.typing_utils import Optional
|
|
3
3
|
from biolib.api.client import ApiClient, ApiClientInitDict
|
|
4
4
|
from biolib.app import BioLibApp
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Session:
|
|
8
|
-
def __init__(self, _init_dict: ApiClientInitDict) -> None:
|
|
8
|
+
def __init__(self, _init_dict: ApiClientInitDict, _experiment: Optional[str] = None) -> None:
|
|
9
9
|
self._api = ApiClient(_init_dict=_init_dict)
|
|
10
|
+
self._experiment = _experiment
|
|
10
11
|
|
|
11
12
|
@staticmethod
|
|
12
|
-
def get_session(refresh_token: str, base_url: Optional[str] = None, client_type: Optional[str] = None) -> 'Session':
|
|
13
|
+
def get_session(refresh_token: str, base_url: Optional[str] = None, client_type: Optional[str] = None, experiment: Optional[str] = None) -> 'Session':
|
|
13
14
|
return Session(
|
|
14
15
|
_init_dict=ApiClientInitDict(
|
|
15
16
|
refresh_token=refresh_token,
|
|
16
17
|
base_url=base_url or utils.load_base_url_from_env(),
|
|
17
18
|
client_type=client_type,
|
|
18
|
-
)
|
|
19
|
+
),
|
|
20
|
+
_experiment=experiment,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
def load(self, uri: str, suppress_version_warning: bool = False) -> BioLibApp:
|
|
@@ -39,4 +41,4 @@ class Session:
|
|
|
39
41
|
>>> app = biolib.load('https://biolib.com/biolib/myapp/')
|
|
40
42
|
>>> result = app.cli('--help')
|
|
41
43
|
"""
|
|
42
|
-
return BioLibApp(uri=uri, _api_client=self._api, suppress_version_warning=suppress_version_warning)
|
|
44
|
+
return BioLibApp(uri=uri, _api_client=self._api, suppress_version_warning=suppress_version_warning, _experiment=self._experiment)
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from .account import AccountDict, AccountDetailedDict
|
|
2
|
+
from .account_member import AccountMemberDict
|
|
3
|
+
from .app import AppDetailedDict, AppSlimDict
|
|
4
|
+
from .data_record import (
|
|
5
|
+
DataRecordDetailedDict,
|
|
6
|
+
DataRecordSlimDict,
|
|
7
|
+
DataRecordTypeDict,
|
|
8
|
+
DataRecordValidationRuleDict,
|
|
9
|
+
SqliteV1Column,
|
|
10
|
+
SqliteV1DatabaseSchema,
|
|
11
|
+
SqliteV1ForeignKey,
|
|
12
|
+
SqliteV1Table,
|
|
13
|
+
)
|
|
14
|
+
from .experiment import (
|
|
15
|
+
DeprecatedExperimentDict,
|
|
16
|
+
ExperimentDetailedDict,
|
|
17
|
+
ExperimentDict,
|
|
18
|
+
ResultCounts,
|
|
19
|
+
)
|
|
20
|
+
from .file_node import FileNodeDict, FileZipMetadataDict, ZipFileNodeDict
|
|
21
|
+
from .push import PushResponseDict
|
|
22
|
+
from .resource import ResourceDetailedDict, ResourceDict, ResourceTypeLiteral, ResourceUriDict, SemanticVersionDict
|
|
23
|
+
from .resource_deploy_key import ResourceDeployKeyDict, ResourceDeployKeyWithSecretDict
|
|
24
|
+
from .resource_permission import ResourcePermissionDetailedDict, ResourcePermissionDict
|
|
25
|
+
from .resource_version import (
|
|
26
|
+
ResourceVersionAssetsDict,
|
|
27
|
+
ResourceVersionDetailedDict,
|
|
28
|
+
ResourceVersionDict,
|
|
29
|
+
)
|
|
30
|
+
from .result import ResultDetailedDict, ResultDict
|
|
31
|
+
from .typing import Optional
|
|
32
|
+
from .user import EnterpriseSettingsDict, UserDetailedDict, UserDict
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
'AccountDetailedDict',
|
|
36
|
+
'AccountDict',
|
|
37
|
+
'AccountMemberDict',
|
|
38
|
+
'AppDetailedDict',
|
|
39
|
+
'AppSlimDict',
|
|
40
|
+
'DataRecordDetailedDict',
|
|
41
|
+
'DataRecordSlimDict',
|
|
42
|
+
'DataRecordTypeDict',
|
|
43
|
+
'DataRecordValidationRuleDict',
|
|
44
|
+
'DeprecatedExperimentDict',
|
|
45
|
+
'EnterpriseSettingsDict',
|
|
46
|
+
'ExperimentDetailedDict',
|
|
47
|
+
'ExperimentDict',
|
|
48
|
+
'FileNodeDict',
|
|
49
|
+
'FileZipMetadataDict',
|
|
50
|
+
'Optional',
|
|
51
|
+
'PushResponseDict',
|
|
52
|
+
'ResourceDeployKeyDict',
|
|
53
|
+
'ResourceDeployKeyWithSecretDict',
|
|
54
|
+
'ResourceDetailedDict',
|
|
55
|
+
'ResourceDict',
|
|
56
|
+
'ResourceTypeLiteral',
|
|
57
|
+
'ResourcePermissionDetailedDict',
|
|
58
|
+
'ResourcePermissionDict',
|
|
59
|
+
'ResourceUriDict',
|
|
60
|
+
'ResourceVersionAssetsDict',
|
|
61
|
+
'ResourceVersionDetailedDict',
|
|
62
|
+
'ResourceVersionDict',
|
|
63
|
+
'ResultCounts',
|
|
64
|
+
'ResultDetailedDict',
|
|
65
|
+
'ResultDict',
|
|
66
|
+
'SemanticVersionDict',
|
|
67
|
+
'SqliteV1Column',
|
|
68
|
+
'SqliteV1DatabaseSchema',
|
|
69
|
+
'SqliteV1ForeignKey',
|
|
70
|
+
'SqliteV1Table',
|
|
71
|
+
'UserDetailedDict',
|
|
72
|
+
'UserDict',
|
|
73
|
+
'ZipFileNodeDict',
|
|
74
|
+
]
|
|
@@ -20,6 +20,7 @@ class DeprecatedExperimentDict(TypedDict):
|
|
|
20
20
|
class ExperimentDict(DeprecatedExperimentDict):
|
|
21
21
|
uuid: Optional[str]
|
|
22
22
|
name: Optional[str]
|
|
23
|
+
account_uuid: Optional[str]
|
|
23
24
|
created_at: Optional[str]
|
|
24
25
|
finished_at: Optional[str]
|
|
25
26
|
last_created_at: Optional[str]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .experiment import DeprecatedExperimentDict
|
|
2
|
+
from .resource_version import ResourceVersionDetailedDict
|
|
3
|
+
from .typing import Literal, NotRequired, Optional, TypedDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SemanticVersionDict(TypedDict):
|
|
7
|
+
major: int
|
|
8
|
+
minor: int
|
|
9
|
+
patch: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResourceUriDict(TypedDict):
|
|
13
|
+
account_handle_normalized: str
|
|
14
|
+
account_handle: str
|
|
15
|
+
resource_name_normalized: Optional[str]
|
|
16
|
+
resource_name: Optional[str]
|
|
17
|
+
resource_prefix: Optional[str]
|
|
18
|
+
version: Optional[SemanticVersionDict]
|
|
19
|
+
tag: Optional[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ResourceTypeLiteral = Literal['app', 'data-record', 'experiment', 'index']
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResourceDict(TypedDict):
|
|
26
|
+
uuid: str
|
|
27
|
+
uri: str
|
|
28
|
+
name: str
|
|
29
|
+
created_at: str
|
|
30
|
+
description: str
|
|
31
|
+
account_uuid: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResourceDetailedDict(ResourceDict):
|
|
35
|
+
type: ResourceTypeLiteral
|
|
36
|
+
version: NotRequired[ResourceVersionDetailedDict]
|
|
37
|
+
experiment: Optional[DeprecatedExperimentDict]
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from .typing import Literal, NotRequired, TypedDict
|
|
1
|
+
from .typing import Literal, NotRequired, Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ResourceVersionAssetsDict(TypedDict):
|
|
5
|
+
download_url: str
|
|
6
|
+
size_bytes: int
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
class ResourceVersionDict(TypedDict):
|
|
@@ -7,7 +12,8 @@ class ResourceVersionDict(TypedDict):
|
|
|
7
12
|
state: Literal['published', 'unpublished']
|
|
8
13
|
created_at: str
|
|
9
14
|
git_branch_name: NotRequired[str]
|
|
15
|
+
git_commit_hash: NotRequired[str]
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class ResourceVersionDetailedDict(ResourceVersionDict):
|
|
13
|
-
|
|
19
|
+
assets: Optional[ResourceVersionAssetsDict]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .account import AccountDict
|
|
2
|
+
from .typing import Optional, TypedDict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EnterpriseSettingsDict(TypedDict):
|
|
6
|
+
account_uuid: str
|
|
7
|
+
dashboard_message: Optional[str]
|
|
8
|
+
docs_message: Optional[str]
|
|
9
|
+
featured_dashboard_app_version_uuid: Optional[str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UserDict(TypedDict):
|
|
13
|
+
uuid: str
|
|
14
|
+
account: AccountDict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserDetailedDict(UserDict):
|
|
18
|
+
email: str
|
|
19
|
+
enterprise_settings: Optional[EnterpriseSettingsDict]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from biolib._shared.types import Optional
|
|
4
|
+
from biolib._shared.types.resource import ResourceUriDict, SemanticVersionDict
|
|
5
|
+
from biolib.biolib_errors import BioLibError
|
|
6
|
+
|
|
7
|
+
URI_REGEX = re.compile(
|
|
8
|
+
r'^(@(?P<resource_prefix>[\w._-]+)/)?'
|
|
9
|
+
r'(?P<account_handle>[\w-]+)'
|
|
10
|
+
r'(/(?P<resource_name>[\w-]+))?'
|
|
11
|
+
r'(?::(?P<suffix>[^:]+))?$'
|
|
12
|
+
)
|
|
13
|
+
SEMVER_REGEX = re.compile(r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)$')
|
|
14
|
+
TAG_REGEX = re.compile(r'^[a-z0-9-]{1,128}$')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_resource_name(string: str) -> str:
|
|
18
|
+
return string.replace('-', '_').lower()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_semantic_version(semantic_version: str) -> SemanticVersionDict:
|
|
22
|
+
if match := SEMVER_REGEX.fullmatch(semantic_version):
|
|
23
|
+
return SemanticVersionDict(
|
|
24
|
+
major=int(match.group('major')),
|
|
25
|
+
minor=int(match.group('minor')),
|
|
26
|
+
patch=int(match.group('patch')),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
raise ValueError('The version must be a valid semantic version in the format of major.minor.patch (1.2.3).')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_resource_uri(uri: str, use_account_as_name_default: bool = True) -> ResourceUriDict:
|
|
33
|
+
matches = URI_REGEX.match(uri)
|
|
34
|
+
if matches is None:
|
|
35
|
+
raise BioLibError(f"Could not parse resource uri '{uri}', uri did not match regex")
|
|
36
|
+
|
|
37
|
+
version: Optional[SemanticVersionDict] = None
|
|
38
|
+
tag: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
suffix = matches.group('suffix')
|
|
41
|
+
if suffix and suffix != '*':
|
|
42
|
+
try:
|
|
43
|
+
version = parse_semantic_version(suffix)
|
|
44
|
+
except ValueError:
|
|
45
|
+
if TAG_REGEX.fullmatch(suffix):
|
|
46
|
+
tag = suffix
|
|
47
|
+
else:
|
|
48
|
+
raise BioLibError(
|
|
49
|
+
f'Invalid version or tag "{suffix}". '
|
|
50
|
+
'Versions must be semantic versions like "1.2.3". '
|
|
51
|
+
'Tags must be lowercase alphanumeric or dashes and at most 128 characters.'
|
|
52
|
+
) from None
|
|
53
|
+
|
|
54
|
+
resource_prefix_raw: Optional[str] = matches.group('resource_prefix')
|
|
55
|
+
resource_prefix = resource_prefix_raw.lower() if resource_prefix_raw is not None else None
|
|
56
|
+
account_handle: str = matches.group('account_handle')
|
|
57
|
+
account_handle_normalized: str = normalize_resource_name(account_handle)
|
|
58
|
+
resource_name: Optional[str] = matches.group('resource_name')
|
|
59
|
+
|
|
60
|
+
if resource_name:
|
|
61
|
+
resource_name_normalized = normalize_resource_name(resource_name)
|
|
62
|
+
elif use_account_as_name_default:
|
|
63
|
+
resource_name_normalized = account_handle_normalized
|
|
64
|
+
else:
|
|
65
|
+
resource_name_normalized = None
|
|
66
|
+
|
|
67
|
+
return ResourceUriDict(
|
|
68
|
+
resource_prefix=resource_prefix,
|
|
69
|
+
account_handle=account_handle,
|
|
70
|
+
account_handle_normalized=account_handle_normalized,
|
|
71
|
+
resource_name_normalized=resource_name_normalized,
|
|
72
|
+
resource_name=resource_name if resource_name is not None or not use_account_as_name_default else account_handle,
|
|
73
|
+
version=version,
|
|
74
|
+
tag=tag,
|
|
75
|
+
)
|
biolib/api/client.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import binascii
|
|
3
|
-
import json
|
|
4
1
|
from datetime import datetime, timezone
|
|
5
2
|
from json.decoder import JSONDecodeError
|
|
6
3
|
from urllib.parse import urlencode, urljoin
|
|
@@ -8,7 +5,8 @@ from urllib.parse import urlencode, urljoin
|
|
|
8
5
|
import importlib_metadata
|
|
9
6
|
|
|
10
7
|
from biolib._internal.http_client import HttpClient, HttpResponse
|
|
11
|
-
from biolib._internal.
|
|
8
|
+
from biolib._internal.utils.auth import decode_jwt_without_checking_signature
|
|
9
|
+
from biolib._shared.types.typing import Dict, Optional, TypedDict, Union, cast
|
|
12
10
|
from biolib.biolib_api_client import BiolibApiClient as DeprecatedApiClient
|
|
13
11
|
from biolib.biolib_errors import BioLibError
|
|
14
12
|
from biolib.biolib_logging import logger
|
|
@@ -33,10 +31,6 @@ class ApiClientInitDict(TypedDict):
|
|
|
33
31
|
client_type: Optional[str]
|
|
34
32
|
|
|
35
33
|
|
|
36
|
-
class JwtDecodeError(Exception):
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
|
|
40
34
|
class ApiClient(HttpClient):
|
|
41
35
|
_biolib_package_version: str = _get_biolib_package_version()
|
|
42
36
|
|
|
@@ -65,6 +59,7 @@ class ApiClient(HttpClient):
|
|
|
65
59
|
self,
|
|
66
60
|
path: str,
|
|
67
61
|
data: Optional[Union[Dict, bytes]] = None,
|
|
62
|
+
params: Optional[Dict[str, Union[str, int]]] = None,
|
|
68
63
|
headers: OptionalHeaders = None,
|
|
69
64
|
authenticate: bool = True,
|
|
70
65
|
retries: int = 50, # TODO: reduce this back to 5 when timeout errors have been solved
|
|
@@ -74,7 +69,7 @@ class ApiClient(HttpClient):
|
|
|
74
69
|
headers=self._get_headers(opt_headers=headers, authenticate=authenticate),
|
|
75
70
|
method='POST',
|
|
76
71
|
retries=retries,
|
|
77
|
-
url=self._get_absolute_url(path=path, query_params=
|
|
72
|
+
url=self._get_absolute_url(path=path, query_params=params),
|
|
78
73
|
)
|
|
79
74
|
|
|
80
75
|
def patch(
|
|
@@ -147,7 +142,7 @@ class ApiClient(HttpClient):
|
|
|
147
142
|
|
|
148
143
|
def _get_access_token(self) -> str:
|
|
149
144
|
if self._access_token:
|
|
150
|
-
decoded_token =
|
|
145
|
+
decoded_token = decode_jwt_without_checking_signature(self._access_token)
|
|
151
146
|
if datetime.now(tz=timezone.utc).timestamp() < decoded_token['payload']['exp'] - 60: # 60 second buffer
|
|
152
147
|
# Token has not expired yet
|
|
153
148
|
return self._access_token
|
|
@@ -171,41 +166,3 @@ class ApiClient(HttpClient):
|
|
|
171
166
|
|
|
172
167
|
self._access_token = cast(str, response_dict['access'])
|
|
173
168
|
return self._access_token
|
|
174
|
-
|
|
175
|
-
@staticmethod
|
|
176
|
-
def _decode_jwt_without_checking_signature(jwt: str) -> Dict[str, Any]:
|
|
177
|
-
jwt_bytes = jwt.encode('utf-8')
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
signing_input, _ = jwt_bytes.rsplit(b'.', 1)
|
|
181
|
-
header_segment, payload_segment = signing_input.split(b'.', 1)
|
|
182
|
-
except ValueError as error:
|
|
183
|
-
raise JwtDecodeError('Not enough segments') from error
|
|
184
|
-
|
|
185
|
-
try:
|
|
186
|
-
header_data = base64.urlsafe_b64decode(header_segment)
|
|
187
|
-
except (TypeError, binascii.Error) as error:
|
|
188
|
-
raise JwtDecodeError('Invalid header padding') from error
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
header = json.loads(header_data)
|
|
192
|
-
except ValueError as error:
|
|
193
|
-
raise JwtDecodeError(f'Invalid header string: {error}') from error
|
|
194
|
-
|
|
195
|
-
if not isinstance(header, dict):
|
|
196
|
-
raise JwtDecodeError('Invalid header string: must be a json object')
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
payload_data = base64.urlsafe_b64decode(payload_segment)
|
|
200
|
-
except (TypeError, binascii.Error) as error:
|
|
201
|
-
raise JwtDecodeError('Invalid payload padding') from error
|
|
202
|
-
|
|
203
|
-
try:
|
|
204
|
-
payload = json.loads(payload_data)
|
|
205
|
-
except ValueError as error:
|
|
206
|
-
raise JwtDecodeError(f'Invalid payload string: {error}') from error
|
|
207
|
-
|
|
208
|
-
if not isinstance(header, dict):
|
|
209
|
-
raise JwtDecodeError('Invalid payload string: must be a json object')
|
|
210
|
-
|
|
211
|
-
return dict(header=header, payload=payload)
|