Skip to content

Commit 95202a3

Browse files
committed
Show deployment uploading progress
1 parent dbeacda commit 95202a3

File tree

2 files changed

+70
-10
lines changed

2 files changed

+70
-10
lines changed

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from rich.text import Text
1818
from rich_toolkit import RichToolkit
1919
from rich_toolkit.menu import Option
20+
from rich_toolkit.progress import Progress
2021

2122
from fastapi_cloud_cli.commands.login import login
2223
from fastapi_cloud_cli.utils.api import (
@@ -29,6 +30,7 @@
2930
from fastapi_cloud_cli.utils.apps import AppConfig, get_app_config, write_app_config
3031
from fastapi_cloud_cli.utils.auth import Identity
3132
from fastapi_cloud_cli.utils.cli import get_rich_toolkit, handle_http_errors
33+
from fastapi_cloud_cli.utils.progress_file import ProgressFile
3234

3335
logger = logging.getLogger(__name__)
3436

@@ -201,16 +203,32 @@ class RequestUploadResponse(BaseModel):
201203
fields: dict[str, str]
202204

203205

204-
def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
206+
def _format_size(size_in_bytes: int) -> str:
207+
if size_in_bytes >= 1024 * 1024:
208+
return f"{size_in_bytes / (1024 * 1024):.2f} MB"
209+
elif size_in_bytes >= 1024:
210+
return f"{size_in_bytes / 1024:.2f} KB"
211+
else:
212+
return f"{size_in_bytes} bytes"
213+
214+
215+
def _upload_deployment(
216+
deployment_id: str, archive_path: Path, progress: Progress
217+
) -> None:
218+
archive_size = archive_path.stat().st_size
219+
archive_size_str = _format_size(archive_size)
220+
221+
progress.log(f"Uploading deployment ({archive_size_str})...")
205222
logger.debug(
206223
"Starting deployment upload for deployment: %s",
207224
deployment_id,
208225
)
209-
logger.debug(
210-
"Archive path: %s, size: %s bytes",
211-
archive_path,
212-
archive_path.stat().st_size,
213-
)
226+
logger.debug("Archive path: %s, size: %s bytes", archive_path, archive_size)
227+
228+
def progress_callback(bytes_read: int):
229+
progress.log(
230+
f"Uploading deployment ({_format_size(bytes_read)} of {archive_size_str})..."
231+
)
214232

215233
with APIClient() as fastapi_client, Client() as client:
216234
# Get the upload URL
@@ -223,10 +241,13 @@ def _upload_deployment(deployment_id: str, archive_path: Path) -> None:
223241

224242
logger.debug("Starting file upload to S3")
225243
with open(archive_path, "rb") as archive_file:
244+
archive_file_with_progress = ProgressFile(
245+
archive_file, progress_callback=progress_callback
246+
)
226247
upload_response = client.post(
227248
upload_data.url,
228249
data=upload_data.fields,
229-
files={"file": archive_file},
250+
files={"file": archive_file_with_progress},
230251
)
231252

232253
upload_response.raise_for_status()
@@ -767,9 +788,7 @@ def deploy(
767788
f"Deployment created successfully! Deployment slug: {deployment.slug}"
768789
)
769790

770-
progress.log("Uploading deployment...")
771-
772-
_upload_deployment(deployment.id, archive_path)
791+
_upload_deployment(deployment.id, archive_path, progress=progress)
773792

774793
progress.log("Deployment uploaded successfully!")
775794
except KeyboardInterrupt:
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from collections.abc import Callable
2+
from datetime import datetime
3+
from typing import BinaryIO
4+
5+
6+
class ProgressFile:
7+
"""Wraps a file object to track read progress."""
8+
9+
def __init__(
10+
self,
11+
file: BinaryIO,
12+
progress_callback: Callable[[int], None],
13+
update_interval: float = 0.5,
14+
):
15+
self._file = file
16+
self._progress_callback = progress_callback
17+
self._update_interval = update_interval
18+
self._last_update_time = 0.0
19+
self._bytes_read = 0
20+
21+
def read(self, n=-1):
22+
data = self._file.read(n)
23+
self._bytes_read += len(data)
24+
now_ = datetime.now().timestamp()
25+
if now_ - self._last_update_time >= self._update_interval:
26+
self._progress_callback(self._bytes_read)
27+
self._last_update_time = now_
28+
return data
29+
30+
def __iter__(self):
31+
return self._file.__iter__()
32+
33+
@property
34+
def name(self):
35+
return self._file.name
36+
37+
def seek(self, offset: int, whence: int = 0, /):
38+
return self._file.seek(offset, whence)
39+
40+
def tell(self):
41+
return self._file.tell()

0 commit comments

Comments
 (0)