1"""Lazy ZIP over HTTP"""
3__all__ = [
"HTTPRangeRequestUnsupported",
"dist_from_wheel_url"]
5from bisect
import bisect_left, bisect_right
6from contextlib
import contextmanager
7from tempfile
import NamedTemporaryFile
8from typing
import Any, Dict, Generator, List, Optional, Tuple
9from zipfile
import BadZipFile, ZipFile
23def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
24 """Return a distribution object from the given wheel URL.
26 This uses HTTP range requests to only fetch the portion of the wheel
27 containing metadata, just enough for the object to be constructed.
28 If such requests are not supported, HTTPRangeRequestUnsupported
31 with LazyZipOverHTTP(url, session)
as zf:
34 wheel = MemoryWheel(
zf.name, zf)
37 return get_wheel_distribution(wheel, canonicalize_name(name))
41 """File-like object mapped to a ZIP file over HTTP.
43 This uses HTTP range requests to lazily fetch the file's content,
44 which is supposed to be fed to ZipFile. If such requests are not
45 supported by the server, raise HTTPRangeRequestUnsupported
46 during initialization.
50 self, url: str, session: PipSession, chunk_size: int = CONTENT_CHUNK_SIZE
53 raise_for_status(head)
57 self.
_file = NamedTemporaryFile()
59 self._left: List[int] = []
60 self._right: List[int] = []
67 """Opening mode, which is always rb."""
71 def name(self) -> str:
72 """Path to the underlying file."""
73 return self.
_file.name
76 """Return whether random access is supported, which is True."""
85 """Whether the file is closed."""
86 return self.
_file.closed
88 def read(self, size: int = -1) -> bytes:
89 """Read up to size bytes from the object and return them.
91 As a convenience, if size is unspecified or -1,
92 all bytes until EOF are returned. Fewer than
93 size bytes may be returned if EOF is reached.
97 stop = length
if size < 0
else min(start + download_size, length)
98 start = max(0, stop - download_size)
103 """Return whether the file is readable, which is True."""
106 def seek(self, offset: int, whence: int = 0) -> int:
107 """Change stream position and return the new absolute position.
109 Seek to offset relative position indicated by whence:
110 * 0: Start of stream (the default). pos should be >= 0;
111 * 1: Current position - pos may be negative;
112 * 2: End of stream - pos usually negative.
117 """Return the current position."""
120 def truncate(self, size: Optional[int] =
None) -> int:
121 """Resize the stream to the given size in bytes.
123 If size is unspecified resize to the current position.
124 The current stream position isn't changed.
126 Return the new file size.
142 def _stay(self) -> Generator[None, None, None]:
143 """Return a context manager keeping the position.
145 At the end of the block, seek back to original position.
154 """Check and download until the file is a valid ZIP."""
169 self, start: int, end: int, base_headers: Dict[str, str] = HEADERS
171 """Return HTTP response to a range request from start to end."""
173 headers[
"Range"] = f
"bytes={start}-{end}"
175 headers[
"Cache-Control"] =
"no-cache"
176 return self.
_session.get(self.
_url, headers=headers, stream=
True)
179 self, start: int, end: int, left: int, right: int
180 ) -> Generator[Tuple[int, int],
None,
None]:
181 """Return a generator of intervals to be fetched.
184 start (int): Start of needed interval
185 end (int): End of needed interval
186 left (int): Index of first overlapping downloaded data
187 right (int): Index after last overlapping downloaded data
189 lslice, rslice = self._left[left:right], self._right[left:right]
190 i = start = min([start] + lslice[:1])
191 end = max([end] + rslice[-1:])
192 for j, k
in zip(lslice, rslice):
198 self._left[left:right], self._right[left:right] = [start], [end]
201 """Download bytes from start to end inclusively."""
203 left = bisect_left(self._right, start)
204 right = bisect_right(self._left, end)
205 for start, end
in self.
_merge(start, end, left, right):
209 for chunk
in response_chunks(response, self.
_chunk_size):
210 self.
_file.write(chunk)
bytes read(self, int size=-1)
None __exit__(self, *Any exc)
int truncate(self, Optional[int] size=None)
Generator[None, None, None] _stay(self)
int seek(self, int offset, int whence=0)
None _download(self, int start, int end)
"LazyZipOverHTTP" __enter__(self)
Generator[Tuple[int, int], None, None] _merge(self, int start, int end, int left, int right)
Response _stream_response(self, int start, int end, Dict[str, str] base_headers=HEADERS)
None __init__(self, str url, PipSession session, int chunk_size=CONTENT_CHUNK_SIZE)