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.
@@ -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(config_file)
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(config_file)
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(config_file)
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(config_file)
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 file located at {config_file.resolve()}.")
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("remove")
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(config_file)
58
+ config = load_user_config()
85
59
  removed_project = config.remove_project(name)
86
60
 
87
61
  if not removed_project:
88
- print(f"The project '{name}' was not found in your config file located at {config_file.resolve()}.")
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(config_file)
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(config_file)
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(config_file)
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(config_file)
110
+ config = load_user_config()
143
111
  console = Console()
144
112
 
145
- console.print(f"[bold]Your DivBase user configuration file's contents located at:[/bold] '{config_file}'")
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.user_config_cli import CONFIG_FILE_OPTION
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, config_path=config_file)
49
- logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
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, config_path=config_file)
74
- logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
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
- console.print(table)
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, config_path=config_file)
113
- logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
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, config_path=config_file)
144
- logged_in_url = ensure_logged_in(config_path=config_file, desired_url=project_config.divbase_url)
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
- DEFAULT_CONFIG_PATH = Path.home() / ".config" / "divbase" / "config.yaml"
12
- DEFAULT_TOKENS_PATH = Path.home() / ".config" / "divbase" / ".secrets"
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
- DEFAULT_DIVBASE_API_URL = "http://localhost:8000/api" # TODO - change to production URL when time comes
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
- NOTE: Do not create an instance of this class yourself,
23
- import the 'settings' instance created at this module's load time.
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("DIVBASE_CONFIG_PATH", DEFAULT_CONFIG_PATH))
27
- TOKENS_PATH: Path = Path(os.getenv("DIVBASE_TOKENS_PATH", DEFAULT_TOKENS_PATH))
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()
@@ -4,7 +4,7 @@ Custom exceptions for the divbase CLI.
4
4
 
5
5
  from pathlib import Path
6
6
 
7
- from divbase_lib.api_schemas.s3 import ExistingFileResponse
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 = None,
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: list[ExistingFileResponse], project_name: str):
91
- files_list = "\n".join(f"- '{obj.object_name}'" for obj in existing_files)
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, config_path: Path):
110
- self.config_path = config_path
111
+ def __init__(self):
111
112
  error_message = (
112
- "No project name provided. \n"
113
- f"Please either set a default project in your user configuration file at '{config_path.resolve()}'.\n"
114
- f"or pass the flag '--project <project_name>' to this command.\n"
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 ConfigFileNotFoundError(DivBaseCLIError):
139
- """Raised when the user's config file cannot be found."""
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
- error_message: str = (
144
- "You're DivBase configuration file was not found or does not exist.\n"
145
- "To create a user configuration file, run 'divbase-cli config create'.\n"
146
- "If you already have a user configuration file that but it is not stored in the default location, "
147
- "you can pass the '--config <path>' flag to specify the location. \n"
148
- "You very probably want to just run 'divbase-cli config create' though."
149
- ),
150
- ):
151
- super().__init__(error_message)
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)
@@ -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 provider user input and their config file.
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(config_path: Path, desired_url: str | None = None) -> str:
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(config_path)
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, config_path: Path) -> ProjectConfig:
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(config_path)
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(config_path=config_path)
45
+ raise ProjectNameNotSpecifiedError()
46
46
  return config.project_info(project_name)
47
47
 
48
48
 
49
- def resolve_divbase_api_url(url: str | None, config_path: Path) -> str:
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(config_path=config_path)
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, config_path: Path) -> Path:
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(config_path)
79
+ config = load_user_config()
80
80
  download_dir = config.default_download_dir
81
81
 
82
82
  if download_dir and download_dir != ".":
@@ -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.stdout)])
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