| | from __future__ import annotations |
| |
|
| | import json as _json |
| | import logging |
| | import typing |
| | from contextlib import contextmanager |
| | from dataclasses import dataclass |
| | from http.client import HTTPException as HTTPException |
| | from io import BytesIO, IOBase |
| |
|
| | from ...exceptions import InvalidHeader, TimeoutError |
| | from ...response import BaseHTTPResponse |
| | from ...util.retry import Retry |
| | from .request import EmscriptenRequest |
| |
|
| | if typing.TYPE_CHECKING: |
| | from ..._base_connection import BaseHTTPConnection, BaseHTTPSConnection |
| |
|
| | log = logging.getLogger(__name__) |
| |
|
| |
|
| | @dataclass |
| | class EmscriptenResponse: |
| | status_code: int |
| | headers: dict[str, str] |
| | body: IOBase | bytes |
| | request: EmscriptenRequest |
| |
|
| |
|
| | class EmscriptenHttpResponseWrapper(BaseHTTPResponse): |
| | def __init__( |
| | self, |
| | internal_response: EmscriptenResponse, |
| | url: str | None = None, |
| | connection: BaseHTTPConnection | BaseHTTPSConnection | None = None, |
| | ): |
| | self._pool = None |
| | self._body = None |
| | self._response = internal_response |
| | self._url = url |
| | self._connection = connection |
| | self._closed = False |
| | super().__init__( |
| | headers=internal_response.headers, |
| | status=internal_response.status_code, |
| | request_url=url, |
| | version=0, |
| | version_string="HTTP/?", |
| | reason="", |
| | decode_content=True, |
| | ) |
| | self.length_remaining = self._init_length(self._response.request.method) |
| | self.length_is_certain = False |
| |
|
| | @property |
| | def url(self) -> str | None: |
| | return self._url |
| |
|
| | @url.setter |
| | def url(self, url: str | None) -> None: |
| | self._url = url |
| |
|
| | @property |
| | def connection(self) -> BaseHTTPConnection | BaseHTTPSConnection | None: |
| | return self._connection |
| |
|
| | @property |
| | def retries(self) -> Retry | None: |
| | return self._retries |
| |
|
| | @retries.setter |
| | def retries(self, retries: Retry | None) -> None: |
| | |
| | self._retries = retries |
| |
|
| | def stream( |
| | self, amt: int | None = 2**16, decode_content: bool | None = None |
| | ) -> typing.Generator[bytes]: |
| | """ |
| | A generator wrapper for the read() method. A call will block until |
| | ``amt`` bytes have been read from the connection or until the |
| | connection is closed. |
| | |
| | :param amt: |
| | How much of the content to read. The generator will return up to |
| | much data per iteration, but may return less. This is particularly |
| | likely when using compressed data. However, the empty string will |
| | never be returned. |
| | |
| | :param decode_content: |
| | If True, will attempt to decode the body based on the |
| | 'content-encoding' header. |
| | """ |
| | while True: |
| | data = self.read(amt=amt, decode_content=decode_content) |
| |
|
| | if data: |
| | yield data |
| | else: |
| | break |
| |
|
| | def _init_length(self, request_method: str | None) -> int | None: |
| | length: int | None |
| | content_length: str | None = self.headers.get("content-length") |
| |
|
| | if content_length is not None: |
| | try: |
| | |
| | |
| | |
| | |
| | |
| | lengths = {int(val) for val in content_length.split(",")} |
| | if len(lengths) > 1: |
| | raise InvalidHeader( |
| | "Content-Length contained multiple " |
| | "unmatching values (%s)" % content_length |
| | ) |
| | length = lengths.pop() |
| | except ValueError: |
| | length = None |
| | else: |
| | if length < 0: |
| | length = None |
| |
|
| | else: |
| | length = None |
| |
|
| | |
| | if ( |
| | self.status in (204, 304) |
| | or 100 <= self.status < 200 |
| | or request_method == "HEAD" |
| | ): |
| | length = 0 |
| |
|
| | return length |
| |
|
| | def read( |
| | self, |
| | amt: int | None = None, |
| | decode_content: bool | None = None, |
| | cache_content: bool = False, |
| | ) -> bytes: |
| | if ( |
| | self._closed |
| | or self._response is None |
| | or (isinstance(self._response.body, IOBase) and self._response.body.closed) |
| | ): |
| | return b"" |
| |
|
| | with self._error_catcher(): |
| | |
| | if not isinstance(self._response.body, IOBase): |
| | self.length_remaining = len(self._response.body) |
| | self.length_is_certain = True |
| | |
| | self._response.body = BytesIO(self._response.body) |
| | if amt is not None and amt >= 0: |
| | |
| | cache_content = False |
| | data = self._response.body.read(amt) |
| | else: |
| | data = self._response.body.read() |
| | if cache_content: |
| | self._body = data |
| | if self.length_remaining is not None: |
| | self.length_remaining = max(self.length_remaining - len(data), 0) |
| | if len(data) == 0 or ( |
| | self.length_is_certain and self.length_remaining == 0 |
| | ): |
| | |
| | self._response.body.close() |
| | return typing.cast(bytes, data) |
| |
|
| | def read_chunked( |
| | self, |
| | amt: int | None = None, |
| | decode_content: bool | None = None, |
| | ) -> typing.Generator[bytes]: |
| | |
| | while True: |
| | bytes = self.read(amt, decode_content) |
| | if not bytes: |
| | break |
| | yield bytes |
| |
|
| | def release_conn(self) -> None: |
| | if not self._pool or not self._connection: |
| | return None |
| |
|
| | self._pool._put_conn(self._connection) |
| | self._connection = None |
| |
|
| | def drain_conn(self) -> None: |
| | self.close() |
| |
|
| | @property |
| | def data(self) -> bytes: |
| | if self._body: |
| | return self._body |
| | else: |
| | return self.read(cache_content=True) |
| |
|
| | def json(self) -> typing.Any: |
| | """ |
| | Deserializes the body of the HTTP response as a Python object. |
| | |
| | The body of the HTTP response must be encoded using UTF-8, as per |
| | `RFC 8529 Section 8.1 <https://www.rfc-editor.org/rfc/rfc8259#section-8.1>`_. |
| | |
| | To use a custom JSON decoder pass the result of :attr:`HTTPResponse.data` to |
| | your custom decoder instead. |
| | |
| | If the body of the HTTP response is not decodable to UTF-8, a |
| | `UnicodeDecodeError` will be raised. If the body of the HTTP response is not a |
| | valid JSON document, a `json.JSONDecodeError` will be raised. |
| | |
| | Read more :ref:`here <json_content>`. |
| | |
| | :returns: The body of the HTTP response as a Python object. |
| | """ |
| | data = self.data.decode("utf-8") |
| | return _json.loads(data) |
| |
|
| | def close(self) -> None: |
| | if not self._closed: |
| | if isinstance(self._response.body, IOBase): |
| | self._response.body.close() |
| | if self._connection: |
| | self._connection.close() |
| | self._connection = None |
| | self._closed = True |
| |
|
| | @contextmanager |
| | def _error_catcher(self) -> typing.Generator[None]: |
| | """ |
| | Catch Emscripten specific exceptions thrown by fetch.py, |
| | instead re-raising urllib3 variants, so that low-level exceptions |
| | are not leaked in the high-level api. |
| | |
| | On exit, release the connection back to the pool. |
| | """ |
| | from .fetch import _RequestError, _TimeoutError |
| |
|
| | clean_exit = False |
| |
|
| | try: |
| | yield |
| | |
| | |
| | clean_exit = True |
| | except _TimeoutError as e: |
| | raise TimeoutError(str(e)) |
| | except _RequestError as e: |
| | raise HTTPException(str(e)) |
| | finally: |
| | |
| | |
| | if not clean_exit: |
| | |
| | |
| | if ( |
| | isinstance(self._response.body, IOBase) |
| | and not self._response.body.closed |
| | ): |
| | self._response.body.close() |
| | |
| | self.release_conn() |
| | else: |
| | |
| | |
| | if ( |
| | isinstance(self._response.body, IOBase) |
| | and self._response.body.closed |
| | ): |
| | self.release_conn() |
| |
|