divbase-cli 0.1.0a12__tar.gz → 0.1.0a15__tar.gz
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.
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/.gitignore +3 -2
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/PKG-INFO +5 -5
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/pyproject.toml +3 -3
- divbase_cli-0.1.0a15/src/divbase_cli/__init__.py +1 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/auth_cli.py +2 -2
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/dimensions_cli.py +15 -22
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/file_cli.py +50 -72
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/query_cli.py +11 -17
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/task_history_cli.py +6 -5
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/version_cli.py +27 -39
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_exceptions.py +3 -6
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/config_resolver.py +20 -25
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/display_task_history.py +9 -23
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/services/s3_files.py +1 -1
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/user_auth.py +7 -7
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/user_config.py +1 -1
- divbase_cli-0.1.0a12/src/divbase_cli/__init__.py +0 -1
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/README.md +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/__init__.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/shared_args_options.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/user_config_cli.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_config.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/divbase_cli.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/retries.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/services/__init__.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/services/announcements.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/services/pre_signed_urls.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/services/project_versions.py +0 -0
- {divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/utils.py +0 -0
|
@@ -50,9 +50,10 @@ docker/logs/*
|
|
|
50
50
|
#MacOS artifacts
|
|
51
51
|
.DS_Store
|
|
52
52
|
|
|
53
|
-
# mkdocs build cache
|
|
53
|
+
# mkdocs/zensical build cache
|
|
54
54
|
.cache/
|
|
55
|
-
|
|
55
|
+
site/
|
|
56
|
+
# documentation site's auto generated files
|
|
56
57
|
docs/cli/_auto_generated/*.md
|
|
57
58
|
docs/releases/index.md
|
|
58
59
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: divbase-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a15
|
|
4
4
|
Summary: Command Line Interface for Divbase
|
|
5
5
|
Project-URL: Homepage, https://divbase.scilifelab-2-prod.sys.kth.se
|
|
6
6
|
Project-URL: Documentation, https://scilifelabdatacentre.github.io/divbase
|
|
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: divbase-lib==0.1.
|
|
22
|
-
Requires-Dist: keyring
|
|
23
|
-
Requires-Dist: typer==0.26.
|
|
21
|
+
Requires-Dist: divbase-lib==0.1.0a15
|
|
22
|
+
Requires-Dist: keyring==25.7.0
|
|
23
|
+
Requires-Dist: typer==0.26.8
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
# divbase-cli
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0a15"
|
|
@@ -4,7 +4,6 @@ CLI subcommand for managing user auth with DivBase server.
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
|
-
from datetime import datetime
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
from pydantic import SecretStr
|
|
@@ -31,6 +30,7 @@ from divbase_cli.user_auth import (
|
|
|
31
30
|
)
|
|
32
31
|
from divbase_cli.user_config import load_user_config
|
|
33
32
|
from divbase_lib.divbase_constants import PAT_TOKEN_PREFIX
|
|
33
|
+
from divbase_lib.utils import format_unix_timestamp_for_cli
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
@@ -63,7 +63,7 @@ def login(
|
|
|
63
63
|
session_expires_at = check_existing_session(divbase_url=divbase_url, config=config)
|
|
64
64
|
if session_expires_at:
|
|
65
65
|
print(f"Already logged in to {divbase_url} with email: {config.logged_in_email}.")
|
|
66
|
-
print(f"Session expires: {
|
|
66
|
+
print(f"Session expires: {format_unix_timestamp_for_cli(unix_timestamp=session_expires_at)}")
|
|
67
67
|
|
|
68
68
|
if not typer.confirm("Do you want to login again? This will replace your current session."):
|
|
69
69
|
print("Login cancelled.")
|
{divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/dimensions_cli.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import logging
|
|
3
|
+
from datetime import datetime
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import typer
|
|
@@ -8,7 +9,7 @@ from rich import print
|
|
|
8
9
|
from rich.table import Table
|
|
9
10
|
|
|
10
11
|
from divbase_cli.cli_commands.shared_args_options import PROJECT_NAME_OPTION
|
|
11
|
-
from divbase_cli.config_resolver import
|
|
12
|
+
from divbase_cli.config_resolver import resolve_and_authenticate_project
|
|
12
13
|
from divbase_cli.user_auth import make_authenticated_request
|
|
13
14
|
from divbase_cli.utils import print_rich_table_as_tsv
|
|
14
15
|
from divbase_lib.api_schemas.vcf_dimensions import (
|
|
@@ -18,6 +19,7 @@ from divbase_lib.api_schemas.vcf_dimensions import (
|
|
|
18
19
|
DimensionsVCFFilesResult,
|
|
19
20
|
)
|
|
20
21
|
from divbase_lib.metadata_validator import SharedMetadataValidator
|
|
22
|
+
from divbase_lib.utils import format_datetime_for_cli
|
|
21
23
|
|
|
22
24
|
logger = logging.getLogger(__name__)
|
|
23
25
|
|
|
@@ -32,10 +34,8 @@ dimensions_app = typer.Typer(
|
|
|
32
34
|
def update_dimensions_index(
|
|
33
35
|
project: str | None = PROJECT_NAME_OPTION,
|
|
34
36
|
) -> None:
|
|
35
|
-
"""Calculate and add the dimensions of all VCF files in the project to the dimensions
|
|
36
|
-
|
|
37
|
-
project_config = resolve_project(project_name=project)
|
|
38
|
-
|
|
37
|
+
"""Calculate and add the dimensions of all VCF files in the project to the dimensions cache in the project."""
|
|
38
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
39
39
|
response = make_authenticated_request(
|
|
40
40
|
method="PUT",
|
|
41
41
|
divbase_base_url=project_config.divbase_url,
|
|
@@ -96,19 +96,17 @@ def show_dimensions_index(
|
|
|
96
96
|
project: str | None = PROJECT_NAME_OPTION,
|
|
97
97
|
) -> None:
|
|
98
98
|
"""
|
|
99
|
-
Show the dimensions
|
|
99
|
+
Show the dimensions cache file for a project.
|
|
100
100
|
When running --unique-scaffolds, the sorting separates between numeric and non-numeric scaffold names.
|
|
101
101
|
"""
|
|
102
|
-
project_config = resolve_project(project_name=project)
|
|
103
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
104
|
-
|
|
105
102
|
if sample_names_output and sample_names_stdout:
|
|
106
103
|
raise typer.BadParameter("Use only one of --sample-names-output or --sample-names-stdout.")
|
|
107
104
|
|
|
105
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
108
106
|
if unique_samples:
|
|
109
107
|
response = make_authenticated_request(
|
|
110
108
|
method="GET",
|
|
111
|
-
divbase_base_url=
|
|
109
|
+
divbase_base_url=project_config.divbase_url,
|
|
112
110
|
api_route=f"v1/vcf-dimensions/projects/{project_config.name}/samples",
|
|
113
111
|
)
|
|
114
112
|
unique_sample_names_sorted = DimensionsSamplesResult(**response.json()).unique_samples
|
|
@@ -236,7 +234,7 @@ def _format_api_response_for_display_in_terminal(api_response: DimensionsShowRes
|
|
|
236
234
|
dimensions_entry = {
|
|
237
235
|
"filename": entry["vcf_file_s3_key"],
|
|
238
236
|
"file_version_ID_in_bucket": entry["s3_version_id"],
|
|
239
|
-
"last_updated":
|
|
237
|
+
"last_updated": format_datetime_for_cli(datetime.fromisoformat(entry["updated_at"])),
|
|
240
238
|
"dimensions": {
|
|
241
239
|
"scaffold_count": len(sorted_scaffolds),
|
|
242
240
|
"scaffolds": sorted_scaffolds,
|
|
@@ -278,12 +276,10 @@ def create_metadata_template_with_project_samples_names(
|
|
|
278
276
|
"""
|
|
279
277
|
Create a template sample metadata file (TSV format) pre-filled with the sample names from the project's VCF files based on the information stored in the project's VCF dimensions cache. Tip: run 'divbase-cli dimensions update' first to ensure that the VCF dimensions are up-to-date.
|
|
280
278
|
"""
|
|
281
|
-
project_config =
|
|
282
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
283
|
-
|
|
279
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
284
280
|
response = make_authenticated_request(
|
|
285
281
|
method="GET",
|
|
286
|
-
divbase_base_url=
|
|
282
|
+
divbase_base_url=project_config.divbase_url,
|
|
287
283
|
api_route=f"v1/vcf-dimensions/projects/{project_config.name}/samples",
|
|
288
284
|
)
|
|
289
285
|
unique_sample_names_sorted = DimensionsSamplesResult(**response.json()).unique_samples
|
|
@@ -294,7 +290,7 @@ def create_metadata_template_with_project_samples_names(
|
|
|
294
290
|
)
|
|
295
291
|
|
|
296
292
|
if sample_count == 0:
|
|
297
|
-
# Fallback in case there are no samples in the dimensions
|
|
293
|
+
# Fallback in case there are no samples in the dimensions cache. If no dimensions entry for the project
|
|
298
294
|
# VCFDimensionsEntryMissingError will be returned. But for some reason, there are no samples in the VCF, this will catch that.
|
|
299
295
|
print("No samples found for this project. No file written.")
|
|
300
296
|
return
|
|
@@ -341,17 +337,14 @@ def validate_metadata_template_versus_dimensions_and_formatting_constraints(
|
|
|
341
337
|
"""
|
|
342
338
|
# Client-side validation of a sidecar metadata TSV file, intended to be run before upload to DivBase.
|
|
343
339
|
# Uses the SharedMetadataValidator (that is also used on the server-side) which checks for formatting errors and also validates that the sample names
|
|
344
|
-
# in the TSV file match the sample names in the dimensions
|
|
345
|
-
|
|
346
|
-
project_config = resolve_project(project_name=project)
|
|
347
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
348
|
-
|
|
340
|
+
# in the TSV file match the sample names in the dimensions cache for the project
|
|
341
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
349
342
|
print(f"Validating local metadata file: {input_path}")
|
|
350
343
|
print(f"Project: {project_config.name}\n")
|
|
351
344
|
|
|
352
345
|
response = make_authenticated_request(
|
|
353
346
|
method="GET",
|
|
354
|
-
divbase_base_url=
|
|
347
|
+
divbase_base_url=project_config.divbase_url,
|
|
355
348
|
api_route=f"v1/vcf-dimensions/projects/{project_config.name}/samples",
|
|
356
349
|
)
|
|
357
350
|
unique_sample_names = DimensionsSamplesResult(**response.json()).unique_samples
|
|
@@ -3,10 +3,8 @@ Command line interface for managing files in a DivBase project's store on DivBas
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from collections import defaultdict
|
|
6
|
-
from datetime import datetime
|
|
7
6
|
from glob import glob
|
|
8
7
|
from pathlib import Path
|
|
9
|
-
from zoneinfo import ZoneInfo
|
|
10
8
|
|
|
11
9
|
import typer
|
|
12
10
|
from rich import print
|
|
@@ -24,7 +22,11 @@ from divbase_cli.cli_exceptions import (
|
|
|
24
22
|
ShellExpandedGlobError,
|
|
25
23
|
UnsupportedCharactersError,
|
|
26
24
|
)
|
|
27
|
-
from divbase_cli.config_resolver import
|
|
25
|
+
from divbase_cli.config_resolver import (
|
|
26
|
+
resolve_and_authenticate_project,
|
|
27
|
+
resolve_download_dir,
|
|
28
|
+
)
|
|
29
|
+
from divbase_cli.services.pre_signed_urls import DownloadOutcome
|
|
28
30
|
from divbase_cli.services.project_versions import get_version_details_command
|
|
29
31
|
from divbase_cli.services.s3_files import (
|
|
30
32
|
ToDownload,
|
|
@@ -48,14 +50,13 @@ from divbase_lib.divbase_constants import (
|
|
|
48
50
|
UNSUPPORTED_CHARACTERS_DISPLAY,
|
|
49
51
|
UNSUPPORTED_CHARACTERS_IN_FILENAMES,
|
|
50
52
|
)
|
|
51
|
-
from divbase_lib.utils import format_file_size
|
|
53
|
+
from divbase_lib.utils import format_datetime_for_cli, format_file_size
|
|
52
54
|
|
|
53
55
|
file_app = typer.Typer(no_args_is_help=True, help="Download/upload/list files to/from the project's store on DivBase.")
|
|
54
56
|
|
|
55
57
|
NO_FILES_SPECIFIED_MSG = "No files specified for the command, exiting..."
|
|
56
58
|
NO_UPLOAD_MATCHES_MSG = "The following file paths or glob patterns did not match any existing files:"
|
|
57
59
|
|
|
58
|
-
|
|
59
60
|
PREFIX_ARGUMENT = typer.Argument(
|
|
60
61
|
None,
|
|
61
62
|
help="Optional prefix to filter on. Use a '/' to specify a folder. e.g. 'vcfs/'",
|
|
@@ -143,12 +144,11 @@ def list_files(
|
|
|
143
144
|
if detailed and format_output_as_tsv:
|
|
144
145
|
raise typer.BadParameter(message="The --detailed option and --tsv option cannot be used together.")
|
|
145
146
|
|
|
146
|
-
project_config =
|
|
147
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
147
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
148
148
|
|
|
149
149
|
if show_deleted_files:
|
|
150
150
|
files = list_soft_deleted_files_command(
|
|
151
|
-
divbase_base_url=
|
|
151
|
+
divbase_base_url=project_config.divbase_url,
|
|
152
152
|
project_name=project_config.name,
|
|
153
153
|
prefix=prefix,
|
|
154
154
|
)
|
|
@@ -159,17 +159,17 @@ def list_files(
|
|
|
159
159
|
|
|
160
160
|
print(f"Soft deleted files for the project '{project_config.name}':")
|
|
161
161
|
for file_details in files:
|
|
162
|
-
|
|
162
|
+
deleted_at = format_datetime_for_cli(dt=file_details.last_modified)
|
|
163
163
|
if file_details.name.endswith("/"):
|
|
164
|
-
print(f"- [bold blue]'{file_details.name}'[/bold blue] (deleted at: '{
|
|
164
|
+
print(f"- [bold blue]'{file_details.name}'[/bold blue] (deleted at: '{deleted_at}')")
|
|
165
165
|
else:
|
|
166
|
-
print(f"- '{file_details.name}' (deleted at: '{
|
|
166
|
+
print(f"- '{file_details.name}' (deleted at: '{deleted_at}')")
|
|
167
167
|
print("\nTo restore a soft deleted file, use the 'divbase-cli files restore' command.")
|
|
168
168
|
print("To get more information about one of the soft deleted files, use the 'divbase-cli files info' command.")
|
|
169
169
|
return
|
|
170
170
|
|
|
171
171
|
files, folders = list_files_command(
|
|
172
|
-
divbase_base_url=
|
|
172
|
+
divbase_base_url=project_config.divbase_url,
|
|
173
173
|
project_name=project_config.name,
|
|
174
174
|
prefix=prefix,
|
|
175
175
|
include_results_files=include_results_files,
|
|
@@ -211,17 +211,14 @@ def tree_files(
|
|
|
211
211
|
- Show the tree starting from the 'vcfs/' folder:
|
|
212
212
|
divbase-cli files tree vcfs/
|
|
213
213
|
"""
|
|
214
|
-
project_config =
|
|
215
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
216
|
-
|
|
214
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
217
215
|
files, _ = list_files_command(
|
|
218
|
-
divbase_base_url=
|
|
216
|
+
divbase_base_url=project_config.divbase_url,
|
|
219
217
|
project_name=project_config.name,
|
|
220
218
|
prefix=prefix,
|
|
221
219
|
include_results_files=include_results_files,
|
|
222
220
|
file_system_view=False,
|
|
223
221
|
)
|
|
224
|
-
|
|
225
222
|
if not files:
|
|
226
223
|
print("No files found in the project's store on DivBase.")
|
|
227
224
|
return
|
|
@@ -247,16 +244,14 @@ def file_info(
|
|
|
247
244
|
- Get information about a specific file in the project:
|
|
248
245
|
divbase-cli files info my_file.vcf.gz
|
|
249
246
|
"""
|
|
250
|
-
project_config = resolve_project(project_name=project)
|
|
251
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
252
|
-
|
|
253
247
|
if file_name.endswith("/"):
|
|
254
248
|
raise typer.BadParameter(
|
|
255
249
|
message="The 'info' command is only for files, not folders. Please provide a file name without a trailing '/'."
|
|
256
250
|
)
|
|
257
251
|
|
|
252
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
258
253
|
file_info = get_file_info_command(
|
|
259
|
-
divbase_base_url=
|
|
254
|
+
divbase_base_url=project_config.divbase_url,
|
|
260
255
|
project_name=project_config.name,
|
|
261
256
|
object_name=file_name,
|
|
262
257
|
)
|
|
@@ -278,16 +273,16 @@ def file_info(
|
|
|
278
273
|
caption="Versions shown are ordered with the latest/current version first/at the top",
|
|
279
274
|
)
|
|
280
275
|
table.add_column("File size", justify="left", style="magenta", no_wrap=True)
|
|
281
|
-
table.add_column("Upload date
|
|
276
|
+
table.add_column("Upload date", justify="left", style="green", no_wrap=True)
|
|
282
277
|
table.add_column("MD5 checksum", justify="left", style="yellow")
|
|
283
278
|
table.add_column("Version ID", justify="left", style="cyan")
|
|
284
279
|
|
|
285
280
|
for version in file_info.versions:
|
|
286
|
-
|
|
281
|
+
upload_date = format_datetime_for_cli(dt=version.last_modified)
|
|
287
282
|
file_size = format_file_size(size_bytes=version.size)
|
|
288
283
|
table.add_row(
|
|
289
284
|
file_size,
|
|
290
|
-
|
|
285
|
+
upload_date,
|
|
291
286
|
version.etag,
|
|
292
287
|
version.version_id,
|
|
293
288
|
)
|
|
@@ -336,27 +331,26 @@ def download_files(
|
|
|
336
331
|
- Mix latest and versioned files in one command:
|
|
337
332
|
divbase-cli files download file1.vcf.gz "file2.tsv:VERSION_ID"
|
|
338
333
|
"""
|
|
339
|
-
project_config = resolve_project(project_name=project)
|
|
340
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
341
|
-
download_dir_path = resolve_download_dir(download_dir=download_dir)
|
|
342
|
-
|
|
343
334
|
if project_version == "":
|
|
344
335
|
raise typer.BadParameter(
|
|
345
336
|
"It seems like you provided an empty string for --project-version. \n"
|
|
346
337
|
"Please provide a valid project version or omit the flag."
|
|
347
338
|
)
|
|
348
339
|
|
|
340
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
341
|
+
download_dir_path = resolve_download_dir(download_dir=download_dir)
|
|
342
|
+
|
|
349
343
|
raw_files_input = _resolve_file_inputs(files=files, file_list=file_list)
|
|
350
344
|
raw_files_input = _expand_folder_prefixes(
|
|
351
345
|
files=raw_files_input,
|
|
352
|
-
divbase_base_url=
|
|
346
|
+
divbase_base_url=project_config.divbase_url,
|
|
353
347
|
project_name=project_config.name,
|
|
354
348
|
)
|
|
355
349
|
if flatten:
|
|
356
350
|
check_no_overlap_on_flattened_downloads(to_download=raw_files_input)
|
|
357
351
|
|
|
358
352
|
download_results = download_files_command(
|
|
359
|
-
divbase_base_url=
|
|
353
|
+
divbase_base_url=project_config.divbase_url,
|
|
360
354
|
project_name=project_config.name,
|
|
361
355
|
raw_files_input=raw_files_input,
|
|
362
356
|
download_dir=download_dir_path,
|
|
@@ -416,14 +410,15 @@ def download_all_files(
|
|
|
416
410
|
"Please provide a valid project version or omit the flag."
|
|
417
411
|
)
|
|
418
412
|
|
|
419
|
-
project_config =
|
|
420
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
413
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
421
414
|
download_dir_path = resolve_download_dir(download_dir=download_dir)
|
|
422
415
|
|
|
423
416
|
all_files: list[ToDownload] = []
|
|
424
417
|
if project_version:
|
|
425
418
|
version_details = get_version_details_command(
|
|
426
|
-
project_name=project_config.name,
|
|
419
|
+
project_name=project_config.name,
|
|
420
|
+
divbase_base_url=project_config.divbase_url,
|
|
421
|
+
version_name=project_version,
|
|
427
422
|
)
|
|
428
423
|
for version_name, file_details in version_details.files.items():
|
|
429
424
|
all_files.append(
|
|
@@ -437,7 +432,7 @@ def download_all_files(
|
|
|
437
432
|
|
|
438
433
|
else:
|
|
439
434
|
files, _ = list_files_command(
|
|
440
|
-
divbase_base_url=
|
|
435
|
+
divbase_base_url=project_config.divbase_url,
|
|
441
436
|
project_name=project_config.name,
|
|
442
437
|
prefix=None,
|
|
443
438
|
include_results_files=False,
|
|
@@ -502,7 +497,7 @@ def download_all_files(
|
|
|
502
497
|
else:
|
|
503
498
|
raw_files_input = [file.s3_key for file in files_to_download]
|
|
504
499
|
download_results = download_files_command(
|
|
505
|
-
divbase_base_url=
|
|
500
|
+
divbase_base_url=project_config.divbase_url,
|
|
506
501
|
project_name=project_config.name,
|
|
507
502
|
raw_files_input=raw_files_input,
|
|
508
503
|
download_dir=download_dir_path,
|
|
@@ -542,11 +537,9 @@ def stream_file(
|
|
|
542
537
|
- Run a bcftools command (the "-" tells bcftools to read from standard input):
|
|
543
538
|
divbase-cli files stream my_file.vcf.gz | bcftools view -h -
|
|
544
539
|
"""
|
|
545
|
-
project_config =
|
|
546
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
547
|
-
|
|
540
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
548
541
|
stream_file_command(
|
|
549
|
-
divbase_base_url=
|
|
542
|
+
divbase_base_url=project_config.divbase_url,
|
|
550
543
|
project_name=project_config.name,
|
|
551
544
|
file_name=file_name,
|
|
552
545
|
version_id=version_id,
|
|
@@ -621,19 +614,15 @@ def upload_files(
|
|
|
621
614
|
- Upload from a text file list (one file path per line):
|
|
622
615
|
divbase-cli files upload --file-list files_to_upload.txt --to experiment1/
|
|
623
616
|
"""
|
|
624
|
-
project_config = resolve_project(project_name=project)
|
|
625
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
626
|
-
|
|
627
617
|
if bool(files) + bool(file_list) > 1:
|
|
628
618
|
raise typer.BadParameter("You cannot specify files as arguments and provide a --file-list.")
|
|
629
|
-
|
|
630
619
|
if skip_existing and disable_safe_mode:
|
|
631
620
|
raise typer.BadParameter(
|
|
632
621
|
"The --skip-existing and --disable-safe-mode options cannot be used together.\n"
|
|
633
622
|
"Safe mode calculates file checksums, "
|
|
634
623
|
"and these checksums are needed for --skip-existing to know what files to skip uploading."
|
|
635
624
|
)
|
|
636
|
-
|
|
625
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
637
626
|
to_upload = _resolve_user_upload_inputs(
|
|
638
627
|
files=files,
|
|
639
628
|
file_list=file_list,
|
|
@@ -645,7 +634,7 @@ def upload_files(
|
|
|
645
634
|
|
|
646
635
|
uploaded_results = upload_files_command(
|
|
647
636
|
project_name=project_config.name,
|
|
648
|
-
divbase_base_url=
|
|
637
|
+
divbase_base_url=project_config.divbase_url,
|
|
649
638
|
all_files=to_upload,
|
|
650
639
|
safe_mode=not disable_safe_mode,
|
|
651
640
|
skip_existing=skip_existing,
|
|
@@ -697,13 +686,12 @@ def make_directory(
|
|
|
697
686
|
- Create multiple directories at once:
|
|
698
687
|
divbase-cli files mkdir vcfs metadata results
|
|
699
688
|
"""
|
|
700
|
-
project_config =
|
|
701
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
702
|
-
|
|
689
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
703
690
|
cleaned_dir_names = _sanitize_directory_names(directories)
|
|
691
|
+
|
|
704
692
|
dirs_created = make_directories_command(
|
|
705
693
|
directories=cleaned_dir_names,
|
|
706
|
-
divbase_base_url=
|
|
694
|
+
divbase_base_url=project_config.divbase_url,
|
|
707
695
|
project_name=project_config.name,
|
|
708
696
|
)
|
|
709
697
|
if dirs_created.created:
|
|
@@ -741,13 +729,12 @@ def remove_directory(
|
|
|
741
729
|
- Remove multiple directories at once:
|
|
742
730
|
divbase-cli files rmdir vcfs/ metadata/
|
|
743
731
|
"""
|
|
744
|
-
project_config =
|
|
745
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
746
|
-
|
|
732
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
747
733
|
directories = _sanitize_directory_names(directories)
|
|
734
|
+
|
|
748
735
|
for dir in directories:
|
|
749
736
|
files, _ = list_files_command(
|
|
750
|
-
divbase_base_url=
|
|
737
|
+
divbase_base_url=project_config.divbase_url,
|
|
751
738
|
project_name=project_config.name,
|
|
752
739
|
prefix=dir,
|
|
753
740
|
include_results_files=True,
|
|
@@ -763,7 +750,7 @@ def remove_directory(
|
|
|
763
750
|
|
|
764
751
|
# NOTE: as dirs are just empty files which end with "/", we can just do a regular soft delete on them.
|
|
765
752
|
deleted_dirs = soft_delete_objects_command(
|
|
766
|
-
divbase_base_url=
|
|
753
|
+
divbase_base_url=project_config.divbase_url,
|
|
767
754
|
project_name=project_config.name,
|
|
768
755
|
all_files=directories,
|
|
769
756
|
)
|
|
@@ -800,9 +787,7 @@ def remove_files(
|
|
|
800
787
|
- Run a dry run to see what files would be deleted without actually deleting them:
|
|
801
788
|
divbase-cli files rm file1.vcf.gz file2.tsv --dry-run
|
|
802
789
|
"""
|
|
803
|
-
project_config =
|
|
804
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
805
|
-
|
|
790
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
806
791
|
all_files = _resolve_file_inputs(files=files, file_list=file_list)
|
|
807
792
|
|
|
808
793
|
if dry_run:
|
|
@@ -812,7 +797,7 @@ def remove_files(
|
|
|
812
797
|
return
|
|
813
798
|
|
|
814
799
|
deleted_files = soft_delete_objects_command(
|
|
815
|
-
divbase_base_url=
|
|
800
|
+
divbase_base_url=project_config.divbase_url,
|
|
816
801
|
project_name=project_config.name,
|
|
817
802
|
all_files=all_files,
|
|
818
803
|
)
|
|
@@ -845,13 +830,11 @@ def restore_soft_deleted_files(
|
|
|
845
830
|
- Restore files from a text file list (one file name per line):
|
|
846
831
|
divbase-cli files restore --file-list files_to_restore.txt
|
|
847
832
|
"""
|
|
848
|
-
project_config =
|
|
849
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
850
|
-
|
|
833
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
851
834
|
all_files = _resolve_file_inputs(files=files, file_list=file_list)
|
|
852
835
|
|
|
853
836
|
restored_objects_response = restore_objects_command(
|
|
854
|
-
divbase_base_url=
|
|
837
|
+
divbase_base_url=project_config.divbase_url,
|
|
855
838
|
project_name=project_config.name,
|
|
856
839
|
all_files=all_files,
|
|
857
840
|
)
|
|
@@ -923,8 +906,8 @@ def _print_ls_tsv_format(files: list[ObjectDetails], folders: list[str]) -> None
|
|
|
923
906
|
for folder in folders:
|
|
924
907
|
print(f"{markup_escape(folder)}\t-\t-")
|
|
925
908
|
for f in files:
|
|
926
|
-
|
|
927
|
-
print(f"{markup_escape(f.name)}\t{format_file_size(f.size)}\t{
|
|
909
|
+
last_modified = format_datetime_for_cli(dt=f.last_modified)
|
|
910
|
+
print(f"{markup_escape(f.name)}\t{format_file_size(f.size)}\t{last_modified}")
|
|
928
911
|
|
|
929
912
|
|
|
930
913
|
def _print_ls_detailed(files: list[ObjectDetails], folders: list[str]) -> None:
|
|
@@ -934,8 +917,8 @@ def _print_ls_detailed(files: list[ObjectDetails], folders: list[str]) -> None:
|
|
|
934
917
|
console.print(f"[bold blue]{markup_escape(folder)}[/bold blue]")
|
|
935
918
|
for f in files:
|
|
936
919
|
size_str = format_file_size(f.size).rjust(10)
|
|
937
|
-
|
|
938
|
-
console.print(f"{markup_escape(f.name)} [magenta]{size_str}[/magenta] [green]{
|
|
920
|
+
last_modified = format_datetime_for_cli(dt=f.last_modified)
|
|
921
|
+
console.print(f"{markup_escape(f.name)} [magenta]{size_str}[/magenta] [green]{last_modified}[/green]")
|
|
939
922
|
|
|
940
923
|
|
|
941
924
|
def _resolve_file_inputs(files: list[str] | None, file_list: Path | None) -> list[str]:
|
|
@@ -1191,7 +1174,7 @@ def _sanitize_directory_names(directories: list[str]) -> list[str]:
|
|
|
1191
1174
|
return cleaned_dir_names
|
|
1192
1175
|
|
|
1193
1176
|
|
|
1194
|
-
def _pretty_print_download_results(download_results):
|
|
1177
|
+
def _pretty_print_download_results(download_results: DownloadOutcome) -> None:
|
|
1195
1178
|
"""Helper fn used by download and download all commands to print the results of the download in a nice format."""
|
|
1196
1179
|
if download_results.successful:
|
|
1197
1180
|
print("\n[green bold]Successfully downloaded the following files:[/green bold]")
|
|
@@ -1203,8 +1186,3 @@ def _pretty_print_download_results(download_results):
|
|
|
1203
1186
|
for failed in download_results.failed:
|
|
1204
1187
|
print(f"[red]- '{failed.object_name}': [/red] Exception: '{failed.exception}'")
|
|
1205
1188
|
raise typer.Exit(1)
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
def _format_time_stamp(time_stamp: datetime) -> str:
|
|
1209
|
-
"""Helper fn to format a datetime object into a string for printing."""
|
|
1210
|
-
return time_stamp.astimezone(ZoneInfo("CET")).strftime("%Y-%m-%d %H:%M %Z")
|
|
@@ -23,9 +23,8 @@ from divbase_cli.cli_commands.shared_args_options import DOWNLOAD_DIR_OPTION, PR
|
|
|
23
23
|
from divbase_cli.cli_config import cli_settings
|
|
24
24
|
from divbase_cli.cli_exceptions import PolledTaskNotFinalError, QueryTaskFailedError
|
|
25
25
|
from divbase_cli.config_resolver import (
|
|
26
|
-
|
|
26
|
+
resolve_and_authenticate_project,
|
|
27
27
|
resolve_download_dir,
|
|
28
|
-
resolve_project,
|
|
29
28
|
)
|
|
30
29
|
from divbase_cli.retries import (
|
|
31
30
|
retry_polling_until_final_or_retryable_api_errors,
|
|
@@ -110,19 +109,16 @@ def sample_metadata_query(
|
|
|
110
109
|
Query the tsv sidecar metadata file for the VCF files in the project's data store on DivBase.
|
|
111
110
|
Returns the sample IDs and filenames that match the query.
|
|
112
111
|
"""
|
|
113
|
-
project_config =
|
|
114
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
115
|
-
|
|
112
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
116
113
|
request_data = SampleMetadataQueryRequest(tsv_filter=tsv_filter, metadata_tsv_name=metadata_tsv_name)
|
|
117
114
|
|
|
118
115
|
response = make_authenticated_request(
|
|
119
116
|
method="POST",
|
|
120
|
-
divbase_base_url=
|
|
117
|
+
divbase_base_url=project_config.divbase_url,
|
|
121
118
|
api_route=f"v1/query/sample-metadata/projects/{project_config.name}",
|
|
122
119
|
json=request_data.model_dump(),
|
|
123
120
|
timeout=20, # This is longer than default (5), as api call response is query result, not a task-id.
|
|
124
121
|
)
|
|
125
|
-
|
|
126
122
|
results = SampleMetadataQueryTaskResult(**response.json())
|
|
127
123
|
|
|
128
124
|
if results.warnings:
|
|
@@ -211,9 +207,7 @@ def vcf_query(
|
|
|
211
207
|
print(f" • {warning}")
|
|
212
208
|
print()
|
|
213
209
|
|
|
214
|
-
project_config =
|
|
215
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
216
|
-
|
|
210
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
217
211
|
request_data = BcftoolsQueryRequest(
|
|
218
212
|
tsv_filter=tsv_filter,
|
|
219
213
|
command=command,
|
|
@@ -221,10 +215,9 @@ def vcf_query(
|
|
|
221
215
|
samples=normalized_samples,
|
|
222
216
|
all_samples=all_samples,
|
|
223
217
|
)
|
|
224
|
-
|
|
225
218
|
response = make_authenticated_request(
|
|
226
219
|
method="POST",
|
|
227
|
-
divbase_base_url=
|
|
220
|
+
divbase_base_url=project_config.divbase_url,
|
|
228
221
|
api_route=f"v1/query/vcf/projects/{project_config.name}",
|
|
229
222
|
json=request_data.model_dump(),
|
|
230
223
|
)
|
|
@@ -260,11 +253,12 @@ def get_results_from_query_job_by_task_id(
|
|
|
260
253
|
f"--max-wait-mins must be between 1 and {MAX_WAIT_MINS_HARD_LIMIT} minutes, got {max_wait_mins}."
|
|
261
254
|
)
|
|
262
255
|
|
|
263
|
-
project_config =
|
|
264
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
256
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
265
257
|
|
|
266
258
|
task_status = poll_task_until_final_state_reached(
|
|
267
|
-
divbase_url=
|
|
259
|
+
divbase_url=project_config.divbase_url,
|
|
260
|
+
task_id=task_id,
|
|
261
|
+
timeout_mins=max_wait_mins,
|
|
268
262
|
)
|
|
269
263
|
|
|
270
264
|
resolved_download_dir = resolve_download_dir(download_dir=download_dir)
|
|
@@ -273,7 +267,7 @@ def get_results_from_query_job_by_task_id(
|
|
|
273
267
|
if task_status == "SUCCESS":
|
|
274
268
|
result_filename = f"{QUERY_RESULTS_FILE_PREFIX}{task_id}.vcf.gz"
|
|
275
269
|
download_results = download_files_command(
|
|
276
|
-
divbase_base_url=
|
|
270
|
+
divbase_base_url=project_config.divbase_url,
|
|
277
271
|
project_name=project_config.name,
|
|
278
272
|
raw_files_input=[result_filename],
|
|
279
273
|
download_dir=resolved_download_dir,
|
|
@@ -328,7 +322,7 @@ def poll_task_until_final_state_reached(divbase_url: str, task_id: int, timeout_
|
|
|
328
322
|
f"Task {task_id} has unsupported task type '{task_results.name}'. Only VCF query jobs are supported for this CLI command."
|
|
329
323
|
)
|
|
330
324
|
|
|
331
|
-
if task_results.status in FINAL_STATES:
|
|
325
|
+
if task_results.status and task_results.status in FINAL_STATES:
|
|
332
326
|
return task_results.status
|
|
333
327
|
|
|
334
328
|
raise PolledTaskNotFinalError(f"Task {task_id} state is {task_results.status}")
|
{divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/task_history_cli.py
RENAMED
|
@@ -10,7 +10,10 @@ import logging
|
|
|
10
10
|
import typer
|
|
11
11
|
|
|
12
12
|
from divbase_cli.cli_commands.shared_args_options import FORMAT_AS_TSV_OPTION
|
|
13
|
-
from divbase_cli.config_resolver import
|
|
13
|
+
from divbase_cli.config_resolver import (
|
|
14
|
+
resolve_and_authenticate_project,
|
|
15
|
+
resolve_url_for_non_project_specific_commands,
|
|
16
|
+
)
|
|
14
17
|
from divbase_cli.display_task_history import TaskHistoryDisplayManager
|
|
15
18
|
from divbase_cli.user_auth import make_authenticated_request
|
|
16
19
|
from divbase_cli.user_config import load_user_config
|
|
@@ -104,15 +107,13 @@ def list_task_history_for_project(
|
|
|
104
107
|
Check status of all tasks submitted for a project. Requires a manager role in the project. Displays the latest 10 tasks by default, unless --limit is specified.
|
|
105
108
|
"""
|
|
106
109
|
# TODO add option to sort ASC/DESC by task timestamp
|
|
107
|
-
project_config =
|
|
108
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
110
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
109
111
|
|
|
110
112
|
task_history_response = make_authenticated_request(
|
|
111
113
|
method="GET",
|
|
112
|
-
divbase_base_url=
|
|
114
|
+
divbase_base_url=project_config.divbase_url,
|
|
113
115
|
api_route=f"v1/task-history/projects/{project_config.name}",
|
|
114
116
|
)
|
|
115
|
-
|
|
116
117
|
task_history_data = [TaskHistoryResult(**item) for item in task_history_response.json()]
|
|
117
118
|
|
|
118
119
|
TaskHistoryDisplayManager(
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
"""CLI commands for managing project versions in DivBase."""
|
|
2
2
|
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from zoneinfo import ZoneInfo
|
|
5
|
-
|
|
6
3
|
import typer
|
|
7
4
|
from rich import print
|
|
8
5
|
from rich.table import Table
|
|
9
6
|
|
|
10
7
|
from divbase_cli.cli_commands.shared_args_options import FORMAT_AS_TSV_OPTION, PROJECT_NAME_OPTION
|
|
11
|
-
from divbase_cli.config_resolver import
|
|
8
|
+
from divbase_cli.config_resolver import resolve_and_authenticate_project
|
|
12
9
|
from divbase_cli.services.project_versions import (
|
|
13
10
|
add_version_command,
|
|
14
11
|
delete_version_command,
|
|
@@ -17,7 +14,7 @@ from divbase_cli.services.project_versions import (
|
|
|
17
14
|
update_version_command,
|
|
18
15
|
)
|
|
19
16
|
from divbase_cli.utils import print_rich_table_as_tsv
|
|
20
|
-
from divbase_lib.utils import format_file_size
|
|
17
|
+
from divbase_lib.utils import format_datetime_for_cli, format_file_size
|
|
21
18
|
|
|
22
19
|
version_app = typer.Typer(
|
|
23
20
|
no_args_is_help=True,
|
|
@@ -25,13 +22,6 @@ version_app = typer.Typer(
|
|
|
25
22
|
)
|
|
26
23
|
|
|
27
24
|
|
|
28
|
-
def format_timestamp(timestamp_str: str) -> str:
|
|
29
|
-
"""Format ISO timestamp to Europe/Stockholm format with timezone"""
|
|
30
|
-
dt = datetime.fromisoformat(timestamp_str)
|
|
31
|
-
cet_dt = dt.astimezone(ZoneInfo("Europe/Stockholm"))
|
|
32
|
-
return cet_dt.strftime("%d/%m/%Y %H:%M:%S %Z")
|
|
33
|
-
|
|
34
|
-
|
|
35
25
|
@version_app.command("add")
|
|
36
26
|
def add_version(
|
|
37
27
|
name: str = typer.Argument(help="Name of the version (e.g., semantic version).", show_default=False),
|
|
@@ -39,14 +29,12 @@ def add_version(
|
|
|
39
29
|
project: str | None = PROJECT_NAME_OPTION,
|
|
40
30
|
):
|
|
41
31
|
"""Add a new project version entry which specifies the current state of all files in the project at the current timestamp."""
|
|
42
|
-
project_config =
|
|
43
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
44
|
-
|
|
32
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
45
33
|
add_version_response = add_version_command(
|
|
46
34
|
name=name,
|
|
47
35
|
description=description,
|
|
48
36
|
project_name=project_config.name,
|
|
49
|
-
divbase_base_url=
|
|
37
|
+
divbase_base_url=project_config.divbase_url,
|
|
50
38
|
)
|
|
51
39
|
print(f"New version: '{add_version_response.name}' added to the project: '{project_config.name}'")
|
|
52
40
|
|
|
@@ -75,17 +63,14 @@ def update_version(
|
|
|
75
63
|
To see your current version entries run: `divbase-cli version ls`
|
|
76
64
|
"""
|
|
77
65
|
if not new_name and not new_description:
|
|
78
|
-
|
|
66
|
+
raise typer.BadParameter(
|
|
79
67
|
"No updates specified. Please provide a new name (--new-name) and/or description (--new-description) to update for this entry."
|
|
80
68
|
)
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
project_config = resolve_project(project_name=project)
|
|
84
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
85
69
|
|
|
70
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
86
71
|
updated_version = update_version_command(
|
|
87
72
|
project_name=project_config.name,
|
|
88
|
-
divbase_base_url=
|
|
73
|
+
divbase_base_url=project_config.divbase_url,
|
|
89
74
|
version_name=version_name,
|
|
90
75
|
new_name=new_name,
|
|
91
76
|
new_description=new_description,
|
|
@@ -111,11 +96,11 @@ def list_versions(
|
|
|
111
96
|
If you specify --include-deleted, soft-deleted versions will also be shown.
|
|
112
97
|
Soft-deleted versions can be restored by a DivBase admin within 30 days of deletion.
|
|
113
98
|
"""
|
|
114
|
-
project_config =
|
|
115
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
116
|
-
|
|
99
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
117
100
|
versions_info = list_versions_command(
|
|
118
|
-
project_name=project_config.name,
|
|
101
|
+
project_name=project_config.name,
|
|
102
|
+
include_deleted=include_deleted,
|
|
103
|
+
divbase_base_url=project_config.divbase_url,
|
|
119
104
|
)
|
|
120
105
|
|
|
121
106
|
if not versions_info:
|
|
@@ -125,6 +110,7 @@ def list_versions(
|
|
|
125
110
|
table = Table(title=f"Versions for {project_config.name}")
|
|
126
111
|
table.add_column("Version", style="cyan", no_wrap=True)
|
|
127
112
|
table.add_column("Created ", style="magenta")
|
|
113
|
+
table.add_column("Last Updated", style="blue")
|
|
128
114
|
table.add_column("Description", style="green")
|
|
129
115
|
if include_deleted:
|
|
130
116
|
table.add_column("Soft Deleted", style="red")
|
|
@@ -132,12 +118,13 @@ def list_versions(
|
|
|
132
118
|
for version in versions_info:
|
|
133
119
|
name = version.name
|
|
134
120
|
desc = version.description or "No description provided"
|
|
135
|
-
created_at =
|
|
121
|
+
created_at = format_datetime_for_cli(dt=version.created_at)
|
|
122
|
+
updated_at = format_datetime_for_cli(dt=version.updated_at)
|
|
136
123
|
if include_deleted:
|
|
137
124
|
soft_deleted = "Yes" if version.is_deleted else "No"
|
|
138
|
-
table.add_row(name, created_at, desc, soft_deleted)
|
|
125
|
+
table.add_row(name, created_at, updated_at, desc, soft_deleted)
|
|
139
126
|
else:
|
|
140
|
-
table.add_row(name, created_at, desc)
|
|
127
|
+
table.add_row(name, created_at, updated_at, desc)
|
|
141
128
|
|
|
142
129
|
if not format_output_as_tsv:
|
|
143
130
|
print(table)
|
|
@@ -154,11 +141,11 @@ def get_version_info(
|
|
|
154
141
|
"""
|
|
155
142
|
Provide detailed information about a user specified project version, including all files present and their unique hashes.
|
|
156
143
|
"""
|
|
157
|
-
project_config =
|
|
158
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
159
|
-
|
|
144
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
160
145
|
version_details = get_version_details_command(
|
|
161
|
-
project_name=project_config.name,
|
|
146
|
+
project_name=project_config.name,
|
|
147
|
+
divbase_base_url=project_config.divbase_url,
|
|
148
|
+
version_name=version,
|
|
162
149
|
)
|
|
163
150
|
|
|
164
151
|
table = Table(title=f"Project version files for {project_config.name}")
|
|
@@ -178,7 +165,8 @@ def get_version_info(
|
|
|
178
165
|
|
|
179
166
|
if not format_output_as_tsv:
|
|
180
167
|
print(f"Project version entry for project: '{project_config.name}' with name: '{version_details.name}'")
|
|
181
|
-
print(f"Entry created at: {
|
|
168
|
+
print(f"Entry created at: {format_datetime_for_cli(dt=version_details.created_at)}")
|
|
169
|
+
print(f"Entry last updated at: {format_datetime_for_cli(dt=version_details.updated_at)}")
|
|
182
170
|
if version_details.description:
|
|
183
171
|
print(f"Description: {version_details.description}")
|
|
184
172
|
if version_details.is_deleted:
|
|
@@ -201,14 +189,14 @@ def delete_version(
|
|
|
201
189
|
Deleted version entries older than 30 days will be permanently deleted.
|
|
202
190
|
You can ask a DivBase admin to restore a deleted version within that time period.
|
|
203
191
|
"""
|
|
204
|
-
project_config =
|
|
205
|
-
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
206
|
-
|
|
192
|
+
project_config = resolve_and_authenticate_project(project_name=project)
|
|
207
193
|
deleted_version = delete_version_command(
|
|
208
|
-
project_name=project_config.name,
|
|
194
|
+
project_name=project_config.name,
|
|
195
|
+
divbase_base_url=project_config.divbase_url,
|
|
196
|
+
version_name=name,
|
|
209
197
|
)
|
|
210
198
|
if deleted_version.already_deleted:
|
|
211
|
-
date_deleted =
|
|
199
|
+
date_deleted = format_datetime_for_cli(dt=deleted_version.date_deleted)
|
|
212
200
|
print(f"The version: '{deleted_version.name}' has already been soft-deleted on {date_deleted}.")
|
|
213
201
|
else:
|
|
214
202
|
print(f"The version: '{deleted_version.name}' was deleted from the project: '{project_config.name}'")
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Custom exceptions for the divbase CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
class DivBaseCLIError(Exception):
|
|
9
7
|
"""Base exception for all divbase CLI errors."""
|
|
@@ -173,13 +171,12 @@ class ProjectNotInConfigError(DivBaseCLIError):
|
|
|
173
171
|
But info about the project could not be obtained from the user config file.
|
|
174
172
|
"""
|
|
175
173
|
|
|
176
|
-
def __init__(self,
|
|
177
|
-
self.config_path = config_path
|
|
174
|
+
def __init__(self, project_name: str):
|
|
178
175
|
self.project_name = project_name
|
|
179
176
|
error_message = (
|
|
180
177
|
f"Couldn't get information about the project named: '{project_name}' \n"
|
|
181
|
-
f"
|
|
182
|
-
f"
|
|
178
|
+
f"Use the command 'divbase-cli config add <project_name>' to add the project to your config.\n"
|
|
179
|
+
f"You can run 'divbase-cli config show' to view the contents of your config file.\n"
|
|
183
180
|
)
|
|
184
181
|
super().__init__(error_message)
|
|
185
182
|
|
|
@@ -14,22 +14,33 @@ from divbase_cli.user_auth import get_pat_for_authentication
|
|
|
14
14
|
from divbase_cli.user_config import ProjectConfig, load_user_config
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def resolve_and_authenticate_project(project_name: str | None) -> ProjectConfig:
|
|
18
18
|
"""
|
|
19
|
-
|
|
19
|
+
Helper function that resolves the project to use for a CLI command and ensures
|
|
20
|
+
the user is logged in to the correct DivBase API URL for that project.
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
Returns the ProjectConfig object for the resolved project, will raise errors otherwise.
|
|
22
23
|
"""
|
|
23
|
-
|
|
24
|
-
if
|
|
25
|
-
|
|
24
|
+
user_config = load_user_config()
|
|
25
|
+
if not project_name:
|
|
26
|
+
project_name = user_config.default_project
|
|
27
|
+
if not project_name:
|
|
28
|
+
raise ProjectNameNotSpecifiedError()
|
|
29
|
+
|
|
30
|
+
project_config = user_config.project_info(project_name)
|
|
31
|
+
|
|
32
|
+
if user_config.logged_in_url:
|
|
33
|
+
if project_config.divbase_url != user_config.logged_in_url:
|
|
26
34
|
raise AuthenticationError(
|
|
27
|
-
f"You are
|
|
35
|
+
f"You are trying to run a command for the project '{project_name}' which has the DivBase URL: {project_config.divbase_url} \n"
|
|
36
|
+
f"You are however logged in to this URL: {user_config.logged_in_url} \n"
|
|
37
|
+
"Please log in again."
|
|
28
38
|
)
|
|
29
|
-
return
|
|
39
|
+
return project_config
|
|
30
40
|
|
|
31
41
|
if get_pat_for_authentication():
|
|
32
|
-
|
|
42
|
+
# if a user has a PAT, we will assume the PAT is for the correct URL
|
|
43
|
+
return project_config
|
|
33
44
|
|
|
34
45
|
raise AuthenticationError("You are not logged in. Please log in with 'divbase-cli auth login [EMAIL]'.")
|
|
35
46
|
|
|
@@ -54,22 +65,6 @@ def resolve_url_for_non_project_specific_commands() -> str:
|
|
|
54
65
|
raise AuthenticationError("You are not logged in. Please log in with 'divbase-cli auth login [EMAIL]'.")
|
|
55
66
|
|
|
56
67
|
|
|
57
|
-
def resolve_project(project_name: str | None) -> ProjectConfig:
|
|
58
|
-
"""
|
|
59
|
-
Helper function to resolve the project to use for a CLI command.
|
|
60
|
-
Falls back to the default project set in the user config if not explicitly provided.
|
|
61
|
-
|
|
62
|
-
Once the project is resolved a ProjectConfig object is returned,
|
|
63
|
-
which contains the name and API URL of the project.
|
|
64
|
-
"""
|
|
65
|
-
config = load_user_config()
|
|
66
|
-
if not project_name:
|
|
67
|
-
project_name = config.default_project
|
|
68
|
-
if not project_name:
|
|
69
|
-
raise ProjectNameNotSpecifiedError()
|
|
70
|
-
return config.project_info(project_name)
|
|
71
|
-
|
|
72
|
-
|
|
73
68
|
def resolve_download_dir(download_dir: str | None) -> Path:
|
|
74
69
|
"""
|
|
75
70
|
Helper function to resolve the download directory to use for a CLI command involving downloading files.
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from zoneinfo import ZoneInfo
|
|
4
2
|
|
|
5
3
|
from rich.console import Console
|
|
6
4
|
from rich.table import Table
|
|
@@ -12,6 +10,7 @@ from divbase_lib.api_schemas.task_history import (
|
|
|
12
10
|
SampleMetadataQueryTaskResult,
|
|
13
11
|
TaskHistoryResult,
|
|
14
12
|
)
|
|
13
|
+
from divbase_lib.utils import format_datetime_for_cli
|
|
15
14
|
|
|
16
15
|
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
@@ -53,7 +52,7 @@ class TaskHistoryDisplayManager:
|
|
|
53
52
|
def print_task_history(self) -> None:
|
|
54
53
|
"""Display the task history fetched from the results backend in a formatted table."""
|
|
55
54
|
|
|
56
|
-
sorted_tasks = sorted(self.task_items, key=lambda x: x.created_at
|
|
55
|
+
sorted_tasks = sorted(self.task_items, key=lambda x: x.created_at, reverse=True)
|
|
57
56
|
display_limit = self.display_limit
|
|
58
57
|
limited_tasks = sorted_tasks[:display_limit]
|
|
59
58
|
|
|
@@ -72,16 +71,16 @@ class TaskHistoryDisplayManager:
|
|
|
72
71
|
colour = self.STATE_COLOURS.get(state, "white")
|
|
73
72
|
state_with_colour = f"[{colour}]{state}[/{colour}]"
|
|
74
73
|
|
|
75
|
-
submitter = task.submitter_email
|
|
74
|
+
submitter = task.submitter_email
|
|
76
75
|
result = self._format_result(task, state)
|
|
77
76
|
|
|
78
77
|
table.add_row(
|
|
79
78
|
submitter,
|
|
80
79
|
str(task.id),
|
|
81
80
|
state_with_colour,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
format_datetime_for_cli(task.created_at),
|
|
82
|
+
format_datetime_for_cli(task.started_at) if task.started_at else "N/A",
|
|
83
|
+
f"{task.runtime:.2f}" if task.runtime is not None else "N/A",
|
|
85
84
|
result,
|
|
86
85
|
)
|
|
87
86
|
|
|
@@ -91,7 +90,7 @@ class TaskHistoryDisplayManager:
|
|
|
91
90
|
console = Console()
|
|
92
91
|
console.print(table)
|
|
93
92
|
|
|
94
|
-
def _create_task_history_table(self):
|
|
93
|
+
def _create_task_history_table(self) -> Table:
|
|
95
94
|
"""
|
|
96
95
|
Use the Rich library to initiate a table for displaying task history.
|
|
97
96
|
"""
|
|
@@ -119,7 +118,7 @@ class TaskHistoryDisplayManager:
|
|
|
119
118
|
table.add_column("Result", style="white", width=35, overflow="fold")
|
|
120
119
|
return table
|
|
121
120
|
|
|
122
|
-
def _format_result(self, task, state):
|
|
121
|
+
def _format_result(self, task: TaskHistoryResult, state: str) -> str:
|
|
123
122
|
"""
|
|
124
123
|
Format the result message based on the task state and type.
|
|
125
124
|
"""
|
|
@@ -156,7 +155,7 @@ class TaskHistoryDisplayManager:
|
|
|
156
155
|
|
|
157
156
|
elif isinstance(task.result, DimensionUpdateTaskResult):
|
|
158
157
|
result_message = (
|
|
159
|
-
f"VCF file dimensions
|
|
158
|
+
f"VCF file dimensions cache added or updated:\n {task.result.VCF_files_added}\n"
|
|
160
159
|
f"VCF files skipped by this job (previous DivBase-generated result VCFs):\n {task.result.VCF_files_skipped}\n"
|
|
161
160
|
f"VCF files that have been deleted from the project and now are dropped from the index:\n {task.result.VCF_files_deleted}"
|
|
162
161
|
)
|
|
@@ -182,16 +181,3 @@ class TaskHistoryDisplayManager:
|
|
|
182
181
|
if self.format_output_as_tsv:
|
|
183
182
|
return text
|
|
184
183
|
return f"[{colour}]{text}[/{colour}]"
|
|
185
|
-
|
|
186
|
-
def _to_cet(self, timestamp_str):
|
|
187
|
-
"""
|
|
188
|
-
Convert a UTC timestamp string in '%Y-%m-%d %H:%M:%S UTC' format to CET.
|
|
189
|
-
"""
|
|
190
|
-
if not timestamp_str or timestamp_str == "N/A":
|
|
191
|
-
return "N/A"
|
|
192
|
-
try:
|
|
193
|
-
dt = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S %Z")
|
|
194
|
-
cet_dt = dt.astimezone(ZoneInfo("Europe/Stockholm"))
|
|
195
|
-
return cet_dt.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
196
|
-
except Exception:
|
|
197
|
-
return timestamp_str
|
|
@@ -267,7 +267,7 @@ def download_files_command(
|
|
|
267
267
|
else:
|
|
268
268
|
# parse raw file inputs to see if any specific version ids were provided using format:
|
|
269
269
|
# file_name:version_id (not possible when using project_version)
|
|
270
|
-
json_data = []
|
|
270
|
+
json_data: list[dict[str, str | None]] = []
|
|
271
271
|
for file_input in raw_files_input:
|
|
272
272
|
if ":" in file_input:
|
|
273
273
|
name, version_id = file_input.split(sep=":", maxsplit=1)
|
|
@@ -18,7 +18,6 @@ import os
|
|
|
18
18
|
import time
|
|
19
19
|
import warnings
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
from datetime import datetime
|
|
22
21
|
from json import JSONDecodeError
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
from typing import Any
|
|
@@ -34,9 +33,10 @@ from divbase_cli import __version__ as cli_version
|
|
|
34
33
|
from divbase_cli.cli_config import cli_settings
|
|
35
34
|
from divbase_cli.cli_exceptions import AuthenticationError, DivBaseAPIConnectionError, DivBaseAPIError
|
|
36
35
|
from divbase_cli.retries import retry_only_on_retryable_divbase_api_errors
|
|
37
|
-
from divbase_cli.user_config import load_user_config
|
|
36
|
+
from divbase_cli.user_config import UserConfig, load_user_config
|
|
38
37
|
from divbase_lib.api_schemas.auth import LogoutRequest
|
|
39
38
|
from divbase_lib.divbase_constants import CLI_VERSION_HEADER_KEY
|
|
39
|
+
from divbase_lib.utils import format_unix_timestamp_for_cli
|
|
40
40
|
|
|
41
41
|
LOGIN_AGAIN_MESSAGE = "Your session has expired. Please log in again with 'divbase-cli auth login [EMAIL]'."
|
|
42
42
|
PERSONAL_ACCESS_TOKEN_EXPIRED_MESSAGE = (
|
|
@@ -113,15 +113,15 @@ class PATData:
|
|
|
113
113
|
|
|
114
114
|
def is_pat_expired(self) -> bool:
|
|
115
115
|
"""Check if the PAT is expired"""
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
if self.pat_expires_at is None:
|
|
117
|
+
return False
|
|
118
|
+
return self.pat_expires_at <= time.time() + 10 # 10 second buffer to account for clock skew or time delay
|
|
118
119
|
|
|
119
120
|
def pat_expiry_formatted(self) -> str:
|
|
120
121
|
"""Returns the token's expiry time in a human readable format in user's current timezone."""
|
|
121
122
|
if self.pat_expires_at is None:
|
|
122
123
|
return "Never"
|
|
123
|
-
|
|
124
|
-
return expiry.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
124
|
+
return format_unix_timestamp_for_cli(unix_timestamp=self.pat_expires_at)
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def delete_stored_pat(pat_path: Path = cli_settings.PATS_FALLBACK_PATH) -> None:
|
|
@@ -131,7 +131,7 @@ def delete_stored_pat(pat_path: Path = cli_settings.PATS_FALLBACK_PATH) -> None:
|
|
|
131
131
|
pat_path.unlink(missing_ok=True)
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
def check_existing_session(divbase_url: str, config) -> int | None:
|
|
134
|
+
def check_existing_session(divbase_url: str, config: UserConfig) -> int | None:
|
|
135
135
|
"""
|
|
136
136
|
Check if a user is already logged in to DivBase.
|
|
137
137
|
Used to prevent unnecessary multiple logins.
|
|
@@ -128,7 +128,7 @@ class UserConfig:
|
|
|
128
128
|
for project in self.projects:
|
|
129
129
|
if project.name == name:
|
|
130
130
|
return project
|
|
131
|
-
raise ProjectNotInConfigError(
|
|
131
|
+
raise ProjectNotInConfigError(project_name=name)
|
|
132
132
|
|
|
133
133
|
def set_login_status(self, url: str | None, email: str | None) -> None:
|
|
134
134
|
"""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0a12"
|
|
File without changes
|
|
File without changes
|
{divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/shared_args_options.py
RENAMED
|
File without changes
|
{divbase_cli-0.1.0a12 → divbase_cli-0.1.0a15}/src/divbase_cli/cli_commands/user_config_cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|