280 lines
9.8 KiB
Python
280 lines
9.8 KiB
Python
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[typing.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 request body. Values are processed
|
|
by :func:`urllib.parse.urlencode`.
|
|
|
|
: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 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.
|
|
"""
|
|
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)
|