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
biolib/cli/auth.py CHANGED
@@ -3,7 +3,7 @@ import sys
3
3
 
4
4
  import click
5
5
 
6
- from biolib import api, biolib_errors
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
- user_uuid = None
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
- intrinsic_account = [account for account in user_dict['accounts'] if account['role'] == 'intrinsic'][0]
54
- display_name = intrinsic_account['display_name']
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 utils # Import like this to let BASE_URL_IS_PUBLIC_BIOLIB be set correctly
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
- @click.command(help='Initialize a BioLib project', hidden=True)
14
- def init() -> None:
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
- app_uri = input('What URI do you want to create the application under? (leave blank to skip): ')
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 not app_uri:
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
- template_dir = templates.init_template()
28
- conflicting_files = []
29
- files_to_overwrite = set()
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
- # First pass: check for conflicts
33
- for root, dirs, filenames in os.walk(template_dir):
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
- # Second pass: copy files (only if no conflicts)
59
- for root, dirs, filenames in os.walk(template_dir):
60
- dirs[:] = [d for d in dirs if '__pycache__' not in d]
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
@@ -13,7 +13,7 @@ from biolib.biolib_logging import logger, logger_no_user_data
13
13
  from biolib.typing_utils import Optional
14
14
 
15
15
 
16
- @click.group(help='Manage Large File Systems')
16
+ @click.group(help='Manage Large File Systems', hidden=True)
17
17
  def lfs() -> None:
18
18
  pass
19
19