earthscope-cli 1.1.1__tar.gz → 1.2.0__tar.gz
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.
- {earthscope_cli-1.1.1/src/earthscope_cli.egg-info → earthscope_cli-1.2.0}/PKG-INFO +9 -6
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/README.md +1 -1
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/pyproject.toml +9 -6
- earthscope_cli-1.2.0/src/earthscope_cli/__init__.py +1 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/common.py +1 -4
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/debug.py +1 -5
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/dropoff/render.py +167 -25
- earthscope_cli-1.2.0/src/earthscope_cli/dropoff/typer.py +311 -0
- earthscope_cli-1.2.0/src/earthscope_cli/dropoff/util.py +210 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/login.py +1 -4
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/main.py +1 -4
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/user.py +1 -5
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0/src/earthscope_cli.egg-info}/PKG-INFO +9 -6
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli.egg-info/requires.txt +3 -2
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/tests/test_dropoff.py +623 -307
- earthscope_cli-1.1.1/src/earthscope_cli/__init__.py +0 -1
- earthscope_cli-1.1.1/src/earthscope_cli/dropoff/typer.py +0 -205
- earthscope_cli-1.1.1/src/earthscope_cli/dropoff/util.py +0 -152
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/LICENSE +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/setup.cfg +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/setup.py +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli/util.py +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli.egg-info/SOURCES.txt +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli.egg-info/dependency_links.txt +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli.egg-info/entry_points.txt +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/src/earthscope_cli.egg-info/top_level.txt +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/tests/test_cli.py +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/tests/test_login.py +0 -0
- {earthscope_cli-1.1.1 → earthscope_cli-1.2.0}/tests/test_user.py +0 -0
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: earthscope-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A CLI for the EarthScope API
|
|
5
5
|
Author-email: EarthScope <data-help@earthscope.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
|
-
Project-URL: Homepage, https://
|
|
7
|
+
Project-URL: Homepage, https://docs.earthscope.org/projects/CLI
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/earthscope/public/earthscope-cli
|
|
9
|
+
Project-URL: Changelog, https://docs.earthscope.org/projects/CLI/page/changelog.html
|
|
8
10
|
Classifier: Programming Language :: Python
|
|
9
11
|
Classifier: Programming Language :: Python :: 3
|
|
10
12
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.10
|
|
12
14
|
Description-Content-Type: text/markdown
|
|
13
15
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: typer
|
|
16
|
+
Requires-Dist: typer<0.24.0
|
|
15
17
|
Requires-Dist: typer-di>=0.1.3
|
|
16
|
-
Requires-Dist: earthscope-sdk>=1.
|
|
18
|
+
Requires-Dist: earthscope-sdk>=1.4.0
|
|
17
19
|
Requires-Dist: rich>=13.8.0
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: black; extra == "dev"
|
|
20
22
|
Requires-Dist: bumpver; extra == "dev"
|
|
21
23
|
Requires-Dist: build; extra == "dev"
|
|
24
|
+
Requires-Dist: pip>=22.0; extra == "dev"
|
|
22
25
|
Requires-Dist: pre-commit; extra == "dev"
|
|
23
26
|
Requires-Dist: pytest; extra == "dev"
|
|
24
27
|
Requires-Dist: twine>=6.0.1; extra == "dev"
|
|
@@ -36,7 +39,7 @@ A CLI for interacting with EarthScope's APIs
|
|
|
36
39
|
To use the CLI you must have:
|
|
37
40
|
|
|
38
41
|
- Registered an account with Earthscope ([sign up now](https://earthscope.org/user/login)). See [here](https://www.earthscope.org/data/authentication) for more information
|
|
39
|
-
- Python >= 3.
|
|
42
|
+
- Python >= 3.10
|
|
40
43
|
|
|
41
44
|
### Installation
|
|
42
45
|
|
|
@@ -9,7 +9,7 @@ A CLI for interacting with EarthScope's APIs
|
|
|
9
9
|
To use the CLI you must have:
|
|
10
10
|
|
|
11
11
|
- Registered an account with Earthscope ([sign up now](https://earthscope.org/user/login)). See [here](https://www.earthscope.org/data/authentication) for more information
|
|
12
|
-
- Python >= 3.
|
|
12
|
+
- Python >= 3.10
|
|
13
13
|
|
|
14
14
|
### Installation
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "earthscope-cli"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "A CLI for the EarthScope API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "EarthScope", email = "data-help@earthscope.org" }]
|
|
@@ -18,18 +18,19 @@ classifiers = [
|
|
|
18
18
|
|
|
19
19
|
#Suggestion not to pin dependencies since the package should work in many different python environments
|
|
20
20
|
dependencies = [
|
|
21
|
-
"typer
|
|
21
|
+
"typer<0.24.0",
|
|
22
22
|
"typer-di>=0.1.3",
|
|
23
|
-
"earthscope-sdk>=1.
|
|
23
|
+
"earthscope-sdk>=1.4.0",
|
|
24
24
|
"rich>=13.8.0",
|
|
25
25
|
]
|
|
26
|
-
requires-python = ">=3.
|
|
26
|
+
requires-python = ">=3.10"
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
29
|
dev = [
|
|
30
30
|
"black",
|
|
31
31
|
"bumpver",
|
|
32
32
|
"build",
|
|
33
|
+
"pip>=22.0",
|
|
33
34
|
"pre-commit",
|
|
34
35
|
"pytest",
|
|
35
36
|
"twine>=6.0.1",
|
|
@@ -37,13 +38,15 @@ dev = [
|
|
|
37
38
|
]
|
|
38
39
|
|
|
39
40
|
[project.urls]
|
|
40
|
-
Homepage = "https://
|
|
41
|
+
Homepage = "https://docs.earthscope.org/projects/CLI"
|
|
42
|
+
Repository = "https://gitlab.com/earthscope/public/earthscope-cli"
|
|
43
|
+
Changelog = "https://docs.earthscope.org/projects/CLI/page/changelog.html"
|
|
41
44
|
|
|
42
45
|
[project.scripts]
|
|
43
46
|
es = "earthscope_cli.main:app"
|
|
44
47
|
|
|
45
48
|
[tool.bumpver]
|
|
46
|
-
current_version = "1.
|
|
49
|
+
current_version = "1.2.0"
|
|
47
50
|
version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]"
|
|
48
51
|
commit_message = "chore: bump version {old_version} -> {new_version}"
|
|
49
52
|
commit = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.0"
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
from contextlib import suppress
|
|
2
1
|
from typing import Annotated, Optional
|
|
3
2
|
|
|
4
3
|
import typer
|
|
4
|
+
from rich import print
|
|
5
5
|
from typer_di import Depends
|
|
6
6
|
|
|
7
|
-
with suppress(ModuleNotFoundError):
|
|
8
|
-
from rich import print
|
|
9
|
-
|
|
10
7
|
GLOBAL_HELP_PANEL = "Global Options"
|
|
11
8
|
|
|
12
9
|
|
|
@@ -2,7 +2,7 @@ import threading
|
|
|
2
2
|
from typing import Dict, List, Optional
|
|
3
3
|
|
|
4
4
|
from earthscope_sdk.client.dropoff._multipart_uploader import UploadStatus
|
|
5
|
-
from earthscope_sdk.client.dropoff.models import DropoffObject
|
|
5
|
+
from earthscope_sdk.client.dropoff.models import DropoffObject, DropoffSummary
|
|
6
6
|
from rich import print
|
|
7
7
|
from rich.console import Group
|
|
8
8
|
from rich.live import Live
|
|
@@ -45,12 +45,25 @@ def _colorize_status(status: str) -> Text:
|
|
|
45
45
|
return Text(f"{status or '-'}", style="dim")
|
|
46
46
|
|
|
47
47
|
|
|
48
|
+
def _format_details(details: List[str], is_truncated: bool) -> Text:
|
|
49
|
+
"""Format structured_messages (with truncation indicator) or fall back to status_message."""
|
|
50
|
+
if details:
|
|
51
|
+
content = "\n".join(details)
|
|
52
|
+
result = Text(content)
|
|
53
|
+
if is_truncated:
|
|
54
|
+
result.append("\n(truncated)", style="dim")
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
return Text("-", style="dim")
|
|
58
|
+
|
|
59
|
+
|
|
48
60
|
def print_dropoff_objects_table(
|
|
49
61
|
objects: List[DropoffObject],
|
|
50
62
|
*,
|
|
51
63
|
title: str,
|
|
52
64
|
has_next: bool,
|
|
53
65
|
next_offset: int,
|
|
66
|
+
show_details: bool = False,
|
|
54
67
|
):
|
|
55
68
|
if has_next:
|
|
56
69
|
caption = f"Use --offset {next_offset} to view more results."
|
|
@@ -69,15 +82,52 @@ def print_dropoff_objects_table(
|
|
|
69
82
|
table.add_column("Last Modified", justify="right")
|
|
70
83
|
table.add_column("Status", justify="center")
|
|
71
84
|
table.add_column("Message")
|
|
85
|
+
if show_details:
|
|
86
|
+
table.add_column("Details")
|
|
87
|
+
for o in objects:
|
|
88
|
+
table.add_row(
|
|
89
|
+
safe_str(o.path),
|
|
90
|
+
filesize.decimal(o.size),
|
|
91
|
+
safe_str(o.received_at),
|
|
92
|
+
_colorize_status(o.status),
|
|
93
|
+
safe_str(o.status_message),
|
|
94
|
+
_format_details(
|
|
95
|
+
o.structured_messages, o.structured_messages_were_truncated
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
for o in objects:
|
|
100
|
+
table.add_row(
|
|
101
|
+
safe_str(o.path),
|
|
102
|
+
filesize.decimal(o.size),
|
|
103
|
+
safe_str(o.received_at),
|
|
104
|
+
_colorize_status(o.status),
|
|
105
|
+
safe_str(o.status_message),
|
|
106
|
+
)
|
|
107
|
+
print()
|
|
108
|
+
print(table)
|
|
109
|
+
print()
|
|
72
110
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
111
|
+
|
|
112
|
+
def print_dropoff_summary(summary: DropoffSummary, *, prefix: Optional[str] = None):
|
|
113
|
+
if not summary.status_counts:
|
|
114
|
+
print(f"[yellow]No objects found in prefix '{prefix}'")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
title = "Dropoff Summary"
|
|
118
|
+
if prefix:
|
|
119
|
+
title += f" for {prefix}"
|
|
120
|
+
|
|
121
|
+
table = Table(
|
|
122
|
+
title=title,
|
|
123
|
+
show_header=True,
|
|
124
|
+
title_style="bold magenta",
|
|
125
|
+
header_style="bold magenta",
|
|
126
|
+
)
|
|
127
|
+
table.add_column("Status")
|
|
128
|
+
table.add_column("Count")
|
|
129
|
+
for status in summary.status_counts:
|
|
130
|
+
table.add_row(status.status, str(status.count))
|
|
81
131
|
|
|
82
132
|
print()
|
|
83
133
|
print(table)
|
|
@@ -181,6 +231,23 @@ class CompositeProgressBarColumn(ProgressColumn):
|
|
|
181
231
|
return result
|
|
182
232
|
|
|
183
233
|
|
|
234
|
+
class UploadSummary:
|
|
235
|
+
def __init__(self, total: int, succeeded: int):
|
|
236
|
+
self.total = total
|
|
237
|
+
self.succeeded = succeeded
|
|
238
|
+
self.failed = total - succeeded
|
|
239
|
+
|
|
240
|
+
def print(self):
|
|
241
|
+
color = (
|
|
242
|
+
"green"
|
|
243
|
+
if self.failed == 0
|
|
244
|
+
else ("red" if self.succeeded == 0 else "yellow")
|
|
245
|
+
)
|
|
246
|
+
print(
|
|
247
|
+
f"[{color}]Summary: {self.succeeded}/{self.total} files succeeded, {self.failed} failed"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
184
251
|
class ConcurrentUploadProgressDisplay:
|
|
185
252
|
"""
|
|
186
253
|
Rich progress display for concurrent file uploads.
|
|
@@ -194,10 +261,10 @@ class ConcurrentUploadProgressDisplay:
|
|
|
194
261
|
hidden to keep the display clean and focused on active uploads.
|
|
195
262
|
|
|
196
263
|
Usage:
|
|
197
|
-
display = ConcurrentUploadProgressDisplay(
|
|
264
|
+
display = ConcurrentUploadProgressDisplay(num_collected=10)
|
|
198
265
|
with display:
|
|
199
|
-
sdk.dropoff.
|
|
200
|
-
|
|
266
|
+
sdk.dropoff.put_objects(
|
|
267
|
+
objects=validated_files_generator(),
|
|
201
268
|
category="miniseed",
|
|
202
269
|
progress_cb=display.callback,
|
|
203
270
|
)
|
|
@@ -205,7 +272,7 @@ class ConcurrentUploadProgressDisplay:
|
|
|
205
272
|
|
|
206
273
|
def __init__(
|
|
207
274
|
self,
|
|
208
|
-
|
|
275
|
+
num_collected: int,
|
|
209
276
|
hide_completed_threshold: int = 10,
|
|
210
277
|
hide_delay_seconds: float = 1.5,
|
|
211
278
|
):
|
|
@@ -213,17 +280,29 @@ class ConcurrentUploadProgressDisplay:
|
|
|
213
280
|
Initialize the progress display.
|
|
214
281
|
|
|
215
282
|
Args:
|
|
216
|
-
|
|
283
|
+
num_collected: Total number of files collected (to be validated then uploaded)
|
|
217
284
|
hide_completed_threshold: Threshold for hiding completed progress bars
|
|
218
285
|
hide_delay_seconds: Delay in seconds before hiding completed progress bars
|
|
219
286
|
"""
|
|
220
287
|
self.hide_completed_threshold = hide_completed_threshold
|
|
221
288
|
self.hide_delay_seconds = hide_delay_seconds
|
|
222
|
-
self.
|
|
289
|
+
self.num_collected = num_collected
|
|
290
|
+
self.num_valid = 0
|
|
291
|
+
self.num_invalid = 0
|
|
292
|
+
self.validation_complete = False
|
|
223
293
|
self.files_completed = 0
|
|
224
294
|
self.upload_started = False # Track if we've received any bytes yet
|
|
225
295
|
|
|
226
|
-
#
|
|
296
|
+
# Validation progress
|
|
297
|
+
self.validation_progress = Progress(
|
|
298
|
+
TextColumn("[bold blue]{task.description}"),
|
|
299
|
+
BarColumn(),
|
|
300
|
+
MofNCompleteColumn(),
|
|
301
|
+
TextColumn("•"),
|
|
302
|
+
TimeElapsedColumn(),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Overall upload progress (files completed, plus aggregate throughput)
|
|
227
306
|
self.overall_progress = Progress(
|
|
228
307
|
TextColumn("[bold blue]{task.description}"),
|
|
229
308
|
BarColumn(),
|
|
@@ -249,6 +328,7 @@ class ConcurrentUploadProgressDisplay:
|
|
|
249
328
|
self.file_tasks: Dict[str, TaskID] = {}
|
|
250
329
|
self.file_bytes_done: Dict[str, int] = {} # Track current bytes_done per file
|
|
251
330
|
self.file_bytes_resumed: Dict[str, int] = {} # Track bytes resumed per file
|
|
331
|
+
self.validation_task: Optional[TaskID] = None
|
|
252
332
|
self.overall_task: Optional[TaskID] = None
|
|
253
333
|
self.completed_files: set[str] = set()
|
|
254
334
|
self.live: Optional[Live] = None
|
|
@@ -256,12 +336,17 @@ class ConcurrentUploadProgressDisplay:
|
|
|
256
336
|
|
|
257
337
|
def __enter__(self):
|
|
258
338
|
"""Start the live progress display."""
|
|
259
|
-
#
|
|
260
|
-
|
|
261
|
-
|
|
339
|
+
# Validation progress task
|
|
340
|
+
self.validation_task = self.validation_progress.add_task(
|
|
341
|
+
"[bold]Validating...",
|
|
342
|
+
total=self.num_collected,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Upload progress task — total starts at num_collected, decremented as files fail validation
|
|
346
|
+
# Don't start the timer yet — wait for first byte update
|
|
262
347
|
self.overall_task = self.overall_progress.add_task(
|
|
263
|
-
|
|
264
|
-
total=self.
|
|
348
|
+
"[bold]Uploading...",
|
|
349
|
+
total=self.num_collected,
|
|
265
350
|
visual_bytes=0,
|
|
266
351
|
throughput_bytes=0,
|
|
267
352
|
start=False,
|
|
@@ -269,6 +354,7 @@ class ConcurrentUploadProgressDisplay:
|
|
|
269
354
|
|
|
270
355
|
# Create progress group
|
|
271
356
|
progress_group = Group(
|
|
357
|
+
self.validation_progress,
|
|
272
358
|
self.overall_progress,
|
|
273
359
|
self.file_progress,
|
|
274
360
|
)
|
|
@@ -287,10 +373,10 @@ class ConcurrentUploadProgressDisplay:
|
|
|
287
373
|
|
|
288
374
|
if self.live:
|
|
289
375
|
# Update final status
|
|
290
|
-
if exc_type is None and self.overall_task is not None:
|
|
376
|
+
if exc_type is None and self.overall_task is not None and self.num_valid > 0:
|
|
291
377
|
self.overall_progress.update(
|
|
292
378
|
self.overall_task,
|
|
293
|
-
description=f"[bold green]✓ Completed {self.
|
|
379
|
+
description=f"[bold green]✓ Completed {self.num_valid} uploads",
|
|
294
380
|
)
|
|
295
381
|
|
|
296
382
|
# Hide all individual file progress bars when complete
|
|
@@ -301,6 +387,62 @@ class ConcurrentUploadProgressDisplay:
|
|
|
301
387
|
self.live.__exit__(exc_type, exc_val, exc_tb)
|
|
302
388
|
return False
|
|
303
389
|
|
|
390
|
+
def validation_passed(self):
|
|
391
|
+
"""Called when a file passes validation and will be uploaded."""
|
|
392
|
+
self.num_valid += 1
|
|
393
|
+
if self.validation_task is not None:
|
|
394
|
+
self.validation_progress.advance(self.validation_task, 1)
|
|
395
|
+
|
|
396
|
+
def validation_failed(self):
|
|
397
|
+
"""Called when a file fails validation and will not be uploaded."""
|
|
398
|
+
self.num_invalid += 1
|
|
399
|
+
if self.validation_task is not None:
|
|
400
|
+
self.validation_progress.advance(self.validation_task, 1)
|
|
401
|
+
self.validation_progress.update(
|
|
402
|
+
self.validation_task,
|
|
403
|
+
description=f"[bold blue]Validating... [dim]({self.num_invalid} failed)",
|
|
404
|
+
)
|
|
405
|
+
# Decrement upload total since this file won't be uploaded
|
|
406
|
+
if self.overall_task is not None:
|
|
407
|
+
self.overall_progress.update(
|
|
408
|
+
self.overall_task,
|
|
409
|
+
total=self.num_collected - self.num_invalid,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def finalize_validation(self):
|
|
413
|
+
"""Called when all validation is complete."""
|
|
414
|
+
self.validation_complete = True
|
|
415
|
+
if self.validation_task is not None:
|
|
416
|
+
self.validation_progress.stop_task(self.validation_task)
|
|
417
|
+
if self.num_invalid == 0:
|
|
418
|
+
self.validation_progress.update(
|
|
419
|
+
self.validation_task,
|
|
420
|
+
description=f"[bold green]✓ Validated {self.num_valid} files",
|
|
421
|
+
)
|
|
422
|
+
elif self.num_valid > 0:
|
|
423
|
+
self.validation_progress.update(
|
|
424
|
+
self.validation_task,
|
|
425
|
+
description=f"[bold yellow]✓ Validated {self.num_valid}/{self.num_collected} files ({self.num_invalid} failed)",
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
self.validation_progress.update(
|
|
429
|
+
self.validation_task,
|
|
430
|
+
description=f"[bold red]✗ Validation failed for all {self.num_invalid} files",
|
|
431
|
+
)
|
|
432
|
+
# Update upload description with final count
|
|
433
|
+
if self.overall_task is not None and self.num_valid > 0:
|
|
434
|
+
self.overall_progress.update(
|
|
435
|
+
self.overall_task,
|
|
436
|
+
description=f"[bold]Uploading {self.num_valid} files...",
|
|
437
|
+
)
|
|
438
|
+
# Check if all uploads already completed
|
|
439
|
+
if (
|
|
440
|
+
self.overall_task is not None
|
|
441
|
+
and self.num_valid > 0
|
|
442
|
+
and len(self.completed_files) == self.num_valid
|
|
443
|
+
):
|
|
444
|
+
self.overall_progress.stop_task(self.overall_task)
|
|
445
|
+
|
|
304
446
|
def _hide_completed_task(self, task_id: TaskID, key: str):
|
|
305
447
|
"""
|
|
306
448
|
Hide a completed task from the display.
|
|
@@ -384,11 +526,11 @@ class ConcurrentUploadProgressDisplay:
|
|
|
384
526
|
self.overall_progress.advance(self.overall_task, 1)
|
|
385
527
|
|
|
386
528
|
# If all files are complete, stop the timer to freeze throughput
|
|
387
|
-
if len(self.completed_files) == self.
|
|
529
|
+
if self.validation_complete and len(self.completed_files) == self.num_valid:
|
|
388
530
|
self.overall_progress.stop_task(self.overall_task)
|
|
389
531
|
|
|
390
532
|
# For large uploads, hide completed progress bars after a delay
|
|
391
|
-
if self.
|
|
533
|
+
if self.num_collected > self.hide_completed_threshold:
|
|
392
534
|
# Schedule hiding the task after a delay
|
|
393
535
|
timer = threading.Timer(
|
|
394
536
|
self.hide_delay_seconds,
|