|
12 | 12 |
|
13 | 13 | from collections.abc import Mapping |
14 | 14 | from contextlib import contextmanager |
| 15 | +from contextlib import suppress |
| 16 | +from functools import cached_property |
15 | 17 | from pathlib import Path |
16 | 18 | from typing import TYPE_CHECKING |
17 | 19 | from typing import Any |
18 | 20 | from typing import overload |
19 | 21 |
|
| 22 | +import requests |
| 23 | + |
20 | 24 | from requests.utils import atomic_open |
21 | 25 |
|
22 | 26 | from poetry.utils.constants import REQUESTS_TIMEOUT |
@@ -100,43 +104,61 @@ def download_file( |
100 | 104 | session: Authenticator | Session | None = None, |
101 | 105 | chunk_size: int = 1024, |
102 | 106 | ) -> None: |
103 | | - import requests |
104 | | - |
105 | 107 | from poetry.puzzle.provider import Indicator |
106 | 108 |
|
107 | | - get = requests.get if not session else session.get |
108 | | - |
109 | | - response = get(url, stream=True, timeout=REQUESTS_TIMEOUT) |
110 | | - response.raise_for_status() |
| 109 | + downloader = Downloader(url, dest, session) |
111 | 110 |
|
112 | 111 | set_indicator = False |
113 | 112 | with Indicator.context() as update_context: |
114 | 113 | update_context(f"Downloading {url}") |
115 | 114 |
|
116 | | - if "Content-Length" in response.headers: |
117 | | - try: |
118 | | - total_size = int(response.headers["Content-Length"]) |
119 | | - except ValueError: |
120 | | - total_size = 0 |
121 | | - |
| 115 | + total_size = downloader.total_size |
| 116 | + if total_size > 0: |
122 | 117 | fetched_size = 0 |
123 | 118 | last_percent = 0 |
124 | 119 |
|
125 | 120 | # if less than 1MB, we simply show that we're downloading |
126 | 121 | # but skip the updating |
127 | 122 | set_indicator = total_size > 1024 * 1024 |
128 | 123 |
|
129 | | - with atomic_open(dest) as f: |
130 | | - for chunk in response.iter_content(chunk_size=chunk_size): |
| 124 | + for fetched_size in downloader.download_with_progress(chunk_size): |
| 125 | + if set_indicator: |
| 126 | + percent = (fetched_size * 100) // total_size |
| 127 | + if percent > last_percent: |
| 128 | + last_percent = percent |
| 129 | + update_context(f"Downloading {url} {percent:3}%") |
| 130 | + |
| 131 | + |
| 132 | +class Downloader: |
| 133 | + def __init__( |
| 134 | + self, |
| 135 | + url: str, |
| 136 | + dest: Path, |
| 137 | + session: Authenticator | Session | None = None, |
| 138 | + ): |
| 139 | + self._dest = dest |
| 140 | + |
| 141 | + get = requests.get if not session else session.get |
| 142 | + |
| 143 | + self._response = get(url, stream=True, timeout=REQUESTS_TIMEOUT) |
| 144 | + self._response.raise_for_status() |
| 145 | + |
| 146 | + @cached_property |
| 147 | + def total_size(self) -> int: |
| 148 | + total_size = 0 |
| 149 | + if "Content-Length" in self._response.headers: |
| 150 | + with suppress(ValueError): |
| 151 | + total_size = int(self._response.headers["Content-Length"]) |
| 152 | + return total_size |
| 153 | + |
| 154 | + def download_with_progress(self, chunk_size: int = 1024) -> Iterator[int]: |
| 155 | + fetched_size = 0 |
| 156 | + with atomic_open(self._dest) as f: |
| 157 | + for chunk in self._response.iter_content(chunk_size=chunk_size): |
131 | 158 | if chunk: |
132 | 159 | f.write(chunk) |
133 | | - |
134 | | - if set_indicator: |
135 | | - fetched_size += len(chunk) |
136 | | - percent = (fetched_size * 100) // total_size |
137 | | - if percent > last_percent: |
138 | | - last_percent = percent |
139 | | - update_context(f"Downloading {url} {percent:3}%") |
| 160 | + fetched_size += len(chunk) |
| 161 | + yield fetched_size |
140 | 162 |
|
141 | 163 |
|
142 | 164 | def get_package_version_display_string( |
|
0 commit comments