596 lines
20 KiB
Python
596 lines
20 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
import io
|
||
|
import typing as t
|
||
|
from functools import partial
|
||
|
from functools import update_wrapper
|
||
|
|
||
|
from .exceptions import ClientDisconnected
|
||
|
from .exceptions import RequestEntityTooLarge
|
||
|
from .sansio import utils as _sansio_utils
|
||
|
from .sansio.utils import host_is_trusted # noqa: F401 # Imported as part of API
|
||
|
|
||
|
if t.TYPE_CHECKING:
|
||
|
from _typeshed.wsgi import WSGIApplication
|
||
|
from _typeshed.wsgi import WSGIEnvironment
|
||
|
|
||
|
|
||
|
def responder(f: t.Callable[..., WSGIApplication]) -> WSGIApplication:
|
||
|
"""Marks a function as responder. Decorate a function with it and it
|
||
|
will automatically call the return value as WSGI application.
|
||
|
|
||
|
Example::
|
||
|
|
||
|
@responder
|
||
|
def application(environ, start_response):
|
||
|
return Response('Hello World!')
|
||
|
"""
|
||
|
return update_wrapper(lambda *a: f(*a)(*a[-2:]), f)
|
||
|
|
||
|
|
||
|
def get_current_url(
|
||
|
environ: WSGIEnvironment,
|
||
|
root_only: bool = False,
|
||
|
strip_querystring: bool = False,
|
||
|
host_only: bool = False,
|
||
|
trusted_hosts: t.Iterable[str] | None = None,
|
||
|
) -> str:
|
||
|
"""Recreate the URL for a request from the parts in a WSGI
|
||
|
environment.
|
||
|
|
||
|
The URL is an IRI, not a URI, so it may contain Unicode characters.
|
||
|
Use :func:`~werkzeug.urls.iri_to_uri` to convert it to ASCII.
|
||
|
|
||
|
:param environ: The WSGI environment to get the URL parts from.
|
||
|
:param root_only: Only build the root path, don't include the
|
||
|
remaining path or query string.
|
||
|
:param strip_querystring: Don't include the query string.
|
||
|
:param host_only: Only build the scheme and host.
|
||
|
:param trusted_hosts: A list of trusted host names to validate the
|
||
|
host against.
|
||
|
"""
|
||
|
parts = {
|
||
|
"scheme": environ["wsgi.url_scheme"],
|
||
|
"host": get_host(environ, trusted_hosts),
|
||
|
}
|
||
|
|
||
|
if not host_only:
|
||
|
parts["root_path"] = environ.get("SCRIPT_NAME", "")
|
||
|
|
||
|
if not root_only:
|
||
|
parts["path"] = environ.get("PATH_INFO", "")
|
||
|
|
||
|
if not strip_querystring:
|
||
|
parts["query_string"] = environ.get("QUERY_STRING", "").encode("latin1")
|
||
|
|
||
|
return _sansio_utils.get_current_url(**parts)
|
||
|
|
||
|
|
||
|
def _get_server(
|
||
|
environ: WSGIEnvironment,
|
||
|
) -> tuple[str, int | None] | None:
|
||
|
name = environ.get("SERVER_NAME")
|
||
|
|
||
|
if name is None:
|
||
|
return None
|
||
|
|
||
|
try:
|
||
|
port: int | None = int(environ.get("SERVER_PORT", None))
|
||
|
except (TypeError, ValueError):
|
||
|
# unix socket
|
||
|
port = None
|
||
|
|
||
|
return name, port
|
||
|
|
||
|
|
||
|
def get_host(
|
||
|
environ: WSGIEnvironment, trusted_hosts: t.Iterable[str] | None = None
|
||
|
) -> str:
|
||
|
"""Return the host for the given WSGI environment.
|
||
|
|
||
|
The ``Host`` header is preferred, then ``SERVER_NAME`` if it's not
|
||
|
set. The returned host will only contain the port if it is different
|
||
|
than the standard port for the protocol.
|
||
|
|
||
|
Optionally, verify that the host is trusted using
|
||
|
:func:`host_is_trusted` and raise a
|
||
|
:exc:`~werkzeug.exceptions.SecurityError` if it is not.
|
||
|
|
||
|
:param environ: A WSGI environment dict.
|
||
|
:param trusted_hosts: A list of trusted host names.
|
||
|
|
||
|
:return: Host, with port if necessary.
|
||
|
:raise ~werkzeug.exceptions.SecurityError: If the host is not
|
||
|
trusted.
|
||
|
"""
|
||
|
return _sansio_utils.get_host(
|
||
|
environ["wsgi.url_scheme"],
|
||
|
environ.get("HTTP_HOST"),
|
||
|
_get_server(environ),
|
||
|
trusted_hosts,
|
||
|
)
|
||
|
|
||
|
|
||
|
def get_content_length(environ: WSGIEnvironment) -> int | None:
|
||
|
"""Return the ``Content-Length`` header value as an int. If the header is not given
|
||
|
or the ``Transfer-Encoding`` header is ``chunked``, ``None`` is returned to indicate
|
||
|
a streaming request. If the value is not an integer, or negative, 0 is returned.
|
||
|
|
||
|
:param environ: The WSGI environ to get the content length from.
|
||
|
|
||
|
.. versionadded:: 0.9
|
||
|
"""
|
||
|
return _sansio_utils.get_content_length(
|
||
|
http_content_length=environ.get("CONTENT_LENGTH"),
|
||
|
http_transfer_encoding=environ.get("HTTP_TRANSFER_ENCODING"),
|
||
|
)
|
||
|
|
||
|
|
||
|
def get_input_stream(
|
||
|
environ: WSGIEnvironment,
|
||
|
safe_fallback: bool = True,
|
||
|
max_content_length: int | None = None,
|
||
|
) -> t.IO[bytes]:
|
||
|
"""Return the WSGI input stream, wrapped so that it may be read safely without going
|
||
|
past the ``Content-Length`` header value or ``max_content_length``.
|
||
|
|
||
|
If ``Content-Length`` exceeds ``max_content_length``, a
|
||
|
:exc:`RequestEntityTooLarge`` ``413 Content Too Large`` error is raised.
|
||
|
|
||
|
If the WSGI server sets ``environ["wsgi.input_terminated"]``, it indicates that the
|
||
|
server handles terminating the stream, so it is safe to read directly. For example,
|
||
|
a server that knows how to handle chunked requests safely would set this.
|
||
|
|
||
|
If ``max_content_length`` is set, it can be enforced on streams if
|
||
|
``wsgi.input_terminated`` is set. Otherwise, an empty stream is returned unless the
|
||
|
user explicitly disables this safe fallback.
|
||
|
|
||
|
If the limit is reached before the underlying stream is exhausted (such as a file
|
||
|
that is too large, or an infinite stream), the remaining contents of the stream
|
||
|
cannot be read safely. Depending on how the server handles this, clients may show a
|
||
|
"connection reset" failure instead of seeing the 413 response.
|
||
|
|
||
|
:param environ: The WSGI environ containing the stream.
|
||
|
:param safe_fallback: Return an empty stream when ``Content-Length`` is not set.
|
||
|
Disabling this allows infinite streams, which can be a denial-of-service risk.
|
||
|
:param max_content_length: The maximum length that content-length or streaming
|
||
|
requests may not exceed.
|
||
|
|
||
|
.. versionchanged:: 2.3.2
|
||
|
``max_content_length`` is only applied to streaming requests if the server sets
|
||
|
``wsgi.input_terminated``.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Check ``max_content_length`` and raise an error if it is exceeded.
|
||
|
|
||
|
.. versionadded:: 0.9
|
||
|
"""
|
||
|
stream = t.cast(t.IO[bytes], environ["wsgi.input"])
|
||
|
content_length = get_content_length(environ)
|
||
|
|
||
|
if content_length is not None and max_content_length is not None:
|
||
|
if content_length > max_content_length:
|
||
|
raise RequestEntityTooLarge()
|
||
|
|
||
|
# A WSGI server can set this to indicate that it terminates the input stream. In
|
||
|
# that case the stream is safe without wrapping, or can enforce a max length.
|
||
|
if "wsgi.input_terminated" in environ:
|
||
|
if max_content_length is not None:
|
||
|
# If this is moved above, it can cause the stream to hang if a read attempt
|
||
|
# is made when the client sends no data. For example, the development server
|
||
|
# does not handle buffering except for chunked encoding.
|
||
|
return t.cast(
|
||
|
t.IO[bytes], LimitedStream(stream, max_content_length, is_max=True)
|
||
|
)
|
||
|
|
||
|
return stream
|
||
|
|
||
|
# No limit given, return an empty stream unless the user explicitly allows the
|
||
|
# potentially infinite stream. An infinite stream is dangerous if it's not expected,
|
||
|
# as it can tie up a worker indefinitely.
|
||
|
if content_length is None:
|
||
|
return io.BytesIO() if safe_fallback else stream
|
||
|
|
||
|
return t.cast(t.IO[bytes], LimitedStream(stream, content_length))
|
||
|
|
||
|
|
||
|
def get_path_info(environ: WSGIEnvironment) -> str:
|
||
|
"""Return ``PATH_INFO`` from the WSGI environment.
|
||
|
|
||
|
:param environ: WSGI environment to get the path from.
|
||
|
|
||
|
.. versionchanged:: 3.0
|
||
|
The ``charset`` and ``errors`` parameters were removed.
|
||
|
|
||
|
.. versionadded:: 0.9
|
||
|
"""
|
||
|
path: bytes = environ.get("PATH_INFO", "").encode("latin1")
|
||
|
return path.decode(errors="replace")
|
||
|
|
||
|
|
||
|
class ClosingIterator:
|
||
|
"""The WSGI specification requires that all middlewares and gateways
|
||
|
respect the `close` callback of the iterable returned by the application.
|
||
|
Because it is useful to add another close action to a returned iterable
|
||
|
and adding a custom iterable is a boring task this class can be used for
|
||
|
that::
|
||
|
|
||
|
return ClosingIterator(app(environ, start_response), [cleanup_session,
|
||
|
cleanup_locals])
|
||
|
|
||
|
If there is just one close function it can be passed instead of the list.
|
||
|
|
||
|
A closing iterator is not needed if the application uses response objects
|
||
|
and finishes the processing if the response is started::
|
||
|
|
||
|
try:
|
||
|
return response(environ, start_response)
|
||
|
finally:
|
||
|
cleanup_session()
|
||
|
cleanup_locals()
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
iterable: t.Iterable[bytes],
|
||
|
callbacks: None
|
||
|
| (t.Callable[[], None] | t.Iterable[t.Callable[[], None]]) = None,
|
||
|
) -> None:
|
||
|
iterator = iter(iterable)
|
||
|
self._next = t.cast(t.Callable[[], bytes], partial(next, iterator))
|
||
|
if callbacks is None:
|
||
|
callbacks = []
|
||
|
elif callable(callbacks):
|
||
|
callbacks = [callbacks]
|
||
|
else:
|
||
|
callbacks = list(callbacks)
|
||
|
iterable_close = getattr(iterable, "close", None)
|
||
|
if iterable_close:
|
||
|
callbacks.insert(0, iterable_close)
|
||
|
self._callbacks = callbacks
|
||
|
|
||
|
def __iter__(self) -> ClosingIterator:
|
||
|
return self
|
||
|
|
||
|
def __next__(self) -> bytes:
|
||
|
return self._next()
|
||
|
|
||
|
def close(self) -> None:
|
||
|
for callback in self._callbacks:
|
||
|
callback()
|
||
|
|
||
|
|
||
|
def wrap_file(
|
||
|
environ: WSGIEnvironment, file: t.IO[bytes], buffer_size: int = 8192
|
||
|
) -> t.Iterable[bytes]:
|
||
|
"""Wraps a file. This uses the WSGI server's file wrapper if available
|
||
|
or otherwise the generic :class:`FileWrapper`.
|
||
|
|
||
|
.. versionadded:: 0.5
|
||
|
|
||
|
If the file wrapper from the WSGI server is used it's important to not
|
||
|
iterate over it from inside the application but to pass it through
|
||
|
unchanged. If you want to pass out a file wrapper inside a response
|
||
|
object you have to set :attr:`Response.direct_passthrough` to `True`.
|
||
|
|
||
|
More information about file wrappers are available in :pep:`333`.
|
||
|
|
||
|
:param file: a :class:`file`-like object with a :meth:`~file.read` method.
|
||
|
:param buffer_size: number of bytes for one iteration.
|
||
|
"""
|
||
|
return environ.get("wsgi.file_wrapper", FileWrapper)( # type: ignore
|
||
|
file, buffer_size
|
||
|
)
|
||
|
|
||
|
|
||
|
class FileWrapper:
|
||
|
"""This class can be used to convert a :class:`file`-like object into
|
||
|
an iterable. It yields `buffer_size` blocks until the file is fully
|
||
|
read.
|
||
|
|
||
|
You should not use this class directly but rather use the
|
||
|
:func:`wrap_file` function that uses the WSGI server's file wrapper
|
||
|
support if it's available.
|
||
|
|
||
|
.. versionadded:: 0.5
|
||
|
|
||
|
If you're using this object together with a :class:`Response` you have
|
||
|
to use the `direct_passthrough` mode.
|
||
|
|
||
|
:param file: a :class:`file`-like object with a :meth:`~file.read` method.
|
||
|
:param buffer_size: number of bytes for one iteration.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, file: t.IO[bytes], buffer_size: int = 8192) -> None:
|
||
|
self.file = file
|
||
|
self.buffer_size = buffer_size
|
||
|
|
||
|
def close(self) -> None:
|
||
|
if hasattr(self.file, "close"):
|
||
|
self.file.close()
|
||
|
|
||
|
def seekable(self) -> bool:
|
||
|
if hasattr(self.file, "seekable"):
|
||
|
return self.file.seekable()
|
||
|
if hasattr(self.file, "seek"):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def seek(self, *args: t.Any) -> None:
|
||
|
if hasattr(self.file, "seek"):
|
||
|
self.file.seek(*args)
|
||
|
|
||
|
def tell(self) -> int | None:
|
||
|
if hasattr(self.file, "tell"):
|
||
|
return self.file.tell()
|
||
|
return None
|
||
|
|
||
|
def __iter__(self) -> FileWrapper:
|
||
|
return self
|
||
|
|
||
|
def __next__(self) -> bytes:
|
||
|
data = self.file.read(self.buffer_size)
|
||
|
if data:
|
||
|
return data
|
||
|
raise StopIteration()
|
||
|
|
||
|
|
||
|
class _RangeWrapper:
|
||
|
# private for now, but should we make it public in the future ?
|
||
|
|
||
|
"""This class can be used to convert an iterable object into
|
||
|
an iterable that will only yield a piece of the underlying content.
|
||
|
It yields blocks until the underlying stream range is fully read.
|
||
|
The yielded blocks will have a size that can't exceed the original
|
||
|
iterator defined block size, but that can be smaller.
|
||
|
|
||
|
If you're using this object together with a :class:`Response` you have
|
||
|
to use the `direct_passthrough` mode.
|
||
|
|
||
|
:param iterable: an iterable object with a :meth:`__next__` method.
|
||
|
:param start_byte: byte from which read will start.
|
||
|
:param byte_range: how many bytes to read.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
iterable: t.Iterable[bytes] | t.IO[bytes],
|
||
|
start_byte: int = 0,
|
||
|
byte_range: int | None = None,
|
||
|
):
|
||
|
self.iterable = iter(iterable)
|
||
|
self.byte_range = byte_range
|
||
|
self.start_byte = start_byte
|
||
|
self.end_byte = None
|
||
|
|
||
|
if byte_range is not None:
|
||
|
self.end_byte = start_byte + byte_range
|
||
|
|
||
|
self.read_length = 0
|
||
|
self.seekable = hasattr(iterable, "seekable") and iterable.seekable()
|
||
|
self.end_reached = False
|
||
|
|
||
|
def __iter__(self) -> _RangeWrapper:
|
||
|
return self
|
||
|
|
||
|
def _next_chunk(self) -> bytes:
|
||
|
try:
|
||
|
chunk = next(self.iterable)
|
||
|
self.read_length += len(chunk)
|
||
|
return chunk
|
||
|
except StopIteration:
|
||
|
self.end_reached = True
|
||
|
raise
|
||
|
|
||
|
def _first_iteration(self) -> tuple[bytes | None, int]:
|
||
|
chunk = None
|
||
|
if self.seekable:
|
||
|
self.iterable.seek(self.start_byte) # type: ignore
|
||
|
self.read_length = self.iterable.tell() # type: ignore
|
||
|
contextual_read_length = self.read_length
|
||
|
else:
|
||
|
while self.read_length <= self.start_byte:
|
||
|
chunk = self._next_chunk()
|
||
|
if chunk is not None:
|
||
|
chunk = chunk[self.start_byte - self.read_length :]
|
||
|
contextual_read_length = self.start_byte
|
||
|
return chunk, contextual_read_length
|
||
|
|
||
|
def _next(self) -> bytes:
|
||
|
if self.end_reached:
|
||
|
raise StopIteration()
|
||
|
chunk = None
|
||
|
contextual_read_length = self.read_length
|
||
|
if self.read_length == 0:
|
||
|
chunk, contextual_read_length = self._first_iteration()
|
||
|
if chunk is None:
|
||
|
chunk = self._next_chunk()
|
||
|
if self.end_byte is not None and self.read_length >= self.end_byte:
|
||
|
self.end_reached = True
|
||
|
return chunk[: self.end_byte - contextual_read_length]
|
||
|
return chunk
|
||
|
|
||
|
def __next__(self) -> bytes:
|
||
|
chunk = self._next()
|
||
|
if chunk:
|
||
|
return chunk
|
||
|
self.end_reached = True
|
||
|
raise StopIteration()
|
||
|
|
||
|
def close(self) -> None:
|
||
|
if hasattr(self.iterable, "close"):
|
||
|
self.iterable.close()
|
||
|
|
||
|
|
||
|
class LimitedStream(io.RawIOBase):
|
||
|
"""Wrap a stream so that it doesn't read more than a given limit. This is used to
|
||
|
limit ``wsgi.input`` to the ``Content-Length`` header value or
|
||
|
:attr:`.Request.max_content_length`.
|
||
|
|
||
|
When attempting to read after the limit has been reached, :meth:`on_exhausted` is
|
||
|
called. When the limit is a maximum, this raises :exc:`.RequestEntityTooLarge`.
|
||
|
|
||
|
If reading from the stream returns zero bytes or raises an error,
|
||
|
:meth:`on_disconnect` is called, which raises :exc:`.ClientDisconnected`. When the
|
||
|
limit is a maximum and zero bytes were read, no error is raised, since it may be the
|
||
|
end of the stream.
|
||
|
|
||
|
If the limit is reached before the underlying stream is exhausted (such as a file
|
||
|
that is too large, or an infinite stream), the remaining contents of the stream
|
||
|
cannot be read safely. Depending on how the server handles this, clients may show a
|
||
|
"connection reset" failure instead of seeing the 413 response.
|
||
|
|
||
|
:param stream: The stream to read from. Must be a readable binary IO object.
|
||
|
:param limit: The limit in bytes to not read past. Should be either the
|
||
|
``Content-Length`` header value or ``request.max_content_length``.
|
||
|
:param is_max: Whether the given ``limit`` is ``request.max_content_length`` instead
|
||
|
of the ``Content-Length`` header value. This changes how exhausted and
|
||
|
disconnect events are handled.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Handle ``max_content_length`` differently than ``Content-Length``.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Implements ``io.RawIOBase`` rather than ``io.IOBase``.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, stream: t.IO[bytes], limit: int, is_max: bool = False) -> None:
|
||
|
self._stream = stream
|
||
|
self._pos = 0
|
||
|
self.limit = limit
|
||
|
self._limit_is_max = is_max
|
||
|
|
||
|
@property
|
||
|
def is_exhausted(self) -> bool:
|
||
|
"""Whether the current stream position has reached the limit."""
|
||
|
return self._pos >= self.limit
|
||
|
|
||
|
def on_exhausted(self) -> None:
|
||
|
"""Called when attempting to read after the limit has been reached.
|
||
|
|
||
|
The default behavior is to do nothing, unless the limit is a maximum, in which
|
||
|
case it raises :exc:`.RequestEntityTooLarge`.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Raises ``RequestEntityTooLarge`` if the limit is a maximum.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Any return value is ignored.
|
||
|
"""
|
||
|
if self._limit_is_max:
|
||
|
raise RequestEntityTooLarge()
|
||
|
|
||
|
def on_disconnect(self, error: Exception | None = None) -> None:
|
||
|
"""Called when an attempted read receives zero bytes before the limit was
|
||
|
reached. This indicates that the client disconnected before sending the full
|
||
|
request body.
|
||
|
|
||
|
The default behavior is to raise :exc:`.ClientDisconnected`, unless the limit is
|
||
|
a maximum and no error was raised.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Added the ``error`` parameter. Do nothing if the limit is a maximum and no
|
||
|
error was raised.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Any return value is ignored.
|
||
|
"""
|
||
|
if not self._limit_is_max or error is not None:
|
||
|
raise ClientDisconnected()
|
||
|
|
||
|
# If the limit is a maximum, then we may have read zero bytes because the
|
||
|
# streaming body is complete. There's no way to distinguish that from the
|
||
|
# client disconnecting early.
|
||
|
|
||
|
def exhaust(self) -> bytes:
|
||
|
"""Exhaust the stream by reading until the limit is reached or the client
|
||
|
disconnects, returning the remaining data.
|
||
|
|
||
|
.. versionchanged:: 2.3
|
||
|
Return the remaining data.
|
||
|
|
||
|
.. versionchanged:: 2.2.3
|
||
|
Handle case where wrapped stream returns fewer bytes than requested.
|
||
|
"""
|
||
|
if not self.is_exhausted:
|
||
|
return self.readall()
|
||
|
|
||
|
return b""
|
||
|
|
||
|
def readinto(self, b: bytearray) -> int | None: # type: ignore[override]
|
||
|
size = len(b)
|
||
|
remaining = self.limit - self._pos
|
||
|
|
||
|
if remaining <= 0:
|
||
|
self.on_exhausted()
|
||
|
return 0
|
||
|
|
||
|
if hasattr(self._stream, "readinto"):
|
||
|
# Use stream.readinto if it's available.
|
||
|
if size <= remaining:
|
||
|
# The size fits in the remaining limit, use the buffer directly.
|
||
|
try:
|
||
|
out_size: int | None = self._stream.readinto(b)
|
||
|
except (OSError, ValueError) as e:
|
||
|
self.on_disconnect(error=e)
|
||
|
return 0
|
||
|
else:
|
||
|
# Use a temp buffer with the remaining limit as the size.
|
||
|
temp_b = bytearray(remaining)
|
||
|
|
||
|
try:
|
||
|
out_size = self._stream.readinto(temp_b)
|
||
|
except (OSError, ValueError) as e:
|
||
|
self.on_disconnect(error=e)
|
||
|
return 0
|
||
|
|
||
|
if out_size:
|
||
|
b[:out_size] = temp_b
|
||
|
else:
|
||
|
# WSGI requires that stream.read is available.
|
||
|
try:
|
||
|
data = self._stream.read(min(size, remaining))
|
||
|
except (OSError, ValueError) as e:
|
||
|
self.on_disconnect(error=e)
|
||
|
return 0
|
||
|
|
||
|
out_size = len(data)
|
||
|
b[:out_size] = data
|
||
|
|
||
|
if not out_size:
|
||
|
# Read zero bytes from the stream.
|
||
|
self.on_disconnect()
|
||
|
return 0
|
||
|
|
||
|
self._pos += out_size
|
||
|
return out_size
|
||
|
|
||
|
def readall(self) -> bytes:
|
||
|
if self.is_exhausted:
|
||
|
self.on_exhausted()
|
||
|
return b""
|
||
|
|
||
|
out = bytearray()
|
||
|
|
||
|
# The parent implementation uses "while True", which results in an extra read.
|
||
|
while not self.is_exhausted:
|
||
|
data = self.read(1024 * 64)
|
||
|
|
||
|
# Stream may return empty before a max limit is reached.
|
||
|
if not data:
|
||
|
break
|
||
|
|
||
|
out.extend(data)
|
||
|
|
||
|
return bytes(out)
|
||
|
|
||
|
def tell(self) -> int:
|
||
|
"""Return the current stream position.
|
||
|
|
||
|
.. versionadded:: 0.9
|
||
|
"""
|
||
|
return self._pos
|
||
|
|
||
|
def readable(self) -> bool:
|
||
|
return True
|