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.
@@ -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.
@@ -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
- response = make_authenticated_request(
117
- method="DELETE",
118
- divbase_base_url=divbase_base_url,
119
- api_route=f"v1/s3/?project_name={project_name}",
120
- json=all_files,
121
- )
122
- return response.json()
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
- response = make_authenticated_request(
131
- method="POST",
132
- divbase_base_url=divbase_base_url,
133
- api_route=f"v1/s3/restore?project_name={project_name}",
134
- json=all_files,
135
- )
136
- return RestoreObjectsResponse(**response.json())
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
- # we validated in CLI command that
181
- to_download = {file: project_version_details.files[file] for file in raw_files_input}
182
- json_data = [{"name": obj, "version_id": to_download[obj]} for obj in raw_files_input]
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
- if file.stat().st_size > S3_MULTIPART_UPLOAD_THRESHOLD:
344
- calculated_checksum = calculate_composite_md5_s3_etag(file_path=file)
345
- else:
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
- Log in to the DivBase server and return user tokens.
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
- f"{divbase_url}/v1/auth/login",
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={"Content-Type": "application/x-www-form-urlencoded"},
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
- if response.status_code == 401:
102
- error_message = response.json().get("detail", "Invalid email or password.")
103
- raise AuthenticationError(error_message)
104
-
105
- response.raise_for_status()
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
- error_details = response.json().get("detail", "No error details provided")
223
- error_type = response.json().get("type", "unknown")
224
- raise DivBaseAPIError(
225
- error_details=error_details,
226
- status_code=response.status_code,
227
- error_type=error_type,
228
- http_method=method,
229
- url=url,
230
- ) from None
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=f"{divbase_base_url}/v1/auth/refresh",
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 as e:
261
- raise DivBaseAPIError(
262
- error_details=response.json().get("detail", "No error details provided"),
263
- status_code=response.status_code,
264
- error_type=response.json().get("type", "unknown"),
265
- http_method="POST",
266
- url=f"{divbase_base_url}/v1/auth/refresh",
267
- ) from e
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.dev3
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.dev3
22
- Requires-Dist: httpx<1,>=0.28.1
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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,