diff options
| author | AlexSm <[email protected]> | 2024-01-09 18:56:40 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-01-09 18:56:40 +0100 |
| commit | e95f266d2a3e48e62015220588a4fd73d5d5a5cb (patch) | |
| tree | a8a784b6931fe52ad5f511cfef85af14e5f63991 /contrib/python/httpx | |
| parent | 50a65e3b48a82d5b51f272664da389f2e0b0c99a (diff) | |
Library import 6 (#888)
Diffstat (limited to 'contrib/python/httpx')
| -rw-r--r-- | contrib/python/httpx/.dist-info/METADATA | 20 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/__version__.py | 2 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_api.py | 20 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_client.py | 43 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_content.py | 2 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_decoders.py | 9 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_exceptions.py | 10 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_main.py | 21 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_models.py | 7 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_multipart.py | 17 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_transports/asgi.py | 2 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_transports/default.py | 15 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_types.py | 3 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_urlparse.py | 96 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_urls.py | 22 | ||||
| -rw-r--r-- | contrib/python/httpx/httpx/_utils.py | 16 | ||||
| -rw-r--r-- | contrib/python/httpx/ya.make | 2 |
17 files changed, 216 insertions, 91 deletions
diff --git a/contrib/python/httpx/.dist-info/METADATA b/contrib/python/httpx/.dist-info/METADATA index 247ac2e1189..ab2b707fcdc 100644 --- a/contrib/python/httpx/.dist-info/METADATA +++ b/contrib/python/httpx/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: httpx -Version: 0.25.2 +Version: 0.26.0 Summary: The next generation HTTP client. Project-URL: Changelog, https://github.com/encode/httpx/blob/master/CHANGELOG.md Project-URL: Documentation, https://www.python-httpx.org @@ -31,8 +31,8 @@ Requires-Dist: httpcore==1.* Requires-Dist: idna Requires-Dist: sniffio Provides-Extra: brotli -Requires-Dist: brotli; platform_python_implementation == 'CPython' and extra == 'brotli' -Requires-Dist: brotlicffi; platform_python_implementation != 'CPython' and extra == 'brotli' +Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli' +Requires-Dist: brotlicffi; (platform_python_implementation != 'CPython') and extra == 'brotli' Provides-Extra: cli Requires-Dist: click==8.*; extra == 'cli' Requires-Dist: pygments==2.*; extra == 'cli' @@ -196,7 +196,19 @@ inspiration around the lower-level networking details. ### Added -* Add missing type hints to few `__init__()` methods. (#2938) +* The `proxy` argument was added. You should use the `proxy` argument instead of the deprecated `proxies`, or use `mounts=` for more complex configurations. (#2879) + +### Deprecated + +* The `proxies` argument is now deprecated. It will still continue to work, but it will be removed in the future. (#2879) + +### Fixed + +* Fix cases of double escaping of URL path components. Allow / as a safe character in the query portion. (#2990) +* Handle `NO_PROXY` envvar cases when a fully qualified URL is supplied as the value. (#2741) +* Allow URLs where username or password contains unescaped '@'. (#2986) +* Ensure ASGI `raw_path` does not include URL query component. (#2999) +* Ensure `Response.iter_text()` cannot yield empty strings. (#2998) --- diff --git a/contrib/python/httpx/httpx/__version__.py b/contrib/python/httpx/httpx/__version__.py index c6bc0ac0230..3edc842c69e 100644 --- a/contrib/python/httpx/httpx/__version__.py +++ b/contrib/python/httpx/httpx/__version__.py @@ -1,3 +1,3 @@ __title__ = "httpx" __description__ = "A next generation HTTP client, for Python 3." -__version__ = "0.25.2" +__version__ = "0.26.0" diff --git a/contrib/python/httpx/httpx/_api.py b/contrib/python/httpx/httpx/_api.py index 571289cf2b3..c7af947218a 100644 --- a/contrib/python/httpx/httpx/_api.py +++ b/contrib/python/httpx/httpx/_api.py @@ -10,6 +10,7 @@ from ._types import ( CookieTypes, HeaderTypes, ProxiesTypes, + ProxyTypes, QueryParamTypes, RequestContent, RequestData, @@ -32,6 +33,7 @@ def request( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, @@ -63,6 +65,7 @@ def request( request. * **auth** - *(optional)* An authentication class to use when sending the request. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy URLs. * **timeout** - *(optional)* The timeout configuration to use when sending the request. @@ -91,6 +94,7 @@ def request( """ with Client( cookies=cookies, + proxy=proxy, proxies=proxies, cert=cert, verify=verify, @@ -124,6 +128,7 @@ def stream( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, @@ -143,6 +148,7 @@ def stream( """ with Client( cookies=cookies, + proxy=proxy, proxies=proxies, cert=cert, verify=verify, @@ -171,6 +177,7 @@ def get( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -193,6 +200,7 @@ def get( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -209,6 +217,7 @@ def options( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -231,6 +240,7 @@ def options( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -247,6 +257,7 @@ def head( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -269,6 +280,7 @@ def head( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -289,6 +301,7 @@ def post( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -312,6 +325,7 @@ def post( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -332,6 +346,7 @@ def put( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -355,6 +370,7 @@ def put( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -375,6 +391,7 @@ def patch( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -398,6 +415,7 @@ def patch( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, @@ -414,6 +432,7 @@ def delete( headers: typing.Optional[HeaderTypes] = None, cookies: typing.Optional[CookieTypes] = None, auth: typing.Optional[AuthTypes] = None, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, follow_redirects: bool = False, cert: typing.Optional[CertTypes] = None, @@ -436,6 +455,7 @@ def delete( headers=headers, cookies=cookies, auth=auth, + proxy=proxy, proxies=proxies, follow_redirects=follow_redirects, cert=cert, diff --git a/contrib/python/httpx/httpx/_client.py b/contrib/python/httpx/httpx/_client.py index 2c7ae030f50..2813a84f010 100644 --- a/contrib/python/httpx/httpx/_client.py +++ b/contrib/python/httpx/httpx/_client.py @@ -36,6 +36,7 @@ from ._types import ( CookieTypes, HeaderTypes, ProxiesTypes, + ProxyTypes, QueryParamTypes, RequestContent, RequestData, @@ -597,6 +598,7 @@ class Client(BaseClient): to authenticate the client. Either a path to an SSL certificate file, or two-tuple of (certificate file, key file), or a three-tuple of (certificate file, key file, password). + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy URLs. * **timeout** - *(optional)* The timeout configuration to use when sending @@ -628,8 +630,11 @@ class Client(BaseClient): cert: typing.Optional[CertTypes] = None, http1: bool = True, http2: bool = False, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, - mounts: typing.Optional[typing.Mapping[str, BaseTransport]] = None, + mounts: typing.Optional[ + typing.Mapping[str, typing.Optional[BaseTransport]] + ] = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -666,8 +671,17 @@ class Client(BaseClient): "Make sure to install httpx using `pip install httpx[http2]`." ) from None + if proxies: + message = ( + "The 'proxies' argument is now deprecated." + " Use 'proxy' or 'mounts' instead." + ) + warnings.warn(message, DeprecationWarning) + if proxy: + raise RuntimeError("Use either `proxy` or 'proxies', not both.") + allow_env_proxies = trust_env and app is None and transport is None - proxy_map = self._get_proxy_map(proxies, allow_env_proxies) + proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies) self._transport = self._init_transport( verify=verify, @@ -1264,7 +1278,9 @@ class Client(BaseClient): if self._state != ClientState.UNOPENED: msg = { ClientState.OPENED: "Cannot open a client instance more than once.", - ClientState.CLOSED: "Cannot reopen a client instance, once it has been closed.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), }[self._state] raise RuntimeError(msg) @@ -1322,6 +1338,7 @@ class AsyncClient(BaseClient): file, key file, password). * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be enabled. Defaults to `False`. + * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy URLs. * **timeout** - *(optional)* The timeout configuration to use when sending @@ -1353,8 +1370,11 @@ class AsyncClient(BaseClient): cert: typing.Optional[CertTypes] = None, http1: bool = True, http2: bool = False, + proxy: typing.Optional[ProxyTypes] = None, proxies: typing.Optional[ProxiesTypes] = None, - mounts: typing.Optional[typing.Mapping[str, AsyncBaseTransport]] = None, + mounts: typing.Optional[ + typing.Mapping[str, typing.Optional[AsyncBaseTransport]] + ] = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, limits: Limits = DEFAULT_LIMITS, @@ -1391,8 +1411,17 @@ class AsyncClient(BaseClient): "Make sure to install httpx using `pip install httpx[http2]`." ) from None + if proxies: + message = ( + "The 'proxies' argument is now deprecated." + " Use 'proxy' or 'mounts' instead." + ) + warnings.warn(message, DeprecationWarning) + if proxy: + raise RuntimeError("Use either `proxy` or 'proxies', not both.") + allow_env_proxies = trust_env and app is None and transport is None - proxy_map = self._get_proxy_map(proxies, allow_env_proxies) + proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies) self._transport = self._init_transport( verify=verify, @@ -1980,7 +2009,9 @@ class AsyncClient(BaseClient): if self._state != ClientState.UNOPENED: msg = { ClientState.OPENED: "Cannot open a client instance more than once.", - ClientState.CLOSED: "Cannot reopen a client instance, once it has been closed.", + ClientState.CLOSED: ( + "Cannot reopen a client instance, once it has been closed." + ), }[self._state] raise RuntimeError(msg) diff --git a/contrib/python/httpx/httpx/_content.py b/contrib/python/httpx/httpx/_content.py index 0aaea33749a..cd0d17f1711 100644 --- a/contrib/python/httpx/httpx/_content.py +++ b/contrib/python/httpx/httpx/_content.py @@ -105,7 +105,7 @@ class UnattachedStream(AsyncByteStream, SyncByteStream): def encode_content( - content: Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] + content: Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]], ) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: if isinstance(content, (bytes, str)): body = content.encode("utf-8") if isinstance(content, str) else content diff --git a/contrib/python/httpx/httpx/_decoders.py b/contrib/python/httpx/httpx/_decoders.py index b4ac9a44af6..3f507c8e040 100644 --- a/contrib/python/httpx/httpx/_decoders.py +++ b/contrib/python/httpx/httpx/_decoders.py @@ -212,7 +212,7 @@ class TextChunker: def decode(self, content: str) -> typing.List[str]: if self._chunk_size is None: - return [content] + return [content] if content else [] self._buffer.write(content) if self._buffer.tell() >= self._chunk_size: @@ -259,7 +259,8 @@ class LineDecoder: """ Handles incrementally reading lines from text. - Has the same behaviour as the stdllib splitlines, but handling the input iteratively. + Has the same behaviour as the stdllib splitlines, + but handling the input iteratively. """ def __init__(self) -> None: @@ -279,7 +280,9 @@ class LineDecoder: text = text[:-1] if not text: - return [] + # NOTE: the edge case input of empty text doesn't occur in practice, + # because other httpx internals filter out this value + return [] # pragma: no cover trailing_newline = text[-1] in NEWLINE_CHARS lines = text.splitlines() diff --git a/contrib/python/httpx/httpx/_exceptions.py b/contrib/python/httpx/httpx/_exceptions.py index 24a4f8aba33..123692955b1 100644 --- a/contrib/python/httpx/httpx/_exceptions.py +++ b/contrib/python/httpx/httpx/_exceptions.py @@ -313,7 +313,10 @@ class ResponseNotRead(StreamError): """ def __init__(self) -> None: - message = "Attempted to access streaming response content, without having called `read()`." + message = ( + "Attempted to access streaming response content," + " without having called `read()`." + ) super().__init__(message) @@ -323,7 +326,10 @@ class RequestNotRead(StreamError): """ def __init__(self) -> None: - message = "Attempted to access streaming request content, without having called `read()`." + message = ( + "Attempted to access streaming request content," + " without having called `read()`." + ) super().__init__(message) diff --git a/contrib/python/httpx/httpx/_main.py b/contrib/python/httpx/httpx/_main.py index 7c12ce841d3..adb57d5fc0a 100644 --- a/contrib/python/httpx/httpx/_main.py +++ b/contrib/python/httpx/httpx/_main.py @@ -63,20 +63,21 @@ def print_help() -> None: ) table.add_row( "--auth [cyan]<USER PASS>", - "Username and password to include in the request. Specify '-' for the password to use " - "a password prompt. Note that using --verbose/-v will expose the Authorization " - "header, including the password encoding in a trivially reversible format.", + "Username and password to include in the request. Specify '-' for the password" + " to use a password prompt. Note that using --verbose/-v will expose" + " the Authorization header, including the password encoding" + " in a trivially reversible format.", ) table.add_row( - "--proxies [cyan]URL", + "--proxy [cyan]URL", "Send the request via a proxy. Should be the URL giving the proxy address.", ) table.add_row( "--timeout [cyan]FLOAT", - "Timeout value to use for network operations, such as establishing the connection, " - "reading some data, etc... [Default: 5.0]", + "Timeout value to use for network operations, such as establishing the" + " connection, reading some data, etc... [Default: 5.0]", ) table.add_row("--follow-redirects", "Automatically follow redirects.") @@ -385,8 +386,8 @@ def handle_help( ), ) @click.option( - "--proxies", - "proxies", + "--proxy", + "proxy", type=str, default=None, help="Send the request via a proxy. Should be the URL giving the proxy address.", @@ -455,7 +456,7 @@ def main( headers: typing.List[typing.Tuple[str, str]], cookies: typing.List[typing.Tuple[str, str]], auth: typing.Optional[typing.Tuple[str, str]], - proxies: str, + proxy: str, timeout: float, follow_redirects: bool, verify: bool, @@ -472,7 +473,7 @@ def main( try: with Client( - proxies=proxies, + proxy=proxy, timeout=timeout, verify=verify, http2=http2, diff --git a/contrib/python/httpx/httpx/_models.py b/contrib/python/httpx/httpx/_models.py index 8a5e6280f3f..b8617cdab56 100644 --- a/contrib/python/httpx/httpx/_models.py +++ b/contrib/python/httpx/httpx/_models.py @@ -358,7 +358,8 @@ class Request: # Using `content=...` implies automatically populated `Host` and content # headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`. # - # Using `stream=...` will not automatically include *any* auto-populated headers. + # Using `stream=...` will not automatically include *any* + # auto-populated headers. # # As an end-user you don't really need `stream=...`. It's only # useful when: @@ -852,7 +853,7 @@ class Response: yield chunk text_content = decoder.flush() for chunk in chunker.decode(text_content): - yield chunk + yield chunk # pragma: no cover for chunk in chunker.flush(): yield chunk @@ -956,7 +957,7 @@ class Response: yield chunk text_content = decoder.flush() for chunk in chunker.decode(text_content): - yield chunk + yield chunk # pragma: no cover for chunk in chunker.flush(): yield chunk diff --git a/contrib/python/httpx/httpx/_multipart.py b/contrib/python/httpx/httpx/_multipart.py index 6d5baa86398..5122d5114fb 100644 --- a/contrib/python/httpx/httpx/_multipart.py +++ b/contrib/python/httpx/httpx/_multipart.py @@ -48,7 +48,8 @@ class DataField: ) if value is not None and not isinstance(value, (str, bytes, int, float)): raise TypeError( - f"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}" + "Invalid type for value. Expected primitive type," + f" got {type(value)}: {value!r}" ) self.name = name self.value: typing.Union[str, bytes] = ( @@ -96,11 +97,13 @@ class FileField: content_type: typing.Optional[str] = None # This large tuple based API largely mirror's requests' API - # It would be good to think of better APIs for this that we could include in httpx 2.0 - # since variable length tuples (especially of 4 elements) are quite unwieldly + # It would be good to think of better APIs for this that we could + # include in httpx 2.0 since variable length tuples(especially of 4 elements) + # are quite unwieldly if isinstance(value, tuple): if len(value) == 2: - # neither the 3rd parameter (content_type) nor the 4th (headers) was included + # neither the 3rd parameter (content_type) nor the 4th (headers) + # was included filename, fileobj = value # type: ignore elif len(value) == 3: filename, fileobj, content_type = value # type: ignore @@ -116,9 +119,9 @@ class FileField: has_content_type_header = any("content-type" in key.lower() for key in headers) if content_type is not None and not has_content_type_header: - # note that unlike requests, we ignore the content_type - # provided in the 3rd tuple element if it is also included in the headers - # requests does the opposite (it overwrites the header with the 3rd tuple element) + # note that unlike requests, we ignore the content_type provided in the 3rd + # tuple element if it is also included in the headers requests does + # the opposite (it overwrites the headerwith the 3rd tuple element) headers["Content-Type"] = content_type if isinstance(fileobj, io.StringIO): diff --git a/contrib/python/httpx/httpx/_transports/asgi.py b/contrib/python/httpx/httpx/_transports/asgi.py index f67f0fbd5b5..08cd392f75c 100644 --- a/contrib/python/httpx/httpx/_transports/asgi.py +++ b/contrib/python/httpx/httpx/_transports/asgi.py @@ -103,7 +103,7 @@ class ASGITransport(AsyncBaseTransport): "headers": [(k.lower(), v) for (k, v) in request.headers.raw], "scheme": request.url.scheme, "path": request.url.path, - "raw_path": request.url.raw_path, + "raw_path": request.url.raw_path.split(b"?")[0], "query_string": request.url.query, "server": (request.url.host, request.url.port), "client": self.client, diff --git a/contrib/python/httpx/httpx/_transports/default.py b/contrib/python/httpx/httpx/_transports/default.py index 343c588f9ff..14a087389a8 100644 --- a/contrib/python/httpx/httpx/_transports/default.py +++ b/contrib/python/httpx/httpx/_transports/default.py @@ -47,7 +47,8 @@ from .._exceptions import ( WriteTimeout, ) from .._models import Request, Response -from .._types import AsyncByteStream, CertTypes, SyncByteStream, VerifyTypes +from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream, VerifyTypes +from .._urls import URL from .base import AsyncBaseTransport, BaseTransport T = typing.TypeVar("T", bound="HTTPTransport") @@ -124,13 +125,14 @@ class HTTPTransport(BaseTransport): http2: bool = False, limits: Limits = DEFAULT_LIMITS, trust_env: bool = True, - proxy: typing.Optional[Proxy] = None, + proxy: typing.Optional[ProxyTypes] = None, uds: typing.Optional[str] = None, local_address: typing.Optional[str] = None, retries: int = 0, socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, ) -> None: ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy if proxy is None: self._pool = httpcore.ConnectionPool( @@ -190,7 +192,8 @@ class HTTPTransport(BaseTransport): ) else: # pragma: no cover raise ValueError( - f"Proxy protocol must be either 'http', 'https', or 'socks5', but got {proxy.url.scheme!r}." + "Proxy protocol must be either 'http', 'https', or 'socks5'," + f" but got {proxy.url.scheme!r}." ) def __enter__(self: T) -> T: # Use generics for subclass support. @@ -263,13 +266,14 @@ class AsyncHTTPTransport(AsyncBaseTransport): http2: bool = False, limits: Limits = DEFAULT_LIMITS, trust_env: bool = True, - proxy: typing.Optional[Proxy] = None, + proxy: typing.Optional[ProxyTypes] = None, uds: typing.Optional[str] = None, local_address: typing.Optional[str] = None, retries: int = 0, socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] = None, ) -> None: ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) + proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy if proxy is None: self._pool = httpcore.AsyncConnectionPool( @@ -328,7 +332,8 @@ class AsyncHTTPTransport(AsyncBaseTransport): ) else: # pragma: no cover raise ValueError( - f"Proxy protocol must be either 'http', 'https', or 'socks5', but got {proxy.url.scheme!r}." + "Proxy protocol must be either 'http', 'https', or 'socks5'," + " but got {proxy.url.scheme!r}." ) async def __aenter__(self: A) -> A: # Use generics for subclass support. diff --git a/contrib/python/httpx/httpx/_types.py b/contrib/python/httpx/httpx/_types.py index 83cf35a32a4..649d101d54a 100644 --- a/contrib/python/httpx/httpx/_types.py +++ b/contrib/python/httpx/httpx/_types.py @@ -78,7 +78,8 @@ TimeoutTypes = Union[ Tuple[Optional[float], Optional[float], Optional[float], Optional[float]], "Timeout", ] -ProxiesTypes = Union[URLTypes, "Proxy", Dict[URLTypes, Union[None, URLTypes, "Proxy"]]] +ProxyTypes = Union[URLTypes, "Proxy"] +ProxiesTypes = Union[ProxyTypes, Dict[URLTypes, Union[None, ProxyTypes]]] AuthTypes = Union[ Tuple[Union[str, bytes], Union[str, bytes]], diff --git a/contrib/python/httpx/httpx/_urlparse.py b/contrib/python/httpx/httpx/_urlparse.py index 8e060424e89..07bbea9070c 100644 --- a/contrib/python/httpx/httpx/_urlparse.py +++ b/contrib/python/httpx/httpx/_urlparse.py @@ -62,8 +62,8 @@ AUTHORITY_REGEX = re.compile( ( r"(?:(?P<userinfo>{userinfo})@)?" r"(?P<host>{host})" r":?(?P<port>{port})?" ).format( - userinfo="[^@]*", # Any character sequence not including '@'. - host="(\\[.*\\]|[^:]*)", # Either any character sequence not including ':', + userinfo=".*", # Any character sequence. + host="(\\[.*\\]|[^:@]*)", # Either any character sequence excluding ':' or '@', # or an IPv6 address enclosed within square brackets. port=".*", # Any character sequence. ) @@ -260,10 +260,8 @@ def urlparse(url: str = "", **kwargs: typing.Optional[str]) -> ParseResult: # For 'path' we need to drop ? and # from the GEN_DELIMS set. parsed_path: str = quote(path, safe=SUB_DELIMS + ":/[]@") # For 'query' we need to drop '#' from the GEN_DELIMS set. - # We also exclude '/' because it is more robust to replace it with a percent - # encoding despite it not being a requirement of the spec. parsed_query: typing.Optional[str] = ( - None if query is None else quote(query, safe=SUB_DELIMS + ":?[]@") + None if query is None else quote(query, safe=SUB_DELIMS + ":/?[]@") ) # For 'fragment' we can include all of the GEN_DELIMS set. parsed_fragment: typing.Optional[str] = ( @@ -360,24 +358,25 @@ def normalize_port( def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None: """ - Path validation rules that depend on if the URL contains a scheme or authority component. + Path validation rules that depend on if the URL contains + a scheme or authority component. See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3 """ if has_authority: - # > If a URI contains an authority component, then the path component - # > must either be empty or begin with a slash ("/") character." + # If a URI contains an authority component, then the path component + # must either be empty or begin with a slash ("/") character." if path and not path.startswith("/"): raise InvalidURL("For absolute URLs, path must be empty or begin with '/'") else: - # > If a URI does not contain an authority component, then the path cannot begin - # > with two slash characters ("//"). + # If a URI does not contain an authority component, then the path cannot begin + # with two slash characters ("//"). if path.startswith("//"): raise InvalidURL( "URLs with no authority component cannot have a path starting with '//'" ) - # > In addition, a URI reference (Section 4.1) may be a relative-path reference, in which - # > case the first path segment cannot contain a colon (":") character. + # In addition, a URI reference (Section 4.1) may be a relative-path reference, + # in which case the first path segment cannot contain a colon (":") character. if path.startswith(":") and not has_scheme: raise InvalidURL( "URLs with no scheme component cannot have a path starting with ':'" @@ -431,13 +430,12 @@ def is_safe(string: str, safe: str = "/") -> bool: if char not in NON_ESCAPED_CHARS: return False - # Any '%' characters must be valid '%xx' escape sequences. - return string.count("%") == len(PERCENT_ENCODED_REGEX.findall(string)) + return True -def quote(string: str, safe: str = "/") -> str: +def percent_encoded(string: str, safe: str = "/") -> str: """ - Use percent-encoding to quote a string if required. + Use percent-encoding to quote a string. """ if is_safe(string, safe=safe): return string @@ -448,17 +446,57 @@ def quote(string: str, safe: str = "/") -> str: ) +def quote(string: str, safe: str = "/") -> str: + """ + Use percent-encoding to quote a string, omitting existing '%xx' escape sequences. + + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.1 + + * `string`: The string to be percent-escaped. + * `safe`: A string containing characters that may be treated as safe, and do not + need to be escaped. Unreserved characters are always treated as safe. + See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3 + """ + parts = [] + current_position = 0 + for match in re.finditer(PERCENT_ENCODED_REGEX, string): + start_position, end_position = match.start(), match.end() + matched_text = match.group(0) + # Add any text up to the '%xx' escape sequence. + if start_position != current_position: + leading_text = string[current_position:start_position] + parts.append(percent_encoded(leading_text, safe=safe)) + + # Add the '%xx' escape sequence. + parts.append(matched_text) + current_position = end_position + + # Add any text after the final '%xx' escape sequence. + if current_position != len(string): + trailing_text = string[current_position:] + parts.append(percent_encoded(trailing_text, safe=safe)) + + return "".join(parts) + + def urlencode(items: typing.List[typing.Tuple[str, str]]) -> str: - # We can use a much simpler version of the stdlib urlencode here because - # we don't need to handle a bunch of different typing cases, such as bytes vs str. - # - # https://github.com/python/cpython/blob/b2f7b2ef0b5421e01efb8c7bee2ef95d3bab77eb/Lib/urllib/parse.py#L926 - # - # Note that we use '%20' encoding for spaces. and '%2F for '/'. - # This is slightly different than `requests`, but is the behaviour that browsers use. - # - # See - # - https://github.com/encode/httpx/issues/2536 - # - https://github.com/encode/httpx/issues/2721 - # - https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode - return "&".join([quote(k, safe="") + "=" + quote(v, safe="") for k, v in items]) + """ + We can use a much simpler version of the stdlib urlencode here because + we don't need to handle a bunch of different typing cases, such as bytes vs str. + + https://github.com/python/cpython/blob/b2f7b2ef0b5421e01efb8c7bee2ef95d3bab77eb/Lib/urllib/parse.py#L926 + + Note that we use '%20' encoding for spaces. and '%2F for '/'. + This is slightly different than `requests`, but is the behaviour that browsers use. + + See + - https://github.com/encode/httpx/issues/2536 + - https://github.com/encode/httpx/issues/2721 + - https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode + """ + return "&".join( + [ + percent_encoded(k, safe="") + "=" + percent_encoded(v, safe="") + for k, v in items + ] + ) diff --git a/contrib/python/httpx/httpx/_urls.py b/contrib/python/httpx/httpx/_urls.py index b023941b623..26202e95db2 100644 --- a/contrib/python/httpx/httpx/_urls.py +++ b/contrib/python/httpx/httpx/_urls.py @@ -51,21 +51,23 @@ class URL: assert url.raw_host == b"xn--fiqs8s.icom.museum" * `url.port` is either None or an integer. URLs that include the default port for - "http", "https", "ws", "wss", and "ftp" schemes have their port normalized to `None`. + "http", "https", "ws", "wss", and "ftp" schemes have their port + normalized to `None`. assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80") assert httpx.URL("http://example.com").port is None assert httpx.URL("http://example.com:80").port is None - * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work with - `url.username` and `url.password` instead, which handle the URL escaping. + * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work + with `url.username` and `url.password` instead, which handle the URL escaping. * `url.raw_path` is raw bytes of both the path and query, without URL escaping. This portion is used as the target when constructing HTTP requests. Usually you'll want to work with `url.path` instead. - * `url.query` is raw bytes, without URL escaping. A URL query string portion can only - be properly URL escaped when decoding the parameter names and values themselves. + * `url.query` is raw bytes, without URL escaping. A URL query string portion can + only be properly URL escaped when decoding the parameter names and values + themselves. """ def __init__( @@ -115,7 +117,8 @@ class URL: self._uri_reference = url._uri_reference.copy_with(**kwargs) else: raise TypeError( - f"Invalid type for url. Expected str or httpx.URL, got {type(url)}: {url!r}" + "Invalid type for url. Expected str or httpx.URL," + f" got {type(url)}: {url!r}" ) @property @@ -305,7 +308,8 @@ class URL: Provides the (scheme, host, port, target) for the outgoing request. In older versions of `httpx` this was used in the low-level transport API. - We no longer use `RawURL`, and this property will be deprecated in a future release. + We no longer use `RawURL`, and this property will be deprecated + in a future release. """ return RawURL( self.raw_scheme, @@ -342,7 +346,9 @@ class URL: For example: - url = httpx.URL("https://www.example.com").copy_with(username="[email protected]", password="a secret") + url = httpx.URL("https://www.example.com").copy_with( + username="[email protected]", password="a secret" + ) assert url == "https://jo%40email.com:a%[email protected]" """ return URL(self, **kwargs) diff --git a/contrib/python/httpx/httpx/_utils.py b/contrib/python/httpx/httpx/_utils.py index ba5807c0487..bc3cb001dd7 100644 --- a/contrib/python/httpx/httpx/_utils.py +++ b/contrib/python/httpx/httpx/_utils.py @@ -152,7 +152,7 @@ SENSITIVE_HEADERS = {"authorization", "proxy-authorization"} def obfuscate_sensitive_headers( - items: typing.Iterable[typing.Tuple[typing.AnyStr, typing.AnyStr]] + items: typing.Iterable[typing.Tuple[typing.AnyStr, typing.AnyStr]], ) -> typing.Iterator[typing.Tuple[typing.AnyStr, typing.AnyStr]]: for k, v in items: if to_str(k.lower()) in SENSITIVE_HEADERS: @@ -227,7 +227,9 @@ def get_environment_proxies() -> typing.Dict[str, typing.Optional[str]]: # (But not "wwwgoogle.com") # NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost" # NO_PROXY=example.com,::1,localhost,192.168.0.0/16 - if is_ipv4_hostname(hostname): + if "://" in hostname: + mounts[hostname] = None + elif is_ipv4_hostname(hostname): mounts[f"all://{hostname}"] = None elif is_ipv6_hostname(hostname): mounts[f"all://[{hostname}]"] = None @@ -293,14 +295,10 @@ class Timer: import trio return trio.current_time() - elif library == "curio": # pragma: no cover - import curio - - return typing.cast(float, await curio.clock()) - - import asyncio + else: + import asyncio - return asyncio.get_event_loop().time() + return asyncio.get_event_loop().time() def sync_start(self) -> None: self.started = time.perf_counter() diff --git a/contrib/python/httpx/ya.make b/contrib/python/httpx/ya.make index f07d3a08ce1..2f8eaa9087f 100644 --- a/contrib/python/httpx/ya.make +++ b/contrib/python/httpx/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(0.25.2) +VERSION(0.26.0) LICENSE(BSD-3-Clause) |
