Coverage for apio/managers/downloader.py: 93%
35 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
1# -*- coding: utf-8 -*-
2# -- This file is part of the Apio project
3# -- (C) 2016-2019 FPGAwars
4# -- Author Jesús Arroyo
5# -- License GPLv2
6# -- Derived from:
7# ---- Platformio project
8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com>
9# ---- License Apache v2
10"""Implement a remote file downloader. Used to fetch packages from github
11packages release repositories.
12"""
14# TODO: capture all the exceptions and return them as method return status.
15# Motivation is simplifying the usage.
17from math import ceil
18from pathlib import Path
19import requests
20from rich.progress import track
21from apio.utils import util
22from apio.common.apio_console import cout, console
23from apio.common.apio_styles import ERROR
26# -- Timeout for getting a response from the server when downloading
27# -- a file (in seconds). We had github tests failing with timeout=10
28TIMEOUT_SECS = 30
31class FileDownloader:
32 """Class for downloading files"""
34 CHUNK_SIZE = 1024
36 def __init__(self, url: str, dest_dir=None):
37 """Initialize a FileDownloader object
38 * INPUTs:
39 * url: File to download (full url)
40 (Ex. 'https://github.com/FPGAwars/apio-examples/
41 releases/download/0.0.35/apio-examples-0.0.35.zip')
42 * dest_dir: Destination folder (where to download the file)
43 """
45 # -- Initialize the request field first, so that __del__ works even
46 # -- if the construction fails below, e.g. on a connection timeout.
47 self._request = None
49 # -- Store the url
50 self._url = url
52 # -- Get the file from the url
53 # -- Ex: 'apio-examples-0.0.35.zip'
54 self.fname = url.split("/")[-1]
56 # -- Build the destination path
57 self.destination: Path = Path(self.fname)
58 if dest_dir:
60 # -- Add the path
61 self.destination = dest_dir / self.fname
63 # -- Request the file
64 self._request = requests.get(url, stream=True, timeout=TIMEOUT_SECS)
66 # -- Raise an exception in case of download error...
67 if self._request.status_code != 200: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 cout(
69 "Got an unexpected HTTP status code: "
70 f"{self._request.status_code}",
71 f"When downloading {url}",
72 style=ERROR,
73 )
74 raise util.ApioException()
76 def get_size(self) -> int:
77 """Return the size (in bytes) of the latest bytes block received"""
79 return int(self._request.headers["content-length"])
81 def start(self):
82 """Start the downloading of the file"""
84 # -- Download iterator
85 itercontent = self._request.iter_content(chunk_size=self.CHUNK_SIZE)
87 # -- Open destination file, for writing bytes
88 with open(self.destination, "wb") as file:
90 # -- Get the file length in Kbytes
91 num_chunks = int(ceil(self.get_size() / float(self.CHUNK_SIZE)))
93 # -- Download and write the chunks, while displaying the progress.
94 for _ in track(
95 range(num_chunks),
96 description="Downloading",
97 console=console(),
98 ):
100 file.write(next(itercontent))
102 # -- Check that the iterator reached its end. When the end is
103 # -- reached, next() returns the default value None.
104 assert next(itercontent, None) is None
106 # -- Download done!
107 self._request.close()
109 def __del__(self):
110 """Close any pending request"""
112 # -- Using getattr() since __del__ can run on a partially constructed
113 # -- object, and an exception here would surface as an 'unraisable
114 # -- exception' at an arbitrary point of the program. Comparing to
115 # -- None rather than testing truthiness since bool(Response) is
116 # -- Response.ok, which is False for non-2xx error responses.
117 request = getattr(self, "_request", None)
118 if request is not None:
119 request.close()