Skip to content

Commit 935473e

Browse files
committed
Add progress bar support for file uploads
Introduced _ProgressFileWrapper to wrap file-like objects and update a rich progress bar during file uploads. Improved error handling in upload retries and added an assertion for file handler presence. Refactored code for clarity and consistency.
1 parent d191eab commit 935473e

1 file changed

Lines changed: 53 additions & 13 deletions

File tree

dvuploader/nativeupload.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import asyncio
2-
from io import BytesIO
3-
from pathlib import Path
4-
import httpx
52
import json
63
import os
74
import tempfile
5+
from io import BytesIO
6+
from pathlib import Path
7+
from typing import IO, AsyncGenerator, Dict, List, Optional, Tuple
8+
9+
import httpx
810
import rich
911
import tenacity
10-
from typing import List, Optional, Tuple, Dict
11-
1212
from rich.progress import Progress, TaskID
1313

1414
from dvuploader.file import File
@@ -55,6 +55,36 @@
5555
ZIP_LIMIT_MESSAGE = "The number of files in the zip archive is over the limit"
5656

5757

58+
class _ProgressFileWrapper:
59+
"""
60+
Wrap a binary file-like object and update a rich progress bar on reads.
61+
httpx's multipart expects a synchronous file-like object exposing .read().
62+
"""
63+
64+
def __init__(
65+
self,
66+
file: IO[bytes],
67+
progress: Progress,
68+
pbar: TaskID,
69+
chunk_size: int = 1024 * 1024,
70+
):
71+
self._file = file
72+
self._progress = progress
73+
self._pbar = pbar
74+
self._chunk_size = chunk_size
75+
76+
def read(self, size: int = -1) -> bytes:
77+
if size is None or size < 0:
78+
size = self._chunk_size
79+
data = self._file.read(size)
80+
if data:
81+
self._progress.update(self._pbar, advance=len(data))
82+
return data
83+
84+
def __getattr__(self, name):
85+
return getattr(self._file, name)
86+
87+
5888
async def native_upload(
5989
files: List[File],
6090
dataverse_url: str,
@@ -92,8 +122,12 @@ async def native_upload(
92122
}
93123

94124
files_new = [file for file in files if not file.to_replace]
95-
files_new_metadata = [file for file in files if file.to_replace and file._unchanged_data]
96-
files_replace = [file for file in files if file.to_replace and not file._unchanged_data]
125+
files_new_metadata = [
126+
file for file in files if file.to_replace and file._unchanged_data
127+
]
128+
files_replace = [
129+
file for file in files if file.to_replace and not file._unchanged_data
130+
]
97131

98132
# These are not in a package but need a metadtata update, ensure even for zips
99133
for file in files_new_metadata:
@@ -114,7 +148,7 @@ async def native_upload(
114148
file.file_name, # type: ignore
115149
total=file._size,
116150
),
117-
file
151+
file,
118152
)
119153
for file in files_replace
120154
]
@@ -233,7 +267,9 @@ def _reset_progress(
233267
@tenacity.retry(
234268
wait=RETRY_STRAT,
235269
stop=tenacity.stop_after_attempt(MAX_RETRIES),
236-
retry=tenacity.retry_if_exception_type((httpx.HTTPStatusError,)),
270+
retry=tenacity.retry_if_exception_type(
271+
(httpx.HTTPStatusError, httpx.ReadError, httpx.RequestError)
272+
),
237273
)
238274
async def _single_native_upload(
239275
session: httpx.AsyncClient,
@@ -270,10 +306,12 @@ async def _single_native_upload(
270306

271307
json_data = _get_json_data(file)
272308

309+
assert file.handler is not None, "File handler is required for native upload"
310+
273311
files = {
274312
"file": (
275313
file.file_name,
276-
file.handler,
314+
_ProgressFileWrapper(file.handler, progress, pbar), # type: ignore[arg-type]
277315
file.mimeType,
278316
),
279317
"jsonData": (
@@ -285,7 +323,7 @@ async def _single_native_upload(
285323

286324
response = await session.post(
287325
endpoint,
288-
files=files, # type: ignore
326+
files=files,
289327
)
290328

291329
if response.status_code == 400 and response.json()["message"].startswith(
@@ -374,8 +412,10 @@ async def _update_metadata(
374412
if _tab_extension(dv_path) in file_mapping:
375413
file_id = file_mapping[_tab_extension(dv_path)]
376414
elif (
377-
file.file_name and _is_zip(file.file_name)
378-
and not file._is_inside_zip and not file._enforce_metadata_update
415+
file.file_name
416+
and _is_zip(file.file_name)
417+
and not file._is_inside_zip
418+
and not file._enforce_metadata_update
379419
):
380420
# When the file is a zip package it will be unpacked and thus
381421
# the expected file name of the zip will not be in the

0 commit comments

Comments
 (0)