It's a great example of how to use semaphore, asyncio and aiohttp to download files much faster.
importasyncioimportaiohttpimporttimefromaiohttpimportClientSessionfromtypingimportDict,Optionalfromtqdmimporttqdmimportrandomasyncdefdownload_chunk(session:ClientSession,url:str,start:int,stop:int,headers:Dict[str,str],buffer:bytearray,progress_bar:tqdm,retries:int=3,):"""Download a specific chunk of the file and write it to the correct position in the buffer."""# Make a local copy of headers so that each call has its own header dictlocal_headers=headers.copy()local_headers.update({"Range":f"bytes={start}-{stop}"})attempt=0whileattempt<retries:try:# print(f"Downloading chunk {start}-{stop}")asyncwithsession.get(url,headers=local_headers)asresponse:ifresponse.status!=206:# 206 Partial Content is expectedraiseException(f"Failed to download chunk {start}-{stop}: HTTP {response.status}")content=awaitresponse.read()# Write the downloaded content into the buffer at the correct offsetbuffer[start:start+len(content)]=content# Update progress bar by the number of bytes expected for this chunk.# (Note: the final chunk might be a bit smaller, but that's fine.)progress_bar.update(stop-start+1)return# Successful download; exit the loopexceptExceptionase:print(f"Error downloading chunk {start}-{stop}: {e}")attempt+=1ifattempt<retries:wait_time=random.uniform(1,3)print(f"Retrying chunk {start}-{stop} in {wait_time:.2f} seconds...")awaitasyncio.sleep(wait_time)else:print(f"Failed to download chunk {start}-{stop} after {retries} retries.")raiseeasyncdefdownload_file(url:str,filename:str,chunk_size:int,max_connections:int,headers:Optional[Dict[str,str]]=None,):"""Download a file in parallel chunks using asyncio and aiohttp, storing data in a preallocated bytearray."""headers=headersor{}# Get total file size (handling redirects if necessary)asyncwithaiohttp.ClientSession()assession:asyncwithsession.head(url,headers=headers)asresponse:ifresponse.status==302:location=response.headers.get("Location")iflocation:# print(f"Redirecting to {location}")url=locationasyncwithsession.head(url,headers=headers)asnew_response:ifnew_response.status!=200:raiseException(f"Failed to get file info: HTTP {new_response.status}")content_length=int(new_response.headers.get("Content-Length",0))else:raiseException(f"Failed to get file info: HTTP {response.status} - No Location header found.")elifresponse.status==200:content_length=int(response.headers.get("Content-Length",0))else:raiseException(f"Failed to get file info: HTTP {response.status}")print(f"Total file size: {content_length} bytes")# Preallocate a bytearray for the filebuffer=bytearray(content_length)withtqdm(total=content_length,unit="B",unit_scale=True,desc=filename)asprogress_bar:tasks=[]semaphore=asyncio.Semaphore(max_connections)asyncwithaiohttp.ClientSession()assession:forstartinrange(0,content_length,chunk_size):stop=min(start+chunk_size-1,content_length-1)# print(f"Chunk {start}-{stop} will be downloaded.")# Capture start and stop in the local scope of the task.asyncdeflimited_download(start=start,stop=stop):asyncwithsemaphore:awaitdownload_chunk(session,url,start,stop,headers,buffer,progress_bar)tasks.append(asyncio.create_task(limited_download()))awaitasyncio.gather(*tasks)# After downloading all chunks, write the complete buffer to file.withopen(filename,"wb")asf:f.write(buffer)# Example usage:if__name__=="__main__":url="https://huggingface.co/microsoft/OmniParser-v2.0/resolve/main/icon_caption/model.safetensors"filename="model-byte-range.safetensors"chunk_size=1024*1024*1# 1 MB chunksmax_connections=16# Limit parallel connectionsprint(f"Downloading with {max_connections} connections and chunk size of {chunk_size} bytes")start_time=time.time()asyncio.run(download_file(url,filename,chunk_size,max_connections))end_time=time.time()elapsed_time=end_time-start_timeprint(f"Download completed in {elapsed_time:.2f} seconds.")