Coverage for apio/managers/downloader.py: 88%
33 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +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 # -- Store the url
46 self._url = url
48 # -- Get the file from the url
49 # -- Ex: 'apio-examples-0.0.35.zip'
50 self.fname = url.split("/")[-1]
52 # -- Build the destination path
53 self.destination: Path = Path(self.fname)
54 if dest_dir: 54 ↛ 60line 54 didn't jump to line 60 because the condition on line 54 was always true
56 # -- Add the path
57 self.destination = dest_dir / self.fname
59 # -- Request the file
60 self._request = requests.get(url, stream=True, timeout=TIMEOUT_SECS)
62 # -- Raise an exception in case of download error...
63 if self._request.status_code != 200: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 cout(
65 "Got an unexpected HTTP status code: "
66 f"{self._request.status_code}",
67 f"When downloading {url}",
68 style=ERROR,
69 )
70 raise util.ApioException()
72 def get_size(self) -> int:
73 """Return the size (in bytes) of the latest bytes block received"""
75 return int(self._request.headers["content-length"])
77 def start(self):
78 """Start the downloading of the file"""
80 # -- Download iterator
81 itercontent = self._request.iter_content(chunk_size=self.CHUNK_SIZE)
83 # -- Open destination file, for writing bytes
84 with open(self.destination, "wb") as file:
86 # -- Get the file length in Kbytes
87 num_chunks = int(ceil(self.get_size() / float(self.CHUNK_SIZE)))
89 # -- Download and write the chunks, while displaying the progress.
90 for _ in track(
91 range(num_chunks),
92 description="Downloading",
93 console=console(),
94 ):
96 file.write(next(itercontent))
98 # -- Check that the iterator reached its end. When the end is
99 # -- reached, next() returns the default value None.
100 assert next(itercontent, None) is None
102 # -- Download done!
103 self._request.close()
105 def __del__(self):
106 """Close any pending request"""
108 if self._request: 108 ↛ exitline 108 didn't return from function '__del__' because the condition on line 108 was always true
109 self._request.close()