divbase-cli 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev3__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.
- divbase_cli/__init__.py +1 -1
- divbase_cli/cli_commands/auth_cli.py +4 -9
- divbase_cli/cli_commands/dimensions_cli.py +4 -8
- divbase_cli/cli_commands/file_cli.py +284 -70
- divbase_cli/cli_commands/query_cli.py +3 -7
- divbase_cli/cli_commands/shared_args_options.py +20 -0
- divbase_cli/cli_commands/task_history_cli.py +3 -8
- divbase_cli/cli_commands/user_config_cli.py +14 -44
- divbase_cli/cli_commands/version_cli.py +16 -24
- divbase_cli/cli_config.py +18 -7
- divbase_cli/cli_exceptions.py +37 -22
- divbase_cli/config_resolver.py +10 -10
- divbase_cli/divbase_cli.py +1 -1
- divbase_cli/retries.py +34 -0
- divbase_cli/services/__init__.py +0 -0
- divbase_cli/services/pre_signed_urls.py +446 -0
- divbase_cli/services/project_versions.py +77 -0
- divbase_cli/services/s3_files.py +355 -0
- divbase_cli/user_auth.py +26 -13
- divbase_cli/user_config.py +20 -9
- divbase_cli/utils.py +47 -0
- {divbase_cli-0.1.0.dev2.dist-info → divbase_cli-0.1.0.dev3.dist-info}/METADATA +4 -3
- divbase_cli-0.1.0.dev3.dist-info/RECORD +27 -0
- divbase_cli/pre_signed_urls.py +0 -169
- divbase_cli/services.py +0 -219
- divbase_cli-0.1.0.dev2.dist-info/RECORD +0 -22
- {divbase_cli-0.1.0.dev2.dist-info → divbase_cli-0.1.0.dev3.dist-info}/WHEEL +0 -0
- {divbase_cli-0.1.0.dev2.dist-info → divbase_cli-0.1.0.dev3.dist-info}/entry_points.txt +0 -0
|
@@ -6,11 +6,9 @@ Submits a query for fetching the Celery task history for the user to the DivBase
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from pathlib import Path
|
|
10
9
|
|
|
11
10
|
import typer
|
|
12
11
|
|
|
13
|
-
from divbase_cli.cli_commands.user_config_cli import CONFIG_FILE_OPTION
|
|
14
12
|
from divbase_cli.cli_exceptions import AuthenticationError
|
|
15
13
|
from divbase_cli.display_task_history import TaskHistoryDisplayManager
|
|
16
14
|
from divbase_cli.user_auth import make_authenticated_request
|
|
@@ -28,7 +26,6 @@ task_history_app = typer.Typer(
|
|
|
28
26
|
|
|
29
27
|
@task_history_app.command("user")
|
|
30
28
|
def list_task_history_for_user(
|
|
31
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
32
29
|
limit: int = typer.Option(10, help="Maximum number of tasks to display in the terminal. Sorted by recency."),
|
|
33
30
|
project: str | None = typer.Option(
|
|
34
31
|
None, help="Optional project name to filter the user's task history by project."
|
|
@@ -40,7 +37,7 @@ def list_task_history_for_user(
|
|
|
40
37
|
|
|
41
38
|
# TODO add option to sort ASC/DESC by task timestamp
|
|
42
39
|
|
|
43
|
-
config = load_user_config(
|
|
40
|
+
config = load_user_config()
|
|
44
41
|
logged_in_url = config.logged_in_url
|
|
45
42
|
|
|
46
43
|
if not logged_in_url:
|
|
@@ -73,13 +70,12 @@ def list_task_history_for_user(
|
|
|
73
70
|
@task_history_app.command("id")
|
|
74
71
|
def task_history_by_id(
|
|
75
72
|
task_id: int | None = typer.Argument(..., help="Task ID to check the status of a specific query job."),
|
|
76
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
77
73
|
):
|
|
78
74
|
"""
|
|
79
75
|
Check status of a specific task submitted by the user by its task ID.
|
|
80
76
|
"""
|
|
81
77
|
|
|
82
|
-
config = load_user_config(
|
|
78
|
+
config = load_user_config()
|
|
83
79
|
logged_in_url = config.logged_in_url
|
|
84
80
|
|
|
85
81
|
if not logged_in_url:
|
|
@@ -104,7 +100,6 @@ def task_history_by_id(
|
|
|
104
100
|
|
|
105
101
|
@task_history_app.command("project")
|
|
106
102
|
def list_task_history_for_project(
|
|
107
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
108
103
|
limit: int = typer.Option(10, help="Maximum number of tasks to display in the terminal. Sorted by recency."),
|
|
109
104
|
project: str = typer.Argument(..., help="Project name to check the task history for."),
|
|
110
105
|
):
|
|
@@ -115,7 +110,7 @@ def list_task_history_for_project(
|
|
|
115
110
|
# TODO add option to sort ASC/DESC by task timestamp
|
|
116
111
|
# TODO use default project from config if not --project specified
|
|
117
112
|
|
|
118
|
-
config = load_user_config(
|
|
113
|
+
config = load_user_config()
|
|
119
114
|
logged_in_url = config.logged_in_url
|
|
120
115
|
|
|
121
116
|
if not logged_in_url:
|
|
@@ -4,8 +4,6 @@ config subcommand for the divbase-cli package.
|
|
|
4
4
|
Controls the user config file stored at "~/.config/.divbase_tools.yaml" (unless specified otherwise).
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
7
|
import typer
|
|
10
8
|
from rich import print
|
|
11
9
|
from rich.console import Console
|
|
@@ -13,34 +11,12 @@ from rich.table import Table
|
|
|
13
11
|
|
|
14
12
|
from divbase_cli.cli_config import cli_settings
|
|
15
13
|
from divbase_cli.user_config import (
|
|
16
|
-
create_user_config,
|
|
17
14
|
load_user_config,
|
|
18
15
|
)
|
|
19
16
|
|
|
20
|
-
CONFIG_FILE_OPTION = typer.Option(
|
|
21
|
-
cli_settings.CONFIG_PATH,
|
|
22
|
-
"--config",
|
|
23
|
-
"-c",
|
|
24
|
-
help="Path to your user configuration file. If you didn't specify a custom path when you created it, you don't need to set this.",
|
|
25
|
-
)
|
|
26
|
-
|
|
27
17
|
config_app = typer.Typer(help="Manage your user configuration file for the DivBase CLI.", no_args_is_help=True)
|
|
28
18
|
|
|
29
19
|
|
|
30
|
-
@config_app.command("create")
|
|
31
|
-
def create_user_config_command(
|
|
32
|
-
config_file: Path = typer.Option(
|
|
33
|
-
cli_settings.CONFIG_PATH,
|
|
34
|
-
"--config",
|
|
35
|
-
"-c",
|
|
36
|
-
help="Where to store your config file locally on your pc.",
|
|
37
|
-
),
|
|
38
|
-
):
|
|
39
|
-
"""Create a user configuration file for divbase-cli."""
|
|
40
|
-
create_user_config(config_path=config_file)
|
|
41
|
-
print(f"User configuration file created at {config_file.resolve()}.")
|
|
42
|
-
|
|
43
|
-
|
|
44
20
|
@config_app.command("add")
|
|
45
21
|
def add_project_command(
|
|
46
22
|
name: str = typer.Argument(..., help="Name of the project to add to your config file."),
|
|
@@ -56,17 +32,16 @@ def add_project_command(
|
|
|
56
32
|
"-d",
|
|
57
33
|
help="Set this project as the default project in your config file.",
|
|
58
34
|
),
|
|
59
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
60
35
|
):
|
|
61
36
|
"""Add a new project to your user configuration file."""
|
|
62
|
-
config = load_user_config(
|
|
37
|
+
config = load_user_config()
|
|
63
38
|
project = config.add_project(
|
|
64
39
|
name=name,
|
|
65
40
|
divbase_url=divbase_url,
|
|
66
41
|
is_default=make_default,
|
|
67
42
|
)
|
|
68
43
|
|
|
69
|
-
print(f"Project: '{project.name}' added to your config
|
|
44
|
+
print(f"Project: '{project.name}' added to your config.")
|
|
70
45
|
print(f"The URL: {project.divbase_url} was set as the DivBase API URL for this project.")
|
|
71
46
|
|
|
72
47
|
if make_default:
|
|
@@ -75,17 +50,16 @@ def add_project_command(
|
|
|
75
50
|
print(f"To make '{project.name}' your default project you can run: 'divbase config set-default {project.name}'")
|
|
76
51
|
|
|
77
52
|
|
|
78
|
-
@config_app.command("
|
|
53
|
+
@config_app.command("rm")
|
|
79
54
|
def remove_project_command(
|
|
80
55
|
name: str = typer.Argument(..., help="Name of the project to remove from your user configuration file."),
|
|
81
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
82
56
|
):
|
|
83
57
|
"""Remove a project from your user configuration file."""
|
|
84
|
-
config = load_user_config(
|
|
58
|
+
config = load_user_config()
|
|
85
59
|
removed_project = config.remove_project(name)
|
|
86
60
|
|
|
87
61
|
if not removed_project:
|
|
88
|
-
print(f"
|
|
62
|
+
print(f"Nothing to do, the project '{name}' was not found in your user config")
|
|
89
63
|
else:
|
|
90
64
|
print(f"The project '{removed_project}' was removed from your config.")
|
|
91
65
|
|
|
@@ -93,20 +67,17 @@ def remove_project_command(
|
|
|
93
67
|
@config_app.command("set-default")
|
|
94
68
|
def set_default_project_command(
|
|
95
69
|
name: str = typer.Argument(..., help="Name of the project to add to the user configuration file."),
|
|
96
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
97
70
|
):
|
|
98
71
|
"""Set your default project to use in all divbase-cli commands."""
|
|
99
|
-
config = load_user_config(
|
|
72
|
+
config = load_user_config()
|
|
100
73
|
default_project_name = config.set_default_project(name=name)
|
|
101
74
|
print(f"Default project is now set to '{default_project_name}'.")
|
|
102
75
|
|
|
103
76
|
|
|
104
77
|
@config_app.command("show-default")
|
|
105
|
-
def show_default_project_command(
|
|
106
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
107
|
-
) -> None:
|
|
78
|
+
def show_default_project_command() -> None:
|
|
108
79
|
"""Print the currently set default project to the console."""
|
|
109
|
-
config = load_user_config(
|
|
80
|
+
config = load_user_config()
|
|
110
81
|
|
|
111
82
|
if config.default_project:
|
|
112
83
|
print(config.default_project)
|
|
@@ -123,10 +94,9 @@ def set_default_dload_dir_command(
|
|
|
123
94
|
You can specify an absolute path.
|
|
124
95
|
You can use '.' to refer to the directory you run the command from.""",
|
|
125
96
|
),
|
|
126
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
127
97
|
):
|
|
128
98
|
"""Set the default download dir"""
|
|
129
|
-
config = load_user_config(
|
|
99
|
+
config = load_user_config()
|
|
130
100
|
dload_dir = config.set_default_download_dir(download_dir=download_dir)
|
|
131
101
|
if dload_dir == ".":
|
|
132
102
|
print("The default download directory will be whereever you run the command from.")
|
|
@@ -135,14 +105,14 @@ def set_default_dload_dir_command(
|
|
|
135
105
|
|
|
136
106
|
|
|
137
107
|
@config_app.command("show")
|
|
138
|
-
def show_user_config(
|
|
139
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
140
|
-
):
|
|
108
|
+
def show_user_config():
|
|
141
109
|
"""Pretty print the contents of your current config file."""
|
|
142
|
-
config = load_user_config(
|
|
110
|
+
config = load_user_config()
|
|
143
111
|
console = Console()
|
|
144
112
|
|
|
145
|
-
console.print(
|
|
113
|
+
console.print(
|
|
114
|
+
f"[bold]Your DivBase user configuration file's contents located at:[/bold] '{cli_settings.CONFIG_PATH.resolve()}'\n"
|
|
115
|
+
)
|
|
146
116
|
|
|
147
117
|
if not config.default_download_dir:
|
|
148
118
|
dload_dir_info = "Not specified, meaning the working directory of wherever you run the download command from."
|
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
"""CLI commands for managing project versions in DivBase."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from pathlib import Path
|
|
5
4
|
from zoneinfo import ZoneInfo
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
from rich import print
|
|
9
|
-
from rich.console import Console
|
|
10
8
|
from rich.table import Table
|
|
11
9
|
|
|
12
|
-
from divbase_cli.cli_commands.
|
|
10
|
+
from divbase_cli.cli_commands.shared_args_options import FORMAT_AS_TSV_OPTION, PROJECT_NAME_OPTION
|
|
13
11
|
from divbase_cli.config_resolver import ensure_logged_in, resolve_project
|
|
14
|
-
from divbase_cli.services import (
|
|
12
|
+
from divbase_cli.services.project_versions import (
|
|
15
13
|
add_version_command,
|
|
16
14
|
delete_version_command,
|
|
17
15
|
get_version_details_command,
|
|
18
16
|
list_versions_command,
|
|
19
17
|
)
|
|
20
|
-
|
|
21
|
-
PROJECT_NAME_OPTION = typer.Option(
|
|
22
|
-
None,
|
|
23
|
-
help="Name of the DivBase project, if not provided uses the default in your DivBase config file",
|
|
24
|
-
show_default=False,
|
|
25
|
-
)
|
|
18
|
+
from divbase_cli.utils import print_rich_table_as_tsv
|
|
26
19
|
|
|
27
20
|
version_app = typer.Typer(
|
|
28
21
|
no_args_is_help=True,
|
|
@@ -42,11 +35,10 @@ def add_version(
|
|
|
42
35
|
name: str = typer.Argument(help="Name of the version (e.g., semantic version).", show_default=False),
|
|
43
36
|
description: str = typer.Option("", help="Optional description of the version."),
|
|
44
37
|
project: str | None = PROJECT_NAME_OPTION,
|
|
45
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
46
38
|
):
|
|
47
39
|
"""Add a new project version entry which specifies the current state of all files in the project at the current timestamp."""
|
|
48
|
-
project_config = resolve_project(project_name=project
|
|
49
|
-
logged_in_url = ensure_logged_in(
|
|
40
|
+
project_config = resolve_project(project_name=project)
|
|
41
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
50
42
|
|
|
51
43
|
add_version_response = add_version_command(
|
|
52
44
|
name=name,
|
|
@@ -60,8 +52,8 @@ def add_version(
|
|
|
60
52
|
@version_app.command("list")
|
|
61
53
|
def list_versions(
|
|
62
54
|
project: str | None = PROJECT_NAME_OPTION,
|
|
63
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
64
55
|
include_deleted: bool = typer.Option(False, help="Include soft-deleted versions in the listing."),
|
|
56
|
+
format_output_as_tsv: bool = FORMAT_AS_TSV_OPTION,
|
|
65
57
|
):
|
|
66
58
|
"""
|
|
67
59
|
List all entries in the project versioning file.
|
|
@@ -70,8 +62,8 @@ def list_versions(
|
|
|
70
62
|
If you specify --include-deleted, soft-deleted versions will also be shown.
|
|
71
63
|
Soft-deleted versions can be restored by a DivBase admin within 30 days of deletion.
|
|
72
64
|
"""
|
|
73
|
-
project_config = resolve_project(project_name=project
|
|
74
|
-
logged_in_url = ensure_logged_in(
|
|
65
|
+
project_config = resolve_project(project_name=project)
|
|
66
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
75
67
|
|
|
76
68
|
versions_info = list_versions_command(
|
|
77
69
|
project_name=project_config.name, include_deleted=include_deleted, divbase_base_url=logged_in_url
|
|
@@ -81,7 +73,6 @@ def list_versions(
|
|
|
81
73
|
print(f"No versions found for project: {project_config.name}.")
|
|
82
74
|
return
|
|
83
75
|
|
|
84
|
-
console = Console()
|
|
85
76
|
table = Table(title=f"Versions for {project_config.name}")
|
|
86
77
|
table.add_column("Version", style="cyan", no_wrap=True)
|
|
87
78
|
table.add_column("Created ", style="magenta")
|
|
@@ -99,18 +90,20 @@ def list_versions(
|
|
|
99
90
|
else:
|
|
100
91
|
table.add_row(name, created_at, desc)
|
|
101
92
|
|
|
102
|
-
|
|
93
|
+
if not format_output_as_tsv:
|
|
94
|
+
print(table)
|
|
95
|
+
else:
|
|
96
|
+
print_rich_table_as_tsv(table=table)
|
|
103
97
|
|
|
104
98
|
|
|
105
99
|
@version_app.command("info")
|
|
106
100
|
def get_version_info(
|
|
107
101
|
version: str = typer.Argument(help="Specific version to retrieve information for"),
|
|
108
102
|
project: str | None = PROJECT_NAME_OPTION,
|
|
109
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
110
103
|
):
|
|
111
104
|
"""Provide detailed information about a user specified project version, including all files present and their unique hashes."""
|
|
112
|
-
project_config = resolve_project(project_name=project
|
|
113
|
-
logged_in_url = ensure_logged_in(
|
|
105
|
+
project_config = resolve_project(project_name=project)
|
|
106
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
114
107
|
|
|
115
108
|
version_details = get_version_details_command(
|
|
116
109
|
project_name=project_config.name, divbase_base_url=logged_in_url, version_name=version
|
|
@@ -133,15 +126,14 @@ def get_version_info(
|
|
|
133
126
|
def delete_version(
|
|
134
127
|
name: str = typer.Argument(help="Name of the version (e.g., semantic version).", show_default=False),
|
|
135
128
|
project: str | None = PROJECT_NAME_OPTION,
|
|
136
|
-
config_file: Path = CONFIG_FILE_OPTION,
|
|
137
129
|
):
|
|
138
130
|
"""
|
|
139
131
|
Delete a version entry in the project versioning table. This does not delete the files themselves.
|
|
140
132
|
Deleted version entries older than 30 days will be permanently deleted.
|
|
141
133
|
You can ask a DivBase admin to restore a deleted version within that time period.
|
|
142
134
|
"""
|
|
143
|
-
project_config = resolve_project(project_name=project
|
|
144
|
-
logged_in_url = ensure_logged_in(
|
|
135
|
+
project_config = resolve_project(project_name=project)
|
|
136
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
145
137
|
|
|
146
138
|
deleted_version = delete_version_command(
|
|
147
139
|
project_name=project_config.name, divbase_base_url=logged_in_url, version_name=name
|
divbase_cli/cli_config.py
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
Settings for DivBase CLI.
|
|
3
3
|
|
|
4
4
|
This class creates a single 'settings' object at module load time that can be imported and used throughout the entire package.
|
|
5
|
+
|
|
6
|
+
The user config and tokens are stored in the user local app dir:
|
|
7
|
+
https://typer.tiangolo.com/tutorial/app-dir/
|
|
5
8
|
"""
|
|
6
9
|
|
|
7
10
|
import os
|
|
8
11
|
from dataclasses import dataclass
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
APP_NAME = "divbase-cli"
|
|
17
|
+
APP_DIR = Path(typer.get_app_dir(APP_NAME))
|
|
18
|
+
CONFIG_PATH = APP_DIR / "config.yaml"
|
|
19
|
+
TOKENS_PATH = APP_DIR / ".secrets"
|
|
13
20
|
DEFAULT_METADATA_TSV_NAME = "sample_metadata.tsv"
|
|
14
|
-
|
|
21
|
+
# TODO - change to production URL when time comes
|
|
22
|
+
DEFAULT_DIVBASE_API_URL = "https://divbase-dev.scilifelab-2-dev.sys.kth.se/api/"
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
@dataclass
|
|
@@ -19,12 +27,12 @@ class DivBaseCLISettings:
|
|
|
19
27
|
"""
|
|
20
28
|
Settings for DivBase CLI.
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
import the '
|
|
30
|
+
Your do not need to create an instance of this class yourself,
|
|
31
|
+
instead, import the 'cli_settings' instance created at this module's load time.
|
|
24
32
|
"""
|
|
25
33
|
|
|
26
|
-
CONFIG_PATH: Path = Path(os.getenv("
|
|
27
|
-
TOKENS_PATH: Path = Path(os.getenv("
|
|
34
|
+
CONFIG_PATH: Path = Path(os.getenv("DIVBASE_CLI_CONFIG_PATH", CONFIG_PATH))
|
|
35
|
+
TOKENS_PATH: Path = Path(os.getenv("DIVBASE_CLI_TOKENS_PATH", TOKENS_PATH))
|
|
28
36
|
DIVBASE_API_URL: str = os.getenv("DIVBASE_API_URL", DEFAULT_DIVBASE_API_URL)
|
|
29
37
|
METADATA_TSV_NAME: str = os.getenv("DIVBASE_METADATA_TSV_NAME", DEFAULT_METADATA_TSV_NAME)
|
|
30
38
|
LOGGING_ON: bool = bool(os.getenv("DIVBASE_LOGGING_ON", "True") == "True")
|
|
@@ -35,5 +43,8 @@ class DivBaseCLISettings:
|
|
|
35
43
|
if self.LOG_LEVEL not in valid_levels:
|
|
36
44
|
raise ValueError(f"Invalid LOG_LEVEL: {self.LOG_LEVEL}. Must be one of {valid_levels}.")
|
|
37
45
|
|
|
46
|
+
if self.DIVBASE_API_URL.endswith("/"):
|
|
47
|
+
self.DIVBASE_API_URL = self.DIVBASE_API_URL[:-1]
|
|
48
|
+
|
|
38
49
|
|
|
39
50
|
cli_settings = DivBaseCLISettings()
|
divbase_cli/cli_exceptions.py
CHANGED
|
@@ -4,7 +4,7 @@ Custom exceptions for the divbase CLI.
|
|
|
4
4
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from divbase_lib.
|
|
7
|
+
from divbase_lib.divbase_constants import SUPPORTED_DIVBASE_FILE_TYPES, UNSUPPORTED_CHARACTERS_IN_FILENAMES
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class DivBaseCLIError(Exception):
|
|
@@ -41,7 +41,7 @@ class DivBaseAPIError(DivBaseCLIError):
|
|
|
41
41
|
self,
|
|
42
42
|
error_details: str = "Not Provided",
|
|
43
43
|
error_type: str = "unknown",
|
|
44
|
-
status_code: int =
|
|
44
|
+
status_code: int = 500,
|
|
45
45
|
http_method: str = "unknown",
|
|
46
46
|
url: str = "unknown",
|
|
47
47
|
):
|
|
@@ -87,8 +87,10 @@ class FilesAlreadyInProjectError(DivBaseCLIError):
|
|
|
87
87
|
and the user does not want to accidently create a new version of any file.
|
|
88
88
|
"""
|
|
89
89
|
|
|
90
|
-
def __init__(self, existing_files:
|
|
91
|
-
files_list = "\n".join(
|
|
90
|
+
def __init__(self, existing_files: dict[Path, str], project_name: str):
|
|
91
|
+
files_list = "\n".join(
|
|
92
|
+
f"'{file_path}' (Checksum: {checksum})" for file_path, checksum in existing_files.items()
|
|
93
|
+
)
|
|
92
94
|
self.existing_files = existing_files
|
|
93
95
|
self.project_name = project_name
|
|
94
96
|
|
|
@@ -106,12 +108,12 @@ class ProjectNameNotSpecifiedError(DivBaseCLIError):
|
|
|
106
108
|
no default project is set in the user config file.
|
|
107
109
|
"""
|
|
108
110
|
|
|
109
|
-
def __init__(self
|
|
110
|
-
self.config_path = config_path
|
|
111
|
+
def __init__(self):
|
|
111
112
|
error_message = (
|
|
112
|
-
"No project name provided
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
"No project name provided.\n"
|
|
114
|
+
"Please either set a default project in your user configuration file.\n"
|
|
115
|
+
"or pass the flag '--project <project_name>' to this command.\n"
|
|
116
|
+
"To set a default project, you can run 'divbase-cli config set-default <project_name>'.\n"
|
|
115
117
|
)
|
|
116
118
|
super().__init__(error_message)
|
|
117
119
|
|
|
@@ -135,17 +137,30 @@ class ProjectNotInConfigError(DivBaseCLIError):
|
|
|
135
137
|
super().__init__(error_message)
|
|
136
138
|
|
|
137
139
|
|
|
138
|
-
class
|
|
139
|
-
"""Raised when
|
|
140
|
+
class UnsupportedFileTypeError(DivBaseCLIError):
|
|
141
|
+
"""Raised when one or more files to be uploaded are not supported by DivBase (based on file extension)."""
|
|
140
142
|
|
|
141
|
-
def __init__(
|
|
142
|
-
self
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
143
|
+
def __init__(self, unsupported_files: list[Path], supported_types: tuple[str, ...] = SUPPORTED_DIVBASE_FILE_TYPES):
|
|
144
|
+
self.unsupported_files = unsupported_files
|
|
145
|
+
self.supported_types = supported_types
|
|
146
|
+
message = (
|
|
147
|
+
f"The following file(s) have types that are not supported by DivBase and therefore cannot be uploaded: \n"
|
|
148
|
+
f"{'\n'.join(str(file) for file in unsupported_files)}\n"
|
|
149
|
+
f"DivBase currently supports the following file types: {', '.join(SUPPORTED_DIVBASE_FILE_TYPES)}\n"
|
|
150
|
+
"If you want us to support another file type, please let us know."
|
|
151
|
+
)
|
|
152
|
+
super().__init__(message)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class UnsupportedFileNameError(DivBaseCLIError):
|
|
156
|
+
"""Raised when one or more files to be uploaded have unsupported characters in their filenames."""
|
|
157
|
+
|
|
158
|
+
def __init__(self, unsupported_files: list[Path]):
|
|
159
|
+
self.unsupported_files = unsupported_files
|
|
160
|
+
message = (
|
|
161
|
+
f"The following file(s) have unsupported characters in their filenames and therefore cannot be uploaded: \n"
|
|
162
|
+
f"{'\n'.join(str(file) for file in unsupported_files)}\n"
|
|
163
|
+
f"Filenames cannot contain any of the following characters: {', '.join(UNSUPPORTED_CHARACTERS_IN_FILENAMES)}\n"
|
|
164
|
+
"Please rename the files and try again."
|
|
165
|
+
)
|
|
166
|
+
super().__init__(message)
|
divbase_cli/config_resolver.py
CHANGED
|
@@ -3,7 +3,7 @@ Functions that resolve for the CLI commands things like:
|
|
|
3
3
|
- which project to use
|
|
4
4
|
- which download directory to use
|
|
5
5
|
- which DivBase API URL to use
|
|
6
|
-
Based on
|
|
6
|
+
Based on provided user input and their config file.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
@@ -12,7 +12,7 @@ from divbase_cli.cli_exceptions import AuthenticationError, ProjectNameNotSpecif
|
|
|
12
12
|
from divbase_cli.user_config import ProjectConfig, load_user_config
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def ensure_logged_in(
|
|
15
|
+
def ensure_logged_in(desired_url: str | None = None) -> str:
|
|
16
16
|
"""
|
|
17
17
|
Ensure the user is logged in by checking the logged_in_url value in the user config.
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ def ensure_logged_in(config_path: Path, desired_url: str | None = None) -> str:
|
|
|
20
20
|
|
|
21
21
|
Returns the logged_in_url if valid, otherwise raises an AuthenticationError.
|
|
22
22
|
"""
|
|
23
|
-
config = load_user_config(
|
|
23
|
+
config = load_user_config()
|
|
24
24
|
if not config.logged_in_url:
|
|
25
25
|
raise AuthenticationError("You are not logged in. Please log in with 'divbase-cli auth login [EMAIL]'.")
|
|
26
26
|
if desired_url and config.logged_in_url != desired_url:
|
|
@@ -30,7 +30,7 @@ def ensure_logged_in(config_path: Path, desired_url: str | None = None) -> str:
|
|
|
30
30
|
return config.logged_in_url
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def resolve_project(project_name: str | None
|
|
33
|
+
def resolve_project(project_name: str | None) -> ProjectConfig:
|
|
34
34
|
"""
|
|
35
35
|
Helper function to resolve the project to use for a CLI command.
|
|
36
36
|
Falls back to the default project set in the user config if not explicitly provided.
|
|
@@ -38,15 +38,15 @@ def resolve_project(project_name: str | None, config_path: Path) -> ProjectConfi
|
|
|
38
38
|
Once the project is resolved a ProjectConfig object is returned,
|
|
39
39
|
which contains the name and API URL of the project.
|
|
40
40
|
"""
|
|
41
|
-
config = load_user_config(
|
|
41
|
+
config = load_user_config()
|
|
42
42
|
if not project_name:
|
|
43
43
|
project_name = config.default_project
|
|
44
44
|
if not project_name:
|
|
45
|
-
raise ProjectNameNotSpecifiedError(
|
|
45
|
+
raise ProjectNameNotSpecifiedError()
|
|
46
46
|
return config.project_info(project_name)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def resolve_divbase_api_url(url: str | None
|
|
49
|
+
def resolve_divbase_api_url(url: str | None) -> str:
|
|
50
50
|
"""
|
|
51
51
|
Helper function to resolve the DivBase API URL to use for a CLI command.
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ def resolve_divbase_api_url(url: str | None, config_path: Path) -> str:
|
|
|
56
56
|
if url:
|
|
57
57
|
return url
|
|
58
58
|
|
|
59
|
-
config = load_user_config(
|
|
59
|
+
config = load_user_config()
|
|
60
60
|
|
|
61
61
|
if not config.default_project:
|
|
62
62
|
raise ValueError(
|
|
@@ -68,7 +68,7 @@ def resolve_divbase_api_url(url: str | None, config_path: Path) -> str:
|
|
|
68
68
|
return project_config.divbase_url
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def resolve_download_dir(download_dir: str | None
|
|
71
|
+
def resolve_download_dir(download_dir: str | None) -> Path:
|
|
72
72
|
"""
|
|
73
73
|
Helper function to resolve the download directory to use for a CLI command involving downloading files.
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ def resolve_download_dir(download_dir: str | None, config_path: Path) -> Path:
|
|
|
76
76
|
Note: "." or None should default to the current working directory.
|
|
77
77
|
"""
|
|
78
78
|
if not download_dir:
|
|
79
|
-
config = load_user_config(
|
|
79
|
+
config = load_user_config()
|
|
80
80
|
download_dir = config.default_download_dir
|
|
81
81
|
|
|
82
82
|
if download_dir and download_dir != ".":
|
divbase_cli/divbase_cli.py
CHANGED
|
@@ -67,7 +67,7 @@ app.add_typer(task_history_app, name="task-history")
|
|
|
67
67
|
|
|
68
68
|
def main():
|
|
69
69
|
if cli_settings.LOGGING_ON:
|
|
70
|
-
logging.basicConfig(level=cli_settings.LOG_LEVEL, handlers=[logging.StreamHandler(sys.
|
|
70
|
+
logging.basicConfig(level=cli_settings.LOG_LEVEL, handlers=[logging.StreamHandler(sys.stderr)])
|
|
71
71
|
logger.info(f"Starting divbase_cli CLI application with logging level: {cli_settings.LOG_LEVEL}")
|
|
72
72
|
app()
|
|
73
73
|
|
divbase_cli/retries.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Retry logic for the divbase-cli package.
|
|
3
|
+
|
|
4
|
+
Defines functions to determine whether to retry based on the type of exceptions raised.
|
|
5
|
+
Functions are used in stamina (package) decorators.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from divbase_cli.cli_exceptions import DivBaseAPIConnectionError, DivBaseAPIError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def retry_only_on_retryable_http_errors(exc: Exception) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Used by stamina's (library for retries) decorators to determine whether to retry the function or not.
|
|
16
|
+
We avoid retrying on HTTPStatusError for 4xx errors as no point (e.g. 404 Not Found or 403 Forbidden etc...).
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
19
|
+
return exc.response.status_code >= 500
|
|
20
|
+
|
|
21
|
+
# Want to retry on other HTTPError (parent of HTTPStatusError),
|
|
22
|
+
# as this includes timeouts, connection errors, etc.
|
|
23
|
+
return isinstance(exc, httpx.HTTPError)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def retry_only_on_retryable_divbase_api_errors(exception: Exception) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Retry condition function for stamina to only retry on retryable DivBase API errors.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(exception, DivBaseAPIConnectionError):
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
# Retry only for server errors (5xx) and rate limiting (429)
|
|
34
|
+
return isinstance(exception, DivBaseAPIError) and (exception.status_code == 429 or exception.status_code >= 500)
|
|
File without changes
|