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
biolib/cli/auth.py
CHANGED
|
@@ -3,7 +3,7 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
|
-
from biolib import api
|
|
6
|
+
from biolib import api
|
|
7
7
|
from biolib.biolib_api_client.api_client import BiolibApiClient
|
|
8
8
|
from biolib.biolib_logging import logger, logger_no_user_data
|
|
9
9
|
from biolib.user import sign_in, sign_out
|
|
@@ -35,23 +35,11 @@ def logout() -> None:
|
|
|
35
35
|
def whoami() -> None:
|
|
36
36
|
client = BiolibApiClient.get()
|
|
37
37
|
if client.is_signed_in:
|
|
38
|
-
|
|
39
|
-
if client.access_token is None:
|
|
40
|
-
print('Unable to fetch user credentials. Please try logging out and logging in again.')
|
|
41
|
-
exit(1)
|
|
42
|
-
try:
|
|
43
|
-
user_uuid = client.decode_jwt_without_checking_signature(jwt=client.access_token)['payload']['public_id']
|
|
44
|
-
except biolib_errors.BioLibError as error:
|
|
45
|
-
print(
|
|
46
|
-
f'Unable to reference user public_id in access token:\n {error.message}',
|
|
47
|
-
file=sys.stderr,
|
|
48
|
-
)
|
|
49
|
-
exit(1)
|
|
50
|
-
response = api.client.get(path=f'/user/{user_uuid}/')
|
|
38
|
+
response = api.client.get(path='/users/me/')
|
|
51
39
|
user_dict = response.json()
|
|
52
40
|
email = user_dict['email']
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
display_name = user_dict['account']['display_name']
|
|
42
|
+
|
|
55
43
|
print(f'Name: {display_name}\nEmail: {email}\nLogged into: {client.base_url}')
|
|
56
44
|
else:
|
|
57
45
|
print('Not logged in', file=sys.stderr)
|
biolib/cli/data_record.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import os
|
|
4
|
+
import sys
|
|
4
5
|
from typing import Dict, List
|
|
5
6
|
|
|
6
7
|
import click
|
|
8
|
+
import rich.progress
|
|
7
9
|
|
|
8
10
|
from biolib._data_record.data_record import DataRecord
|
|
9
11
|
from biolib.biolib_api_client import BiolibApiClient
|
|
@@ -80,3 +82,83 @@ def describe(uri: str, output_as_json: bool) -> None:
|
|
|
80
82
|
size_string = str(file_info['size_bytes'])
|
|
81
83
|
leading_space_string = ' ' * (10 - len(size_string))
|
|
82
84
|
print(f"{leading_space_string}{size_string} {file_info['path']}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@data_record.command(help='Delete a Data Record')
|
|
88
|
+
@click.argument('uri', required=True)
|
|
89
|
+
def delete(uri: str) -> None:
|
|
90
|
+
record = DataRecord.get_by_uri(uri=uri)
|
|
91
|
+
|
|
92
|
+
print(f'You are about to delete the data record: {record.uri}')
|
|
93
|
+
print('This action cannot be undone.')
|
|
94
|
+
|
|
95
|
+
confirmation = input(f'To confirm deletion, please type the data record name "{record.name}": ')
|
|
96
|
+
if confirmation != record.name:
|
|
97
|
+
print('Data record name does not match. Deletion cancelled.')
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
record.delete()
|
|
101
|
+
print(f'Data record {record.uri} has been deleted.')
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _clone_data_record_with_progress(
|
|
105
|
+
source_record: DataRecord,
|
|
106
|
+
dest_record: DataRecord,
|
|
107
|
+
) -> None:
|
|
108
|
+
# pylint: disable=protected-access
|
|
109
|
+
total_size_in_bytes = source_record._get_zip_size_bytes()
|
|
110
|
+
# pylint: enable=protected-access
|
|
111
|
+
|
|
112
|
+
if total_size_in_bytes == 0:
|
|
113
|
+
logger.info('Source data record has no data to clone')
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if sys.stdout.isatty():
|
|
117
|
+
with rich.progress.Progress(
|
|
118
|
+
rich.progress.TextColumn('[bold blue]{task.description}'),
|
|
119
|
+
rich.progress.BarColumn(),
|
|
120
|
+
rich.progress.TaskProgressColumn(),
|
|
121
|
+
rich.progress.TimeRemainingColumn(),
|
|
122
|
+
rich.progress.TransferSpeedColumn(),
|
|
123
|
+
) as progress:
|
|
124
|
+
task_id = progress.add_task('Cloning data record', total=total_size_in_bytes)
|
|
125
|
+
|
|
126
|
+
def on_progress(bytes_uploaded: int, _total_bytes: int) -> None:
|
|
127
|
+
progress.update(task_id, completed=bytes_uploaded)
|
|
128
|
+
|
|
129
|
+
DataRecord.clone(source=source_record, destination=dest_record, on_progress=on_progress)
|
|
130
|
+
else:
|
|
131
|
+
logger.info(f'Cloning ~{round(total_size_in_bytes / 10**6)}mb of data')
|
|
132
|
+
DataRecord.clone(source=source_record, destination=dest_record)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_or_create_destination_record(destination_uri: str) -> Optional[DataRecord]:
|
|
136
|
+
try:
|
|
137
|
+
return DataRecord.get_by_uri(uri=destination_uri)
|
|
138
|
+
except Exception:
|
|
139
|
+
print(f'Destination data record "{destination_uri}" does not exist.')
|
|
140
|
+
confirmation = input('Would you like to create it? [y/N]: ')
|
|
141
|
+
if confirmation.lower() != 'y':
|
|
142
|
+
print('Clone cancelled.')
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
return DataRecord.create(destination=destination_uri)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@data_record.command(help='Clone a Data Record to another location')
|
|
149
|
+
@click.argument('source_uri', required=True)
|
|
150
|
+
@click.argument('destination_uri', required=True)
|
|
151
|
+
def clone(source_uri: str, destination_uri: str) -> None:
|
|
152
|
+
BiolibApiClient.assert_is_signed_in(authenticated_action_description='clone a Data Record')
|
|
153
|
+
|
|
154
|
+
logger.info(f'Fetching source data record: {source_uri}')
|
|
155
|
+
source_record = DataRecord.get_by_uri(uri=source_uri)
|
|
156
|
+
|
|
157
|
+
logger.info(f'Checking destination data record: {destination_uri}')
|
|
158
|
+
dest_record = _get_or_create_destination_record(destination_uri)
|
|
159
|
+
if dest_record is None:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
logger.info(f'Cloning from {source_record.uri} to {dest_record.uri}...')
|
|
163
|
+
_clone_data_record_with_progress(source_record=source_record, dest_record=dest_record)
|
|
164
|
+
logger.info('Clone completed successfully.')
|
biolib/cli/index.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from biolib._index.index import Index
|
|
8
|
+
from biolib.biolib_errors import BioLibError
|
|
9
|
+
from biolib.biolib_logging import logger, logger_no_user_data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group(help='Manage Indexes')
|
|
13
|
+
def index() -> None:
|
|
14
|
+
logger.configure(default_log_level=logging.INFO)
|
|
15
|
+
logger_no_user_data.configure(default_log_level=logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@index.command(help='Create an Index')
|
|
19
|
+
@click.argument('uri', required=True)
|
|
20
|
+
@click.option('--config-path', required=True, type=click.Path(exists=True), help='Path to JSON config file')
|
|
21
|
+
def create(uri: str, config_path: str) -> None:
|
|
22
|
+
try:
|
|
23
|
+
Index.create_from_config_file(uri=uri, config_path=config_path)
|
|
24
|
+
except json.JSONDecodeError as error:
|
|
25
|
+
print(f'Error: Invalid JSON in config file: {error}', file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
except BioLibError as error:
|
|
28
|
+
print(f'Error creating index: {error.message}', file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
except Exception as error:
|
|
31
|
+
print(f'Error reading config file: {error}', file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|
biolib/cli/init.py
CHANGED
|
@@ -1,98 +1,420 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
|
-
from biolib import
|
|
8
|
+
from biolib import (
|
|
9
|
+
biolib_errors,
|
|
10
|
+
utils, # Import like this to let BASE_URL_IS_PUBLIC_BIOLIB be set correctly
|
|
11
|
+
)
|
|
8
12
|
from biolib._internal.add_copilot_prompts import add_copilot_prompts
|
|
13
|
+
from biolib._internal.add_gui_files import add_gui_files
|
|
14
|
+
from biolib._internal.http_client import HttpClient, HttpError
|
|
15
|
+
from biolib._internal.string_utils import normalize_for_docker_tag
|
|
9
16
|
from biolib._internal.templates import templates
|
|
17
|
+
from biolib._internal.utils import get_pip_command
|
|
18
|
+
from biolib.api import client as api_client
|
|
19
|
+
from biolib.biolib_api_client.api_client import BiolibApiClient
|
|
20
|
+
from biolib.biolib_api_client.biolib_app_api import BiolibAppApi
|
|
21
|
+
from biolib.biolib_logging import logger_no_user_data
|
|
22
|
+
from biolib.typing_utils import Dict, List, Optional, Set
|
|
23
|
+
from biolib.user.sign_in import sign_in
|
|
10
24
|
from biolib.utils import BIOLIB_PACKAGE_VERSION
|
|
11
25
|
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
def _get_latest_pypi_version() -> Optional[str]:
|
|
28
|
+
try:
|
|
29
|
+
response = HttpClient.request(
|
|
30
|
+
url='https://pypi.org/pypi/pybiolib/json',
|
|
31
|
+
timeout_in_seconds=5,
|
|
32
|
+
retries=1,
|
|
33
|
+
)
|
|
34
|
+
data = response.json()
|
|
35
|
+
version = data.get('info', {}).get('version')
|
|
36
|
+
if isinstance(version, str):
|
|
37
|
+
return version
|
|
38
|
+
return None
|
|
39
|
+
except Exception as error:
|
|
40
|
+
logger_no_user_data.debug(f'Failed to fetch latest version from PyPI: {error}')
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_current_version_outdated(current: str, latest: str) -> bool:
|
|
45
|
+
try:
|
|
46
|
+
current_parts = [int(x) for x in current.split('.')]
|
|
47
|
+
latest_parts = [int(x) for x in latest.split('.')]
|
|
48
|
+
return current_parts < latest_parts
|
|
49
|
+
except (ValueError, AttributeError):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check_version_and_prompt_upgrade() -> bool:
|
|
54
|
+
latest_version = _get_latest_pypi_version()
|
|
55
|
+
if latest_version and _is_current_version_outdated(BIOLIB_PACKAGE_VERSION, latest_version):
|
|
56
|
+
print(f'A newer version of pybiolib is available: {latest_version} (current: {BIOLIB_PACKAGE_VERSION})')
|
|
57
|
+
pip_command = get_pip_command()
|
|
58
|
+
print(f'To upgrade, run: {pip_command} install --upgrade pybiolib')
|
|
59
|
+
print()
|
|
60
|
+
continue_input = input('Do you want to continue with the current version? [y/N]: ')
|
|
61
|
+
if continue_input.lower() not in ['y', 'yes']:
|
|
62
|
+
print('Please upgrade pybiolib and run `biolib init` again.')
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _prompt_for_app_uri(prompt_text: str) -> Optional[str]:
|
|
68
|
+
app_uri = input(prompt_text)
|
|
69
|
+
|
|
70
|
+
if app_uri and not app_uri.startswith('@'):
|
|
71
|
+
try:
|
|
72
|
+
response = api_client.get('system/enterprise/config/', authenticate=False)
|
|
73
|
+
config = response.json()
|
|
74
|
+
prefix = config.get('resource_hostname_prefix')
|
|
75
|
+
if prefix:
|
|
76
|
+
app_uri = f'@{prefix}/{app_uri}'
|
|
77
|
+
print(f'Detected enterprise deployment, using URI: {app_uri}')
|
|
78
|
+
except HttpError as error:
|
|
79
|
+
if error.code not in [404, 501]:
|
|
80
|
+
print(f'Warning: Could not detect enterprise configuration: {error}')
|
|
81
|
+
except Exception as error:
|
|
82
|
+
print(f'Warning: Could not detect enterprise configuration: {error}')
|
|
83
|
+
|
|
84
|
+
return app_uri if app_uri else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_or_create_app(app_uri: str) -> bool:
|
|
88
|
+
try:
|
|
89
|
+
if BiolibApiClient.is_reauthentication_needed():
|
|
90
|
+
sign_in_input = input('You need to sign in to validate/create apps. Would you like to sign in? [y/N]: ')
|
|
91
|
+
if sign_in_input.lower() in ['y', 'yes']:
|
|
92
|
+
sign_in()
|
|
93
|
+
else:
|
|
94
|
+
print('Skipping app validation and creation. You can set the URI in .biolib/config.yml later.')
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
BiolibAppApi.get_by_uri(app_uri)
|
|
98
|
+
print(f'App {app_uri} already exists.')
|
|
99
|
+
except biolib_errors.NotFound:
|
|
100
|
+
create_app_input = input(f'App {app_uri} does not exist. Would you like to create it? [y/N]: ')
|
|
101
|
+
if create_app_input.lower() in ['y', 'yes']:
|
|
102
|
+
try:
|
|
103
|
+
BiolibAppApi.create_app(app_uri)
|
|
104
|
+
print(f'Successfully created app {app_uri}')
|
|
105
|
+
except Exception as error:
|
|
106
|
+
print(f'Failed to create app {app_uri}: {str(error)}')
|
|
107
|
+
print('You can create the app manually later or set the URI in .biolib/config.yml')
|
|
108
|
+
else:
|
|
109
|
+
print('App creation skipped. You can create the app manually later or set the URI in .biolib/config.yml')
|
|
110
|
+
except Exception as error:
|
|
111
|
+
print(f'Failed to validate app {app_uri}: {str(error)}')
|
|
112
|
+
print('Continuing with initialization...')
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _find_conflicting_files(template_dir: str, cwd: str) -> List[str]:
|
|
118
|
+
conflicting_files: List[str] = []
|
|
119
|
+
for root, dirs, filenames in os.walk(template_dir):
|
|
120
|
+
dirs[:] = [d for d in dirs if '__pycache__' not in d]
|
|
121
|
+
relative_dir = os.path.relpath(root, template_dir)
|
|
122
|
+
destination_dir = cwd if relative_dir == '.' else os.path.join(cwd, relative_dir)
|
|
123
|
+
|
|
124
|
+
for filename in filenames:
|
|
125
|
+
source_file = os.path.join(root, filename)
|
|
126
|
+
destination_file = os.path.join(destination_dir, filename)
|
|
127
|
+
if os.path.exists(destination_file):
|
|
128
|
+
with open(source_file, 'rb') as fsrc, open(destination_file, 'rb') as fdest:
|
|
129
|
+
if fsrc.read() != fdest.read():
|
|
130
|
+
conflicting_files.append(os.path.relpath(destination_file, cwd))
|
|
131
|
+
return conflicting_files
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _prompt_for_overwrites(conflicting_files: List[str]) -> Set[str]:
|
|
135
|
+
files_to_overwrite: Set[str] = set()
|
|
136
|
+
if conflicting_files:
|
|
137
|
+
print('The following files already exist and would be overwritten:')
|
|
138
|
+
for conflicting_file in conflicting_files:
|
|
139
|
+
print(f' {conflicting_file}')
|
|
140
|
+
print()
|
|
141
|
+
|
|
142
|
+
for conflicting_file in conflicting_files:
|
|
143
|
+
choice = input(f'Overwrite {conflicting_file}? [y/N]: ').lower().strip()
|
|
144
|
+
if choice in ['y', 'yes']:
|
|
145
|
+
files_to_overwrite.add(conflicting_file)
|
|
146
|
+
return files_to_overwrite
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _copy_template_files(
|
|
150
|
+
template_dir: str,
|
|
151
|
+
cwd: str,
|
|
152
|
+
files_to_overwrite: Set[str],
|
|
153
|
+
replacements: Dict[str, str],
|
|
154
|
+
) -> None:
|
|
155
|
+
for root, dirs, filenames in os.walk(template_dir):
|
|
156
|
+
dirs[:] = [d for d in dirs if '__pycache__' not in d]
|
|
157
|
+
relative_dir = os.path.relpath(root, template_dir)
|
|
158
|
+
destination_dir = os.path.join(cwd, relative_dir)
|
|
159
|
+
|
|
160
|
+
os.makedirs(destination_dir, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
for filename in filenames:
|
|
163
|
+
if utils.BASE_URL_IS_PUBLIC_BIOLIB and filename == 'biolib.yml':
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
source_file = os.path.join(root, filename)
|
|
167
|
+
destination_file = os.path.join(destination_dir, filename)
|
|
168
|
+
relative_file_path = os.path.relpath(destination_file, cwd)
|
|
169
|
+
|
|
170
|
+
if not os.path.exists(destination_file) or relative_file_path in files_to_overwrite:
|
|
171
|
+
try:
|
|
172
|
+
with open(source_file) as f:
|
|
173
|
+
content = f.read()
|
|
174
|
+
|
|
175
|
+
new_content = content
|
|
176
|
+
for old_value, new_value in replacements.items():
|
|
177
|
+
new_content = new_content.replace(old_value, new_value)
|
|
178
|
+
|
|
179
|
+
with open(destination_file, 'w') as f:
|
|
180
|
+
f.write(new_content)
|
|
181
|
+
except UnicodeDecodeError:
|
|
182
|
+
shutil.copy2(source_file, destination_file)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _create_readme_if_needed(cwd: str, app_name: Optional[str]) -> None:
|
|
186
|
+
readme_path = os.path.join(cwd, 'README.md')
|
|
187
|
+
if not os.path.exists(readme_path) and app_name:
|
|
188
|
+
with open(readme_path, 'w') as readme_file:
|
|
189
|
+
readme_file.write(f'# {app_name}\n')
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _copy_gitignore(cwd: str) -> None:
|
|
193
|
+
gitignore_template_dir = templates.gitignore_template()
|
|
194
|
+
source_file = os.path.join(gitignore_template_dir, '.gitignore')
|
|
195
|
+
destination_file = os.path.join(cwd, '.gitignore')
|
|
196
|
+
if not os.path.exists(destination_file):
|
|
197
|
+
shutil.copy2(source_file, destination_file)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _copy_github_workflow(cwd: str, replacements: Dict[str, str]) -> None:
|
|
201
|
+
workflow_template_dir = templates.github_workflow_template()
|
|
202
|
+
source_file = os.path.join(workflow_template_dir, '.github', 'workflows', 'biolib.yml')
|
|
203
|
+
destination_dir = os.path.join(cwd, '.github', 'workflows')
|
|
204
|
+
destination_file = os.path.join(destination_dir, 'biolib.yml')
|
|
205
|
+
|
|
206
|
+
if not os.path.exists(destination_file):
|
|
207
|
+
os.makedirs(destination_dir, exist_ok=True)
|
|
208
|
+
with open(source_file) as f:
|
|
209
|
+
content = f.read()
|
|
210
|
+
for old_value, new_value in replacements.items():
|
|
211
|
+
content = content.replace(old_value, new_value)
|
|
212
|
+
with open(destination_file, 'w') as f:
|
|
213
|
+
f.write(content)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _prompt_and_run_yarn_install() -> None:
|
|
217
|
+
yarn_install_input = input('Do you want to run yarn install? [Y/n]: ')
|
|
218
|
+
if yarn_install_input.lower() not in ['n', 'no']:
|
|
219
|
+
print('Running yarn install...')
|
|
220
|
+
try:
|
|
221
|
+
subprocess.run(['yarn', 'install'], check=True)
|
|
222
|
+
print('yarn install completed successfully.')
|
|
223
|
+
except FileNotFoundError:
|
|
224
|
+
print(
|
|
225
|
+
'Error: yarn is not installed or not found in PATH. Please install yarn and run yarn install manually.'
|
|
226
|
+
)
|
|
227
|
+
except subprocess.CalledProcessError as error:
|
|
228
|
+
print(f'yarn install failed with exit code {error.returncode}. Please run yarn install manually.')
|
|
229
|
+
except Exception as error:
|
|
230
|
+
print(f'yarn install failed: {error}. Please run yarn install manually.')
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _create_dashboard_dockerfile(cwd: str) -> None:
|
|
234
|
+
gui_template_dir = templates.gui_template()
|
|
235
|
+
gui_dockerfile = os.path.join(gui_template_dir, 'Dockerfile')
|
|
236
|
+
destination_file = os.path.join(cwd, 'Dockerfile')
|
|
237
|
+
|
|
238
|
+
if not os.path.exists(destination_file):
|
|
239
|
+
with open(gui_dockerfile) as f:
|
|
240
|
+
lines = f.readlines()
|
|
241
|
+
|
|
242
|
+
gui_builder_lines = []
|
|
243
|
+
for line in lines:
|
|
244
|
+
gui_builder_lines.append(line)
|
|
245
|
+
if line.strip() == 'RUN yarn build':
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
dist_export_stage = '\nFROM scratch AS dist_export\nCOPY --from=gui_builder /home/biolib/gui/dist /dist\n'
|
|
249
|
+
with open(destination_file, 'w') as f:
|
|
250
|
+
f.writelines(gui_builder_lines)
|
|
251
|
+
f.write(dist_export_stage)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _copy_gui_files_for_dashboard(cwd: str, app_name: str) -> None:
|
|
255
|
+
gui_template_dir = templates.gui_template()
|
|
256
|
+
gui_root_files = ['package.json', 'vite.config.mts', '.yarnrc.yml']
|
|
257
|
+
|
|
258
|
+
for root, _, filenames in os.walk(gui_template_dir):
|
|
259
|
+
relative_dir = os.path.relpath(root, gui_template_dir)
|
|
260
|
+
|
|
261
|
+
for filename in filenames:
|
|
262
|
+
if filename == 'Dockerfile':
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
if filename in gui_root_files:
|
|
266
|
+
destination_dir = cwd
|
|
267
|
+
else:
|
|
268
|
+
if relative_dir == '.':
|
|
269
|
+
destination_dir = os.path.join(cwd, 'gui')
|
|
270
|
+
else:
|
|
271
|
+
destination_dir = os.path.join(cwd, 'gui', relative_dir)
|
|
272
|
+
|
|
273
|
+
source_file = os.path.join(root, filename)
|
|
274
|
+
destination_file = os.path.join(destination_dir, filename)
|
|
275
|
+
|
|
276
|
+
if not os.path.exists(destination_file):
|
|
277
|
+
os.makedirs(destination_dir, exist_ok=True)
|
|
278
|
+
try:
|
|
279
|
+
with open(source_file) as f:
|
|
280
|
+
content = f.read()
|
|
281
|
+
new_content = content.replace('BIOLIB_REPLACE_APP_NAME', app_name)
|
|
282
|
+
with open(destination_file, 'w') as f:
|
|
283
|
+
f.write(new_content)
|
|
284
|
+
except UnicodeDecodeError:
|
|
285
|
+
shutil.copy2(source_file, destination_file)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _init_dashboard() -> None:
|
|
289
|
+
if not _check_version_and_prompt_upgrade():
|
|
290
|
+
return
|
|
291
|
+
|
|
15
292
|
cwd = os.getcwd()
|
|
293
|
+
app_uri = _prompt_for_app_uri('What URI do you want to create the dashboard under? (leave blank to skip): ')
|
|
294
|
+
app_name = app_uri.split('/')[-1] if app_uri else None
|
|
295
|
+
|
|
296
|
+
if app_uri:
|
|
297
|
+
if not _validate_or_create_app(app_uri):
|
|
298
|
+
return
|
|
299
|
+
else:
|
|
300
|
+
print(
|
|
301
|
+
'Remember to set the app URI in the .biolib/config.yml file later, '
|
|
302
|
+
'and update the .github/workflows/biolib.yml file.'
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
dashboard_template_dir = templates.dashboard_template()
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
conflicting_files = _find_conflicting_files(dashboard_template_dir, cwd)
|
|
309
|
+
files_to_overwrite = _prompt_for_overwrites(conflicting_files)
|
|
310
|
+
|
|
311
|
+
replace_app_uri = app_uri if app_uri else 'PUT_APP_URI_HERE'
|
|
312
|
+
replace_app_name = app_name if app_name else 'biolib-dashboard'
|
|
16
313
|
|
|
17
|
-
|
|
314
|
+
replacements = {
|
|
315
|
+
'BIOLIB_REPLACE_APP_URI': replace_app_uri,
|
|
316
|
+
'BIOLIB_REPLACE_APP_NAME': replace_app_name,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_copy_template_files(dashboard_template_dir, cwd, files_to_overwrite, replacements)
|
|
320
|
+
_create_readme_if_needed(cwd, app_name)
|
|
321
|
+
_copy_gitignore(cwd)
|
|
322
|
+
_copy_github_workflow(
|
|
323
|
+
cwd,
|
|
324
|
+
{
|
|
325
|
+
'BIOLIB_REPLACE_APP_URI': replace_app_uri,
|
|
326
|
+
'BIOLIB_REPLACE_BUILD_COMMAND': 'docker build --target dist_export -o type=local,dest=. .',
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
_create_dashboard_dockerfile(cwd)
|
|
330
|
+
_copy_gui_files_for_dashboard(cwd, replace_app_name)
|
|
331
|
+
_prompt_and_run_yarn_install()
|
|
332
|
+
|
|
333
|
+
print('Dashboard template initialized successfully.')
|
|
334
|
+
|
|
335
|
+
except KeyboardInterrupt:
|
|
336
|
+
print('\nInit command cancelled.', file=sys.stderr)
|
|
337
|
+
exit(1)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@click.command(help='Initialize a BioLib project', hidden=True)
|
|
341
|
+
@click.argument('template_type', required=False, default=None)
|
|
342
|
+
def init(template_type: Optional[str]) -> None:
|
|
343
|
+
if template_type == 'dashboard':
|
|
344
|
+
_init_dashboard()
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
if template_type is not None:
|
|
348
|
+
print(f"Unknown template type: '{template_type}'. Available templates: dashboard")
|
|
349
|
+
print('Run `biolib init` without arguments for the default Python application template.')
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
if not _check_version_and_prompt_upgrade():
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
cwd = os.getcwd()
|
|
356
|
+
app_uri = _prompt_for_app_uri('What URI do you want to create the application under? (leave blank to skip): ')
|
|
18
357
|
app_name = app_uri.split('/')[-1] if app_uri else None
|
|
19
|
-
if
|
|
358
|
+
docker_tag = normalize_for_docker_tag(app_name) if app_name else None
|
|
359
|
+
|
|
360
|
+
if app_uri:
|
|
361
|
+
if not _validate_or_create_app(app_uri):
|
|
362
|
+
return
|
|
363
|
+
else:
|
|
20
364
|
print(
|
|
21
365
|
'Remember to set the app URI in the .biolib/config.yml file later, '
|
|
22
366
|
'and docker image name in the .biolib/config.yml and .github/workflows/biolib.yml files.'
|
|
23
367
|
)
|
|
24
|
-
copilot_input = input('Do you want to include Copilot style prompts? [y/N]: ')
|
|
25
|
-
include_copilot_style = copilot_input.lower() == 'y'
|
|
26
368
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
369
|
+
advanced_setup_input = input('Do you want to set up advanced features like Copilot and GUI? [y/N]: ')
|
|
370
|
+
advanced_setup = advanced_setup_input.lower() == 'y'
|
|
371
|
+
include_copilot = False
|
|
372
|
+
include_gui = False
|
|
373
|
+
if advanced_setup:
|
|
374
|
+
copilot_enabled_input = input('Do you want to include Copilot instructions and prompts? [y/N]: ')
|
|
375
|
+
include_copilot = copilot_enabled_input.lower() == 'y'
|
|
376
|
+
include_gui_input = input('Do you want to include GUI setup? [y/N]: ')
|
|
377
|
+
include_gui = include_gui_input.lower() == 'y'
|
|
378
|
+
|
|
379
|
+
init_template_dir = templates.init_template()
|
|
30
380
|
|
|
31
381
|
try:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
dirs[:] = [d for d in dirs if '__pycache__' not in d]
|
|
35
|
-
relative_dir = os.path.relpath(root, template_dir)
|
|
36
|
-
destination_dir = cwd if relative_dir == '.' else os.path.join(cwd, relative_dir)
|
|
37
|
-
for filename in filenames:
|
|
38
|
-
source_file = os.path.join(root, filename)
|
|
39
|
-
destination_file = os.path.join(destination_dir, filename)
|
|
40
|
-
if os.path.exists(destination_file):
|
|
41
|
-
with open(source_file, 'rb') as fsrc, open(destination_file, 'rb') as fdest:
|
|
42
|
-
if fsrc.read() != fdest.read():
|
|
43
|
-
conflicting_files.append(os.path.relpath(destination_file, cwd))
|
|
44
|
-
|
|
45
|
-
if conflicting_files:
|
|
46
|
-
print('The following files already exist and would be overwritten:')
|
|
47
|
-
for conflicting_file in conflicting_files:
|
|
48
|
-
print(f' {conflicting_file}')
|
|
49
|
-
print()
|
|
50
|
-
|
|
51
|
-
for conflicting_file in conflicting_files:
|
|
52
|
-
choice = input(f'Overwrite {conflicting_file}? [y/N]: ').lower().strip()
|
|
53
|
-
if choice in ['y', 'yes']:
|
|
54
|
-
files_to_overwrite.add(conflicting_file)
|
|
382
|
+
conflicting_files = _find_conflicting_files(init_template_dir, cwd)
|
|
383
|
+
files_to_overwrite = _prompt_for_overwrites(conflicting_files)
|
|
55
384
|
|
|
56
385
|
replace_app_uri = app_uri if app_uri else 'PUT_APP_URI_HERE'
|
|
386
|
+
replace_app_name = app_name if app_name else 'biolib-app'
|
|
387
|
+
|
|
388
|
+
gui_config = "main_output_file: '/result.html'\n" if include_gui else ''
|
|
389
|
+
gui_mv_command = 'mv result.html output/result.html\n' if include_gui else ''
|
|
390
|
+
|
|
391
|
+
replacements = {
|
|
392
|
+
'BIOLIB_REPLACE_PYBIOLIB_VERSION': BIOLIB_PACKAGE_VERSION,
|
|
393
|
+
'BIOLIB_REPLACE_APP_URI': replace_app_uri,
|
|
394
|
+
'BIOLIB_REPLACE_DOCKER_TAG': docker_tag if docker_tag else 'PUT_DOCKER_TAG_HERE',
|
|
395
|
+
'BIOLIB_REPLACE_APP_NAME': replace_app_name,
|
|
396
|
+
'BIOLIB_REPLACE_GUI_CONFIG\n': gui_config,
|
|
397
|
+
'BIOLIB_REPLACE_GUI_MV_COMMAND\n': gui_mv_command,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_copy_template_files(init_template_dir, cwd, files_to_overwrite, replacements)
|
|
401
|
+
_create_readme_if_needed(cwd, app_name)
|
|
402
|
+
_copy_gitignore(cwd)
|
|
403
|
+
build_tag = docker_tag if docker_tag else 'PUT_DOCKER_TAG_HERE'
|
|
404
|
+
_copy_github_workflow(
|
|
405
|
+
cwd,
|
|
406
|
+
{
|
|
407
|
+
'BIOLIB_REPLACE_APP_URI': replace_app_uri,
|
|
408
|
+
'BIOLIB_REPLACE_BUILD_COMMAND': f'docker build -t {build_tag}:latest .',
|
|
409
|
+
},
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if include_copilot:
|
|
413
|
+
add_copilot_prompts(force=False, silent=True)
|
|
57
414
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
relative_dir = os.path.relpath(root, template_dir)
|
|
62
|
-
destination_dir = os.path.join(cwd, relative_dir)
|
|
63
|
-
os.makedirs(destination_dir, exist_ok=True)
|
|
64
|
-
|
|
65
|
-
for filename in filenames:
|
|
66
|
-
if utils.BASE_URL_IS_PUBLIC_BIOLIB and filename == 'biolib.yml':
|
|
67
|
-
continue
|
|
68
|
-
|
|
69
|
-
source_file = os.path.join(root, filename)
|
|
70
|
-
destination_file = os.path.join(destination_dir, filename)
|
|
71
|
-
relative_file_path = os.path.relpath(destination_file, cwd)
|
|
72
|
-
|
|
73
|
-
if not os.path.exists(destination_file) or relative_file_path in files_to_overwrite:
|
|
74
|
-
try:
|
|
75
|
-
with open(source_file) as f:
|
|
76
|
-
content = f.read()
|
|
77
|
-
|
|
78
|
-
new_content = content.replace('BIOLIB_REPLACE_PYBIOLIB_VERSION', BIOLIB_PACKAGE_VERSION)
|
|
79
|
-
new_content = new_content.replace('BIOLIB_REPLACE_APP_URI', replace_app_uri)
|
|
80
|
-
new_content = new_content.replace(
|
|
81
|
-
'BIOLIB_REPLACE_DOCKER_TAG',
|
|
82
|
-
app_name if app_name else 'PUT_DOCKER_TAG_HERE',
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
with open(destination_file, 'w') as f:
|
|
86
|
-
f.write(new_content)
|
|
87
|
-
except UnicodeDecodeError:
|
|
88
|
-
shutil.copy2(source_file, destination_file)
|
|
89
|
-
|
|
90
|
-
readme_path = os.path.join(cwd, 'README.md')
|
|
91
|
-
if not os.path.exists(readme_path) and app_name:
|
|
92
|
-
with open(readme_path, 'w') as readme_file:
|
|
93
|
-
readme_file.write(f'# {app_name}\n')
|
|
94
|
-
|
|
95
|
-
add_copilot_prompts(force=False, style=include_copilot_style, silent=True)
|
|
415
|
+
if include_gui:
|
|
416
|
+
add_gui_files(force=False, silent=True)
|
|
417
|
+
_prompt_and_run_yarn_install()
|
|
96
418
|
|
|
97
419
|
except KeyboardInterrupt:
|
|
98
420
|
print('\nInit command cancelled.', file=sys.stderr)
|
biolib/cli/lfs.py
CHANGED