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.
Files changed (124) hide show
  1. biolib/__init__.py +33 -10
  2. biolib/_data_record/data_record.py +220 -126
  3. biolib/_index/index.py +55 -0
  4. biolib/_index/query_result.py +103 -0
  5. biolib/_internal/add_copilot_prompts.py +24 -11
  6. biolib/_internal/add_gui_files.py +81 -0
  7. biolib/_internal/data_record/__init__.py +1 -1
  8. biolib/_internal/data_record/data_record.py +1 -18
  9. biolib/_internal/data_record/push_data.py +65 -16
  10. biolib/_internal/data_record/remote_storage_endpoint.py +18 -13
  11. biolib/_internal/file_utils.py +48 -0
  12. biolib/_internal/lfs/cache.py +4 -2
  13. biolib/_internal/push_application.py +95 -24
  14. biolib/_internal/runtime.py +2 -0
  15. biolib/_internal/string_utils.py +13 -0
  16. biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/style-general.instructions.md +5 -0
  17. biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
  18. biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
  19. biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
  20. biolib/_internal/templates/{init_template → github_workflow_template}/.github/workflows/biolib.yml +7 -2
  21. biolib/_internal/templates/gitignore_template/.gitignore +10 -0
  22. biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
  23. biolib/_internal/templates/gui_template/App.tsx +53 -0
  24. biolib/_internal/templates/gui_template/Dockerfile +27 -0
  25. biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
  26. biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
  27. biolib/_internal/templates/gui_template/index.css +5 -0
  28. biolib/_internal/templates/gui_template/index.html +13 -0
  29. biolib/_internal/templates/gui_template/index.tsx +10 -0
  30. biolib/_internal/templates/gui_template/package.json +27 -0
  31. biolib/_internal/templates/gui_template/tsconfig.json +24 -0
  32. biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
  33. biolib/_internal/templates/gui_template/vite.config.mts +10 -0
  34. biolib/_internal/templates/init_template/.biolib/config.yml +1 -0
  35. biolib/_internal/templates/init_template/Dockerfile +5 -1
  36. biolib/_internal/templates/init_template/run.py +6 -15
  37. biolib/_internal/templates/init_template/run.sh +1 -0
  38. biolib/_internal/templates/templates.py +21 -1
  39. biolib/_internal/utils/__init__.py +47 -0
  40. biolib/_internal/utils/auth.py +46 -0
  41. biolib/_internal/utils/job_url.py +33 -0
  42. biolib/_internal/utils/multinode.py +12 -14
  43. biolib/_runtime/runtime.py +15 -2
  44. biolib/_session/session.py +7 -5
  45. biolib/_shared/__init__.py +0 -0
  46. biolib/_shared/types/__init__.py +74 -0
  47. biolib/_shared/types/account.py +12 -0
  48. biolib/_shared/types/account_member.py +8 -0
  49. biolib/{_internal → _shared}/types/experiment.py +1 -0
  50. biolib/_shared/types/resource.py +37 -0
  51. biolib/_shared/types/resource_deploy_key.py +11 -0
  52. biolib/{_internal → _shared}/types/resource_version.py +8 -2
  53. biolib/_shared/types/user.py +19 -0
  54. biolib/_shared/utils/__init__.py +7 -0
  55. biolib/_shared/utils/resource_uri.py +75 -0
  56. biolib/api/client.py +5 -48
  57. biolib/app/app.py +97 -55
  58. biolib/biolib_api_client/api_client.py +3 -47
  59. biolib/biolib_api_client/app_types.py +1 -1
  60. biolib/biolib_api_client/biolib_app_api.py +31 -6
  61. biolib/biolib_api_client/biolib_job_api.py +1 -1
  62. biolib/biolib_api_client/user_state.py +34 -2
  63. biolib/biolib_binary_format/module_input.py +8 -0
  64. biolib/biolib_binary_format/remote_endpoints.py +3 -3
  65. biolib/biolib_binary_format/remote_stream_seeker.py +39 -25
  66. biolib/biolib_logging.py +1 -1
  67. biolib/cli/__init__.py +2 -2
  68. biolib/cli/auth.py +4 -16
  69. biolib/cli/data_record.py +82 -0
  70. biolib/cli/index.py +32 -0
  71. biolib/cli/init.py +393 -71
  72. biolib/cli/lfs.py +1 -1
  73. biolib/cli/run.py +9 -6
  74. biolib/cli/start.py +14 -1
  75. biolib/compute_node/job_worker/executors/docker_executor.py +31 -9
  76. biolib/compute_node/job_worker/executors/docker_types.py +1 -1
  77. biolib/compute_node/job_worker/executors/types.py +6 -5
  78. biolib/compute_node/job_worker/job_storage.py +2 -1
  79. biolib/compute_node/job_worker/job_worker.py +155 -90
  80. biolib/compute_node/job_worker/large_file_system.py +2 -6
  81. biolib/compute_node/job_worker/network_alloc.py +99 -0
  82. biolib/compute_node/job_worker/network_buffer.py +240 -0
  83. biolib/compute_node/job_worker/utilization_reporter_thread.py +2 -2
  84. biolib/compute_node/remote_host_proxy.py +163 -79
  85. biolib/compute_node/utils.py +2 -0
  86. biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
  87. biolib/compute_node/webserver/proxy_utils.py +28 -0
  88. biolib/compute_node/webserver/webserver.py +64 -19
  89. biolib/experiments/experiment.py +111 -16
  90. biolib/jobs/job.py +128 -31
  91. biolib/jobs/job_result.py +74 -34
  92. biolib/jobs/types.py +1 -0
  93. biolib/sdk/__init__.py +28 -3
  94. biolib/typing_utils.py +1 -1
  95. biolib/utils/cache_state.py +8 -5
  96. biolib/utils/multipart_uploader.py +24 -18
  97. biolib/utils/seq_util.py +1 -1
  98. pybiolib-1.2.1890.dist-info/METADATA +41 -0
  99. pybiolib-1.2.1890.dist-info/RECORD +177 -0
  100. {pybiolib-1.2.883.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
  101. pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
  102. biolib/_internal/llm_instructions/.github/instructions/style-react-ts.instructions.md +0 -22
  103. biolib/_internal/templates/init_template/.gitignore +0 -2
  104. biolib/_internal/types/__init__.py +0 -6
  105. biolib/_internal/types/resource.py +0 -18
  106. biolib/biolib_download_container.py +0 -38
  107. biolib/cli/download_container.py +0 -14
  108. biolib/utils/app_uri.py +0 -57
  109. pybiolib-1.2.883.dist-info/METADATA +0 -50
  110. pybiolib-1.2.883.dist-info/RECORD +0 -148
  111. pybiolib-1.2.883.dist-info/entry_points.txt +0 -3
  112. /biolib/{_internal/llm_instructions → _index}/__init__.py +0 -0
  113. /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/general-app-knowledge.instructions.md +0 -0
  114. /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/instructions/style-python.instructions.md +0 -0
  115. /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/prompts/biolib_app_inputs.prompt.md +0 -0
  116. /biolib/_internal/{llm_instructions → templates/copilot_template}/.github/prompts/biolib_run_apps.prompt.md +0 -0
  117. /biolib/{_internal → _shared}/types/app.py +0 -0
  118. /biolib/{_internal → _shared}/types/data_record.py +0 -0
  119. /biolib/{_internal → _shared}/types/file_node.py +0 -0
  120. /biolib/{_internal → _shared}/types/push.py +0 -0
  121. /biolib/{_internal → _shared}/types/resource_permission.py +0 -0
  122. /biolib/{_internal → _shared}/types/result.py +0 -0
  123. /biolib/{_internal → _shared}/types/typing.py +0 -0
  124. {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
- current_work_units = 0
90
- total_work_units = 0
89
+ current_longest_seq_len = 0
91
90
  for record in records:
92
- # Add to batch
93
- batch.append(record)
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
- # Increase counters
100
- current_work_units += sequence_work_units
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
- # If above limit, start a new batch
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
- current_work_units = 0
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:
@@ -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
- return Runtime._get_job_data()['job_requested_machine']
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(
@@ -1,21 +1,23 @@
1
1
  from biolib import utils
2
- from biolib._internal.types import Optional
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
+ ]
@@ -0,0 +1,12 @@
1
+ from .typing import TypedDict
2
+
3
+
4
+ class AccountDict(TypedDict):
5
+ uuid: str
6
+ handle: str
7
+ display_name: str
8
+ description: str
9
+
10
+
11
+ class AccountDetailedDict(AccountDict):
12
+ bio: str
@@ -0,0 +1,8 @@
1
+ from .typing import Literal, TypedDict
2
+ from .user import UserDict
3
+
4
+
5
+ class AccountMemberDict(TypedDict):
6
+ user: UserDict
7
+ role: Literal['member', 'admin']
8
+ added_at: str
@@ -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]
@@ -0,0 +1,11 @@
1
+ from .typing import TypedDict
2
+
3
+
4
+ class ResourceDeployKeyDict(TypedDict):
5
+ uuid: str
6
+ created_at: str
7
+ name: str
8
+
9
+
10
+ class ResourceDeployKeyWithSecretDict(ResourceDeployKeyDict):
11
+ secret_key: str
@@ -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
- pass
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,7 @@
1
+ from biolib._shared.utils.resource_uri import normalize_resource_name, parse_resource_uri, parse_semantic_version
2
+
3
+ __all__ = [
4
+ 'normalize_resource_name',
5
+ 'parse_resource_uri',
6
+ 'parse_semantic_version',
7
+ ]
@@ -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.types.typing import Any, Dict, Optional, TypedDict, Union, cast
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=None),
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 = self._decode_jwt_without_checking_signature(self._access_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)