123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- 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)
|