divbase-cli 0.1.0.dev3__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.
- divbase_cli/__init__.py +1 -1
- divbase_cli/cli_commands/auth_cli.py +12 -1
- divbase_cli/cli_commands/dimensions_cli.py +319 -19
- divbase_cli/cli_commands/file_cli.py +209 -36
- divbase_cli/cli_commands/query_cli.py +11 -2
- divbase_cli/cli_commands/shared_args_options.py +3 -0
- divbase_cli/cli_commands/version_cli.py +82 -13
- divbase_cli/cli_config.py +14 -7
- divbase_cli/services/announcements.py +46 -0
- divbase_cli/services/pre_signed_urls.py +10 -0
- divbase_cli/services/project_versions.py +26 -0
- divbase_cli/services/s3_files.py +112 -25
- divbase_cli/user_auth.py +88 -43
- divbase_cli/utils.py +0 -19
- {divbase_cli-0.1.0.dev3.dist-info → divbase_cli-0.1.0.dev4.dist-info}/METADATA +3 -5
- divbase_cli-0.1.0.dev4.dist-info/RECORD +28 -0
- {divbase_cli-0.1.0.dev3.dist-info → divbase_cli-0.1.0.dev4.dist-info}/WHEEL +1 -1
- divbase_cli-0.1.0.dev3.dist-info/RECORD +0 -27
- {divbase_cli-0.1.0.dev3.dist-info → divbase_cli-0.1.0.dev4.dist-info}/entry_points.txt +0 -0
|
@@ -10,6 +10,8 @@ from divbase_lib.api_schemas.project_versions import (
|
|
|
10
10
|
DeleteVersionResponse,
|
|
11
11
|
ProjectVersionDetailResponse,
|
|
12
12
|
ProjectVersionInfo,
|
|
13
|
+
UpdateVersionRequest,
|
|
14
|
+
UpdateVersionResponse,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -59,6 +61,30 @@ def get_version_details_command(
|
|
|
59
61
|
return ProjectVersionDetailResponse(**response.json())
|
|
60
62
|
|
|
61
63
|
|
|
64
|
+
def update_version_command(
|
|
65
|
+
project_name: str,
|
|
66
|
+
divbase_base_url: str,
|
|
67
|
+
version_name: str,
|
|
68
|
+
new_name: str | None,
|
|
69
|
+
new_description: str | None,
|
|
70
|
+
) -> UpdateVersionResponse:
|
|
71
|
+
"""Update the name and/or description of an existing project version entry."""
|
|
72
|
+
request_data = UpdateVersionRequest(
|
|
73
|
+
version_name=version_name,
|
|
74
|
+
new_name=new_name,
|
|
75
|
+
new_description=new_description,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
response = make_authenticated_request(
|
|
79
|
+
method="PATCH",
|
|
80
|
+
divbase_base_url=divbase_base_url,
|
|
81
|
+
api_route=f"v1/project-versions/update?project_name={project_name}",
|
|
82
|
+
json=request_data.model_dump(),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return UpdateVersionResponse(**response.json())
|
|
86
|
+
|
|
87
|
+
|
|
62
88
|
def delete_version_command(project_name: str, divbase_base_url: str, version_name: str) -> DeleteVersionResponse:
|
|
63
89
|
"""
|
|
64
90
|
Delete a version from the project versions table stored on the divbase server.
|
divbase_cli/services/s3_files.py
CHANGED
|
@@ -3,10 +3,12 @@ Service layer for DivBase CLI S3 file operations.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import httpx
|
|
9
10
|
import typer
|
|
11
|
+
from rich import print
|
|
10
12
|
|
|
11
13
|
from divbase_cli.cli_exceptions import (
|
|
12
14
|
FileDoesNotExistInSpecifiedVersionError,
|
|
@@ -32,6 +34,7 @@ from divbase_lib.api_schemas.s3 import (
|
|
|
32
34
|
PreSignedDownloadResponse,
|
|
33
35
|
PreSignedSinglePartUploadResponse,
|
|
34
36
|
RestoreObjectsResponse,
|
|
37
|
+
SoftDeletedObjectDetails,
|
|
35
38
|
)
|
|
36
39
|
from divbase_lib.divbase_constants import (
|
|
37
40
|
MAX_S3_API_BATCH_SIZE,
|
|
@@ -46,6 +49,20 @@ from divbase_lib.s3_checksums import (
|
|
|
46
49
|
)
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
@dataclass
|
|
53
|
+
class ToDownload:
|
|
54
|
+
"""
|
|
55
|
+
Represent a file to be download in the download-all command. This unifies the 2 ways we get the files needed:
|
|
56
|
+
1. From the latest files in the bucket (listing files)
|
|
57
|
+
2. From a user defined project version (getting the files from the version details)"
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
etag: str
|
|
62
|
+
size_bytes: int
|
|
63
|
+
version_id: str | None = None # latest version if None
|
|
64
|
+
|
|
65
|
+
|
|
49
66
|
def list_files_command(
|
|
50
67
|
divbase_base_url: str,
|
|
51
68
|
project_name: str,
|
|
@@ -98,6 +115,17 @@ def list_files_command(
|
|
|
98
115
|
return all_matches
|
|
99
116
|
|
|
100
117
|
|
|
118
|
+
def list_soft_deleted_files_command(divbase_base_url: str, project_name: str) -> list[SoftDeletedObjectDetails]:
|
|
119
|
+
"""List all soft-deleted files in a project."""
|
|
120
|
+
api_route = f"v1/s3/list/soft-deleted?project_name={project_name}"
|
|
121
|
+
response = make_authenticated_request(
|
|
122
|
+
method="GET",
|
|
123
|
+
divbase_base_url=divbase_base_url,
|
|
124
|
+
api_route=api_route,
|
|
125
|
+
)
|
|
126
|
+
return [SoftDeletedObjectDetails(**obj) for obj in response.json()]
|
|
127
|
+
|
|
128
|
+
|
|
101
129
|
def get_file_info_command(divbase_base_url: str, project_name: str, object_name: str) -> ObjectInfoResponse:
|
|
102
130
|
"""Get detailed information about a specific file/object in a project."""
|
|
103
131
|
response = make_authenticated_request(
|
|
@@ -113,13 +141,17 @@ def soft_delete_objects_command(divbase_base_url: str, project_name: str, all_fi
|
|
|
113
141
|
Soft delete objects from the project's bucket.
|
|
114
142
|
Returns a list of the soft deleted objects
|
|
115
143
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
deleted_objects = []
|
|
145
|
+
for i in range(0, len(all_files), MAX_S3_API_BATCH_SIZE):
|
|
146
|
+
batch_files = all_files[i : i + MAX_S3_API_BATCH_SIZE]
|
|
147
|
+
response = make_authenticated_request(
|
|
148
|
+
method="DELETE",
|
|
149
|
+
divbase_base_url=divbase_base_url,
|
|
150
|
+
api_route=f"v1/s3/?project_name={project_name}",
|
|
151
|
+
json=batch_files,
|
|
152
|
+
)
|
|
153
|
+
deleted_objects.extend(response.json())
|
|
154
|
+
return deleted_objects
|
|
123
155
|
|
|
124
156
|
|
|
125
157
|
def restore_objects_command(divbase_base_url: str, project_name: str, all_files: list[str]) -> RestoreObjectsResponse:
|
|
@@ -127,13 +159,21 @@ def restore_objects_command(divbase_base_url: str, project_name: str, all_files:
|
|
|
127
159
|
Restore soft_deleted objects in the project's bucket.
|
|
128
160
|
Returns an object containing a list of the restored objects, and those that were not restored.
|
|
129
161
|
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
all_restored = []
|
|
163
|
+
all_not_restored = []
|
|
164
|
+
for i in range(0, len(all_files), MAX_S3_API_BATCH_SIZE):
|
|
165
|
+
batch_files = all_files[i : i + MAX_S3_API_BATCH_SIZE]
|
|
166
|
+
response = make_authenticated_request(
|
|
167
|
+
method="POST",
|
|
168
|
+
divbase_base_url=divbase_base_url,
|
|
169
|
+
api_route=f"v1/s3/restore?project_name={project_name}",
|
|
170
|
+
json=batch_files,
|
|
171
|
+
)
|
|
172
|
+
batch_response = RestoreObjectsResponse(**response.json())
|
|
173
|
+
all_restored.extend(batch_response.restored)
|
|
174
|
+
all_not_restored.extend(batch_response.not_restored)
|
|
175
|
+
|
|
176
|
+
return RestoreObjectsResponse(restored=all_restored, not_restored=all_not_restored)
|
|
137
177
|
|
|
138
178
|
|
|
139
179
|
def download_files_command(
|
|
@@ -142,6 +182,7 @@ def download_files_command(
|
|
|
142
182
|
raw_files_input: list[str],
|
|
143
183
|
download_dir: Path,
|
|
144
184
|
verify_checksums: bool,
|
|
185
|
+
dry_run: bool,
|
|
145
186
|
project_version: str | None = None,
|
|
146
187
|
) -> DownloadOutcome:
|
|
147
188
|
"""
|
|
@@ -177,9 +218,11 @@ def download_files_command(
|
|
|
177
218
|
project_version=project_version,
|
|
178
219
|
missing_files=missing_objects,
|
|
179
220
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
|
|
222
|
+
json_data = []
|
|
223
|
+
for file_name, file_details in project_version_details.files.items():
|
|
224
|
+
if file_name in raw_files_input:
|
|
225
|
+
json_data.append({"name": file_name, "version_id": file_details["version_id"]})
|
|
183
226
|
else:
|
|
184
227
|
# parse raw file inputs to see if any specific version ids were provided using format:
|
|
185
228
|
# file_name:version_id (not possible when using project_version)
|
|
@@ -191,6 +234,16 @@ def download_files_command(
|
|
|
191
234
|
else:
|
|
192
235
|
json_data.append({"name": file_input, "version_id": None})
|
|
193
236
|
|
|
237
|
+
if dry_run:
|
|
238
|
+
print("\n[green bold]Dry run enabled, The following files would have been downloaded:[/green bold]")
|
|
239
|
+
for file_info in json_data:
|
|
240
|
+
version_id = file_info["version_id"]
|
|
241
|
+
if version_id:
|
|
242
|
+
print(f"- '{file_info['name']}' (version_id: '{version_id}')")
|
|
243
|
+
else:
|
|
244
|
+
print(f"- '{file_info['name']}' (latest version)")
|
|
245
|
+
raise typer.Exit(0)
|
|
246
|
+
|
|
194
247
|
successful_downloads, failed_downloads = [], []
|
|
195
248
|
for i in range(0, len(json_data), MAX_S3_API_BATCH_SIZE):
|
|
196
249
|
batch_json_data = json_data[i : i + MAX_S3_API_BATCH_SIZE]
|
|
@@ -340,16 +393,50 @@ def compare_local_to_s3_checksums(project_name: str, divbase_base_url: str, all_
|
|
|
340
393
|
server_checksums = {item.object_name: item.md5_checksum for item in server_checksum_responses}
|
|
341
394
|
|
|
342
395
|
for file in batch_files:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
calculated_checksum = calculate_md5_checksum(file_path=file, output_format=MD5CheckSumFormat.HEX)
|
|
347
|
-
|
|
348
|
-
local_checksums[file.name] = calculated_checksum
|
|
349
|
-
if server_checksums.get(file.name) and server_checksums[file.name] == calculated_checksum:
|
|
350
|
-
already_uploaded_files[file] = calculated_checksum
|
|
396
|
+
local_checksums[file.name] = _calc_local_checksum(file_path=file)
|
|
397
|
+
if server_checksums.get(file.name) and server_checksums[file.name] == local_checksums[file.name]:
|
|
398
|
+
already_uploaded_files[file] = local_checksums[file.name]
|
|
351
399
|
|
|
352
400
|
if already_uploaded_files:
|
|
353
401
|
raise FilesAlreadyInProjectError(existing_files=already_uploaded_files, project_name=project_name)
|
|
354
402
|
|
|
355
403
|
return local_checksums
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def filter_out_already_downloaded_files(
|
|
407
|
+
all_files: list[ToDownload], download_dir: Path
|
|
408
|
+
) -> tuple[list[ToDownload], list[ToDownload]]:
|
|
409
|
+
"""
|
|
410
|
+
Filter out files that already exist in a local directory with the same checksum.
|
|
411
|
+
|
|
412
|
+
Returns two lists:
|
|
413
|
+
1. Files that do not exist locally and need to be downloaded.
|
|
414
|
+
2. Files that are not identical to the file in S3 and will be overwritten if the user wants to download them.
|
|
415
|
+
"""
|
|
416
|
+
files_to_download, files_to_overwrite = [], []
|
|
417
|
+
|
|
418
|
+
for s3_file in all_files:
|
|
419
|
+
local_file_path = download_dir / s3_file.name
|
|
420
|
+
|
|
421
|
+
if not local_file_path.exists():
|
|
422
|
+
files_to_download.append(s3_file)
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
local_checksum = _calc_local_checksum(file_path=local_file_path)
|
|
426
|
+
if local_checksum != s3_file.etag:
|
|
427
|
+
files_to_overwrite.append(s3_file)
|
|
428
|
+
|
|
429
|
+
return files_to_download, files_to_overwrite
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _calc_local_checksum(file_path: Path) -> str:
|
|
433
|
+
"""
|
|
434
|
+
Calculate the checksum for a local file. Handles whether to use a single or composite checksum based on file size.
|
|
435
|
+
|
|
436
|
+
This is used to validate the integrity of a file before upload.
|
|
437
|
+
Or determine if a file can be skipped from downloading.
|
|
438
|
+
"""
|
|
439
|
+
if file_path.stat().st_size > S3_MULTIPART_UPLOAD_THRESHOLD:
|
|
440
|
+
return calculate_composite_md5_s3_etag(file_path=file_path)
|
|
441
|
+
else:
|
|
442
|
+
return calculate_md5_checksum(file_path=file_path, output_format=MD5CheckSumFormat.HEX)
|
divbase_cli/user_auth.py
CHANGED
|
@@ -7,6 +7,7 @@ This includes login/logout and the getting, storing, using, and refreshing of ac
|
|
|
7
7
|
import time
|
|
8
8
|
import warnings
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from json import JSONDecodeError
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
|
|
12
13
|
import httpx
|
|
@@ -14,11 +15,13 @@ import stamina
|
|
|
14
15
|
import yaml
|
|
15
16
|
from pydantic import SecretStr
|
|
16
17
|
|
|
18
|
+
from divbase_cli import __version__ as cli_version
|
|
17
19
|
from divbase_cli.cli_config import cli_settings
|
|
18
20
|
from divbase_cli.cli_exceptions import AuthenticationError, DivBaseAPIConnectionError, DivBaseAPIError
|
|
19
21
|
from divbase_cli.retries import retry_only_on_retryable_divbase_api_errors
|
|
20
22
|
from divbase_cli.user_config import load_user_config
|
|
21
23
|
from divbase_lib.api_schemas.auth import LogoutRequest
|
|
24
|
+
from divbase_lib.divbase_constants import CLI_VERSION_HEADER_KEY
|
|
22
25
|
|
|
23
26
|
LOGIN_AGAIN_MESSAGE = "Your session has expired. Please log in again with 'divbase-cli auth login [EMAIL]'."
|
|
24
27
|
|
|
@@ -78,31 +81,55 @@ def check_existing_session(divbase_url: str, config) -> int | None:
|
|
|
78
81
|
return token_data.refresh_token_expires_at
|
|
79
82
|
|
|
80
83
|
|
|
84
|
+
def _handle_divbase_api_error(response: httpx.Response, http_method: str, url: str) -> None:
|
|
85
|
+
"""Handles custom display of a HTTP error response returned by DivBase API."""
|
|
86
|
+
try:
|
|
87
|
+
response_body = response.json()
|
|
88
|
+
error_details = response_body.get("detail", "No error message provided.")
|
|
89
|
+
error_type = response_body.get("type", "unknown")
|
|
90
|
+
except (JSONDecodeError, ValueError):
|
|
91
|
+
# most likely situation for this would be if the reverse proxy returns an error, not the server itself.
|
|
92
|
+
error_details = response.text
|
|
93
|
+
error_type = "unexpected_server_error"
|
|
94
|
+
|
|
95
|
+
raise DivBaseAPIError(
|
|
96
|
+
error_details=error_details,
|
|
97
|
+
status_code=response.status_code,
|
|
98
|
+
error_type=error_type,
|
|
99
|
+
http_method=http_method,
|
|
100
|
+
url=url,
|
|
101
|
+
) from None
|
|
102
|
+
|
|
103
|
+
|
|
81
104
|
@stamina.retry(on=retry_only_on_retryable_divbase_api_errors, attempts=3)
|
|
82
105
|
def login_to_divbase(email: str, password: SecretStr, divbase_url: str) -> None:
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
"""
|
|
106
|
+
"""Log in to the DivBase server and return user tokens."""
|
|
107
|
+
login_url = f"{divbase_url}/v1/auth/login"
|
|
86
108
|
try:
|
|
87
109
|
response = httpx.post(
|
|
88
|
-
|
|
110
|
+
url=login_url,
|
|
89
111
|
data={
|
|
90
112
|
"grant_type": "password",
|
|
91
113
|
"username": email, # OAuth2 uses 'username', not 'email'
|
|
92
114
|
"password": password.get_secret_value(),
|
|
93
115
|
},
|
|
94
|
-
headers={
|
|
116
|
+
headers={
|
|
117
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
118
|
+
CLI_VERSION_HEADER_KEY: cli_version,
|
|
119
|
+
},
|
|
95
120
|
)
|
|
96
121
|
except httpx.ConnectError:
|
|
97
122
|
# We don't raise the full error as it contains the password in the stack trace.
|
|
98
123
|
# a user could unknowingly dump this into e.g. a bug report/GitHub issue.
|
|
99
124
|
raise DivBaseAPIConnectionError() from None
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
try:
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
except httpx.HTTPStatusError:
|
|
129
|
+
if response.status_code == 401:
|
|
130
|
+
error_message = response.json().get("detail", "Invalid email or password.")
|
|
131
|
+
raise AuthenticationError(error_message) from None
|
|
132
|
+
_handle_divbase_api_error(response=response, http_method="POST", url=login_url)
|
|
106
133
|
|
|
107
134
|
data = response.json()
|
|
108
135
|
token_data = TokenData(
|
|
@@ -164,9 +191,7 @@ def logout_of_divbase(token_path: Path = cli_settings.TOKENS_PATH) -> None:
|
|
|
164
191
|
|
|
165
192
|
|
|
166
193
|
def load_user_tokens(token_path: Path = cli_settings.TOKENS_PATH) -> TokenData:
|
|
167
|
-
"""
|
|
168
|
-
Load user tokens from the specified path.
|
|
169
|
-
"""
|
|
194
|
+
"""Load user tokens from the specified path."""
|
|
170
195
|
if not token_path.exists():
|
|
171
196
|
raise AuthenticationError(
|
|
172
197
|
f"Your access tokens were not found at {token_path}. Please check you are logged in first."
|
|
@@ -191,9 +216,7 @@ def make_authenticated_request(
|
|
|
191
216
|
token_path: Path = cli_settings.TOKENS_PATH,
|
|
192
217
|
**kwargs,
|
|
193
218
|
) -> httpx.Response:
|
|
194
|
-
"""
|
|
195
|
-
Make an authenticated request to the DivBase server, handles refreshing tokens if needed.
|
|
196
|
-
"""
|
|
219
|
+
"""Make an authenticated request to the DivBase server, handles refreshing tokens if needed."""
|
|
197
220
|
token_data = load_user_tokens(token_path=token_path)
|
|
198
221
|
|
|
199
222
|
if token_data.is_access_token_expired():
|
|
@@ -207,27 +230,52 @@ def make_authenticated_request(
|
|
|
207
230
|
|
|
208
231
|
headers = kwargs.get("headers", {})
|
|
209
232
|
headers["Authorization"] = f"Bearer {token_data.access_token.get_secret_value()}"
|
|
233
|
+
headers[CLI_VERSION_HEADER_KEY] = cli_version
|
|
210
234
|
kwargs["headers"] = headers
|
|
211
235
|
|
|
212
236
|
url = f"{divbase_base_url}/{api_route.lstrip('/')}"
|
|
213
237
|
|
|
214
238
|
try:
|
|
215
|
-
response = httpx.request(method, url, **kwargs)
|
|
239
|
+
response = httpx.request(method=method, url=url, **kwargs)
|
|
216
240
|
except httpx.HTTPError as e:
|
|
217
241
|
raise DivBaseAPIConnectionError() from e
|
|
218
242
|
|
|
219
243
|
try:
|
|
220
244
|
response.raise_for_status()
|
|
221
245
|
except httpx.HTTPStatusError:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
_handle_divbase_api_error(response=response, http_method=method, url=url)
|
|
247
|
+
|
|
248
|
+
return response
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@stamina.retry(on=retry_only_on_retryable_divbase_api_errors, attempts=3)
|
|
252
|
+
def make_unauthenticated_request(
|
|
253
|
+
method: str,
|
|
254
|
+
divbase_base_url: str,
|
|
255
|
+
api_route: str,
|
|
256
|
+
token_path: Path = cli_settings.TOKENS_PATH,
|
|
257
|
+
**kwargs,
|
|
258
|
+
) -> httpx.Response:
|
|
259
|
+
"""
|
|
260
|
+
Make an unauthenticated request to the DivBase server.
|
|
261
|
+
Used for those few endpoints that require no authentication, even if the user is logged in.
|
|
262
|
+
E.g. the announcements endpoint.
|
|
263
|
+
"""
|
|
264
|
+
url = f"{divbase_base_url}/{api_route.lstrip('/')}"
|
|
265
|
+
|
|
266
|
+
headers = kwargs.get("headers", {})
|
|
267
|
+
headers[CLI_VERSION_HEADER_KEY] = cli_version
|
|
268
|
+
kwargs["headers"] = headers
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
response = httpx.request(method=method, url=url, **kwargs)
|
|
272
|
+
except httpx.HTTPError as e:
|
|
273
|
+
raise DivBaseAPIConnectionError() from e
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
response.raise_for_status()
|
|
277
|
+
except httpx.HTTPStatusError:
|
|
278
|
+
_handle_divbase_api_error(response=response, http_method=method, url=url)
|
|
231
279
|
|
|
232
280
|
return response
|
|
233
281
|
|
|
@@ -239,32 +287,29 @@ def _refresh_access_token(token_data: TokenData, divbase_base_url: str) -> Token
|
|
|
239
287
|
Returns the new TokenData object which can be used immediately in a new request.
|
|
240
288
|
NOTE: We do not need retry logic inside this function as the calling function has it.
|
|
241
289
|
"""
|
|
290
|
+
refresh_url = f"{divbase_base_url}/v1/auth/refresh"
|
|
242
291
|
try:
|
|
243
292
|
response = httpx.post(
|
|
244
|
-
url=
|
|
293
|
+
url=refresh_url,
|
|
245
294
|
json={"refresh_token": token_data.refresh_token.get_secret_value()},
|
|
295
|
+
headers={CLI_VERSION_HEADER_KEY: cli_version},
|
|
246
296
|
)
|
|
247
297
|
except httpx.HTTPError as e:
|
|
248
298
|
raise DivBaseAPIConnectionError() from e
|
|
249
299
|
|
|
250
|
-
# Possible if e.g. token revoked on server side.
|
|
251
|
-
if response.status_code == 401:
|
|
252
|
-
# Clear logged in status in user config as tokens no longer valid.
|
|
253
|
-
# Prevents user getting warning about being already logged in when they try to log in again.
|
|
254
|
-
config = load_user_config()
|
|
255
|
-
config.set_login_status(url=None, email=None)
|
|
256
|
-
raise AuthenticationError(LOGIN_AGAIN_MESSAGE)
|
|
257
|
-
|
|
258
300
|
try:
|
|
259
301
|
response.raise_for_status()
|
|
260
|
-
except httpx.HTTPStatusError
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
url=
|
|
267
|
-
|
|
302
|
+
except httpx.HTTPStatusError:
|
|
303
|
+
# Possible if e.g. token revoked on server side.
|
|
304
|
+
if response.status_code == 401:
|
|
305
|
+
# Clear logged in status in user config as tokens no longer valid.
|
|
306
|
+
# Prevents user getting warning about being already logged in when they try to log in again.
|
|
307
|
+
config = load_user_config()
|
|
308
|
+
config.set_login_status(url=None, email=None)
|
|
309
|
+
raise AuthenticationError(LOGIN_AGAIN_MESSAGE) from None
|
|
310
|
+
|
|
311
|
+
_handle_divbase_api_error(response=response, http_method="POST", url=refresh_url)
|
|
312
|
+
|
|
268
313
|
data = response.json()
|
|
269
314
|
|
|
270
315
|
new_token_data = TokenData(
|
divbase_cli/utils.py
CHANGED
|
@@ -6,25 +6,6 @@ import sys
|
|
|
6
6
|
from rich.table import Table
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def format_file_size(size_bytes: int | float | None) -> str:
|
|
10
|
-
"""
|
|
11
|
-
Converts a file size in bytes to a human-readable format.
|
|
12
|
-
|
|
13
|
-
Uses powers of 1000 so KB, MB, GB, TB and not 1024 KiB, MiB, GiB, TiB.
|
|
14
|
-
"""
|
|
15
|
-
if size_bytes is None:
|
|
16
|
-
return "N/A"
|
|
17
|
-
if size_bytes == 0:
|
|
18
|
-
return "0 B"
|
|
19
|
-
power = 1000
|
|
20
|
-
n = 0
|
|
21
|
-
power_labels = {0: "", 1: "K", 2: "M", 3: "G", 4: "T"}
|
|
22
|
-
while size_bytes >= power and n < len(power_labels) - 1:
|
|
23
|
-
size_bytes /= power
|
|
24
|
-
n += 1
|
|
25
|
-
return f"{size_bytes:.2f} {power_labels[n]}B"
|
|
26
|
-
|
|
27
|
-
|
|
28
9
|
def print_rich_table_as_tsv(table: Table) -> None:
|
|
29
10
|
"""
|
|
30
11
|
Helper function to print a rich Table as a TSV file to standard output.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: divbase-cli
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev4
|
|
4
4
|
Summary: Command Line Interface for Divbase
|
|
5
5
|
Project-URL: Homepage, https://divbase.scilifelab.se
|
|
6
6
|
Project-URL: Documentation, https://scilifelabdatacentre.github.io/divbase
|
|
@@ -18,10 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: divbase-lib==0.1.0.
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: stamina>=25.2.0
|
|
24
|
-
Requires-Dist: typer<1,>=0.21.1
|
|
21
|
+
Requires-Dist: divbase-lib==0.1.0.dev4
|
|
22
|
+
Requires-Dist: typer==0.24.1
|
|
25
23
|
Description-Content-Type: text/markdown
|
|
26
24
|
|
|
27
25
|
# divbase-cli
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
divbase_cli/__init__.py,sha256=SDVJzLSfn_nMXXB876-vk1-5mij6jnGStMY33khVIcY,27
|
|
2
|
+
divbase_cli/cli_config.py,sha256=Nx9elop_TCxqOdYimpnjP3iGaBlEZ6okyoWkhU4fTz4,2017
|
|
3
|
+
divbase_cli/cli_exceptions.py,sha256=MeSignmkzTidzlhRDdbUBHkbxD6qOWUYtTRUOfDVz0w,6501
|
|
4
|
+
divbase_cli/config_resolver.py,sha256=olnt5U4HtlYKcTS4zGDV1KyIgLcrOxyY4q87T7lx65U,3004
|
|
5
|
+
divbase_cli/display_task_history.py,sha256=k9V4YeWsqv6HTZn2Ns6sl4vngiWANFiPPj7E5kE0_K4,7628
|
|
6
|
+
divbase_cli/divbase_cli.py,sha256=JrUvPB925gJHroNXez4uSNehKHKfDCGMLzpyipHkGKY,2358
|
|
7
|
+
divbase_cli/retries.py,sha256=UFv4CHudC1Sb0kRe2HuuCidoCNA9TSXB-mOdrt4uDew,1298
|
|
8
|
+
divbase_cli/user_auth.py,sha256=6NHUow8DqJHmr1apmMIG1CJX86_-4JKcIv4nP2GiMB4,12151
|
|
9
|
+
divbase_cli/user_config.py,sha256=oROFuNZN5MoC01070ufZbVh8lwavFN2bTj6u4oy7MYE,6350
|
|
10
|
+
divbase_cli/utils.py,sha256=-D2x3uIiVDcO3YABrrHhWUszEFzc1kQIeXN69EUWS78,962
|
|
11
|
+
divbase_cli/cli_commands/__init__.py,sha256=K_2r8V1QGpEmxDcD2QOyWlXR4HPoc16yytmZwGkIyLw,166
|
|
12
|
+
divbase_cli/cli_commands/auth_cli.py,sha256=-remzGF4PrcE8frMXZ0OPqe3fwU1mvhd5FaF5rMlkDw,3349
|
|
13
|
+
divbase_cli/cli_commands/dimensions_cli.py,sha256=i4r9hjB1f7oXOcuftQrQqpqwSMUBZVf63nvnzsP6Gxs,17495
|
|
14
|
+
divbase_cli/cli_commands/file_cli.py,sha256=kqJb5enK4LpFSU8zohin1Yb6AD6EBgw1jEotVFj3cjA,25754
|
|
15
|
+
divbase_cli/cli_commands/query_cli.py,sha256=Y7luRYCAbzgQljhCosJ7YvVWkJSOewlPK9g3s1msEtc,5811
|
|
16
|
+
divbase_cli/cli_commands/shared_args_options.py,sha256=BGOo1tPjrCM3XOB5v3wAeRZJ8d8NvCPruqa_ZVReLPE,585
|
|
17
|
+
divbase_cli/cli_commands/task_history_cli.py,sha256=fBKyPzzsOBDf9DC-Uice00wy_q-QqMid0pBXcErrO7A,4453
|
|
18
|
+
divbase_cli/cli_commands/user_config_cli.py,sha256=WB1iPPuecEdYmUBxnMQYTT1E3LC5FzU62LF4WUp-lJE,5396
|
|
19
|
+
divbase_cli/cli_commands/version_cli.py,sha256=oC3NWjTrkq_axwXKrwlnmfXL0fkMPDGWCZ0ryaJ9Dy0,8818
|
|
20
|
+
divbase_cli/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
divbase_cli/services/announcements.py,sha256=EYXoHaAcZmzNm3ezgLVi81DQFn3qecZVlIuNHvpQq_c,1531
|
|
22
|
+
divbase_cli/services/pre_signed_urls.py,sha256=zZ4LRz4x2CB8D-hSfIMLBNoBLMejxtbJ_VH6EtteNKQ,16990
|
|
23
|
+
divbase_cli/services/project_versions.py,sha256=LwDjJCKPGJl1fAv5GSQ7aTOrEfrpzn3IVLwR-b10C9k,3628
|
|
24
|
+
divbase_cli/services/s3_files.py,sha256=nFx9NWn5ctg6XMJThJD5E8argUpnEtl-NT4y_enemPY,17734
|
|
25
|
+
divbase_cli-0.1.0.dev4.dist-info/METADATA,sha256=dmhtEjwfQ3ihbOrD55G9MTH-M88UYZJ4_m5PztqShxo,1355
|
|
26
|
+
divbase_cli-0.1.0.dev4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
27
|
+
divbase_cli-0.1.0.dev4.dist-info/entry_points.txt,sha256=vaRJvvGmfesTaMMCCy3kcBhYzf51wZoEPuLqas5LDMg,100
|
|
28
|
+
divbase_cli-0.1.0.dev4.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
divbase_cli/__init__.py,sha256=jQHG8OW4TlfIzPKW4IrX9q58EfEf9pDfm2YgO0ydKaA,27
|
|
2
|
-
divbase_cli/cli_config.py,sha256=0h40kFmWK03-Gi7qU9S_GtpGT5XpsZVHSxLH127Elkk,1810
|
|
3
|
-
divbase_cli/cli_exceptions.py,sha256=MeSignmkzTidzlhRDdbUBHkbxD6qOWUYtTRUOfDVz0w,6501
|
|
4
|
-
divbase_cli/config_resolver.py,sha256=olnt5U4HtlYKcTS4zGDV1KyIgLcrOxyY4q87T7lx65U,3004
|
|
5
|
-
divbase_cli/display_task_history.py,sha256=k9V4YeWsqv6HTZn2Ns6sl4vngiWANFiPPj7E5kE0_K4,7628
|
|
6
|
-
divbase_cli/divbase_cli.py,sha256=JrUvPB925gJHroNXez4uSNehKHKfDCGMLzpyipHkGKY,2358
|
|
7
|
-
divbase_cli/retries.py,sha256=UFv4CHudC1Sb0kRe2HuuCidoCNA9TSXB-mOdrt4uDew,1298
|
|
8
|
-
divbase_cli/user_auth.py,sha256=0CbwED1xL7TgVdA58_x6Vq4Tpvaq8PwE3Pw5LwrVy6I,10308
|
|
9
|
-
divbase_cli/user_config.py,sha256=oROFuNZN5MoC01070ufZbVh8lwavFN2bTj6u4oy7MYE,6350
|
|
10
|
-
divbase_cli/utils.py,sha256=H31UK32jeg21wJlKpFsPry6D1XJlpzOuDgOZIDanKsI,1512
|
|
11
|
-
divbase_cli/cli_commands/__init__.py,sha256=K_2r8V1QGpEmxDcD2QOyWlXR4HPoc16yytmZwGkIyLw,166
|
|
12
|
-
divbase_cli/cli_commands/auth_cli.py,sha256=IbuQGFvljvn8uezGFu8q0H9v62IKKzFsdpTwmehoriU,2871
|
|
13
|
-
divbase_cli/cli_commands/dimensions_cli.py,sha256=zSWTupp0fx-AGSobKlkSpNNGlv-o62EAe8yarVbmF0k,4875
|
|
14
|
-
divbase_cli/cli_commands/file_cli.py,sha256=6DIPilDtEQBYIoVy6wkjWOgSDh12cuqHqL5duHtVB7U,18772
|
|
15
|
-
divbase_cli/cli_commands/query_cli.py,sha256=Hty7BzrBTV0e8P0OCenlxJm-S7EO9LOscjFhOETjPvw,5551
|
|
16
|
-
divbase_cli/cli_commands/shared_args_options.py,sha256=NNFUp0wRPMUpdtmvgmF_e3loKWbUlSL5uuPCSpJMylk,548
|
|
17
|
-
divbase_cli/cli_commands/task_history_cli.py,sha256=fBKyPzzsOBDf9DC-Uice00wy_q-QqMid0pBXcErrO7A,4453
|
|
18
|
-
divbase_cli/cli_commands/user_config_cli.py,sha256=WB1iPPuecEdYmUBxnMQYTT1E3LC5FzU62LF4WUp-lJE,5396
|
|
19
|
-
divbase_cli/cli_commands/version_cli.py,sha256=UKT-t2MvFwFNRPgSldxTsFvwPiS8EM5vSerXRCmibsw,6073
|
|
20
|
-
divbase_cli/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
divbase_cli/services/pre_signed_urls.py,sha256=7evDg2aeWN609VJnlcGCqZdk7M2yg_6VKUsCYIAt0ME,16479
|
|
22
|
-
divbase_cli/services/project_versions.py,sha256=RzuexQ7UmcoeZTnV7LSU9AehHnDpVVqxKJ5G8t_njBM,2865
|
|
23
|
-
divbase_cli/services/s3_files.py,sha256=lURk4m7MyPBzDz0puQO72i8djC25WmIWzVf1oY9nJwA,14470
|
|
24
|
-
divbase_cli-0.1.0.dev3.dist-info/METADATA,sha256=whCWvjLzGCtDuTPOqE2pIsMyo9LCXPGcEWT8LZO8VYk,1421
|
|
25
|
-
divbase_cli-0.1.0.dev3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
-
divbase_cli-0.1.0.dev3.dist-info/entry_points.txt,sha256=vaRJvvGmfesTaMMCCy3kcBhYzf51wZoEPuLqas5LDMg,100
|
|
27
|
-
divbase_cli-0.1.0.dev3.dist-info/RECORD,,
|
|
File without changes
|