divbase-cli 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev4__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.
@@ -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
@@ -0,0 +1,46 @@
1
+ """
2
+ Get announcements from divbase API and display them to the user.
3
+ """
4
+
5
+ from rich import print
6
+
7
+ from divbase_cli.user_auth import make_unauthenticated_request
8
+ from divbase_lib.api_schemas.announcements import AnnouncementResponse
9
+
10
+ COLOR_MAPPING = {
11
+ "info": "blue",
12
+ "success": "green",
13
+ "warning": "yellow",
14
+ "danger": "red",
15
+ }
16
+
17
+
18
+ def get_and_display_announcements(divbase_base_url: str) -> None:
19
+ """
20
+ Get announcements from the API and display them to the user.
21
+ Used when logging in to display any important messages to the user.
22
+ """
23
+ response = make_unauthenticated_request(
24
+ method="GET",
25
+ divbase_base_url=divbase_base_url,
26
+ api_route="/v1/core/announcements",
27
+ )
28
+ announcements_data = response.json()
29
+ if not announcements_data:
30
+ return
31
+ announcements = [AnnouncementResponse.model_validate(ann) for ann in announcements_data]
32
+
33
+ if len(announcements) == 1:
34
+ print("[bold]An Announcement from DivBase Server:[/bold]")
35
+ color = COLOR_MAPPING.get(announcements[0].level)
36
+ print(f"[bold {color}]{announcements[0].heading}[/bold {color}]")
37
+ if announcements[0].message:
38
+ print(f"{announcements[0].message}\n")
39
+ return
40
+
41
+ print("[bold]Announcements from DivBase Server:[/bold]")
42
+ for idx, ann in enumerate(announcements, start=1):
43
+ color = COLOR_MAPPING.get(ann.level)
44
+ print(f"[bold {color}]{idx}) {ann.heading}[/bold {color}]")
45
+ if ann.message:
46
+ print(f"{ann.message}\n")