Spaces:
Running
Running
from __future__ import annotations | |
import json as _json | |
import typing | |
from urllib.parse import urlencode | |
from ._base_connection import _TYPE_BODY | |
from ._collections import HTTPHeaderDict | |
from .filepost import _TYPE_FIELDS, encode_multipart_formdata | |
from .response import BaseHTTPResponse | |
__all__ = ["RequestMethods"] | |
_TYPE_ENCODE_URL_FIELDS = typing.Union[ | |
typing.Sequence[tuple[str, typing.Union[str, bytes]]], | |
typing.Mapping[str, typing.Union[str, bytes]], | |
] | |
class RequestMethods: | |
""" | |
Convenience mixin for classes who implement a :meth:`urlopen` method, such | |
as :class:`urllib3.HTTPConnectionPool` and | |
:class:`urllib3.PoolManager`. | |
Provides behavior for making common types of HTTP request methods and | |
decides which type of request field encoding to use. | |
Specifically, | |
:meth:`.request_encode_url` is for sending requests whose fields are | |
encoded in the URL (such as GET, HEAD, DELETE). | |
:meth:`.request_encode_body` is for sending requests whose fields are | |
encoded in the *body* of the request using multipart or www-form-urlencoded | |
(such as for POST, PUT, PATCH). | |
:meth:`.request` is for making any kind of request, it will look up the | |
appropriate encoding format and use one of the above two methods to make | |
the request. | |
Initializer parameters: | |
:param headers: | |
Headers to include with all requests, unless other headers are given | |
explicitly. | |
""" | |
_encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} | |
def __init__(self, headers: typing.Mapping[str, str] | None = None) -> None: | |
self.headers = headers or {} | |
def urlopen( | |
self, | |
method: str, | |
url: str, | |
body: _TYPE_BODY | None = None, | |
headers: typing.Mapping[str, str] | None = None, | |
encode_multipart: bool = True, | |
multipart_boundary: str | None = None, | |
**kw: typing.Any, | |
) -> BaseHTTPResponse: # Abstract | |
raise NotImplementedError( | |
"Classes extending RequestMethods must implement " | |
"their own ``urlopen`` method." | |
) | |
def request( | |
self, | |
method: str, | |
url: str, | |
body: _TYPE_BODY | None = None, | |
fields: _TYPE_FIELDS | None = None, | |
headers: typing.Mapping[str, str] | None = None, | |
json: typing.Any | None = None, | |
**urlopen_kw: typing.Any, | |
) -> BaseHTTPResponse: | |
""" | |
Make a request using :meth:`urlopen` with the appropriate encoding of | |
``fields`` based on the ``method`` used. | |
This is a convenience method that requires the least amount of manual | |
effort. It can be used in most situations, while still having the | |
option to drop down to more specific methods when necessary, such as | |
:meth:`request_encode_url`, :meth:`request_encode_body`, | |
or even the lowest level :meth:`urlopen`. | |
:param method: | |
HTTP request method (such as GET, POST, PUT, etc.) | |
:param url: | |
The URL to perform the request on. | |
:param body: | |
Data to send in the request body, either :class:`str`, :class:`bytes`, | |
an iterable of :class:`str`/:class:`bytes`, or a file-like object. | |
:param fields: | |
Data to encode and send in the URL or request body, depending on ``method``. | |
:param headers: | |
Dictionary of custom headers to send, such as User-Agent, | |
If-None-Match, etc. If None, pool headers are used. If provided, | |
these headers completely replace any pool-specific headers. | |
:param json: | |
Data to encode and send as JSON with UTF-encoded in the request body. | |
The ``"Content-Type"`` header will be set to ``"application/json"`` | |
unless specified otherwise. | |
""" | |
method = method.upper() | |
if json is not None and body is not None: | |
raise TypeError( | |
"request got values for both 'body' and 'json' parameters which are mutually exclusive" | |
) | |
if json is not None: | |
if headers is None: | |
headers = self.headers | |
if not ("content-type" in map(str.lower, headers.keys())): | |
headers = HTTPHeaderDict(headers) | |
headers["Content-Type"] = "application/json" | |
body = _json.dumps(json, separators=(",", ":"), ensure_ascii=False).encode( | |
"utf-8" | |
) | |
if body is not None: | |
urlopen_kw["body"] = body | |
if method in self._encode_url_methods: | |
return self.request_encode_url( | |
method, | |
url, | |
fields=fields, # type: ignore[arg-type] | |
headers=headers, | |
**urlopen_kw, | |
) | |
else: | |
return self.request_encode_body( | |
method, url, fields=fields, headers=headers, **urlopen_kw | |
) | |
def request_encode_url( | |
self, | |
method: str, | |
url: str, | |
fields: _TYPE_ENCODE_URL_FIELDS | None = None, | |
headers: typing.Mapping[str, str] | None = None, | |
**urlopen_kw: str, | |
) -> BaseHTTPResponse: | |
""" | |
Make a request using :meth:`urlopen` with the ``fields`` encoded in | |
the url. This is useful for request methods like GET, HEAD, DELETE, etc. | |
:param method: | |
HTTP request method (such as GET, POST, PUT, etc.) | |
:param url: | |
The URL to perform the request on. | |
:param fields: | |
Data to encode and send in the URL. | |
:param headers: | |
Dictionary of custom headers to send, such as User-Agent, | |
If-None-Match, etc. If None, pool headers are used. If provided, | |
these headers completely replace any pool-specific headers. | |
""" | |
if headers is None: | |
headers = self.headers | |
extra_kw: dict[str, typing.Any] = {"headers": headers} | |
extra_kw.update(urlopen_kw) | |
if fields: | |
url += "?" + urlencode(fields) | |
return self.urlopen(method, url, **extra_kw) | |
def request_encode_body( | |
self, | |
method: str, | |
url: str, | |
fields: _TYPE_FIELDS | None = None, | |
headers: typing.Mapping[str, str] | None = None, | |
encode_multipart: bool = True, | |
multipart_boundary: str | None = None, | |
**urlopen_kw: str, | |
) -> BaseHTTPResponse: | |
""" | |
Make a request using :meth:`urlopen` with the ``fields`` encoded in | |
the body. This is useful for request methods like POST, PUT, PATCH, etc. | |
When ``encode_multipart=True`` (default), then | |
:func:`urllib3.encode_multipart_formdata` is used to encode | |
the payload with the appropriate content type. Otherwise | |
:func:`urllib.parse.urlencode` is used with the | |
'application/x-www-form-urlencoded' content type. | |
Multipart encoding must be used when posting files, and it's reasonably | |
safe to use it in other times too. However, it may break request | |
signing, such as with OAuth. | |
Supports an optional ``fields`` parameter of key/value strings AND | |
key/filetuple. A filetuple is a (filename, data, MIME type) tuple where | |
the MIME type is optional. For example:: | |
fields = { | |
'foo': 'bar', | |
'fakefile': ('foofile.txt', 'contents of foofile'), | |
'realfile': ('barfile.txt', open('realfile').read()), | |
'typedfile': ('bazfile.bin', open('bazfile').read(), | |
'image/jpeg'), | |
'nonamefile': 'contents of nonamefile field', | |
} | |
When uploading a file, providing a filename (the first parameter of the | |
tuple) is optional but recommended to best mimic behavior of browsers. | |
Note that if ``headers`` are supplied, the 'Content-Type' header will | |
be overwritten because it depends on the dynamic random boundary string | |
which is used to compose the body of the request. The random boundary | |
string can be explicitly set with the ``multipart_boundary`` parameter. | |
:param method: | |
HTTP request method (such as GET, POST, PUT, etc.) | |
:param url: | |
The URL to perform the request on. | |
:param fields: | |
Data to encode and send in the request body. | |
:param headers: | |
Dictionary of custom headers to send, such as User-Agent, | |
If-None-Match, etc. If None, pool headers are used. If provided, | |
these headers completely replace any pool-specific headers. | |
:param encode_multipart: | |
If True, encode the ``fields`` using the multipart/form-data MIME | |
format. | |
:param multipart_boundary: | |
If not specified, then a random boundary will be generated using | |
:func:`urllib3.filepost.choose_boundary`. | |
""" | |
if headers is None: | |
headers = self.headers | |
extra_kw: dict[str, typing.Any] = {"headers": HTTPHeaderDict(headers)} | |
body: bytes | str | |
if fields: | |
if "body" in urlopen_kw: | |
raise TypeError( | |
"request got values for both 'fields' and 'body', can only specify one." | |
) | |
if encode_multipart: | |
body, content_type = encode_multipart_formdata( | |
fields, boundary=multipart_boundary | |
) | |
else: | |
body, content_type = ( | |
urlencode(fields), # type: ignore[arg-type] | |
"application/x-www-form-urlencoded", | |
) | |
extra_kw["body"] = body | |
extra_kw["headers"].setdefault("Content-Type", content_type) | |
extra_kw.update(urlopen_kw) | |
return self.urlopen(method, url, **extra_kw) | |