divbase-cli 0.1.0.dev0__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.0.dev0/.gitignore +38 -0
- divbase_cli-0.1.0.dev0/PKG-INFO +44 -0
- divbase_cli-0.1.0.dev0/README.md +19 -0
- divbase_cli-0.1.0.dev0/pyproject.toml +47 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/__init__.py +1 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/__init__.py +4 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/auth_cli.py +92 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/dimensions_cli.py +143 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/file_cli.py +245 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/query_cli.py +144 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/task_history_cli.py +138 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/user_config_cli.py +175 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_commands/version_cli.py +153 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_config.py +39 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/cli_exceptions.py +151 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/config_resolver.py +84 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/display_task_history.py +182 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/divbase_cli.py +76 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/pre_signed_urls.py +169 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/services.py +219 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/user_auth.py +264 -0
- divbase_cli-0.1.0.dev0/src/divbase_cli/user_config.py +166 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# repo secrets
|
|
2
|
+
.env
|
|
3
|
+
.env*
|
|
4
|
+
|
|
5
|
+
# python stuff
|
|
6
|
+
.venv
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
|
|
12
|
+
.vscode/
|
|
13
|
+
|
|
14
|
+
# project specific files
|
|
15
|
+
/sample_metadata.tsv
|
|
16
|
+
sample_metadata_*.tsv
|
|
17
|
+
*.vcf
|
|
18
|
+
*.vcf.gz
|
|
19
|
+
*.vcf.gz.csi
|
|
20
|
+
!tests/fixtures/*.vcf.gz
|
|
21
|
+
tests/fixtures/temp*
|
|
22
|
+
tests/fixtures/merged*
|
|
23
|
+
|
|
24
|
+
# query job config files
|
|
25
|
+
bcftools_divbase_job_config.json
|
|
26
|
+
|
|
27
|
+
# benchmarking files
|
|
28
|
+
vcf_dimensions.tsv
|
|
29
|
+
mock*.tsv
|
|
30
|
+
task_records*.json
|
|
31
|
+
|
|
32
|
+
#MacOS artifacts
|
|
33
|
+
.DS_Store
|
|
34
|
+
|
|
35
|
+
# mkdocs build cache
|
|
36
|
+
.cache/
|
|
37
|
+
# pypi
|
|
38
|
+
dist/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: divbase-cli
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: Command Line Interface for Divbase
|
|
5
|
+
Project-URL: Homepage, https://divbase.scilifelab.se
|
|
6
|
+
Project-URL: Documentation, https://scilifelabdatacentre.github.io/divbase
|
|
7
|
+
Project-URL: Repository, https://github.com/ScilifelabDataCentre/divbase
|
|
8
|
+
Project-URL: Issues, https://github.com/ScilifelabDataCentre/divbase/issues
|
|
9
|
+
Author-email: SciLifeLab Data Centre <datacentre@scilifelab.se>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: divbase-lib==0.1.0.dev0
|
|
22
|
+
Requires-Dist: httpx<2,>=0.28.1
|
|
23
|
+
Requires-Dist: typer<1,>=0.21.1
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# divbase-cli
|
|
27
|
+
|
|
28
|
+
Command Line Interface for Divbase.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pipx install divbase-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
divbase --help # can also run divbase-cli --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
This package is developed in the [DivBase repository](https://github.com/ScilifelabDataCentre/divbase) by Scilifelab Data Centre.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# divbase-cli
|
|
2
|
+
|
|
3
|
+
Command Line Interface for Divbase.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pipx install divbase-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
divbase --help # can also run divbase-cli --help
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
This package is developed in the [DivBase repository](https://github.com/ScilifelabDataCentre/divbase) by Scilifelab Data Centre.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "divbase-cli"
|
|
3
|
+
description = "Command Line Interface for Divbase"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "SciLifeLab Data Centre", email = "datacentre@scilifelab.se" },
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"divbase-lib==0.1.0.dev0",
|
|
12
|
+
"typer>=0.21.1,<1",
|
|
13
|
+
"httpx>=0.28.1,<2",
|
|
14
|
+
]
|
|
15
|
+
dynamic = ["version"]
|
|
16
|
+
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Intended Audience :: Science/Research",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://divbase.scilifelab.se"
|
|
31
|
+
Documentation = "https://scilifelabdatacentre.github.io/divbase"
|
|
32
|
+
Repository = "https://github.com/ScilifelabDataCentre/divbase"
|
|
33
|
+
Issues = "https://github.com/ScilifelabDataCentre/divbase/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
divbase = "divbase_cli.divbase_cli:main"
|
|
37
|
+
divbase-cli = "divbase_cli.divbase_cli:main" # TODO choose 1
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling >= 1.26"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.version]
|
|
44
|
+
path = "src/divbase_cli/__init__.py"
|
|
45
|
+
|
|
46
|
+
[tool.uv.sources]
|
|
47
|
+
divbase-lib = { workspace = true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0.dev0"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI subcommand for managing user auth with DivBase server.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from pydantic import SecretStr
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from divbase_cli.cli_commands.user_config_cli import CONFIG_FILE_OPTION
|
|
13
|
+
from divbase_cli.cli_config import cli_settings
|
|
14
|
+
from divbase_cli.cli_exceptions import AuthenticationError
|
|
15
|
+
from divbase_cli.user_auth import (
|
|
16
|
+
check_existing_session,
|
|
17
|
+
login_to_divbase,
|
|
18
|
+
logout_of_divbase,
|
|
19
|
+
make_authenticated_request,
|
|
20
|
+
)
|
|
21
|
+
from divbase_cli.user_config import load_user_config
|
|
22
|
+
|
|
23
|
+
auth_app = typer.Typer(
|
|
24
|
+
no_args_is_help=True, help="Login/logout of DivBase server. To register, visit https://divbase.scilifelab.se/."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@auth_app.command("login")
|
|
29
|
+
def login(
|
|
30
|
+
email: str,
|
|
31
|
+
password: Annotated[str, typer.Option(prompt=True, hide_input=True)],
|
|
32
|
+
divbase_url: str = typer.Option(cli_settings.DIVBASE_API_URL, help="DivBase server URL to connect to."),
|
|
33
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
34
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force login again even if already logged in"),
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Log in to the DivBase server.
|
|
38
|
+
|
|
39
|
+
TODO - think abit more about already logged in validation and UX.
|
|
40
|
+
One thing to consider would be use case of very close to refresh token expiry, that could be bad UX.
|
|
41
|
+
(But that is dependent on whether we will allow renewal of refresh tokens...)
|
|
42
|
+
"""
|
|
43
|
+
secret_password = SecretStr(password)
|
|
44
|
+
del password # avoid user passwords showing up in error messages etc...
|
|
45
|
+
|
|
46
|
+
config = load_user_config(config_file)
|
|
47
|
+
|
|
48
|
+
if not force:
|
|
49
|
+
session_expires_at = check_existing_session(divbase_url=divbase_url, config=config)
|
|
50
|
+
if session_expires_at:
|
|
51
|
+
print(f"Already logged in to {divbase_url}")
|
|
52
|
+
print(f"Session expires: {datetime.fromtimestamp(session_expires_at)}")
|
|
53
|
+
|
|
54
|
+
if not typer.confirm("Do you want to login again? This will replace your current session."):
|
|
55
|
+
print("Login cancelled.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
login_to_divbase(email=email, password=secret_password, divbase_url=divbase_url, config_path=config_file)
|
|
59
|
+
print(f"Logged in successfully as: {email}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@auth_app.command("logout")
|
|
63
|
+
def logout():
|
|
64
|
+
"""
|
|
65
|
+
Log out of the DivBase server.
|
|
66
|
+
"""
|
|
67
|
+
logout_of_divbase()
|
|
68
|
+
print("Logged out successfully.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@auth_app.command("whoami")
|
|
72
|
+
def whoami(
|
|
73
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Return information about the currently logged-in user.
|
|
77
|
+
"""
|
|
78
|
+
config = load_user_config(config_file)
|
|
79
|
+
logged_in_url = config.logged_in_url
|
|
80
|
+
|
|
81
|
+
# TODO - move logged in check to the make_authenticated_request function?
|
|
82
|
+
if not logged_in_url:
|
|
83
|
+
raise AuthenticationError("You are not logged in. Please log in with 'divbase-cli auth login [EMAIL]'.")
|
|
84
|
+
|
|
85
|
+
request = make_authenticated_request(
|
|
86
|
+
method="GET",
|
|
87
|
+
divbase_base_url=logged_in_url,
|
|
88
|
+
api_route="v1/auth/whoami",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
current_user = request.json()
|
|
92
|
+
print(f"Currently logged in as: {current_user['email']} (Name: {current_user['name']})")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from divbase_cli.cli_commands.user_config_cli import CONFIG_FILE_OPTION
|
|
8
|
+
from divbase_cli.cli_commands.version_cli import PROJECT_NAME_OPTION
|
|
9
|
+
from divbase_cli.config_resolver import resolve_project
|
|
10
|
+
from divbase_cli.user_auth import make_authenticated_request
|
|
11
|
+
from divbase_lib.api_schemas.vcf_dimensions import DimensionsShowResult
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
dimensions_app = typer.Typer(
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
help="Create and inspect dimensions (number of samples, number of variants, scaffold names) of the VCF files in a project",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dimensions_app.command("update")
|
|
23
|
+
def update_dimensions_index(
|
|
24
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
25
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Calculate and add the dimensions of a VCF file to the dimensions index file in the project."""
|
|
28
|
+
|
|
29
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
30
|
+
|
|
31
|
+
response = make_authenticated_request(
|
|
32
|
+
method="PUT",
|
|
33
|
+
divbase_base_url=project_config.divbase_url,
|
|
34
|
+
api_route=f"v1/vcf-dimensions/projects/{project_config.name}",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
task_id = response.json()
|
|
38
|
+
print(f"Job submitted successfully with task id: {task_id}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dimensions_app.command("show")
|
|
42
|
+
def show_dimensions_index(
|
|
43
|
+
filename: str = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--filename",
|
|
46
|
+
help="If set, will show only the entry for this VCF filename.",
|
|
47
|
+
),
|
|
48
|
+
unique_scaffolds: bool = (
|
|
49
|
+
typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--unique-scaffolds",
|
|
52
|
+
help="If set, will show all unique scaffold names found across all the VCF files in the project.",
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
56
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Show the dimensions index file for a project.
|
|
60
|
+
When running --unique-scaffolds, the sorting separates between numeric and non-numeric scaffold names.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
64
|
+
|
|
65
|
+
response = make_authenticated_request(
|
|
66
|
+
method="GET",
|
|
67
|
+
divbase_base_url=project_config.divbase_url,
|
|
68
|
+
api_route=f"v1/vcf-dimensions/projects/{project_config.name}",
|
|
69
|
+
)
|
|
70
|
+
vcf_dimensions_data = DimensionsShowResult(**response.json())
|
|
71
|
+
|
|
72
|
+
dimensions_info = _format_api_response_for_display_in_terminal(vcf_dimensions_data)
|
|
73
|
+
|
|
74
|
+
if filename:
|
|
75
|
+
record = None
|
|
76
|
+
for entry in dimensions_info.get("indexed_files", []):
|
|
77
|
+
if entry.get("filename") == filename:
|
|
78
|
+
record = entry
|
|
79
|
+
break
|
|
80
|
+
if record:
|
|
81
|
+
print(yaml.safe_dump(record, sort_keys=False))
|
|
82
|
+
else:
|
|
83
|
+
print(
|
|
84
|
+
f"No entry found for filename: {filename}. Please check that the filename is correct and that it is a VCF file (extension: .vcf or .vcf.gz)."
|
|
85
|
+
"\nHint: use 'divbase-cli files list' to view all files in the project."
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if unique_scaffolds:
|
|
90
|
+
unique_scaffold_names = set()
|
|
91
|
+
for entry in dimensions_info.get("indexed_files", []):
|
|
92
|
+
unique_scaffold_names.update(entry.get("dimensions", {}).get("scaffolds", []))
|
|
93
|
+
|
|
94
|
+
numeric_scaffold_names = []
|
|
95
|
+
non_numeric_scaffold_names = []
|
|
96
|
+
for scaffold in unique_scaffold_names:
|
|
97
|
+
if scaffold.isdigit():
|
|
98
|
+
numeric_scaffold_names.append(int(scaffold))
|
|
99
|
+
else:
|
|
100
|
+
non_numeric_scaffold_names.append(scaffold)
|
|
101
|
+
|
|
102
|
+
unique_scaffold_names_sorted = [str(n) for n in sorted(numeric_scaffold_names)] + sorted(
|
|
103
|
+
non_numeric_scaffold_names
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
print(f"Unique scaffold names found across all the VCF files in the project:\n{unique_scaffold_names_sorted}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
print(yaml.safe_dump(dimensions_info, sort_keys=False))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _format_api_response_for_display_in_terminal(api_response: DimensionsShowResult) -> dict:
|
|
113
|
+
"""
|
|
114
|
+
Convert the API response to a YAML-like format for display in the user's terminal.
|
|
115
|
+
"""
|
|
116
|
+
dimensions_list = []
|
|
117
|
+
for entry in api_response.vcf_files:
|
|
118
|
+
dimensions_entry = {
|
|
119
|
+
"filename": entry["vcf_file_s3_key"],
|
|
120
|
+
"file_version_ID_in_bucket": entry["s3_version_id"],
|
|
121
|
+
"last_updated": entry.get("updated_at"),
|
|
122
|
+
"dimensions": {
|
|
123
|
+
"scaffolds": entry.get("scaffolds", []),
|
|
124
|
+
"sample_count": entry.get("sample_count", 0),
|
|
125
|
+
"sample_names": entry.get("samples", []),
|
|
126
|
+
"variants": entry.get("variant_count", 0),
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
dimensions_list.append(dimensions_entry)
|
|
130
|
+
|
|
131
|
+
skipped_list = []
|
|
132
|
+
for entry in api_response.skipped_files:
|
|
133
|
+
skipped_entry = {
|
|
134
|
+
"filename": entry["vcf_file_s3_key"],
|
|
135
|
+
"file_version_ID_in_bucket": entry["s3_version_id"],
|
|
136
|
+
"skip_reason": entry.get("skip_reason", "unknown"),
|
|
137
|
+
}
|
|
138
|
+
skipped_list.append(skipped_entry)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"indexed_files": dimensions_list,
|
|
142
|
+
"skipped_files": skipped_list,
|
|
143
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command line interface for managing files in a DivBase project's store on DivBase.
|
|
3
|
+
|
|
4
|
+
TODO - support for specifying versions of files when downloading files?
|
|
5
|
+
TODO - Download all files option.
|
|
6
|
+
TODO - skip checked option (aka skip files that already exist in same local dir with correct checksum).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich import print
|
|
13
|
+
from typing_extensions import Annotated
|
|
14
|
+
|
|
15
|
+
from divbase_cli.cli_commands.user_config_cli import CONFIG_FILE_OPTION
|
|
16
|
+
from divbase_cli.cli_commands.version_cli import PROJECT_NAME_OPTION
|
|
17
|
+
from divbase_cli.config_resolver import ensure_logged_in, resolve_download_dir, resolve_project
|
|
18
|
+
from divbase_cli.services import (
|
|
19
|
+
download_files_command,
|
|
20
|
+
list_files_command,
|
|
21
|
+
soft_delete_objects_command,
|
|
22
|
+
upload_files_command,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
file_app = typer.Typer(no_args_is_help=True, help="Download/upload/list files to/from the project's store on DivBase.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@file_app.command("list")
|
|
29
|
+
def list_files(
|
|
30
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
31
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
list all files in the project's DivBase store.
|
|
35
|
+
|
|
36
|
+
To see files at a user specified project version (controlled by the 'divbase-cli version' subcommand),
|
|
37
|
+
you can instead use the 'divbase-cli version info [VERSION_NAME]' command.
|
|
38
|
+
"""
|
|
39
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
40
|
+
logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
|
|
41
|
+
|
|
42
|
+
files = list_files_command(divbase_base_url=logged_in_url, project_name=project_config.name)
|
|
43
|
+
if not files:
|
|
44
|
+
print("No files found in the project's store on DivBase.")
|
|
45
|
+
else:
|
|
46
|
+
print(f"Files in the project '{project_config.name}':")
|
|
47
|
+
for file in files:
|
|
48
|
+
print(f"- '{file}'")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@file_app.command("download")
|
|
52
|
+
def download_files(
|
|
53
|
+
files: list[str] = typer.Argument(
|
|
54
|
+
None, help="Space separated list of files/objects to download from the project's store on DivBase."
|
|
55
|
+
),
|
|
56
|
+
file_list: Path | None = typer.Option(None, "--file-list", help="Text file with list of files to upload."),
|
|
57
|
+
download_dir: str = typer.Option(
|
|
58
|
+
None,
|
|
59
|
+
help="""Directory to download the files to.
|
|
60
|
+
If not provided, defaults to what you specified in your user config.
|
|
61
|
+
If also not specified in your user config, downloads to the current directory.
|
|
62
|
+
You can also specify "." to download to the current directory.""",
|
|
63
|
+
),
|
|
64
|
+
disable_verify_checksums: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--disable-verify-checksums",
|
|
68
|
+
help="Turn off checksum verification which is on by default. "
|
|
69
|
+
"Checksum verification means all downloaded files are verified against their MD5 checksums."
|
|
70
|
+
"It is recommended to leave checksum verification enabled unless you have a specific reason to disable it.",
|
|
71
|
+
),
|
|
72
|
+
] = False,
|
|
73
|
+
project_version: str = typer.Option(
|
|
74
|
+
default=None,
|
|
75
|
+
help="User defined version of the project's at which to download the files. If not provided, downloads the latest version of all selected files.",
|
|
76
|
+
),
|
|
77
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
78
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
Download files from the project's store on DivBase. This can be done by either:
|
|
82
|
+
1. providing a list of files paths directly in the command line
|
|
83
|
+
2. providing a directory to download the files to.
|
|
84
|
+
"""
|
|
85
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
86
|
+
logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
|
|
87
|
+
download_dir_path = resolve_download_dir(download_dir=download_dir, config_path=config_file)
|
|
88
|
+
|
|
89
|
+
if bool(files) + bool(file_list) > 1:
|
|
90
|
+
print("Please specify only one of --files or --file-list.")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
all_files: set[str] = set()
|
|
94
|
+
if files:
|
|
95
|
+
all_files.update(files)
|
|
96
|
+
if file_list:
|
|
97
|
+
with open(file_list) as f:
|
|
98
|
+
for object_name in f:
|
|
99
|
+
all_files.add(object_name.strip())
|
|
100
|
+
|
|
101
|
+
if not all_files:
|
|
102
|
+
print("No files specified for download.")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
download_results = download_files_command(
|
|
106
|
+
divbase_base_url=logged_in_url,
|
|
107
|
+
project_name=project_config.name,
|
|
108
|
+
all_files=list(all_files),
|
|
109
|
+
download_dir=download_dir_path,
|
|
110
|
+
verify_checksums=not disable_verify_checksums,
|
|
111
|
+
project_version=project_version,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if download_results.successful:
|
|
115
|
+
print("[green bold]Successfully downloaded the following files:[/green bold]")
|
|
116
|
+
for success in download_results.successful:
|
|
117
|
+
print(f"- '{success.object_name}' downloaded to: '{success.file_path.resolve()}'")
|
|
118
|
+
if download_results.failed:
|
|
119
|
+
print("[red bold]ERROR: Failed to download the following files:[/red bold]")
|
|
120
|
+
for failed in download_results.failed:
|
|
121
|
+
print(f"[red]- '{failed.object_name}': Exception: '{failed.exception}'[/red]")
|
|
122
|
+
|
|
123
|
+
raise typer.Exit(1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@file_app.command("upload")
|
|
127
|
+
def upload_files(
|
|
128
|
+
files: list[Path] | None = typer.Argument(None, help="Space seperated list of files to upload."),
|
|
129
|
+
upload_dir: Path | None = typer.Option(None, "--upload-dir", help="Directory to upload all files from."),
|
|
130
|
+
file_list: Path | None = typer.Option(None, "--file-list", help="Text file with list of files to upload."),
|
|
131
|
+
disable_safe_mode: Annotated[
|
|
132
|
+
bool,
|
|
133
|
+
typer.Option(
|
|
134
|
+
"--disable-safe-mode",
|
|
135
|
+
help="Turn off safe mode which is on by default. Safe mode adds 2 extra bits of security by first calculating the MD5 checksum of each file that you're about to upload:"
|
|
136
|
+
"(1) Checks if any of the files you're about to upload already exist (by comparing name and checksum) and if so stops the upload process."
|
|
137
|
+
"(2) Sends the file's checksum when the file is uploaded so the server can verify the upload was successful (by calculating and comparing the checksums)."
|
|
138
|
+
"It is recommended to leave safe mode enabled unless you have a specific reason to disable it.",
|
|
139
|
+
),
|
|
140
|
+
] = False,
|
|
141
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
142
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
143
|
+
):
|
|
144
|
+
"""
|
|
145
|
+
Upload files to your project's store on DivBase by either:
|
|
146
|
+
1. providing a list of files paths directly in the command line
|
|
147
|
+
2. providing a directory to upload
|
|
148
|
+
3. providing a text file with or a file list.
|
|
149
|
+
"""
|
|
150
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
151
|
+
logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
|
|
152
|
+
|
|
153
|
+
if bool(files) + bool(upload_dir) + bool(file_list) > 1:
|
|
154
|
+
print("Please specify only one of --files, --upload_dir, or --file-list.")
|
|
155
|
+
raise typer.Exit(1)
|
|
156
|
+
|
|
157
|
+
all_files = set()
|
|
158
|
+
if files:
|
|
159
|
+
all_files.update(files)
|
|
160
|
+
if upload_dir:
|
|
161
|
+
all_files.update([p for p in upload_dir.iterdir() if p.is_file()])
|
|
162
|
+
if file_list:
|
|
163
|
+
with open(file_list) as f:
|
|
164
|
+
for line in f:
|
|
165
|
+
path = Path(line.strip())
|
|
166
|
+
if path.is_file():
|
|
167
|
+
all_files.add(path)
|
|
168
|
+
|
|
169
|
+
if not all_files:
|
|
170
|
+
print("No files specified for upload.")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
uploaded_results = upload_files_command(
|
|
174
|
+
project_name=project_config.name,
|
|
175
|
+
divbase_base_url=logged_in_url,
|
|
176
|
+
all_files=list(all_files),
|
|
177
|
+
safe_mode=not disable_safe_mode,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if uploaded_results.successful:
|
|
181
|
+
print("[green bold] The following files were successfully uploaded: [/green bold]")
|
|
182
|
+
for object in uploaded_results.successful:
|
|
183
|
+
print(f"- '{object.object_name}' created from file at: '{object.file_path.resolve()}'")
|
|
184
|
+
|
|
185
|
+
if uploaded_results.failed:
|
|
186
|
+
print("[red bold]ERROR: Failed to upload the following files:[/red bold]")
|
|
187
|
+
for failed in uploaded_results.failed:
|
|
188
|
+
print(f"[red]- '{failed.object_name}': Exception: '{failed.exception}'[/red]")
|
|
189
|
+
|
|
190
|
+
raise typer.Exit(1)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@file_app.command("remove")
|
|
194
|
+
def remove_files(
|
|
195
|
+
files: list[str] | None = typer.Argument(
|
|
196
|
+
None, help="Space seperated list of files/objects in the project's store on DivBase to delete."
|
|
197
|
+
),
|
|
198
|
+
file_list: Path | None = typer.Option(None, "--file-list", help="Text file with list of files to upload."),
|
|
199
|
+
dry_run: bool = typer.Option(
|
|
200
|
+
False, "--dry-run", help="If set, will not actually delete the files, just print what would be deleted."
|
|
201
|
+
),
|
|
202
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
203
|
+
config_file: Path = CONFIG_FILE_OPTION,
|
|
204
|
+
):
|
|
205
|
+
"""
|
|
206
|
+
Remove files from the project's store on DivBase by either:
|
|
207
|
+
1. providing a list of files paths directly in the command line
|
|
208
|
+
2. providing a text file with or a file list.
|
|
209
|
+
|
|
210
|
+
'dry_run' mode will not actually delete the files, just print what would be deleted.
|
|
211
|
+
"""
|
|
212
|
+
project_config = resolve_project(project_name=project, config_path=config_file)
|
|
213
|
+
logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
|
|
214
|
+
|
|
215
|
+
if bool(files) + bool(file_list) > 1:
|
|
216
|
+
print("Please specify only one of --files or --file-list.")
|
|
217
|
+
raise typer.Exit(1)
|
|
218
|
+
|
|
219
|
+
all_files = set()
|
|
220
|
+
|
|
221
|
+
if files:
|
|
222
|
+
all_files.update(files)
|
|
223
|
+
if file_list:
|
|
224
|
+
with open(file_list) as f:
|
|
225
|
+
for line in f:
|
|
226
|
+
all_files.add(line.strip())
|
|
227
|
+
|
|
228
|
+
if dry_run:
|
|
229
|
+
print("Dry run mode enabled. The following files would have been deleted:")
|
|
230
|
+
for file in all_files:
|
|
231
|
+
print(f"- '{file}'")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
deleted_files = soft_delete_objects_command(
|
|
235
|
+
divbase_base_url=logged_in_url,
|
|
236
|
+
project_name=project_config.name,
|
|
237
|
+
all_files=list(all_files),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if deleted_files:
|
|
241
|
+
print("Deleted files:")
|
|
242
|
+
for file in deleted_files:
|
|
243
|
+
print(f"- '{file}'")
|
|
244
|
+
else:
|
|
245
|
+
print("No files were deleted.")
|