11import asyncio
2- from io import BytesIO
3- from pathlib import Path
4- import httpx
52import json
63import os
74import 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
810import rich
911import tenacity
10- from typing import List , Optional , Tuple , Dict
11-
1212from rich .progress import Progress , TaskID
1313
1414from dvuploader .file import File
5555ZIP_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+
5888async 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)
238274async 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