Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/asttokens/__init__.py +24 -0
- lib/python3.12/site-packages/asttokens/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/astroid_compat.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/asttokens.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/line_numbers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/mark_tokens.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/__pycache__/version.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/asttokens/astroid_compat.py +18 -0
- lib/python3.12/site-packages/asttokens/asttokens.py +450 -0
- lib/python3.12/site-packages/asttokens/line_numbers.py +74 -0
- lib/python3.12/site-packages/asttokens/mark_tokens.py +469 -0
- lib/python3.12/site-packages/asttokens/py.typed +0 -0
- lib/python3.12/site-packages/asttokens/version.py +1 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/INSTALLER +1 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA +202 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD +29 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL +5 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt +22 -0
- lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt +1 -0
- lib/python3.12/site-packages/httpcore/__init__.py +141 -0
- lib/python3.12/site-packages/httpcore/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_api.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_exceptions.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_models.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_ssl.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_synchronization.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_trace.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/__pycache__/_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_api.py +94 -0
- lib/python3.12/site-packages/httpcore/_async/__init__.py +39 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/connection.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/connection_pool.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/http11.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/http2.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/http_proxy.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/interfaces.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_async/connection.py +222 -0
- lib/python3.12/site-packages/httpcore/_async/connection_pool.py +420 -0
- lib/python3.12/site-packages/httpcore/_async/http11.py +379 -0
- lib/python3.12/site-packages/httpcore/_async/http2.py +592 -0
- lib/python3.12/site-packages/httpcore/_async/http_proxy.py +367 -0
- lib/python3.12/site-packages/httpcore/_async/interfaces.py +137 -0
- lib/python3.12/site-packages/httpcore/_async/socks_proxy.py +341 -0
- lib/python3.12/site-packages/httpcore/_backends/__init__.py +0 -0
- lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc +0 -0
lib/python3.12/site-packages/asttokens/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Grist Labs, Inc.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
This module enhances the Python AST tree with token and source code information, sufficent to
|
| 17 |
+
detect the source text of each AST node. This is helpful for tools that make source code
|
| 18 |
+
transformations.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
from .line_numbers import LineNumbers
|
| 22 |
+
from .asttokens import ASTText, ASTTokens, supports_tokenless
|
| 23 |
+
|
| 24 |
+
__all__ = ['ASTText', 'ASTTokens', 'LineNumbers', 'supports_tokenless']
|
lib/python3.12/site-packages/asttokens/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (600 Bytes). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/astroid_compat.cpython-312.pyc
ADDED
|
Binary file (668 Bytes). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/asttokens.cpython-312.pyc
ADDED
|
Binary file (18 kB). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/line_numbers.cpython-312.pyc
ADDED
|
Binary file (3.65 kB). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/mark_tokens.cpython-312.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
lib/python3.12/site-packages/asttokens/__pycache__/version.cpython-312.pyc
ADDED
|
Binary file (213 Bytes). View file
|
|
|
lib/python3.12/site-packages/asttokens/astroid_compat.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from astroid import nodes as astroid_node_classes
|
| 3 |
+
|
| 4 |
+
# astroid_node_classes should be whichever module has the NodeNG class
|
| 5 |
+
from astroid.nodes import NodeNG
|
| 6 |
+
from astroid.nodes import BaseContainer
|
| 7 |
+
except Exception:
|
| 8 |
+
try:
|
| 9 |
+
from astroid import node_classes as astroid_node_classes
|
| 10 |
+
from astroid.node_classes import NodeNG
|
| 11 |
+
from astroid.node_classes import _BaseContainer as BaseContainer
|
| 12 |
+
except Exception: # pragma: no cover
|
| 13 |
+
astroid_node_classes = None
|
| 14 |
+
NodeNG = None
|
| 15 |
+
BaseContainer = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
__all__ = ["astroid_node_classes", "NodeNG", "BaseContainer"]
|
lib/python3.12/site-packages/asttokens/asttokens.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Grist Labs, Inc.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import abc
|
| 16 |
+
import ast
|
| 17 |
+
import bisect
|
| 18 |
+
import sys
|
| 19 |
+
import token
|
| 20 |
+
from ast import Module
|
| 21 |
+
from typing import Iterable, Iterator, List, Optional, Tuple, Any, cast, TYPE_CHECKING
|
| 22 |
+
|
| 23 |
+
from .line_numbers import LineNumbers
|
| 24 |
+
from .util import (
|
| 25 |
+
Token, match_token, is_non_coding_token, patched_generate_tokens, last_stmt,
|
| 26 |
+
annotate_fstring_nodes, generate_tokens, is_module, is_stmt
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING: # pragma: no cover
|
| 30 |
+
from .util import AstNode, TokenInfo
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ASTTextBase(metaclass=abc.ABCMeta):
|
| 34 |
+
def __init__(self, source_text: str, filename: str) -> None:
|
| 35 |
+
self._filename = filename
|
| 36 |
+
|
| 37 |
+
# Decode source after parsing to let Python 2 handle coding declarations.
|
| 38 |
+
# (If the encoding was not utf-8 compatible, then even if it parses correctly,
|
| 39 |
+
# we'll fail with a unicode error here.)
|
| 40 |
+
source_text = str(source_text)
|
| 41 |
+
|
| 42 |
+
self._text = source_text
|
| 43 |
+
self._line_numbers = LineNumbers(source_text)
|
| 44 |
+
|
| 45 |
+
@abc.abstractmethod
|
| 46 |
+
def get_text_positions(self, node, padded):
|
| 47 |
+
# type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
|
| 48 |
+
"""
|
| 49 |
+
Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node.
|
| 50 |
+
If the positions can't be determined, or the nodes don't correspond to any particular text,
|
| 51 |
+
returns ``(1, 0)`` for both.
|
| 52 |
+
|
| 53 |
+
``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``.
|
| 54 |
+
This means that if ``padded`` is True, the start position will be adjusted to include
|
| 55 |
+
leading whitespace if ``node`` is a multiline statement.
|
| 56 |
+
"""
|
| 57 |
+
raise NotImplementedError # pragma: no cover
|
| 58 |
+
|
| 59 |
+
def get_text_range(self, node, padded=True):
|
| 60 |
+
# type: (AstNode, bool) -> Tuple[int, int]
|
| 61 |
+
"""
|
| 62 |
+
Returns the (startpos, endpos) positions in source text corresponding to the given node.
|
| 63 |
+
Returns (0, 0) for nodes (like `Load`) that don't correspond to any particular text.
|
| 64 |
+
|
| 65 |
+
See ``get_text_positions()`` for details on the ``padded`` argument.
|
| 66 |
+
"""
|
| 67 |
+
start, end = self.get_text_positions(node, padded)
|
| 68 |
+
return (
|
| 69 |
+
self._line_numbers.line_to_offset(*start),
|
| 70 |
+
self._line_numbers.line_to_offset(*end),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
def get_text(self, node, padded=True):
|
| 74 |
+
# type: (AstNode, bool) -> str
|
| 75 |
+
"""
|
| 76 |
+
Returns the text corresponding to the given node.
|
| 77 |
+
Returns '' for nodes (like `Load`) that don't correspond to any particular text.
|
| 78 |
+
|
| 79 |
+
See ``get_text_positions()`` for details on the ``padded`` argument.
|
| 80 |
+
"""
|
| 81 |
+
start, end = self.get_text_range(node, padded)
|
| 82 |
+
return self._text[start: end]
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class ASTTokens(ASTTextBase):
|
| 86 |
+
"""
|
| 87 |
+
ASTTokens maintains the text of Python code in several forms: as a string, as line numbers, and
|
| 88 |
+
as tokens, and is used to mark and access token and position information.
|
| 89 |
+
|
| 90 |
+
``source_text`` must be a unicode or UTF8-encoded string. If you pass in UTF8 bytes, remember
|
| 91 |
+
that all offsets you'll get are to the unicode text, which is available as the ``.text``
|
| 92 |
+
property.
|
| 93 |
+
|
| 94 |
+
If ``parse`` is set, the ``source_text`` will be parsed with ``ast.parse()``, and the resulting
|
| 95 |
+
tree marked with token info and made available as the ``.tree`` property.
|
| 96 |
+
|
| 97 |
+
If ``tree`` is given, it will be marked and made available as the ``.tree`` property. In
|
| 98 |
+
addition to the trees produced by the ``ast`` module, ASTTokens will also mark trees produced
|
| 99 |
+
using ``astroid`` library <https://www.astroid.org>.
|
| 100 |
+
|
| 101 |
+
If only ``source_text`` is given, you may use ``.mark_tokens(tree)`` to mark the nodes of an AST
|
| 102 |
+
tree created separately.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def __init__(self, source_text, parse=False, tree=None, filename='<unknown>', tokens=None):
|
| 106 |
+
# type: (Any, bool, Optional[Module], str, Optional[Iterable[TokenInfo]]) -> None
|
| 107 |
+
super(ASTTokens, self).__init__(source_text, filename)
|
| 108 |
+
|
| 109 |
+
self._tree = ast.parse(source_text, filename) if parse else tree
|
| 110 |
+
|
| 111 |
+
# Tokenize the code.
|
| 112 |
+
if tokens is None:
|
| 113 |
+
tokens = generate_tokens(self._text)
|
| 114 |
+
self._tokens = list(self._translate_tokens(tokens))
|
| 115 |
+
|
| 116 |
+
# Extract the start positions of all tokens, so that we can quickly map positions to tokens.
|
| 117 |
+
self._token_offsets = [tok.startpos for tok in self._tokens]
|
| 118 |
+
|
| 119 |
+
if self._tree:
|
| 120 |
+
self.mark_tokens(self._tree)
|
| 121 |
+
|
| 122 |
+
def mark_tokens(self, root_node):
|
| 123 |
+
# type: (Module) -> None
|
| 124 |
+
"""
|
| 125 |
+
Given the root of the AST or Astroid tree produced from source_text, visits all nodes marking
|
| 126 |
+
them with token and position information by adding ``.first_token`` and
|
| 127 |
+
``.last_token`` attributes. This is done automatically in the constructor when ``parse`` or
|
| 128 |
+
``tree`` arguments are set, but may be used manually with a separate AST or Astroid tree.
|
| 129 |
+
"""
|
| 130 |
+
# The hard work of this class is done by MarkTokens
|
| 131 |
+
from .mark_tokens import MarkTokens # to avoid import loops
|
| 132 |
+
MarkTokens(self).visit_tree(root_node)
|
| 133 |
+
|
| 134 |
+
def _translate_tokens(self, original_tokens):
|
| 135 |
+
# type: (Iterable[TokenInfo]) -> Iterator[Token]
|
| 136 |
+
"""
|
| 137 |
+
Translates the given standard library tokens into our own representation.
|
| 138 |
+
"""
|
| 139 |
+
for index, tok in enumerate(patched_generate_tokens(original_tokens)):
|
| 140 |
+
tok_type, tok_str, start, end, line = tok
|
| 141 |
+
yield Token(tok_type, tok_str, start, end, line, index,
|
| 142 |
+
self._line_numbers.line_to_offset(start[0], start[1]),
|
| 143 |
+
self._line_numbers.line_to_offset(end[0], end[1]))
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def text(self):
|
| 147 |
+
# type: () -> str
|
| 148 |
+
"""The source code passed into the constructor."""
|
| 149 |
+
return self._text
|
| 150 |
+
|
| 151 |
+
@property
|
| 152 |
+
def tokens(self):
|
| 153 |
+
# type: () -> List[Token]
|
| 154 |
+
"""The list of tokens corresponding to the source code from the constructor."""
|
| 155 |
+
return self._tokens
|
| 156 |
+
|
| 157 |
+
@property
|
| 158 |
+
def tree(self):
|
| 159 |
+
# type: () -> Optional[Module]
|
| 160 |
+
"""The root of the AST tree passed into the constructor or parsed from the source code."""
|
| 161 |
+
return self._tree
|
| 162 |
+
|
| 163 |
+
@property
|
| 164 |
+
def filename(self):
|
| 165 |
+
# type: () -> str
|
| 166 |
+
"""The filename that was parsed"""
|
| 167 |
+
return self._filename
|
| 168 |
+
|
| 169 |
+
def get_token_from_offset(self, offset):
|
| 170 |
+
# type: (int) -> Token
|
| 171 |
+
"""
|
| 172 |
+
Returns the token containing the given character offset (0-based position in source text),
|
| 173 |
+
or the preceeding token if the position is between tokens.
|
| 174 |
+
"""
|
| 175 |
+
return self._tokens[bisect.bisect(self._token_offsets, offset) - 1]
|
| 176 |
+
|
| 177 |
+
def get_token(self, lineno, col_offset):
|
| 178 |
+
# type: (int, int) -> Token
|
| 179 |
+
"""
|
| 180 |
+
Returns the token containing the given (lineno, col_offset) position, or the preceeding token
|
| 181 |
+
if the position is between tokens.
|
| 182 |
+
"""
|
| 183 |
+
# TODO: add test for multibyte unicode. We need to translate offsets from ast module (which
|
| 184 |
+
# are in utf8) to offsets into the unicode text. tokenize module seems to use unicode offsets
|
| 185 |
+
# but isn't explicit.
|
| 186 |
+
return self.get_token_from_offset(self._line_numbers.line_to_offset(lineno, col_offset))
|
| 187 |
+
|
| 188 |
+
def get_token_from_utf8(self, lineno, col_offset):
|
| 189 |
+
# type: (int, int) -> Token
|
| 190 |
+
"""
|
| 191 |
+
Same as get_token(), but interprets col_offset as a UTF8 offset, which is what `ast` uses.
|
| 192 |
+
"""
|
| 193 |
+
return self.get_token(lineno, self._line_numbers.from_utf8_col(lineno, col_offset))
|
| 194 |
+
|
| 195 |
+
def next_token(self, tok, include_extra=False):
|
| 196 |
+
# type: (Token, bool) -> Token
|
| 197 |
+
"""
|
| 198 |
+
Returns the next token after the given one. If include_extra is True, includes non-coding
|
| 199 |
+
tokens from the tokenize module, such as NL and COMMENT.
|
| 200 |
+
"""
|
| 201 |
+
i = tok.index + 1
|
| 202 |
+
if not include_extra:
|
| 203 |
+
while is_non_coding_token(self._tokens[i].type):
|
| 204 |
+
i += 1
|
| 205 |
+
return self._tokens[i]
|
| 206 |
+
|
| 207 |
+
def prev_token(self, tok, include_extra=False):
|
| 208 |
+
# type: (Token, bool) -> Token
|
| 209 |
+
"""
|
| 210 |
+
Returns the previous token before the given one. If include_extra is True, includes non-coding
|
| 211 |
+
tokens from the tokenize module, such as NL and COMMENT.
|
| 212 |
+
"""
|
| 213 |
+
i = tok.index - 1
|
| 214 |
+
if not include_extra:
|
| 215 |
+
while is_non_coding_token(self._tokens[i].type):
|
| 216 |
+
i -= 1
|
| 217 |
+
return self._tokens[i]
|
| 218 |
+
|
| 219 |
+
def find_token(self, start_token, tok_type, tok_str=None, reverse=False):
|
| 220 |
+
# type: (Token, int, Optional[str], bool) -> Token
|
| 221 |
+
"""
|
| 222 |
+
Looks for the first token, starting at start_token, that matches tok_type and, if given, the
|
| 223 |
+
token string. Searches backwards if reverse is True. Returns ENDMARKER token if not found (you
|
| 224 |
+
can check it with `token.ISEOF(t.type)`).
|
| 225 |
+
"""
|
| 226 |
+
t = start_token
|
| 227 |
+
advance = self.prev_token if reverse else self.next_token
|
| 228 |
+
while not match_token(t, tok_type, tok_str) and not token.ISEOF(t.type):
|
| 229 |
+
t = advance(t, include_extra=True)
|
| 230 |
+
return t
|
| 231 |
+
|
| 232 |
+
def token_range(self,
|
| 233 |
+
first_token, # type: Token
|
| 234 |
+
last_token, # type: Token
|
| 235 |
+
include_extra=False, # type: bool
|
| 236 |
+
):
|
| 237 |
+
# type: (...) -> Iterator[Token]
|
| 238 |
+
"""
|
| 239 |
+
Yields all tokens in order from first_token through and including last_token. If
|
| 240 |
+
include_extra is True, includes non-coding tokens such as tokenize.NL and .COMMENT.
|
| 241 |
+
"""
|
| 242 |
+
for i in range(first_token.index, last_token.index + 1):
|
| 243 |
+
if include_extra or not is_non_coding_token(self._tokens[i].type):
|
| 244 |
+
yield self._tokens[i]
|
| 245 |
+
|
| 246 |
+
def get_tokens(self, node, include_extra=False):
|
| 247 |
+
# type: (AstNode, bool) -> Iterator[Token]
|
| 248 |
+
"""
|
| 249 |
+
Yields all tokens making up the given node. If include_extra is True, includes non-coding
|
| 250 |
+
tokens such as tokenize.NL and .COMMENT.
|
| 251 |
+
"""
|
| 252 |
+
return self.token_range(node.first_token, node.last_token, include_extra=include_extra)
|
| 253 |
+
|
| 254 |
+
def get_text_positions(self, node, padded):
|
| 255 |
+
# type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
|
| 256 |
+
"""
|
| 257 |
+
Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node.
|
| 258 |
+
If the positions can't be determined, or the nodes don't correspond to any particular text,
|
| 259 |
+
returns ``(1, 0)`` for both.
|
| 260 |
+
|
| 261 |
+
``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``.
|
| 262 |
+
This means that if ``padded`` is True, the start position will be adjusted to include
|
| 263 |
+
leading whitespace if ``node`` is a multiline statement.
|
| 264 |
+
"""
|
| 265 |
+
if not hasattr(node, 'first_token'):
|
| 266 |
+
return (1, 0), (1, 0)
|
| 267 |
+
|
| 268 |
+
start = node.first_token.start
|
| 269 |
+
end = node.last_token.end
|
| 270 |
+
if padded and any(match_token(t, token.NEWLINE) for t in self.get_tokens(node)):
|
| 271 |
+
# Set col_offset to 0 to include leading indentation for multiline statements.
|
| 272 |
+
start = (start[0], 0)
|
| 273 |
+
|
| 274 |
+
return start, end
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
class ASTText(ASTTextBase):
|
| 278 |
+
"""
|
| 279 |
+
Supports the same ``get_text*`` methods as ``ASTTokens``,
|
| 280 |
+
but uses the AST to determine the text positions instead of tokens.
|
| 281 |
+
This is faster than ``ASTTokens`` as it requires less setup work.
|
| 282 |
+
|
| 283 |
+
It also (sometimes) supports nodes inside f-strings, which ``ASTTokens`` doesn't.
|
| 284 |
+
|
| 285 |
+
Some node types and/or Python versions are not supported.
|
| 286 |
+
In these cases the ``get_text*`` methods will fall back to using ``ASTTokens``
|
| 287 |
+
which incurs the usual setup cost the first time.
|
| 288 |
+
If you want to avoid this, check ``supports_tokenless(node)`` before calling ``get_text*`` methods.
|
| 289 |
+
"""
|
| 290 |
+
def __init__(self, source_text, tree=None, filename='<unknown>'):
|
| 291 |
+
# type: (Any, Optional[Module], str) -> None
|
| 292 |
+
super(ASTText, self).__init__(source_text, filename)
|
| 293 |
+
|
| 294 |
+
self._tree = tree
|
| 295 |
+
if self._tree is not None:
|
| 296 |
+
annotate_fstring_nodes(self._tree)
|
| 297 |
+
|
| 298 |
+
self._asttokens = None # type: Optional[ASTTokens]
|
| 299 |
+
|
| 300 |
+
@property
|
| 301 |
+
def tree(self):
|
| 302 |
+
# type: () -> Module
|
| 303 |
+
if self._tree is None:
|
| 304 |
+
self._tree = ast.parse(self._text, self._filename)
|
| 305 |
+
annotate_fstring_nodes(self._tree)
|
| 306 |
+
return self._tree
|
| 307 |
+
|
| 308 |
+
@property
|
| 309 |
+
def asttokens(self):
|
| 310 |
+
# type: () -> ASTTokens
|
| 311 |
+
if self._asttokens is None:
|
| 312 |
+
self._asttokens = ASTTokens(
|
| 313 |
+
self._text,
|
| 314 |
+
tree=self.tree,
|
| 315 |
+
filename=self._filename,
|
| 316 |
+
)
|
| 317 |
+
return self._asttokens
|
| 318 |
+
|
| 319 |
+
def _get_text_positions_tokenless(self, node, padded):
|
| 320 |
+
# type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
|
| 321 |
+
"""
|
| 322 |
+
Version of ``get_text_positions()`` that doesn't use tokens.
|
| 323 |
+
"""
|
| 324 |
+
if is_module(node):
|
| 325 |
+
# Modules don't have position info, so just return the range of the whole text.
|
| 326 |
+
# The token-using method does something different, but its behavior seems weird and inconsistent.
|
| 327 |
+
# For example, in a file with only comments, it only returns the first line.
|
| 328 |
+
# It's hard to imagine a case when this matters.
|
| 329 |
+
return (1, 0), self._line_numbers.offset_to_line(len(self._text))
|
| 330 |
+
|
| 331 |
+
if getattr(node, 'lineno', None) is None:
|
| 332 |
+
return (1, 0), (1, 0)
|
| 333 |
+
|
| 334 |
+
assert node # tell mypy that node is not None, which we allowed up to here for compatibility
|
| 335 |
+
|
| 336 |
+
decorators = getattr(node, 'decorator_list', [])
|
| 337 |
+
if not decorators:
|
| 338 |
+
# Astroid uses node.decorators.nodes instead of node.decorator_list.
|
| 339 |
+
decorators_node = getattr(node, 'decorators', None)
|
| 340 |
+
decorators = getattr(decorators_node, 'nodes', [])
|
| 341 |
+
if decorators:
|
| 342 |
+
# Function/Class definition nodes are marked by AST as starting at def/class,
|
| 343 |
+
# not the first decorator. This doesn't match the token-using behavior,
|
| 344 |
+
# or inspect.getsource(), and just seems weird.
|
| 345 |
+
start_node = decorators[0]
|
| 346 |
+
else:
|
| 347 |
+
start_node = node
|
| 348 |
+
|
| 349 |
+
start_lineno = start_node.lineno
|
| 350 |
+
end_node = last_stmt(node)
|
| 351 |
+
|
| 352 |
+
# Include leading indentation for multiline statements.
|
| 353 |
+
# This doesn't mean simple statements that happen to be on multiple lines,
|
| 354 |
+
# but compound statements where inner indentation matters.
|
| 355 |
+
# So we don't just compare node.lineno and node.end_lineno,
|
| 356 |
+
# we check for a contained statement starting on a different line.
|
| 357 |
+
if padded and (
|
| 358 |
+
start_lineno != end_node.lineno
|
| 359 |
+
or (
|
| 360 |
+
# Astroid docstrings aren't treated as separate statements.
|
| 361 |
+
# So to handle function/class definitions with a docstring but no other body,
|
| 362 |
+
# we just check that the node is a statement with a docstring
|
| 363 |
+
# and spanning multiple lines in the simple, literal sense.
|
| 364 |
+
start_lineno != node.end_lineno
|
| 365 |
+
and getattr(node, "doc_node", None)
|
| 366 |
+
and is_stmt(node)
|
| 367 |
+
)
|
| 368 |
+
):
|
| 369 |
+
start_col_offset = 0
|
| 370 |
+
else:
|
| 371 |
+
start_col_offset = self._line_numbers.from_utf8_col(start_lineno, start_node.col_offset)
|
| 372 |
+
|
| 373 |
+
start = (start_lineno, start_col_offset)
|
| 374 |
+
|
| 375 |
+
# To match the token-using behaviour, we exclude trailing semicolons and comments.
|
| 376 |
+
# This means that for blocks containing multiple statements, we have to use the last one
|
| 377 |
+
# instead of the actual node for end_lineno and end_col_offset.
|
| 378 |
+
end_lineno = cast(int, end_node.end_lineno)
|
| 379 |
+
end_col_offset = cast(int, end_node.end_col_offset)
|
| 380 |
+
end_col_offset = self._line_numbers.from_utf8_col(end_lineno, end_col_offset)
|
| 381 |
+
end = (end_lineno, end_col_offset)
|
| 382 |
+
|
| 383 |
+
return start, end
|
| 384 |
+
|
| 385 |
+
def get_text_positions(self, node, padded):
|
| 386 |
+
# type: (AstNode, bool) -> Tuple[Tuple[int, int], Tuple[int, int]]
|
| 387 |
+
"""
|
| 388 |
+
Returns two ``(lineno, col_offset)`` tuples for the start and end of the given node.
|
| 389 |
+
If the positions can't be determined, or the nodes don't correspond to any particular text,
|
| 390 |
+
returns ``(1, 0)`` for both.
|
| 391 |
+
|
| 392 |
+
``padded`` corresponds to the ``padded`` argument to ``ast.get_source_segment()``.
|
| 393 |
+
This means that if ``padded`` is True, the start position will be adjusted to include
|
| 394 |
+
leading whitespace if ``node`` is a multiline statement.
|
| 395 |
+
"""
|
| 396 |
+
if getattr(node, "_broken_positions", None):
|
| 397 |
+
# This node was marked in util.annotate_fstring_nodes as having untrustworthy lineno/col_offset.
|
| 398 |
+
return (1, 0), (1, 0)
|
| 399 |
+
|
| 400 |
+
if supports_tokenless(node):
|
| 401 |
+
return self._get_text_positions_tokenless(node, padded)
|
| 402 |
+
|
| 403 |
+
return self.asttokens.get_text_positions(node, padded)
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
# Node types that _get_text_positions_tokenless doesn't support.
|
| 407 |
+
# These initial values are missing lineno.
|
| 408 |
+
_unsupported_tokenless_types = ("arguments", "Arguments", "withitem") # type: Tuple[str, ...]
|
| 409 |
+
if sys.version_info[:2] == (3, 8):
|
| 410 |
+
# _get_text_positions_tokenless works incorrectly for these types due to bugs in Python 3.8.
|
| 411 |
+
_unsupported_tokenless_types += ("arg", "Starred")
|
| 412 |
+
# no lineno in 3.8
|
| 413 |
+
_unsupported_tokenless_types += ("Slice", "ExtSlice", "Index", "keyword")
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def supports_tokenless(node=None):
|
| 417 |
+
# type: (Any) -> bool
|
| 418 |
+
"""
|
| 419 |
+
Returns True if the Python version and the node (if given) are supported by
|
| 420 |
+
the ``get_text*`` methods of ``ASTText`` without falling back to ``ASTTokens``.
|
| 421 |
+
See ``ASTText`` for why this matters.
|
| 422 |
+
|
| 423 |
+
The following cases are not supported:
|
| 424 |
+
|
| 425 |
+
- PyPy
|
| 426 |
+
- ``ast.arguments`` / ``astroid.Arguments``
|
| 427 |
+
- ``ast.withitem``
|
| 428 |
+
- ``astroid.Comprehension``
|
| 429 |
+
- ``astroid.AssignName`` inside ``astroid.Arguments`` or ``astroid.ExceptHandler``
|
| 430 |
+
- The following nodes in Python 3.8 only:
|
| 431 |
+
- ``ast.arg``
|
| 432 |
+
- ``ast.Starred``
|
| 433 |
+
- ``ast.Slice``
|
| 434 |
+
- ``ast.ExtSlice``
|
| 435 |
+
- ``ast.Index``
|
| 436 |
+
- ``ast.keyword``
|
| 437 |
+
"""
|
| 438 |
+
return (
|
| 439 |
+
type(node).__name__ not in _unsupported_tokenless_types
|
| 440 |
+
and not (
|
| 441 |
+
# astroid nodes
|
| 442 |
+
not isinstance(node, ast.AST) and node is not None and (
|
| 443 |
+
(
|
| 444 |
+
type(node).__name__ == "AssignName"
|
| 445 |
+
and type(node.parent).__name__ in ("Arguments", "ExceptHandler")
|
| 446 |
+
)
|
| 447 |
+
)
|
| 448 |
+
)
|
| 449 |
+
and 'pypy' not in sys.version.lower()
|
| 450 |
+
)
|
lib/python3.12/site-packages/asttokens/line_numbers.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Grist Labs, Inc.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import bisect
|
| 16 |
+
import re
|
| 17 |
+
from typing import Dict, List, Tuple
|
| 18 |
+
|
| 19 |
+
_line_start_re = re.compile(r'^', re.M)
|
| 20 |
+
|
| 21 |
+
class LineNumbers:
|
| 22 |
+
"""
|
| 23 |
+
Class to convert between character offsets in a text string, and pairs (line, column) of 1-based
|
| 24 |
+
line and 0-based column numbers, as used by tokens and AST nodes.
|
| 25 |
+
|
| 26 |
+
This class expects unicode for input and stores positions in unicode. But it supports
|
| 27 |
+
translating to and from utf8 offsets, which are used by ast parsing.
|
| 28 |
+
"""
|
| 29 |
+
def __init__(self, text):
|
| 30 |
+
# type: (str) -> None
|
| 31 |
+
# A list of character offsets of each line's first character.
|
| 32 |
+
self._line_offsets = [m.start(0) for m in _line_start_re.finditer(text)]
|
| 33 |
+
self._text = text
|
| 34 |
+
self._text_len = len(text)
|
| 35 |
+
self._utf8_offset_cache = {} # type: Dict[int, List[int]] # maps line num to list of char offset for each byte in line
|
| 36 |
+
|
| 37 |
+
def from_utf8_col(self, line, utf8_column):
|
| 38 |
+
# type: (int, int) -> int
|
| 39 |
+
"""
|
| 40 |
+
Given a 1-based line number and 0-based utf8 column, returns a 0-based unicode column.
|
| 41 |
+
"""
|
| 42 |
+
offsets = self._utf8_offset_cache.get(line)
|
| 43 |
+
if offsets is None:
|
| 44 |
+
end_offset = self._line_offsets[line] if line < len(self._line_offsets) else self._text_len
|
| 45 |
+
line_text = self._text[self._line_offsets[line - 1] : end_offset]
|
| 46 |
+
|
| 47 |
+
offsets = [i for i,c in enumerate(line_text) for byte in c.encode('utf8')]
|
| 48 |
+
offsets.append(len(line_text))
|
| 49 |
+
self._utf8_offset_cache[line] = offsets
|
| 50 |
+
|
| 51 |
+
return offsets[max(0, min(len(offsets)-1, utf8_column))]
|
| 52 |
+
|
| 53 |
+
def line_to_offset(self, line, column):
|
| 54 |
+
# type: (int, int) -> int
|
| 55 |
+
"""
|
| 56 |
+
Converts 1-based line number and 0-based column to 0-based character offset into text.
|
| 57 |
+
"""
|
| 58 |
+
line -= 1
|
| 59 |
+
if line >= len(self._line_offsets):
|
| 60 |
+
return self._text_len
|
| 61 |
+
elif line < 0:
|
| 62 |
+
return 0
|
| 63 |
+
else:
|
| 64 |
+
return min(self._line_offsets[line] + max(0, column), self._text_len)
|
| 65 |
+
|
| 66 |
+
def offset_to_line(self, offset):
|
| 67 |
+
# type: (int) -> Tuple[int, int]
|
| 68 |
+
"""
|
| 69 |
+
Converts 0-based character offset to pair (line, col) of 1-based line and 0-based column
|
| 70 |
+
numbers.
|
| 71 |
+
"""
|
| 72 |
+
offset = max(0, min(self._text_len, offset))
|
| 73 |
+
line_index = bisect.bisect_right(self._line_offsets, offset) - 1
|
| 74 |
+
return (line_index + 1, offset - self._line_offsets[line_index])
|
lib/python3.12/site-packages/asttokens/mark_tokens.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2016 Grist Labs, Inc.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
import ast
|
| 16 |
+
import numbers
|
| 17 |
+
import sys
|
| 18 |
+
import token
|
| 19 |
+
from ast import Module
|
| 20 |
+
from typing import Callable, List, Union, cast, Optional, Tuple, TYPE_CHECKING
|
| 21 |
+
|
| 22 |
+
from . import util
|
| 23 |
+
from .asttokens import ASTTokens
|
| 24 |
+
from .astroid_compat import astroid_node_classes as nc, BaseContainer as AstroidBaseContainer
|
| 25 |
+
|
| 26 |
+
if TYPE_CHECKING:
|
| 27 |
+
from .util import AstNode
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# Mapping of matching braces. To find a token here, look up token[:2].
|
| 31 |
+
_matching_pairs_left = {
|
| 32 |
+
(token.OP, '('): (token.OP, ')'),
|
| 33 |
+
(token.OP, '['): (token.OP, ']'),
|
| 34 |
+
(token.OP, '{'): (token.OP, '}'),
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
_matching_pairs_right = {
|
| 38 |
+
(token.OP, ')'): (token.OP, '('),
|
| 39 |
+
(token.OP, ']'): (token.OP, '['),
|
| 40 |
+
(token.OP, '}'): (token.OP, '{'),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class MarkTokens:
|
| 45 |
+
"""
|
| 46 |
+
Helper that visits all nodes in the AST tree and assigns .first_token and .last_token attributes
|
| 47 |
+
to each of them. This is the heart of the token-marking logic.
|
| 48 |
+
"""
|
| 49 |
+
def __init__(self, code):
|
| 50 |
+
# type: (ASTTokens) -> None
|
| 51 |
+
self._code = code
|
| 52 |
+
self._methods = util.NodeMethods()
|
| 53 |
+
self._iter_children = None # type: Optional[Callable]
|
| 54 |
+
|
| 55 |
+
def visit_tree(self, node):
|
| 56 |
+
# type: (Module) -> None
|
| 57 |
+
self._iter_children = util.iter_children_func(node)
|
| 58 |
+
util.visit_tree(node, self._visit_before_children, self._visit_after_children)
|
| 59 |
+
|
| 60 |
+
def _visit_before_children(self, node, parent_token):
|
| 61 |
+
# type: (AstNode, Optional[util.Token]) -> Tuple[Optional[util.Token], Optional[util.Token]]
|
| 62 |
+
col = getattr(node, 'col_offset', None)
|
| 63 |
+
token = self._code.get_token_from_utf8(node.lineno, col) if col is not None else None
|
| 64 |
+
|
| 65 |
+
if not token and util.is_module(node):
|
| 66 |
+
# We'll assume that a Module node starts at the start of the source code.
|
| 67 |
+
token = self._code.get_token(1, 0)
|
| 68 |
+
|
| 69 |
+
# Use our own token, or our parent's if we don't have one, to pass to child calls as
|
| 70 |
+
# parent_token argument. The second value becomes the token argument of _visit_after_children.
|
| 71 |
+
return (token or parent_token, token)
|
| 72 |
+
|
| 73 |
+
def _visit_after_children(self, node, parent_token, token):
|
| 74 |
+
# type: (AstNode, Optional[util.Token], Optional[util.Token]) -> None
|
| 75 |
+
# This processes the node generically first, after all children have been processed.
|
| 76 |
+
|
| 77 |
+
# Get the first and last tokens that belong to children. Note how this doesn't assume that we
|
| 78 |
+
# iterate through children in order that corresponds to occurrence in source code. This
|
| 79 |
+
# assumption can fail (e.g. with return annotations).
|
| 80 |
+
first = token
|
| 81 |
+
last = None
|
| 82 |
+
for child in cast(Callable, self._iter_children)(node):
|
| 83 |
+
# astroid slices have especially wrong positions, we don't want them to corrupt their parents.
|
| 84 |
+
if util.is_empty_astroid_slice(child):
|
| 85 |
+
continue
|
| 86 |
+
if not first or child.first_token.index < first.index:
|
| 87 |
+
first = child.first_token
|
| 88 |
+
if not last or child.last_token.index > last.index:
|
| 89 |
+
last = child.last_token
|
| 90 |
+
|
| 91 |
+
# If we don't have a first token from _visit_before_children, and there were no children, then
|
| 92 |
+
# use the parent's token as the first token.
|
| 93 |
+
first = first or parent_token
|
| 94 |
+
|
| 95 |
+
# If no children, set last token to the first one.
|
| 96 |
+
last = last or first
|
| 97 |
+
|
| 98 |
+
# Statements continue to before NEWLINE. This helps cover a few different cases at once.
|
| 99 |
+
if util.is_stmt(node):
|
| 100 |
+
last = self._find_last_in_stmt(cast(util.Token, last))
|
| 101 |
+
|
| 102 |
+
# Capture any unmatched brackets.
|
| 103 |
+
first, last = self._expand_to_matching_pairs(cast(util.Token, first), cast(util.Token, last), node)
|
| 104 |
+
|
| 105 |
+
# Give a chance to node-specific methods to adjust.
|
| 106 |
+
nfirst, nlast = self._methods.get(self, node.__class__)(node, first, last)
|
| 107 |
+
|
| 108 |
+
if (nfirst, nlast) != (first, last):
|
| 109 |
+
# If anything changed, expand again to capture any unmatched brackets.
|
| 110 |
+
nfirst, nlast = self._expand_to_matching_pairs(nfirst, nlast, node)
|
| 111 |
+
|
| 112 |
+
node.first_token = nfirst
|
| 113 |
+
node.last_token = nlast
|
| 114 |
+
|
| 115 |
+
def _find_last_in_stmt(self, start_token):
|
| 116 |
+
# type: (util.Token) -> util.Token
|
| 117 |
+
t = start_token
|
| 118 |
+
while (not util.match_token(t, token.NEWLINE) and
|
| 119 |
+
not util.match_token(t, token.OP, ';') and
|
| 120 |
+
not token.ISEOF(t.type)):
|
| 121 |
+
t = self._code.next_token(t, include_extra=True)
|
| 122 |
+
return self._code.prev_token(t)
|
| 123 |
+
|
| 124 |
+
def _expand_to_matching_pairs(self, first_token, last_token, node):
|
| 125 |
+
# type: (util.Token, util.Token, AstNode) -> Tuple[util.Token, util.Token]
|
| 126 |
+
"""
|
| 127 |
+
Scan tokens in [first_token, last_token] range that are between node's children, and for any
|
| 128 |
+
unmatched brackets, adjust first/last tokens to include the closing pair.
|
| 129 |
+
"""
|
| 130 |
+
# We look for opening parens/braces among non-child tokens (i.e. tokens between our actual
|
| 131 |
+
# child nodes). If we find any closing ones, we match them to the opens.
|
| 132 |
+
to_match_right = [] # type: List[Tuple[int, str]]
|
| 133 |
+
to_match_left = []
|
| 134 |
+
for tok in self._code.token_range(first_token, last_token):
|
| 135 |
+
tok_info = tok[:2]
|
| 136 |
+
if to_match_right and tok_info == to_match_right[-1]:
|
| 137 |
+
to_match_right.pop()
|
| 138 |
+
elif tok_info in _matching_pairs_left:
|
| 139 |
+
to_match_right.append(_matching_pairs_left[tok_info])
|
| 140 |
+
elif tok_info in _matching_pairs_right:
|
| 141 |
+
to_match_left.append(_matching_pairs_right[tok_info])
|
| 142 |
+
|
| 143 |
+
# Once done, extend `last_token` to match any unclosed parens/braces.
|
| 144 |
+
for match in reversed(to_match_right):
|
| 145 |
+
last = self._code.next_token(last_token)
|
| 146 |
+
# Allow for trailing commas or colons (allowed in subscripts) before the closing delimiter
|
| 147 |
+
while any(util.match_token(last, token.OP, x) for x in (',', ':')):
|
| 148 |
+
last = self._code.next_token(last)
|
| 149 |
+
# Now check for the actual closing delimiter.
|
| 150 |
+
if util.match_token(last, *match):
|
| 151 |
+
last_token = last
|
| 152 |
+
|
| 153 |
+
# And extend `first_token` to match any unclosed opening parens/braces.
|
| 154 |
+
for match in to_match_left:
|
| 155 |
+
first = self._code.prev_token(first_token)
|
| 156 |
+
if util.match_token(first, *match):
|
| 157 |
+
first_token = first
|
| 158 |
+
|
| 159 |
+
return (first_token, last_token)
|
| 160 |
+
|
| 161 |
+
#----------------------------------------------------------------------
|
| 162 |
+
# Node visitors. Each takes a preliminary first and last tokens, and returns the adjusted pair
|
| 163 |
+
# that will actually be assigned.
|
| 164 |
+
|
| 165 |
+
def visit_default(self, node, first_token, last_token):
|
| 166 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 167 |
+
# pylint: disable=no-self-use
|
| 168 |
+
# By default, we don't need to adjust the token we computed earlier.
|
| 169 |
+
return (first_token, last_token)
|
| 170 |
+
|
| 171 |
+
def handle_comp(self, open_brace, node, first_token, last_token):
|
| 172 |
+
# type: (str, AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 173 |
+
# For list/set/dict comprehensions, we only get the token of the first child, so adjust it to
|
| 174 |
+
# include the opening brace (the closing brace will be matched automatically).
|
| 175 |
+
before = self._code.prev_token(first_token)
|
| 176 |
+
util.expect_token(before, token.OP, open_brace)
|
| 177 |
+
return (before, last_token)
|
| 178 |
+
|
| 179 |
+
def visit_comprehension(self,
|
| 180 |
+
node, # type: AstNode
|
| 181 |
+
first_token, # type: util.Token
|
| 182 |
+
last_token, # type: util.Token
|
| 183 |
+
):
|
| 184 |
+
# type: (...) -> Tuple[util.Token, util.Token]
|
| 185 |
+
# The 'comprehension' node starts with 'for' but we only get first child; we search backwards
|
| 186 |
+
# to find the 'for' keyword.
|
| 187 |
+
first = self._code.find_token(first_token, token.NAME, 'for', reverse=True)
|
| 188 |
+
return (first, last_token)
|
| 189 |
+
|
| 190 |
+
def visit_if(self, node, first_token, last_token):
|
| 191 |
+
# type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 192 |
+
while first_token.string not in ('if', 'elif'):
|
| 193 |
+
first_token = self._code.prev_token(first_token)
|
| 194 |
+
return first_token, last_token
|
| 195 |
+
|
| 196 |
+
def handle_attr(self, node, first_token, last_token):
|
| 197 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 198 |
+
# Attribute node has ".attr" (2 tokens) after the last child.
|
| 199 |
+
dot = self._code.find_token(last_token, token.OP, '.')
|
| 200 |
+
name = self._code.next_token(dot)
|
| 201 |
+
util.expect_token(name, token.NAME)
|
| 202 |
+
return (first_token, name)
|
| 203 |
+
|
| 204 |
+
visit_attribute = handle_attr
|
| 205 |
+
visit_assignattr = handle_attr
|
| 206 |
+
visit_delattr = handle_attr
|
| 207 |
+
|
| 208 |
+
def handle_def(self, node, first_token, last_token):
|
| 209 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 210 |
+
# With astroid, nodes that start with a doc-string can have an empty body, in which case we
|
| 211 |
+
# need to adjust the last token to include the doc string.
|
| 212 |
+
if not node.body and (getattr(node, 'doc_node', None) or getattr(node, 'doc', None)): # type: ignore[union-attr]
|
| 213 |
+
last_token = self._code.find_token(last_token, token.STRING)
|
| 214 |
+
|
| 215 |
+
# Include @ from decorator
|
| 216 |
+
if first_token.index > 0:
|
| 217 |
+
prev = self._code.prev_token(first_token)
|
| 218 |
+
if util.match_token(prev, token.OP, '@'):
|
| 219 |
+
first_token = prev
|
| 220 |
+
return (first_token, last_token)
|
| 221 |
+
|
| 222 |
+
visit_classdef = handle_def
|
| 223 |
+
visit_functiondef = handle_def
|
| 224 |
+
|
| 225 |
+
def handle_following_brackets(self, node, last_token, opening_bracket):
|
| 226 |
+
# type: (AstNode, util.Token, str) -> util.Token
|
| 227 |
+
# This is for calls and subscripts, which have a pair of brackets
|
| 228 |
+
# at the end which may contain no nodes, e.g. foo() or bar[:].
|
| 229 |
+
# We look for the opening bracket and then let the matching pair be found automatically
|
| 230 |
+
# Remember that last_token is at the end of all children,
|
| 231 |
+
# so we are not worried about encountering a bracket that belongs to a child.
|
| 232 |
+
first_child = next(cast(Callable, self._iter_children)(node))
|
| 233 |
+
call_start = self._code.find_token(first_child.last_token, token.OP, opening_bracket)
|
| 234 |
+
if call_start.index > last_token.index:
|
| 235 |
+
last_token = call_start
|
| 236 |
+
return last_token
|
| 237 |
+
|
| 238 |
+
def visit_call(self, node, first_token, last_token):
|
| 239 |
+
# type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 240 |
+
last_token = self.handle_following_brackets(node, last_token, '(')
|
| 241 |
+
|
| 242 |
+
# Handling a python bug with decorators with empty parens, e.g.
|
| 243 |
+
# @deco()
|
| 244 |
+
# def ...
|
| 245 |
+
if util.match_token(first_token, token.OP, '@'):
|
| 246 |
+
first_token = self._code.next_token(first_token)
|
| 247 |
+
return (first_token, last_token)
|
| 248 |
+
|
| 249 |
+
def visit_matchclass(self, node, first_token, last_token):
|
| 250 |
+
# type: (util.Token, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 251 |
+
last_token = self.handle_following_brackets(node, last_token, '(')
|
| 252 |
+
return (first_token, last_token)
|
| 253 |
+
|
| 254 |
+
def visit_subscript(self,
|
| 255 |
+
node, # type: AstNode
|
| 256 |
+
first_token, # type: util.Token
|
| 257 |
+
last_token, # type: util.Token
|
| 258 |
+
):
|
| 259 |
+
# type: (...) -> Tuple[util.Token, util.Token]
|
| 260 |
+
last_token = self.handle_following_brackets(node, last_token, '[')
|
| 261 |
+
return (first_token, last_token)
|
| 262 |
+
|
| 263 |
+
def visit_slice(self, node, first_token, last_token):
|
| 264 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 265 |
+
# consume `:` tokens to the left and right. In Python 3.9, Slice nodes are
|
| 266 |
+
# given a col_offset, (and end_col_offset), so this will always start inside
|
| 267 |
+
# the slice, even if it is the empty slice. However, in 3.8 and below, this
|
| 268 |
+
# will only expand to the full slice if the slice contains a node with a
|
| 269 |
+
# col_offset. So x[:] will only get the correct tokens in 3.9, but x[1:] and
|
| 270 |
+
# x[:1] will even on earlier versions of Python.
|
| 271 |
+
while True:
|
| 272 |
+
prev = self._code.prev_token(first_token)
|
| 273 |
+
if prev.string != ':':
|
| 274 |
+
break
|
| 275 |
+
first_token = prev
|
| 276 |
+
while True:
|
| 277 |
+
next_ = self._code.next_token(last_token)
|
| 278 |
+
if next_.string != ':':
|
| 279 |
+
break
|
| 280 |
+
last_token = next_
|
| 281 |
+
return (first_token, last_token)
|
| 282 |
+
|
| 283 |
+
def handle_bare_tuple(self, node, first_token, last_token):
|
| 284 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 285 |
+
# A bare tuple doesn't include parens; if there is a trailing comma, make it part of the tuple.
|
| 286 |
+
maybe_comma = self._code.next_token(last_token)
|
| 287 |
+
if util.match_token(maybe_comma, token.OP, ','):
|
| 288 |
+
last_token = maybe_comma
|
| 289 |
+
return (first_token, last_token)
|
| 290 |
+
|
| 291 |
+
# In Python3.8 parsed tuples include parentheses when present.
|
| 292 |
+
def handle_tuple_nonempty(self, node, first_token, last_token):
|
| 293 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 294 |
+
assert isinstance(node, ast.Tuple) or isinstance(node, AstroidBaseContainer)
|
| 295 |
+
# It's a bare tuple if the first token belongs to the first child. The first child may
|
| 296 |
+
# include extraneous parentheses (which don't create new nodes), so account for those too.
|
| 297 |
+
child = node.elts[0]
|
| 298 |
+
if TYPE_CHECKING:
|
| 299 |
+
child = cast(AstNode, child)
|
| 300 |
+
child_first, child_last = self._gobble_parens(child.first_token, child.last_token, True)
|
| 301 |
+
if first_token == child_first:
|
| 302 |
+
return self.handle_bare_tuple(node, first_token, last_token)
|
| 303 |
+
return (first_token, last_token)
|
| 304 |
+
|
| 305 |
+
def visit_tuple(self, node, first_token, last_token):
|
| 306 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 307 |
+
assert isinstance(node, ast.Tuple) or isinstance(node, AstroidBaseContainer)
|
| 308 |
+
if not node.elts:
|
| 309 |
+
# An empty tuple is just "()", and we need no further info.
|
| 310 |
+
return (first_token, last_token)
|
| 311 |
+
return self.handle_tuple_nonempty(node, first_token, last_token)
|
| 312 |
+
|
| 313 |
+
def _gobble_parens(self, first_token, last_token, include_all=False):
|
| 314 |
+
# type: (util.Token, util.Token, bool) -> Tuple[util.Token, util.Token]
|
| 315 |
+
# Expands a range of tokens to include one or all pairs of surrounding parentheses, and
|
| 316 |
+
# returns (first, last) tokens that include these parens.
|
| 317 |
+
while first_token.index > 0:
|
| 318 |
+
prev = self._code.prev_token(first_token)
|
| 319 |
+
next = self._code.next_token(last_token)
|
| 320 |
+
if util.match_token(prev, token.OP, '(') and util.match_token(next, token.OP, ')'):
|
| 321 |
+
first_token, last_token = prev, next
|
| 322 |
+
if include_all:
|
| 323 |
+
continue
|
| 324 |
+
break
|
| 325 |
+
return (first_token, last_token)
|
| 326 |
+
|
| 327 |
+
def visit_str(self, node, first_token, last_token):
|
| 328 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 329 |
+
return self.handle_str(first_token, last_token)
|
| 330 |
+
|
| 331 |
+
def visit_joinedstr(self,
|
| 332 |
+
node, # type: AstNode
|
| 333 |
+
first_token, # type: util.Token
|
| 334 |
+
last_token, # type: util.Token
|
| 335 |
+
):
|
| 336 |
+
# type: (...) -> Tuple[util.Token, util.Token]
|
| 337 |
+
if sys.version_info < (3, 12):
|
| 338 |
+
# Older versions don't tokenize the contents of f-strings
|
| 339 |
+
return self.handle_str(first_token, last_token)
|
| 340 |
+
|
| 341 |
+
last = first_token
|
| 342 |
+
while True:
|
| 343 |
+
if util.match_token(last, getattr(token, "FSTRING_START")):
|
| 344 |
+
# Python 3.12+ has tokens for the start (e.g. `f"`) and end (`"`)
|
| 345 |
+
# of the f-string. We can't just look for the next FSTRING_END
|
| 346 |
+
# because f-strings can be nested, e.g. f"{f'{x}'}", so we need
|
| 347 |
+
# to treat this like matching balanced parentheses.
|
| 348 |
+
count = 1
|
| 349 |
+
while count > 0:
|
| 350 |
+
last = self._code.next_token(last)
|
| 351 |
+
# mypy complains about token.FSTRING_START and token.FSTRING_END.
|
| 352 |
+
if util.match_token(last, getattr(token, "FSTRING_START")):
|
| 353 |
+
count += 1
|
| 354 |
+
elif util.match_token(last, getattr(token, "FSTRING_END")):
|
| 355 |
+
count -= 1
|
| 356 |
+
last_token = last
|
| 357 |
+
last = self._code.next_token(last_token)
|
| 358 |
+
elif util.match_token(last, token.STRING):
|
| 359 |
+
# Similar to handle_str, we also need to handle adjacent strings.
|
| 360 |
+
last_token = last
|
| 361 |
+
last = self._code.next_token(last_token)
|
| 362 |
+
else:
|
| 363 |
+
break
|
| 364 |
+
return (first_token, last_token)
|
| 365 |
+
|
| 366 |
+
def visit_bytes(self, node, first_token, last_token):
|
| 367 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 368 |
+
return self.handle_str(first_token, last_token)
|
| 369 |
+
|
| 370 |
+
def handle_str(self, first_token, last_token):
|
| 371 |
+
# type: (util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 372 |
+
# Multiple adjacent STRING tokens form a single string.
|
| 373 |
+
last = self._code.next_token(last_token)
|
| 374 |
+
while util.match_token(last, token.STRING):
|
| 375 |
+
last_token = last
|
| 376 |
+
last = self._code.next_token(last_token)
|
| 377 |
+
return (first_token, last_token)
|
| 378 |
+
|
| 379 |
+
def handle_num(self,
|
| 380 |
+
node, # type: AstNode
|
| 381 |
+
value, # type: Union[complex, int, numbers.Number]
|
| 382 |
+
first_token, # type: util.Token
|
| 383 |
+
last_token, # type: util.Token
|
| 384 |
+
):
|
| 385 |
+
# type: (...) -> Tuple[util.Token, util.Token]
|
| 386 |
+
# A constant like '-1' gets turned into two tokens; this will skip the '-'.
|
| 387 |
+
while util.match_token(last_token, token.OP):
|
| 388 |
+
last_token = self._code.next_token(last_token)
|
| 389 |
+
|
| 390 |
+
if isinstance(value, complex):
|
| 391 |
+
# A complex number like -2j cannot be compared directly to 0
|
| 392 |
+
# A complex number like 1-2j is expressed as a binary operation
|
| 393 |
+
# so we don't need to worry about it
|
| 394 |
+
value = value.imag
|
| 395 |
+
|
| 396 |
+
# This makes sure that the - is included
|
| 397 |
+
if value < 0 and first_token.type == token.NUMBER: # type: ignore[operator]
|
| 398 |
+
first_token = self._code.prev_token(first_token)
|
| 399 |
+
return (first_token, last_token)
|
| 400 |
+
|
| 401 |
+
def visit_num(self, node, first_token, last_token):
|
| 402 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 403 |
+
n = node.n # type: ignore[union-attr] # ast.Num has been removed in python 3.14
|
| 404 |
+
assert isinstance(n, (complex, int, numbers.Number))
|
| 405 |
+
return self.handle_num(node, n, first_token, last_token)
|
| 406 |
+
|
| 407 |
+
def visit_const(self, node, first_token, last_token):
|
| 408 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 409 |
+
assert isinstance(node, ast.Constant) or isinstance(node, nc.Const)
|
| 410 |
+
if isinstance(node.value, numbers.Number):
|
| 411 |
+
return self.handle_num(node, node.value, first_token, last_token)
|
| 412 |
+
elif isinstance(node.value, (str, bytes)):
|
| 413 |
+
return self.visit_str(node, first_token, last_token)
|
| 414 |
+
return (first_token, last_token)
|
| 415 |
+
|
| 416 |
+
visit_constant = visit_const
|
| 417 |
+
|
| 418 |
+
def visit_keyword(self, node, first_token, last_token):
|
| 419 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 420 |
+
# Until python 3.9 (https://bugs.python.org/issue40141),
|
| 421 |
+
# ast.keyword nodes didn't have line info. Astroid has lineno None.
|
| 422 |
+
assert isinstance(node, ast.keyword) or isinstance(node, nc.Keyword)
|
| 423 |
+
if node.arg is not None and getattr(node, 'lineno', None) is None:
|
| 424 |
+
equals = self._code.find_token(first_token, token.OP, '=', reverse=True)
|
| 425 |
+
name = self._code.prev_token(equals)
|
| 426 |
+
util.expect_token(name, token.NAME, node.arg)
|
| 427 |
+
first_token = name
|
| 428 |
+
return (first_token, last_token)
|
| 429 |
+
|
| 430 |
+
def visit_starred(self, node, first_token, last_token):
|
| 431 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 432 |
+
# Astroid has 'Starred' nodes (for "foo(*bar)" type args), but they need to be adjusted.
|
| 433 |
+
if not util.match_token(first_token, token.OP, '*'):
|
| 434 |
+
star = self._code.prev_token(first_token)
|
| 435 |
+
if util.match_token(star, token.OP, '*'):
|
| 436 |
+
first_token = star
|
| 437 |
+
return (first_token, last_token)
|
| 438 |
+
|
| 439 |
+
def visit_assignname(self, node, first_token, last_token):
|
| 440 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 441 |
+
# Astroid may turn 'except' clause into AssignName, but we need to adjust it.
|
| 442 |
+
if util.match_token(first_token, token.NAME, 'except'):
|
| 443 |
+
colon = self._code.find_token(last_token, token.OP, ':')
|
| 444 |
+
first_token = last_token = self._code.prev_token(colon)
|
| 445 |
+
return (first_token, last_token)
|
| 446 |
+
|
| 447 |
+
# Async nodes should typically start with the word 'async'
|
| 448 |
+
# but Python < 3.7 doesn't put the col_offset there
|
| 449 |
+
# AsyncFunctionDef is slightly different because it might have
|
| 450 |
+
# decorators before that, which visit_functiondef handles
|
| 451 |
+
def handle_async(self, node, first_token, last_token):
|
| 452 |
+
# type: (AstNode, util.Token, util.Token) -> Tuple[util.Token, util.Token]
|
| 453 |
+
if not first_token.string == 'async':
|
| 454 |
+
first_token = self._code.prev_token(first_token)
|
| 455 |
+
return (first_token, last_token)
|
| 456 |
+
|
| 457 |
+
visit_asyncfor = handle_async
|
| 458 |
+
visit_asyncwith = handle_async
|
| 459 |
+
|
| 460 |
+
def visit_asyncfunctiondef(self,
|
| 461 |
+
node, # type: AstNode
|
| 462 |
+
first_token, # type: util.Token
|
| 463 |
+
last_token, # type: util.Token
|
| 464 |
+
):
|
| 465 |
+
# type: (...) -> Tuple[util.Token, util.Token]
|
| 466 |
+
if util.match_token(first_token, token.NAME, 'def'):
|
| 467 |
+
# Include the 'async' token
|
| 468 |
+
first_token = self._code.prev_token(first_token)
|
| 469 |
+
return self.visit_functiondef(node, first_token, last_token)
|
lib/python3.12/site-packages/asttokens/py.typed
ADDED
|
File without changes
|
lib/python3.12/site-packages/asttokens/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__version__ = "3.0.1"
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: h11
|
| 3 |
+
Version: 0.16.0
|
| 4 |
+
Summary: A pure-Python, bring-your-own-I/O implementation of HTTP/1.1
|
| 5 |
+
Home-page: https://github.com/python-hyper/h11
|
| 6 |
+
Author: Nathaniel J. Smith
|
| 7 |
+
Author-email: njs@pobox.com
|
| 8 |
+
License: MIT
|
| 9 |
+
Classifier: Development Status :: 3 - Alpha
|
| 10 |
+
Classifier: Intended Audience :: Developers
|
| 11 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 12 |
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
| 13 |
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
| 14 |
+
Classifier: Programming Language :: Python :: 3
|
| 15 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 16 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 17 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 21 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 22 |
+
Classifier: Topic :: System :: Networking
|
| 23 |
+
Requires-Python: >=3.8
|
| 24 |
+
License-File: LICENSE.txt
|
| 25 |
+
Dynamic: author
|
| 26 |
+
Dynamic: author-email
|
| 27 |
+
Dynamic: classifier
|
| 28 |
+
Dynamic: description
|
| 29 |
+
Dynamic: home-page
|
| 30 |
+
Dynamic: license
|
| 31 |
+
Dynamic: license-file
|
| 32 |
+
Dynamic: requires-python
|
| 33 |
+
Dynamic: summary
|
| 34 |
+
|
| 35 |
+
h11
|
| 36 |
+
===
|
| 37 |
+
|
| 38 |
+
.. image:: https://travis-ci.org/python-hyper/h11.svg?branch=master
|
| 39 |
+
:target: https://travis-ci.org/python-hyper/h11
|
| 40 |
+
:alt: Automated test status
|
| 41 |
+
|
| 42 |
+
.. image:: https://codecov.io/gh/python-hyper/h11/branch/master/graph/badge.svg
|
| 43 |
+
:target: https://codecov.io/gh/python-hyper/h11
|
| 44 |
+
:alt: Test coverage
|
| 45 |
+
|
| 46 |
+
.. image:: https://readthedocs.org/projects/h11/badge/?version=latest
|
| 47 |
+
:target: http://h11.readthedocs.io/en/latest/?badge=latest
|
| 48 |
+
:alt: Documentation Status
|
| 49 |
+
|
| 50 |
+
This is a little HTTP/1.1 library written from scratch in Python,
|
| 51 |
+
heavily inspired by `hyper-h2 <https://hyper-h2.readthedocs.io/>`_.
|
| 52 |
+
|
| 53 |
+
It's a "bring-your-own-I/O" library; h11 contains no IO code
|
| 54 |
+
whatsoever. This means you can hook h11 up to your favorite network
|
| 55 |
+
API, and that could be anything you want: synchronous, threaded,
|
| 56 |
+
asynchronous, or your own implementation of `RFC 6214
|
| 57 |
+
<https://tools.ietf.org/html/rfc6214>`_ -- h11 won't judge you.
|
| 58 |
+
(Compare this to the current state of the art, where every time a `new
|
| 59 |
+
network API <https://trio.readthedocs.io/>`_ comes along then someone
|
| 60 |
+
gets to start over reimplementing the entire HTTP protocol from
|
| 61 |
+
scratch.) Cory Benfield made an `excellent blog post describing the
|
| 62 |
+
benefits of this approach
|
| 63 |
+
<https://lukasa.co.uk/2015/10/The_New_Hyper/>`_, or if you like video
|
| 64 |
+
then here's his `PyCon 2016 talk on the same theme
|
| 65 |
+
<https://www.youtube.com/watch?v=7cC3_jGwl_U>`_.
|
| 66 |
+
|
| 67 |
+
This also means that h11 is not immediately useful out of the box:
|
| 68 |
+
it's a toolkit for building programs that speak HTTP, not something
|
| 69 |
+
that could directly replace ``requests`` or ``twisted.web`` or
|
| 70 |
+
whatever. But h11 makes it much easier to implement something like
|
| 71 |
+
``requests`` or ``twisted.web``.
|
| 72 |
+
|
| 73 |
+
At a high level, working with h11 goes like this:
|
| 74 |
+
|
| 75 |
+
1) First, create an ``h11.Connection`` object to track the state of a
|
| 76 |
+
single HTTP/1.1 connection.
|
| 77 |
+
|
| 78 |
+
2) When you read data off the network, pass it to
|
| 79 |
+
``conn.receive_data(...)``; you'll get back a list of objects
|
| 80 |
+
representing high-level HTTP "events".
|
| 81 |
+
|
| 82 |
+
3) When you want to send a high-level HTTP event, create the
|
| 83 |
+
corresponding "event" object and pass it to ``conn.send(...)``;
|
| 84 |
+
this will give you back some bytes that you can then push out
|
| 85 |
+
through the network.
|
| 86 |
+
|
| 87 |
+
For example, a client might instantiate and then send a
|
| 88 |
+
``h11.Request`` object, then zero or more ``h11.Data`` objects for the
|
| 89 |
+
request body (e.g., if this is a POST), and then a
|
| 90 |
+
``h11.EndOfMessage`` to indicate the end of the message. Then the
|
| 91 |
+
server would then send back a ``h11.Response``, some ``h11.Data``, and
|
| 92 |
+
its own ``h11.EndOfMessage``. If either side violates the protocol,
|
| 93 |
+
you'll get a ``h11.ProtocolError`` exception.
|
| 94 |
+
|
| 95 |
+
h11 is suitable for implementing both servers and clients, and has a
|
| 96 |
+
pleasantly symmetric API: the events you send as a client are exactly
|
| 97 |
+
the ones that you receive as a server and vice-versa.
|
| 98 |
+
|
| 99 |
+
`Here's an example of a tiny HTTP client
|
| 100 |
+
<https://github.com/python-hyper/h11/blob/master/examples/basic-client.py>`_
|
| 101 |
+
|
| 102 |
+
It also has `a fine manual <https://h11.readthedocs.io/>`_.
|
| 103 |
+
|
| 104 |
+
FAQ
|
| 105 |
+
---
|
| 106 |
+
|
| 107 |
+
*Whyyyyy?*
|
| 108 |
+
|
| 109 |
+
I wanted to play with HTTP in `Curio
|
| 110 |
+
<https://curio.readthedocs.io/en/latest/tutorial.html>`__ and `Trio
|
| 111 |
+
<https://trio.readthedocs.io>`__, which at the time didn't have any
|
| 112 |
+
HTTP libraries. So I thought, no big deal, Python has, like, a dozen
|
| 113 |
+
different implementations of HTTP, surely I can find one that's
|
| 114 |
+
reusable. I didn't find one, but I did find Cory's call-to-arms
|
| 115 |
+
blog-post. So I figured, well, fine, if I have to implement HTTP from
|
| 116 |
+
scratch, at least I can make sure no-one *else* has to ever again.
|
| 117 |
+
|
| 118 |
+
*Should I use it?*
|
| 119 |
+
|
| 120 |
+
Maybe. You should be aware that it's a very young project. But, it's
|
| 121 |
+
feature complete and has an exhaustive test-suite and complete docs,
|
| 122 |
+
so the next step is for people to try using it and see how it goes
|
| 123 |
+
:-). If you do then please let us know -- if nothing else we'll want
|
| 124 |
+
to talk to you before making any incompatible changes!
|
| 125 |
+
|
| 126 |
+
*What are the features/limitations?*
|
| 127 |
+
|
| 128 |
+
Roughly speaking, it's trying to be a robust, complete, and non-hacky
|
| 129 |
+
implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230:
|
| 130 |
+
HTTP/1.1 Message Syntax and Routing
|
| 131 |
+
<https://tools.ietf.org/html/rfc7230>`_. That is, it mostly focuses on
|
| 132 |
+
implementing HTTP at the level of taking bytes on and off the wire,
|
| 133 |
+
and the headers related to that, and tries to be anal about spec
|
| 134 |
+
conformance. It doesn't know about higher-level concerns like URL
|
| 135 |
+
routing, conditional GETs, cross-origin cookie policies, or content
|
| 136 |
+
negotiation. But it does know how to take care of framing,
|
| 137 |
+
cross-version differences in keep-alive handling, and the "obsolete
|
| 138 |
+
line folding" rule, so you can focus your energies on the hard /
|
| 139 |
+
interesting parts for your application, and it tries to support the
|
| 140 |
+
full specification in the sense that any useful HTTP/1.1 conformant
|
| 141 |
+
application should be able to use h11.
|
| 142 |
+
|
| 143 |
+
It's pure Python, and has no dependencies outside of the standard
|
| 144 |
+
library.
|
| 145 |
+
|
| 146 |
+
It has a test suite with 100.0% coverage for both statements and
|
| 147 |
+
branches.
|
| 148 |
+
|
| 149 |
+
Currently it supports Python 3 (testing on 3.8-3.12) and PyPy 3.
|
| 150 |
+
The last Python 2-compatible version was h11 0.11.x.
|
| 151 |
+
(Originally it had a Cython wrapper for `http-parser
|
| 152 |
+
<https://github.com/nodejs/http-parser>`_ and a beautiful nested state
|
| 153 |
+
machine implemented with ``yield from`` to postprocess the output. But
|
| 154 |
+
I had to take these out -- the new *parser* needs fewer lines-of-code
|
| 155 |
+
than the old *parser wrapper*, is written in pure Python, uses no
|
| 156 |
+
exotic language syntax, and has more features. It's sad, really; that
|
| 157 |
+
old state machine was really slick. I just need a few sentences here
|
| 158 |
+
to mourn that.)
|
| 159 |
+
|
| 160 |
+
I don't know how fast it is. I haven't benchmarked or profiled it yet,
|
| 161 |
+
so it's probably got a few pointless hot spots, and I've been trying
|
| 162 |
+
to err on the side of simplicity and robustness instead of
|
| 163 |
+
micro-optimization. But at the architectural level I tried hard to
|
| 164 |
+
avoid fundamentally bad decisions, e.g., I believe that all the
|
| 165 |
+
parsing algorithms remain linear-time even in the face of pathological
|
| 166 |
+
input like slowloris, and there are no byte-by-byte loops. (I also
|
| 167 |
+
believe that it maintains bounded memory usage in the face of
|
| 168 |
+
arbitrary/pathological input.)
|
| 169 |
+
|
| 170 |
+
The whole library is ~800 lines-of-code. You can read and understand
|
| 171 |
+
the whole thing in less than an hour. Most of the energy invested in
|
| 172 |
+
this so far has been spent on trying to keep things simple by
|
| 173 |
+
minimizing special-cases and ad hoc state manipulation; even though it
|
| 174 |
+
is now quite small and simple, I'm still annoyed that I haven't
|
| 175 |
+
figured out how to make it even smaller and simpler. (Unfortunately,
|
| 176 |
+
HTTP does not lend itself to simplicity.)
|
| 177 |
+
|
| 178 |
+
The API is ~feature complete and I don't expect the general outlines
|
| 179 |
+
to change much, but you can't judge an API's ergonomics until you
|
| 180 |
+
actually document and use it, so I'd expect some changes in the
|
| 181 |
+
details.
|
| 182 |
+
|
| 183 |
+
*How do I try it?*
|
| 184 |
+
|
| 185 |
+
.. code-block:: sh
|
| 186 |
+
|
| 187 |
+
$ pip install h11
|
| 188 |
+
$ git clone git@github.com:python-hyper/h11
|
| 189 |
+
$ cd h11/examples
|
| 190 |
+
$ python basic-client.py
|
| 191 |
+
|
| 192 |
+
and go from there.
|
| 193 |
+
|
| 194 |
+
*License?*
|
| 195 |
+
|
| 196 |
+
MIT
|
| 197 |
+
|
| 198 |
+
*Code of conduct?*
|
| 199 |
+
|
| 200 |
+
Contributors are requested to follow our `code of conduct
|
| 201 |
+
<https://github.com/python-hyper/h11/blob/master/CODE_OF_CONDUCT.md>`_ in
|
| 202 |
+
all project spaces.
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
h11-0.16.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
h11-0.16.0.dist-info/METADATA,sha256=KPMmCYrAn8unm48YD5YIfIQf4kViFct7hyqcfVzRnWQ,8348
|
| 3 |
+
h11-0.16.0.dist-info/RECORD,,
|
| 4 |
+
h11-0.16.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
| 5 |
+
h11-0.16.0.dist-info/licenses/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124
|
| 6 |
+
h11-0.16.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4
|
| 7 |
+
h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507
|
| 8 |
+
h11/__pycache__/__init__.cpython-312.pyc,,
|
| 9 |
+
h11/__pycache__/_abnf.cpython-312.pyc,,
|
| 10 |
+
h11/__pycache__/_connection.cpython-312.pyc,,
|
| 11 |
+
h11/__pycache__/_events.cpython-312.pyc,,
|
| 12 |
+
h11/__pycache__/_headers.cpython-312.pyc,,
|
| 13 |
+
h11/__pycache__/_readers.cpython-312.pyc,,
|
| 14 |
+
h11/__pycache__/_receivebuffer.cpython-312.pyc,,
|
| 15 |
+
h11/__pycache__/_state.cpython-312.pyc,,
|
| 16 |
+
h11/__pycache__/_util.cpython-312.pyc,,
|
| 17 |
+
h11/__pycache__/_version.cpython-312.pyc,,
|
| 18 |
+
h11/__pycache__/_writers.cpython-312.pyc,,
|
| 19 |
+
h11/_abnf.py,sha256=ybixr0xsupnkA6GFAyMubuXF6Tc1lb_hF890NgCsfNc,4815
|
| 20 |
+
h11/_connection.py,sha256=k9YRVf6koZqbttBW36xSWaJpWdZwa-xQVU9AHEo9DuI,26863
|
| 21 |
+
h11/_events.py,sha256=I97aXoal1Wu7dkL548BANBUCkOIbe-x5CioYA9IBY14,11792
|
| 22 |
+
h11/_headers.py,sha256=P7D-lBNxHwdLZPLimmYwrPG-9ZkjElvvJZJdZAgSP-4,10412
|
| 23 |
+
h11/_readers.py,sha256=a4RypORUCC3d0q_kxPuBIM7jTD8iLt5X91TH0FsduN4,8590
|
| 24 |
+
h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252
|
| 25 |
+
h11/_state.py,sha256=_5LG_BGR8FCcFQeBPH-TMHgm_-B-EUcWCnQof_9XjFE,13231
|
| 26 |
+
h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888
|
| 27 |
+
h11/_version.py,sha256=GVSsbPSPDcOuF6ptfIiXnVJoaEm3ygXbMnqlr_Giahw,686
|
| 28 |
+
h11/_writers.py,sha256=oFKm6PtjeHfbj4RLX7VB7KDc1gIY53gXG3_HR9ltmTA,5081
|
| 29 |
+
h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: setuptools (78.1.0)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MIT License (MIT)
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2016 Nathaniel J. Smith <njs@pobox.com> and other contributors
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining
|
| 6 |
+
a copy of this software and associated documentation files (the
|
| 7 |
+
"Software"), to deal in the Software without restriction, including
|
| 8 |
+
without limitation the rights to use, copy, modify, merge, publish,
|
| 9 |
+
distribute, sublicense, and/or sell copies of the Software, and to
|
| 10 |
+
permit persons to whom the Software is furnished to do so, subject to
|
| 11 |
+
the following conditions:
|
| 12 |
+
|
| 13 |
+
The above copyright notice and this permission notice shall be
|
| 14 |
+
included in all copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 17 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
| 18 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
| 19 |
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
| 20 |
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
| 21 |
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
| 22 |
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
lib/python3.12/site-packages/h11-0.16.0.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
h11
|
lib/python3.12/site-packages/httpcore/__init__.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ._api import request, stream
|
| 2 |
+
from ._async import (
|
| 3 |
+
AsyncConnectionInterface,
|
| 4 |
+
AsyncConnectionPool,
|
| 5 |
+
AsyncHTTP2Connection,
|
| 6 |
+
AsyncHTTP11Connection,
|
| 7 |
+
AsyncHTTPConnection,
|
| 8 |
+
AsyncHTTPProxy,
|
| 9 |
+
AsyncSOCKSProxy,
|
| 10 |
+
)
|
| 11 |
+
from ._backends.base import (
|
| 12 |
+
SOCKET_OPTION,
|
| 13 |
+
AsyncNetworkBackend,
|
| 14 |
+
AsyncNetworkStream,
|
| 15 |
+
NetworkBackend,
|
| 16 |
+
NetworkStream,
|
| 17 |
+
)
|
| 18 |
+
from ._backends.mock import AsyncMockBackend, AsyncMockStream, MockBackend, MockStream
|
| 19 |
+
from ._backends.sync import SyncBackend
|
| 20 |
+
from ._exceptions import (
|
| 21 |
+
ConnectError,
|
| 22 |
+
ConnectionNotAvailable,
|
| 23 |
+
ConnectTimeout,
|
| 24 |
+
LocalProtocolError,
|
| 25 |
+
NetworkError,
|
| 26 |
+
PoolTimeout,
|
| 27 |
+
ProtocolError,
|
| 28 |
+
ProxyError,
|
| 29 |
+
ReadError,
|
| 30 |
+
ReadTimeout,
|
| 31 |
+
RemoteProtocolError,
|
| 32 |
+
TimeoutException,
|
| 33 |
+
UnsupportedProtocol,
|
| 34 |
+
WriteError,
|
| 35 |
+
WriteTimeout,
|
| 36 |
+
)
|
| 37 |
+
from ._models import URL, Origin, Proxy, Request, Response
|
| 38 |
+
from ._ssl import default_ssl_context
|
| 39 |
+
from ._sync import (
|
| 40 |
+
ConnectionInterface,
|
| 41 |
+
ConnectionPool,
|
| 42 |
+
HTTP2Connection,
|
| 43 |
+
HTTP11Connection,
|
| 44 |
+
HTTPConnection,
|
| 45 |
+
HTTPProxy,
|
| 46 |
+
SOCKSProxy,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# The 'httpcore.AnyIOBackend' class is conditional on 'anyio' being installed.
|
| 50 |
+
try:
|
| 51 |
+
from ._backends.anyio import AnyIOBackend
|
| 52 |
+
except ImportError: # pragma: nocover
|
| 53 |
+
|
| 54 |
+
class AnyIOBackend: # type: ignore
|
| 55 |
+
def __init__(self, *args, **kwargs): # type: ignore
|
| 56 |
+
msg = (
|
| 57 |
+
"Attempted to use 'httpcore.AnyIOBackend' but 'anyio' is not installed."
|
| 58 |
+
)
|
| 59 |
+
raise RuntimeError(msg)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# The 'httpcore.TrioBackend' class is conditional on 'trio' being installed.
|
| 63 |
+
try:
|
| 64 |
+
from ._backends.trio import TrioBackend
|
| 65 |
+
except ImportError: # pragma: nocover
|
| 66 |
+
|
| 67 |
+
class TrioBackend: # type: ignore
|
| 68 |
+
def __init__(self, *args, **kwargs): # type: ignore
|
| 69 |
+
msg = "Attempted to use 'httpcore.TrioBackend' but 'trio' is not installed."
|
| 70 |
+
raise RuntimeError(msg)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
__all__ = [
|
| 74 |
+
# top-level requests
|
| 75 |
+
"request",
|
| 76 |
+
"stream",
|
| 77 |
+
# models
|
| 78 |
+
"Origin",
|
| 79 |
+
"URL",
|
| 80 |
+
"Request",
|
| 81 |
+
"Response",
|
| 82 |
+
"Proxy",
|
| 83 |
+
# async
|
| 84 |
+
"AsyncHTTPConnection",
|
| 85 |
+
"AsyncConnectionPool",
|
| 86 |
+
"AsyncHTTPProxy",
|
| 87 |
+
"AsyncHTTP11Connection",
|
| 88 |
+
"AsyncHTTP2Connection",
|
| 89 |
+
"AsyncConnectionInterface",
|
| 90 |
+
"AsyncSOCKSProxy",
|
| 91 |
+
# sync
|
| 92 |
+
"HTTPConnection",
|
| 93 |
+
"ConnectionPool",
|
| 94 |
+
"HTTPProxy",
|
| 95 |
+
"HTTP11Connection",
|
| 96 |
+
"HTTP2Connection",
|
| 97 |
+
"ConnectionInterface",
|
| 98 |
+
"SOCKSProxy",
|
| 99 |
+
# network backends, implementations
|
| 100 |
+
"SyncBackend",
|
| 101 |
+
"AnyIOBackend",
|
| 102 |
+
"TrioBackend",
|
| 103 |
+
# network backends, mock implementations
|
| 104 |
+
"AsyncMockBackend",
|
| 105 |
+
"AsyncMockStream",
|
| 106 |
+
"MockBackend",
|
| 107 |
+
"MockStream",
|
| 108 |
+
# network backends, interface
|
| 109 |
+
"AsyncNetworkStream",
|
| 110 |
+
"AsyncNetworkBackend",
|
| 111 |
+
"NetworkStream",
|
| 112 |
+
"NetworkBackend",
|
| 113 |
+
# util
|
| 114 |
+
"default_ssl_context",
|
| 115 |
+
"SOCKET_OPTION",
|
| 116 |
+
# exceptions
|
| 117 |
+
"ConnectionNotAvailable",
|
| 118 |
+
"ProxyError",
|
| 119 |
+
"ProtocolError",
|
| 120 |
+
"LocalProtocolError",
|
| 121 |
+
"RemoteProtocolError",
|
| 122 |
+
"UnsupportedProtocol",
|
| 123 |
+
"TimeoutException",
|
| 124 |
+
"PoolTimeout",
|
| 125 |
+
"ConnectTimeout",
|
| 126 |
+
"ReadTimeout",
|
| 127 |
+
"WriteTimeout",
|
| 128 |
+
"NetworkError",
|
| 129 |
+
"ConnectError",
|
| 130 |
+
"ReadError",
|
| 131 |
+
"WriteError",
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
__version__ = "1.0.9"
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
__locals = locals()
|
| 138 |
+
for __name in __all__:
|
| 139 |
+
# Exclude SOCKET_OPTION, it causes AttributeError on Python 3.14
|
| 140 |
+
if not __name.startswith(("__", "SOCKET_OPTION")):
|
| 141 |
+
setattr(__locals[__name], "__module__", "httpcore") # noqa
|
lib/python3.12/site-packages/httpcore/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (3.16 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_api.cpython-312.pyc
ADDED
|
Binary file (3.75 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_exceptions.cpython-312.pyc
ADDED
|
Binary file (3.18 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_models.cpython-312.pyc
ADDED
|
Binary file (23.1 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_ssl.cpython-312.pyc
ADDED
|
Binary file (608 Bytes). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_synchronization.cpython-312.pyc
ADDED
|
Binary file (14.2 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_trace.cpython-312.pyc
ADDED
|
Binary file (5.61 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/__pycache__/_utils.cpython-312.pyc
ADDED
|
Binary file (1.29 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_api.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from ._models import URL, Extensions, HeaderTypes, Response
|
| 7 |
+
from ._sync.connection_pool import ConnectionPool
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def request(
|
| 11 |
+
method: bytes | str,
|
| 12 |
+
url: URL | bytes | str,
|
| 13 |
+
*,
|
| 14 |
+
headers: HeaderTypes = None,
|
| 15 |
+
content: bytes | typing.Iterator[bytes] | None = None,
|
| 16 |
+
extensions: Extensions | None = None,
|
| 17 |
+
) -> Response:
|
| 18 |
+
"""
|
| 19 |
+
Sends an HTTP request, returning the response.
|
| 20 |
+
|
| 21 |
+
```
|
| 22 |
+
response = httpcore.request("GET", "https://www.example.com/")
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
Arguments:
|
| 26 |
+
method: The HTTP method for the request. Typically one of `"GET"`,
|
| 27 |
+
`"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`.
|
| 28 |
+
url: The URL of the HTTP request. Either as an instance of `httpcore.URL`,
|
| 29 |
+
or as str/bytes.
|
| 30 |
+
headers: The HTTP request headers. Either as a dictionary of str/bytes,
|
| 31 |
+
or as a list of two-tuples of str/bytes.
|
| 32 |
+
content: The content of the request body. Either as bytes,
|
| 33 |
+
or as a bytes iterator.
|
| 34 |
+
extensions: A dictionary of optional extra information included on the request.
|
| 35 |
+
Possible keys include `"timeout"`.
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
An instance of `httpcore.Response`.
|
| 39 |
+
"""
|
| 40 |
+
with ConnectionPool() as pool:
|
| 41 |
+
return pool.request(
|
| 42 |
+
method=method,
|
| 43 |
+
url=url,
|
| 44 |
+
headers=headers,
|
| 45 |
+
content=content,
|
| 46 |
+
extensions=extensions,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@contextlib.contextmanager
|
| 51 |
+
def stream(
|
| 52 |
+
method: bytes | str,
|
| 53 |
+
url: URL | bytes | str,
|
| 54 |
+
*,
|
| 55 |
+
headers: HeaderTypes = None,
|
| 56 |
+
content: bytes | typing.Iterator[bytes] | None = None,
|
| 57 |
+
extensions: Extensions | None = None,
|
| 58 |
+
) -> typing.Iterator[Response]:
|
| 59 |
+
"""
|
| 60 |
+
Sends an HTTP request, returning the response within a content manager.
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
with httpcore.stream("GET", "https://www.example.com/") as response:
|
| 64 |
+
...
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
When using the `stream()` function, the body of the response will not be
|
| 68 |
+
automatically read. If you want to access the response body you should
|
| 69 |
+
either use `content = response.read()`, or `for chunk in response.iter_content()`.
|
| 70 |
+
|
| 71 |
+
Arguments:
|
| 72 |
+
method: The HTTP method for the request. Typically one of `"GET"`,
|
| 73 |
+
`"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`.
|
| 74 |
+
url: The URL of the HTTP request. Either as an instance of `httpcore.URL`,
|
| 75 |
+
or as str/bytes.
|
| 76 |
+
headers: The HTTP request headers. Either as a dictionary of str/bytes,
|
| 77 |
+
or as a list of two-tuples of str/bytes.
|
| 78 |
+
content: The content of the request body. Either as bytes,
|
| 79 |
+
or as a bytes iterator.
|
| 80 |
+
extensions: A dictionary of optional extra information included on the request.
|
| 81 |
+
Possible keys include `"timeout"`.
|
| 82 |
+
|
| 83 |
+
Returns:
|
| 84 |
+
An instance of `httpcore.Response`.
|
| 85 |
+
"""
|
| 86 |
+
with ConnectionPool() as pool:
|
| 87 |
+
with pool.stream(
|
| 88 |
+
method=method,
|
| 89 |
+
url=url,
|
| 90 |
+
headers=headers,
|
| 91 |
+
content=content,
|
| 92 |
+
extensions=extensions,
|
| 93 |
+
) as response:
|
| 94 |
+
yield response
|
lib/python3.12/site-packages/httpcore/_async/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .connection import AsyncHTTPConnection
|
| 2 |
+
from .connection_pool import AsyncConnectionPool
|
| 3 |
+
from .http11 import AsyncHTTP11Connection
|
| 4 |
+
from .http_proxy import AsyncHTTPProxy
|
| 5 |
+
from .interfaces import AsyncConnectionInterface
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from .http2 import AsyncHTTP2Connection
|
| 9 |
+
except ImportError: # pragma: nocover
|
| 10 |
+
|
| 11 |
+
class AsyncHTTP2Connection: # type: ignore
|
| 12 |
+
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
| 13 |
+
raise RuntimeError(
|
| 14 |
+
"Attempted to use http2 support, but the `h2` package is not "
|
| 15 |
+
"installed. Use 'pip install httpcore[http2]'."
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from .socks_proxy import AsyncSOCKSProxy
|
| 21 |
+
except ImportError: # pragma: nocover
|
| 22 |
+
|
| 23 |
+
class AsyncSOCKSProxy: # type: ignore
|
| 24 |
+
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
| 25 |
+
raise RuntimeError(
|
| 26 |
+
"Attempted to use SOCKS support, but the `socksio` package is not "
|
| 27 |
+
"installed. Use 'pip install httpcore[socks]'."
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
"AsyncHTTPConnection",
|
| 33 |
+
"AsyncConnectionPool",
|
| 34 |
+
"AsyncHTTPProxy",
|
| 35 |
+
"AsyncHTTP11Connection",
|
| 36 |
+
"AsyncHTTP2Connection",
|
| 37 |
+
"AsyncConnectionInterface",
|
| 38 |
+
"AsyncSOCKSProxy",
|
| 39 |
+
]
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.62 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/connection.cpython-312.pyc
ADDED
|
Binary file (11.7 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/connection_pool.cpython-312.pyc
ADDED
|
Binary file (19.6 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/http11.cpython-312.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/http2.cpython-312.pyc
ADDED
|
Binary file (31.2 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/http_proxy.cpython-312.pyc
ADDED
|
Binary file (18 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/interfaces.cpython-312.pyc
ADDED
|
Binary file (5.75 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc
ADDED
|
Binary file (16.8 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_async/connection.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import itertools
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import types
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
from .._backends.auto import AutoBackend
|
| 10 |
+
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
|
| 11 |
+
from .._exceptions import ConnectError, ConnectTimeout
|
| 12 |
+
from .._models import Origin, Request, Response
|
| 13 |
+
from .._ssl import default_ssl_context
|
| 14 |
+
from .._synchronization import AsyncLock
|
| 15 |
+
from .._trace import Trace
|
| 16 |
+
from .http11 import AsyncHTTP11Connection
|
| 17 |
+
from .interfaces import AsyncConnectionInterface
|
| 18 |
+
|
| 19 |
+
RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc.
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger("httpcore.connection")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def exponential_backoff(factor: float) -> typing.Iterator[float]:
|
| 26 |
+
"""
|
| 27 |
+
Generate a geometric sequence that has a ratio of 2 and starts with 0.
|
| 28 |
+
|
| 29 |
+
For example:
|
| 30 |
+
- `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...`
|
| 31 |
+
- `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...`
|
| 32 |
+
"""
|
| 33 |
+
yield 0
|
| 34 |
+
for n in itertools.count():
|
| 35 |
+
yield factor * 2**n
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class AsyncHTTPConnection(AsyncConnectionInterface):
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
origin: Origin,
|
| 42 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 43 |
+
keepalive_expiry: float | None = None,
|
| 44 |
+
http1: bool = True,
|
| 45 |
+
http2: bool = False,
|
| 46 |
+
retries: int = 0,
|
| 47 |
+
local_address: str | None = None,
|
| 48 |
+
uds: str | None = None,
|
| 49 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 50 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 51 |
+
) -> None:
|
| 52 |
+
self._origin = origin
|
| 53 |
+
self._ssl_context = ssl_context
|
| 54 |
+
self._keepalive_expiry = keepalive_expiry
|
| 55 |
+
self._http1 = http1
|
| 56 |
+
self._http2 = http2
|
| 57 |
+
self._retries = retries
|
| 58 |
+
self._local_address = local_address
|
| 59 |
+
self._uds = uds
|
| 60 |
+
|
| 61 |
+
self._network_backend: AsyncNetworkBackend = (
|
| 62 |
+
AutoBackend() if network_backend is None else network_backend
|
| 63 |
+
)
|
| 64 |
+
self._connection: AsyncConnectionInterface | None = None
|
| 65 |
+
self._connect_failed: bool = False
|
| 66 |
+
self._request_lock = AsyncLock()
|
| 67 |
+
self._socket_options = socket_options
|
| 68 |
+
|
| 69 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 70 |
+
if not self.can_handle_request(request.url.origin):
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
f"Attempted to send request to {request.url.origin} on connection to {self._origin}"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
async with self._request_lock:
|
| 77 |
+
if self._connection is None:
|
| 78 |
+
stream = await self._connect(request)
|
| 79 |
+
|
| 80 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 81 |
+
http2_negotiated = (
|
| 82 |
+
ssl_object is not None
|
| 83 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 84 |
+
)
|
| 85 |
+
if http2_negotiated or (self._http2 and not self._http1):
|
| 86 |
+
from .http2 import AsyncHTTP2Connection
|
| 87 |
+
|
| 88 |
+
self._connection = AsyncHTTP2Connection(
|
| 89 |
+
origin=self._origin,
|
| 90 |
+
stream=stream,
|
| 91 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
self._connection = AsyncHTTP11Connection(
|
| 95 |
+
origin=self._origin,
|
| 96 |
+
stream=stream,
|
| 97 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 98 |
+
)
|
| 99 |
+
except BaseException as exc:
|
| 100 |
+
self._connect_failed = True
|
| 101 |
+
raise exc
|
| 102 |
+
|
| 103 |
+
return await self._connection.handle_async_request(request)
|
| 104 |
+
|
| 105 |
+
async def _connect(self, request: Request) -> AsyncNetworkStream:
|
| 106 |
+
timeouts = request.extensions.get("timeout", {})
|
| 107 |
+
sni_hostname = request.extensions.get("sni_hostname", None)
|
| 108 |
+
timeout = timeouts.get("connect", None)
|
| 109 |
+
|
| 110 |
+
retries_left = self._retries
|
| 111 |
+
delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR)
|
| 112 |
+
|
| 113 |
+
while True:
|
| 114 |
+
try:
|
| 115 |
+
if self._uds is None:
|
| 116 |
+
kwargs = {
|
| 117 |
+
"host": self._origin.host.decode("ascii"),
|
| 118 |
+
"port": self._origin.port,
|
| 119 |
+
"local_address": self._local_address,
|
| 120 |
+
"timeout": timeout,
|
| 121 |
+
"socket_options": self._socket_options,
|
| 122 |
+
}
|
| 123 |
+
async with Trace("connect_tcp", logger, request, kwargs) as trace:
|
| 124 |
+
stream = await self._network_backend.connect_tcp(**kwargs)
|
| 125 |
+
trace.return_value = stream
|
| 126 |
+
else:
|
| 127 |
+
kwargs = {
|
| 128 |
+
"path": self._uds,
|
| 129 |
+
"timeout": timeout,
|
| 130 |
+
"socket_options": self._socket_options,
|
| 131 |
+
}
|
| 132 |
+
async with Trace(
|
| 133 |
+
"connect_unix_socket", logger, request, kwargs
|
| 134 |
+
) as trace:
|
| 135 |
+
stream = await self._network_backend.connect_unix_socket(
|
| 136 |
+
**kwargs
|
| 137 |
+
)
|
| 138 |
+
trace.return_value = stream
|
| 139 |
+
|
| 140 |
+
if self._origin.scheme in (b"https", b"wss"):
|
| 141 |
+
ssl_context = (
|
| 142 |
+
default_ssl_context()
|
| 143 |
+
if self._ssl_context is None
|
| 144 |
+
else self._ssl_context
|
| 145 |
+
)
|
| 146 |
+
alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 147 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 148 |
+
|
| 149 |
+
kwargs = {
|
| 150 |
+
"ssl_context": ssl_context,
|
| 151 |
+
"server_hostname": sni_hostname
|
| 152 |
+
or self._origin.host.decode("ascii"),
|
| 153 |
+
"timeout": timeout,
|
| 154 |
+
}
|
| 155 |
+
async with Trace("start_tls", logger, request, kwargs) as trace:
|
| 156 |
+
stream = await stream.start_tls(**kwargs)
|
| 157 |
+
trace.return_value = stream
|
| 158 |
+
return stream
|
| 159 |
+
except (ConnectError, ConnectTimeout):
|
| 160 |
+
if retries_left <= 0:
|
| 161 |
+
raise
|
| 162 |
+
retries_left -= 1
|
| 163 |
+
delay = next(delays)
|
| 164 |
+
async with Trace("retry", logger, request, kwargs) as trace:
|
| 165 |
+
await self._network_backend.sleep(delay)
|
| 166 |
+
|
| 167 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 168 |
+
return origin == self._origin
|
| 169 |
+
|
| 170 |
+
async def aclose(self) -> None:
|
| 171 |
+
if self._connection is not None:
|
| 172 |
+
async with Trace("close", logger, None, {}):
|
| 173 |
+
await self._connection.aclose()
|
| 174 |
+
|
| 175 |
+
def is_available(self) -> bool:
|
| 176 |
+
if self._connection is None:
|
| 177 |
+
# If HTTP/2 support is enabled, and the resulting connection could
|
| 178 |
+
# end up as HTTP/2 then we should indicate the connection as being
|
| 179 |
+
# available to service multiple requests.
|
| 180 |
+
return (
|
| 181 |
+
self._http2
|
| 182 |
+
and (self._origin.scheme == b"https" or not self._http1)
|
| 183 |
+
and not self._connect_failed
|
| 184 |
+
)
|
| 185 |
+
return self._connection.is_available()
|
| 186 |
+
|
| 187 |
+
def has_expired(self) -> bool:
|
| 188 |
+
if self._connection is None:
|
| 189 |
+
return self._connect_failed
|
| 190 |
+
return self._connection.has_expired()
|
| 191 |
+
|
| 192 |
+
def is_idle(self) -> bool:
|
| 193 |
+
if self._connection is None:
|
| 194 |
+
return self._connect_failed
|
| 195 |
+
return self._connection.is_idle()
|
| 196 |
+
|
| 197 |
+
def is_closed(self) -> bool:
|
| 198 |
+
if self._connection is None:
|
| 199 |
+
return self._connect_failed
|
| 200 |
+
return self._connection.is_closed()
|
| 201 |
+
|
| 202 |
+
def info(self) -> str:
|
| 203 |
+
if self._connection is None:
|
| 204 |
+
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
|
| 205 |
+
return self._connection.info()
|
| 206 |
+
|
| 207 |
+
def __repr__(self) -> str:
|
| 208 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
| 209 |
+
|
| 210 |
+
# These context managers are not used in the standard flow, but are
|
| 211 |
+
# useful for testing or working with connection instances directly.
|
| 212 |
+
|
| 213 |
+
async def __aenter__(self) -> AsyncHTTPConnection:
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
async def __aexit__(
|
| 217 |
+
self,
|
| 218 |
+
exc_type: type[BaseException] | None = None,
|
| 219 |
+
exc_value: BaseException | None = None,
|
| 220 |
+
traceback: types.TracebackType | None = None,
|
| 221 |
+
) -> None:
|
| 222 |
+
await self.aclose()
|
lib/python3.12/site-packages/httpcore/_async/connection_pool.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import sys
|
| 5 |
+
import types
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._backends.auto import AutoBackend
|
| 9 |
+
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
|
| 10 |
+
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
|
| 11 |
+
from .._models import Origin, Proxy, Request, Response
|
| 12 |
+
from .._synchronization import AsyncEvent, AsyncShieldCancellation, AsyncThreadLock
|
| 13 |
+
from .connection import AsyncHTTPConnection
|
| 14 |
+
from .interfaces import AsyncConnectionInterface, AsyncRequestInterface
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class AsyncPoolRequest:
|
| 18 |
+
def __init__(self, request: Request) -> None:
|
| 19 |
+
self.request = request
|
| 20 |
+
self.connection: AsyncConnectionInterface | None = None
|
| 21 |
+
self._connection_acquired = AsyncEvent()
|
| 22 |
+
|
| 23 |
+
def assign_to_connection(self, connection: AsyncConnectionInterface | None) -> None:
|
| 24 |
+
self.connection = connection
|
| 25 |
+
self._connection_acquired.set()
|
| 26 |
+
|
| 27 |
+
def clear_connection(self) -> None:
|
| 28 |
+
self.connection = None
|
| 29 |
+
self._connection_acquired = AsyncEvent()
|
| 30 |
+
|
| 31 |
+
async def wait_for_connection(
|
| 32 |
+
self, timeout: float | None = None
|
| 33 |
+
) -> AsyncConnectionInterface:
|
| 34 |
+
if self.connection is None:
|
| 35 |
+
await self._connection_acquired.wait(timeout=timeout)
|
| 36 |
+
assert self.connection is not None
|
| 37 |
+
return self.connection
|
| 38 |
+
|
| 39 |
+
def is_queued(self) -> bool:
|
| 40 |
+
return self.connection is None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class AsyncConnectionPool(AsyncRequestInterface):
|
| 44 |
+
"""
|
| 45 |
+
A connection pool for making HTTP requests.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 51 |
+
proxy: Proxy | None = None,
|
| 52 |
+
max_connections: int | None = 10,
|
| 53 |
+
max_keepalive_connections: int | None = None,
|
| 54 |
+
keepalive_expiry: float | None = None,
|
| 55 |
+
http1: bool = True,
|
| 56 |
+
http2: bool = False,
|
| 57 |
+
retries: int = 0,
|
| 58 |
+
local_address: str | None = None,
|
| 59 |
+
uds: str | None = None,
|
| 60 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 61 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 62 |
+
) -> None:
|
| 63 |
+
"""
|
| 64 |
+
A connection pool for making HTTP requests.
|
| 65 |
+
|
| 66 |
+
Parameters:
|
| 67 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 68 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 69 |
+
will be used.
|
| 70 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 71 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 72 |
+
would exceed this amount will block until a connection is available.
|
| 73 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 74 |
+
that will be maintained in the pool.
|
| 75 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 76 |
+
may be maintained for before being expired from the pool.
|
| 77 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 78 |
+
by the connection pool. Defaults to True.
|
| 79 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 80 |
+
the connection pool. Defaults to False.
|
| 81 |
+
retries: The maximum number of retries when trying to establish a
|
| 82 |
+
connection.
|
| 83 |
+
local_address: Local address to connect from. Can also be used to connect
|
| 84 |
+
using a particular address family. Using `local_address="0.0.0.0"`
|
| 85 |
+
will connect using an `AF_INET` address (IPv4), while using
|
| 86 |
+
`local_address="::"` will connect using an `AF_INET6` address (IPv6).
|
| 87 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 88 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 89 |
+
socket_options: Socket options that have to be included
|
| 90 |
+
in the TCP socket when the connection was established.
|
| 91 |
+
"""
|
| 92 |
+
self._ssl_context = ssl_context
|
| 93 |
+
self._proxy = proxy
|
| 94 |
+
self._max_connections = (
|
| 95 |
+
sys.maxsize if max_connections is None else max_connections
|
| 96 |
+
)
|
| 97 |
+
self._max_keepalive_connections = (
|
| 98 |
+
sys.maxsize
|
| 99 |
+
if max_keepalive_connections is None
|
| 100 |
+
else max_keepalive_connections
|
| 101 |
+
)
|
| 102 |
+
self._max_keepalive_connections = min(
|
| 103 |
+
self._max_connections, self._max_keepalive_connections
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
self._keepalive_expiry = keepalive_expiry
|
| 107 |
+
self._http1 = http1
|
| 108 |
+
self._http2 = http2
|
| 109 |
+
self._retries = retries
|
| 110 |
+
self._local_address = local_address
|
| 111 |
+
self._uds = uds
|
| 112 |
+
|
| 113 |
+
self._network_backend = (
|
| 114 |
+
AutoBackend() if network_backend is None else network_backend
|
| 115 |
+
)
|
| 116 |
+
self._socket_options = socket_options
|
| 117 |
+
|
| 118 |
+
# The mutable state on a connection pool is the queue of incoming requests,
|
| 119 |
+
# and the set of connections that are servicing those requests.
|
| 120 |
+
self._connections: list[AsyncConnectionInterface] = []
|
| 121 |
+
self._requests: list[AsyncPoolRequest] = []
|
| 122 |
+
|
| 123 |
+
# We only mutate the state of the connection pool within an 'optional_thread_lock'
|
| 124 |
+
# context. This holds a threading lock unless we're running in async mode,
|
| 125 |
+
# in which case it is a no-op.
|
| 126 |
+
self._optional_thread_lock = AsyncThreadLock()
|
| 127 |
+
|
| 128 |
+
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
|
| 129 |
+
if self._proxy is not None:
|
| 130 |
+
if self._proxy.url.scheme in (b"socks5", b"socks5h"):
|
| 131 |
+
from .socks_proxy import AsyncSocks5Connection
|
| 132 |
+
|
| 133 |
+
return AsyncSocks5Connection(
|
| 134 |
+
proxy_origin=self._proxy.url.origin,
|
| 135 |
+
proxy_auth=self._proxy.auth,
|
| 136 |
+
remote_origin=origin,
|
| 137 |
+
ssl_context=self._ssl_context,
|
| 138 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 139 |
+
http1=self._http1,
|
| 140 |
+
http2=self._http2,
|
| 141 |
+
network_backend=self._network_backend,
|
| 142 |
+
)
|
| 143 |
+
elif origin.scheme == b"http":
|
| 144 |
+
from .http_proxy import AsyncForwardHTTPConnection
|
| 145 |
+
|
| 146 |
+
return AsyncForwardHTTPConnection(
|
| 147 |
+
proxy_origin=self._proxy.url.origin,
|
| 148 |
+
proxy_headers=self._proxy.headers,
|
| 149 |
+
proxy_ssl_context=self._proxy.ssl_context,
|
| 150 |
+
remote_origin=origin,
|
| 151 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 152 |
+
network_backend=self._network_backend,
|
| 153 |
+
)
|
| 154 |
+
from .http_proxy import AsyncTunnelHTTPConnection
|
| 155 |
+
|
| 156 |
+
return AsyncTunnelHTTPConnection(
|
| 157 |
+
proxy_origin=self._proxy.url.origin,
|
| 158 |
+
proxy_headers=self._proxy.headers,
|
| 159 |
+
proxy_ssl_context=self._proxy.ssl_context,
|
| 160 |
+
remote_origin=origin,
|
| 161 |
+
ssl_context=self._ssl_context,
|
| 162 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 163 |
+
http1=self._http1,
|
| 164 |
+
http2=self._http2,
|
| 165 |
+
network_backend=self._network_backend,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
return AsyncHTTPConnection(
|
| 169 |
+
origin=origin,
|
| 170 |
+
ssl_context=self._ssl_context,
|
| 171 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 172 |
+
http1=self._http1,
|
| 173 |
+
http2=self._http2,
|
| 174 |
+
retries=self._retries,
|
| 175 |
+
local_address=self._local_address,
|
| 176 |
+
uds=self._uds,
|
| 177 |
+
network_backend=self._network_backend,
|
| 178 |
+
socket_options=self._socket_options,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def connections(self) -> list[AsyncConnectionInterface]:
|
| 183 |
+
"""
|
| 184 |
+
Return a list of the connections currently in the pool.
|
| 185 |
+
|
| 186 |
+
For example:
|
| 187 |
+
|
| 188 |
+
```python
|
| 189 |
+
>>> pool.connections
|
| 190 |
+
[
|
| 191 |
+
<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 6]>,
|
| 192 |
+
<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 9]> ,
|
| 193 |
+
<AsyncHTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>,
|
| 194 |
+
]
|
| 195 |
+
```
|
| 196 |
+
"""
|
| 197 |
+
return list(self._connections)
|
| 198 |
+
|
| 199 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 200 |
+
"""
|
| 201 |
+
Send an HTTP request, and return an HTTP response.
|
| 202 |
+
|
| 203 |
+
This is the core implementation that is called into by `.request()` or `.stream()`.
|
| 204 |
+
"""
|
| 205 |
+
scheme = request.url.scheme.decode()
|
| 206 |
+
if scheme == "":
|
| 207 |
+
raise UnsupportedProtocol(
|
| 208 |
+
"Request URL is missing an 'http://' or 'https://' protocol."
|
| 209 |
+
)
|
| 210 |
+
if scheme not in ("http", "https", "ws", "wss"):
|
| 211 |
+
raise UnsupportedProtocol(
|
| 212 |
+
f"Request URL has an unsupported protocol '{scheme}://'."
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
timeouts = request.extensions.get("timeout", {})
|
| 216 |
+
timeout = timeouts.get("pool", None)
|
| 217 |
+
|
| 218 |
+
with self._optional_thread_lock:
|
| 219 |
+
# Add the incoming request to our request queue.
|
| 220 |
+
pool_request = AsyncPoolRequest(request)
|
| 221 |
+
self._requests.append(pool_request)
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
while True:
|
| 225 |
+
with self._optional_thread_lock:
|
| 226 |
+
# Assign incoming requests to available connections,
|
| 227 |
+
# closing or creating new connections as required.
|
| 228 |
+
closing = self._assign_requests_to_connections()
|
| 229 |
+
await self._close_connections(closing)
|
| 230 |
+
|
| 231 |
+
# Wait until this request has an assigned connection.
|
| 232 |
+
connection = await pool_request.wait_for_connection(timeout=timeout)
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
# Send the request on the assigned connection.
|
| 236 |
+
response = await connection.handle_async_request(
|
| 237 |
+
pool_request.request
|
| 238 |
+
)
|
| 239 |
+
except ConnectionNotAvailable:
|
| 240 |
+
# In some cases a connection may initially be available to
|
| 241 |
+
# handle a request, but then become unavailable.
|
| 242 |
+
#
|
| 243 |
+
# In this case we clear the connection and try again.
|
| 244 |
+
pool_request.clear_connection()
|
| 245 |
+
else:
|
| 246 |
+
break # pragma: nocover
|
| 247 |
+
|
| 248 |
+
except BaseException as exc:
|
| 249 |
+
with self._optional_thread_lock:
|
| 250 |
+
# For any exception or cancellation we remove the request from
|
| 251 |
+
# the queue, and then re-assign requests to connections.
|
| 252 |
+
self._requests.remove(pool_request)
|
| 253 |
+
closing = self._assign_requests_to_connections()
|
| 254 |
+
|
| 255 |
+
await self._close_connections(closing)
|
| 256 |
+
raise exc from None
|
| 257 |
+
|
| 258 |
+
# Return the response. Note that in this case we still have to manage
|
| 259 |
+
# the point at which the response is closed.
|
| 260 |
+
assert isinstance(response.stream, typing.AsyncIterable)
|
| 261 |
+
return Response(
|
| 262 |
+
status=response.status,
|
| 263 |
+
headers=response.headers,
|
| 264 |
+
content=PoolByteStream(
|
| 265 |
+
stream=response.stream, pool_request=pool_request, pool=self
|
| 266 |
+
),
|
| 267 |
+
extensions=response.extensions,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
def _assign_requests_to_connections(self) -> list[AsyncConnectionInterface]:
|
| 271 |
+
"""
|
| 272 |
+
Manage the state of the connection pool, assigning incoming
|
| 273 |
+
requests to connections as available.
|
| 274 |
+
|
| 275 |
+
Called whenever a new request is added or removed from the pool.
|
| 276 |
+
|
| 277 |
+
Any closing connections are returned, allowing the I/O for closing
|
| 278 |
+
those connections to be handled seperately.
|
| 279 |
+
"""
|
| 280 |
+
closing_connections = []
|
| 281 |
+
|
| 282 |
+
# First we handle cleaning up any connections that are closed,
|
| 283 |
+
# have expired their keep-alive, or surplus idle connections.
|
| 284 |
+
for connection in list(self._connections):
|
| 285 |
+
if connection.is_closed():
|
| 286 |
+
# log: "removing closed connection"
|
| 287 |
+
self._connections.remove(connection)
|
| 288 |
+
elif connection.has_expired():
|
| 289 |
+
# log: "closing expired connection"
|
| 290 |
+
self._connections.remove(connection)
|
| 291 |
+
closing_connections.append(connection)
|
| 292 |
+
elif (
|
| 293 |
+
connection.is_idle()
|
| 294 |
+
and len([connection.is_idle() for connection in self._connections])
|
| 295 |
+
> self._max_keepalive_connections
|
| 296 |
+
):
|
| 297 |
+
# log: "closing idle connection"
|
| 298 |
+
self._connections.remove(connection)
|
| 299 |
+
closing_connections.append(connection)
|
| 300 |
+
|
| 301 |
+
# Assign queued requests to connections.
|
| 302 |
+
queued_requests = [request for request in self._requests if request.is_queued()]
|
| 303 |
+
for pool_request in queued_requests:
|
| 304 |
+
origin = pool_request.request.url.origin
|
| 305 |
+
available_connections = [
|
| 306 |
+
connection
|
| 307 |
+
for connection in self._connections
|
| 308 |
+
if connection.can_handle_request(origin) and connection.is_available()
|
| 309 |
+
]
|
| 310 |
+
idle_connections = [
|
| 311 |
+
connection for connection in self._connections if connection.is_idle()
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
# There are three cases for how we may be able to handle the request:
|
| 315 |
+
#
|
| 316 |
+
# 1. There is an existing connection that can handle the request.
|
| 317 |
+
# 2. We can create a new connection to handle the request.
|
| 318 |
+
# 3. We can close an idle connection and then create a new connection
|
| 319 |
+
# to handle the request.
|
| 320 |
+
if available_connections:
|
| 321 |
+
# log: "reusing existing connection"
|
| 322 |
+
connection = available_connections[0]
|
| 323 |
+
pool_request.assign_to_connection(connection)
|
| 324 |
+
elif len(self._connections) < self._max_connections:
|
| 325 |
+
# log: "creating new connection"
|
| 326 |
+
connection = self.create_connection(origin)
|
| 327 |
+
self._connections.append(connection)
|
| 328 |
+
pool_request.assign_to_connection(connection)
|
| 329 |
+
elif idle_connections:
|
| 330 |
+
# log: "closing idle connection"
|
| 331 |
+
connection = idle_connections[0]
|
| 332 |
+
self._connections.remove(connection)
|
| 333 |
+
closing_connections.append(connection)
|
| 334 |
+
# log: "creating new connection"
|
| 335 |
+
connection = self.create_connection(origin)
|
| 336 |
+
self._connections.append(connection)
|
| 337 |
+
pool_request.assign_to_connection(connection)
|
| 338 |
+
|
| 339 |
+
return closing_connections
|
| 340 |
+
|
| 341 |
+
async def _close_connections(self, closing: list[AsyncConnectionInterface]) -> None:
|
| 342 |
+
# Close connections which have been removed from the pool.
|
| 343 |
+
with AsyncShieldCancellation():
|
| 344 |
+
for connection in closing:
|
| 345 |
+
await connection.aclose()
|
| 346 |
+
|
| 347 |
+
async def aclose(self) -> None:
|
| 348 |
+
# Explicitly close the connection pool.
|
| 349 |
+
# Clears all existing requests and connections.
|
| 350 |
+
with self._optional_thread_lock:
|
| 351 |
+
closing_connections = list(self._connections)
|
| 352 |
+
self._connections = []
|
| 353 |
+
await self._close_connections(closing_connections)
|
| 354 |
+
|
| 355 |
+
async def __aenter__(self) -> AsyncConnectionPool:
|
| 356 |
+
return self
|
| 357 |
+
|
| 358 |
+
async def __aexit__(
|
| 359 |
+
self,
|
| 360 |
+
exc_type: type[BaseException] | None = None,
|
| 361 |
+
exc_value: BaseException | None = None,
|
| 362 |
+
traceback: types.TracebackType | None = None,
|
| 363 |
+
) -> None:
|
| 364 |
+
await self.aclose()
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
class_name = self.__class__.__name__
|
| 368 |
+
with self._optional_thread_lock:
|
| 369 |
+
request_is_queued = [request.is_queued() for request in self._requests]
|
| 370 |
+
connection_is_idle = [
|
| 371 |
+
connection.is_idle() for connection in self._connections
|
| 372 |
+
]
|
| 373 |
+
|
| 374 |
+
num_active_requests = request_is_queued.count(False)
|
| 375 |
+
num_queued_requests = request_is_queued.count(True)
|
| 376 |
+
num_active_connections = connection_is_idle.count(False)
|
| 377 |
+
num_idle_connections = connection_is_idle.count(True)
|
| 378 |
+
|
| 379 |
+
requests_info = (
|
| 380 |
+
f"Requests: {num_active_requests} active, {num_queued_requests} queued"
|
| 381 |
+
)
|
| 382 |
+
connection_info = (
|
| 383 |
+
f"Connections: {num_active_connections} active, {num_idle_connections} idle"
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
return f"<{class_name} [{requests_info} | {connection_info}]>"
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
class PoolByteStream:
|
| 390 |
+
def __init__(
|
| 391 |
+
self,
|
| 392 |
+
stream: typing.AsyncIterable[bytes],
|
| 393 |
+
pool_request: AsyncPoolRequest,
|
| 394 |
+
pool: AsyncConnectionPool,
|
| 395 |
+
) -> None:
|
| 396 |
+
self._stream = stream
|
| 397 |
+
self._pool_request = pool_request
|
| 398 |
+
self._pool = pool
|
| 399 |
+
self._closed = False
|
| 400 |
+
|
| 401 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 402 |
+
try:
|
| 403 |
+
async for part in self._stream:
|
| 404 |
+
yield part
|
| 405 |
+
except BaseException as exc:
|
| 406 |
+
await self.aclose()
|
| 407 |
+
raise exc from None
|
| 408 |
+
|
| 409 |
+
async def aclose(self) -> None:
|
| 410 |
+
if not self._closed:
|
| 411 |
+
self._closed = True
|
| 412 |
+
with AsyncShieldCancellation():
|
| 413 |
+
if hasattr(self._stream, "aclose"):
|
| 414 |
+
await self._stream.aclose()
|
| 415 |
+
|
| 416 |
+
with self._pool._optional_thread_lock:
|
| 417 |
+
self._pool._requests.remove(self._pool_request)
|
| 418 |
+
closing = self._pool._assign_requests_to_connections()
|
| 419 |
+
|
| 420 |
+
await self._pool._close_connections(closing)
|
lib/python3.12/site-packages/httpcore/_async/http11.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import time
|
| 7 |
+
import types
|
| 8 |
+
import typing
|
| 9 |
+
|
| 10 |
+
import h11
|
| 11 |
+
|
| 12 |
+
from .._backends.base import AsyncNetworkStream
|
| 13 |
+
from .._exceptions import (
|
| 14 |
+
ConnectionNotAvailable,
|
| 15 |
+
LocalProtocolError,
|
| 16 |
+
RemoteProtocolError,
|
| 17 |
+
WriteError,
|
| 18 |
+
map_exceptions,
|
| 19 |
+
)
|
| 20 |
+
from .._models import Origin, Request, Response
|
| 21 |
+
from .._synchronization import AsyncLock, AsyncShieldCancellation
|
| 22 |
+
from .._trace import Trace
|
| 23 |
+
from .interfaces import AsyncConnectionInterface
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger("httpcore.http11")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# A subset of `h11.Event` types supported by `_send_event`
|
| 29 |
+
H11SendEvent = typing.Union[
|
| 30 |
+
h11.Request,
|
| 31 |
+
h11.Data,
|
| 32 |
+
h11.EndOfMessage,
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HTTPConnectionState(enum.IntEnum):
|
| 37 |
+
NEW = 0
|
| 38 |
+
ACTIVE = 1
|
| 39 |
+
IDLE = 2
|
| 40 |
+
CLOSED = 3
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class AsyncHTTP11Connection(AsyncConnectionInterface):
|
| 44 |
+
READ_NUM_BYTES = 64 * 1024
|
| 45 |
+
MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
origin: Origin,
|
| 50 |
+
stream: AsyncNetworkStream,
|
| 51 |
+
keepalive_expiry: float | None = None,
|
| 52 |
+
) -> None:
|
| 53 |
+
self._origin = origin
|
| 54 |
+
self._network_stream = stream
|
| 55 |
+
self._keepalive_expiry: float | None = keepalive_expiry
|
| 56 |
+
self._expire_at: float | None = None
|
| 57 |
+
self._state = HTTPConnectionState.NEW
|
| 58 |
+
self._state_lock = AsyncLock()
|
| 59 |
+
self._request_count = 0
|
| 60 |
+
self._h11_state = h11.Connection(
|
| 61 |
+
our_role=h11.CLIENT,
|
| 62 |
+
max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 66 |
+
if not self.can_handle_request(request.url.origin):
|
| 67 |
+
raise RuntimeError(
|
| 68 |
+
f"Attempted to send request to {request.url.origin} on connection "
|
| 69 |
+
f"to {self._origin}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
async with self._state_lock:
|
| 73 |
+
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
|
| 74 |
+
self._request_count += 1
|
| 75 |
+
self._state = HTTPConnectionState.ACTIVE
|
| 76 |
+
self._expire_at = None
|
| 77 |
+
else:
|
| 78 |
+
raise ConnectionNotAvailable()
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
kwargs = {"request": request}
|
| 82 |
+
try:
|
| 83 |
+
async with Trace(
|
| 84 |
+
"send_request_headers", logger, request, kwargs
|
| 85 |
+
) as trace:
|
| 86 |
+
await self._send_request_headers(**kwargs)
|
| 87 |
+
async with Trace("send_request_body", logger, request, kwargs) as trace:
|
| 88 |
+
await self._send_request_body(**kwargs)
|
| 89 |
+
except WriteError:
|
| 90 |
+
# If we get a write error while we're writing the request,
|
| 91 |
+
# then we supress this error and move on to attempting to
|
| 92 |
+
# read the response. Servers can sometimes close the request
|
| 93 |
+
# pre-emptively and then respond with a well formed HTTP
|
| 94 |
+
# error response.
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
async with Trace(
|
| 98 |
+
"receive_response_headers", logger, request, kwargs
|
| 99 |
+
) as trace:
|
| 100 |
+
(
|
| 101 |
+
http_version,
|
| 102 |
+
status,
|
| 103 |
+
reason_phrase,
|
| 104 |
+
headers,
|
| 105 |
+
trailing_data,
|
| 106 |
+
) = await self._receive_response_headers(**kwargs)
|
| 107 |
+
trace.return_value = (
|
| 108 |
+
http_version,
|
| 109 |
+
status,
|
| 110 |
+
reason_phrase,
|
| 111 |
+
headers,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
network_stream = self._network_stream
|
| 115 |
+
|
| 116 |
+
# CONNECT or Upgrade request
|
| 117 |
+
if (status == 101) or (
|
| 118 |
+
(request.method == b"CONNECT") and (200 <= status < 300)
|
| 119 |
+
):
|
| 120 |
+
network_stream = AsyncHTTP11UpgradeStream(network_stream, trailing_data)
|
| 121 |
+
|
| 122 |
+
return Response(
|
| 123 |
+
status=status,
|
| 124 |
+
headers=headers,
|
| 125 |
+
content=HTTP11ConnectionByteStream(self, request),
|
| 126 |
+
extensions={
|
| 127 |
+
"http_version": http_version,
|
| 128 |
+
"reason_phrase": reason_phrase,
|
| 129 |
+
"network_stream": network_stream,
|
| 130 |
+
},
|
| 131 |
+
)
|
| 132 |
+
except BaseException as exc:
|
| 133 |
+
with AsyncShieldCancellation():
|
| 134 |
+
async with Trace("response_closed", logger, request) as trace:
|
| 135 |
+
await self._response_closed()
|
| 136 |
+
raise exc
|
| 137 |
+
|
| 138 |
+
# Sending the request...
|
| 139 |
+
|
| 140 |
+
async def _send_request_headers(self, request: Request) -> None:
|
| 141 |
+
timeouts = request.extensions.get("timeout", {})
|
| 142 |
+
timeout = timeouts.get("write", None)
|
| 143 |
+
|
| 144 |
+
with map_exceptions({h11.LocalProtocolError: LocalProtocolError}):
|
| 145 |
+
event = h11.Request(
|
| 146 |
+
method=request.method,
|
| 147 |
+
target=request.url.target,
|
| 148 |
+
headers=request.headers,
|
| 149 |
+
)
|
| 150 |
+
await self._send_event(event, timeout=timeout)
|
| 151 |
+
|
| 152 |
+
async def _send_request_body(self, request: Request) -> None:
|
| 153 |
+
timeouts = request.extensions.get("timeout", {})
|
| 154 |
+
timeout = timeouts.get("write", None)
|
| 155 |
+
|
| 156 |
+
assert isinstance(request.stream, typing.AsyncIterable)
|
| 157 |
+
async for chunk in request.stream:
|
| 158 |
+
event = h11.Data(data=chunk)
|
| 159 |
+
await self._send_event(event, timeout=timeout)
|
| 160 |
+
|
| 161 |
+
await self._send_event(h11.EndOfMessage(), timeout=timeout)
|
| 162 |
+
|
| 163 |
+
async def _send_event(self, event: h11.Event, timeout: float | None = None) -> None:
|
| 164 |
+
bytes_to_send = self._h11_state.send(event)
|
| 165 |
+
if bytes_to_send is not None:
|
| 166 |
+
await self._network_stream.write(bytes_to_send, timeout=timeout)
|
| 167 |
+
|
| 168 |
+
# Receiving the response...
|
| 169 |
+
|
| 170 |
+
async def _receive_response_headers(
|
| 171 |
+
self, request: Request
|
| 172 |
+
) -> tuple[bytes, int, bytes, list[tuple[bytes, bytes]], bytes]:
|
| 173 |
+
timeouts = request.extensions.get("timeout", {})
|
| 174 |
+
timeout = timeouts.get("read", None)
|
| 175 |
+
|
| 176 |
+
while True:
|
| 177 |
+
event = await self._receive_event(timeout=timeout)
|
| 178 |
+
if isinstance(event, h11.Response):
|
| 179 |
+
break
|
| 180 |
+
if (
|
| 181 |
+
isinstance(event, h11.InformationalResponse)
|
| 182 |
+
and event.status_code == 101
|
| 183 |
+
):
|
| 184 |
+
break
|
| 185 |
+
|
| 186 |
+
http_version = b"HTTP/" + event.http_version
|
| 187 |
+
|
| 188 |
+
# h11 version 0.11+ supports a `raw_items` interface to get the
|
| 189 |
+
# raw header casing, rather than the enforced lowercase headers.
|
| 190 |
+
headers = event.headers.raw_items()
|
| 191 |
+
|
| 192 |
+
trailing_data, _ = self._h11_state.trailing_data
|
| 193 |
+
|
| 194 |
+
return http_version, event.status_code, event.reason, headers, trailing_data
|
| 195 |
+
|
| 196 |
+
async def _receive_response_body(
|
| 197 |
+
self, request: Request
|
| 198 |
+
) -> typing.AsyncIterator[bytes]:
|
| 199 |
+
timeouts = request.extensions.get("timeout", {})
|
| 200 |
+
timeout = timeouts.get("read", None)
|
| 201 |
+
|
| 202 |
+
while True:
|
| 203 |
+
event = await self._receive_event(timeout=timeout)
|
| 204 |
+
if isinstance(event, h11.Data):
|
| 205 |
+
yield bytes(event.data)
|
| 206 |
+
elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)):
|
| 207 |
+
break
|
| 208 |
+
|
| 209 |
+
async def _receive_event(
|
| 210 |
+
self, timeout: float | None = None
|
| 211 |
+
) -> h11.Event | type[h11.PAUSED]:
|
| 212 |
+
while True:
|
| 213 |
+
with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}):
|
| 214 |
+
event = self._h11_state.next_event()
|
| 215 |
+
|
| 216 |
+
if event is h11.NEED_DATA:
|
| 217 |
+
data = await self._network_stream.read(
|
| 218 |
+
self.READ_NUM_BYTES, timeout=timeout
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# If we feed this case through h11 we'll raise an exception like:
|
| 222 |
+
#
|
| 223 |
+
# httpcore.RemoteProtocolError: can't handle event type
|
| 224 |
+
# ConnectionClosed when role=SERVER and state=SEND_RESPONSE
|
| 225 |
+
#
|
| 226 |
+
# Which is accurate, but not very informative from an end-user
|
| 227 |
+
# perspective. Instead we handle this case distinctly and treat
|
| 228 |
+
# it as a ConnectError.
|
| 229 |
+
if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE:
|
| 230 |
+
msg = "Server disconnected without sending a response."
|
| 231 |
+
raise RemoteProtocolError(msg)
|
| 232 |
+
|
| 233 |
+
self._h11_state.receive_data(data)
|
| 234 |
+
else:
|
| 235 |
+
# mypy fails to narrow the type in the above if statement above
|
| 236 |
+
return event # type: ignore[return-value]
|
| 237 |
+
|
| 238 |
+
async def _response_closed(self) -> None:
|
| 239 |
+
async with self._state_lock:
|
| 240 |
+
if (
|
| 241 |
+
self._h11_state.our_state is h11.DONE
|
| 242 |
+
and self._h11_state.their_state is h11.DONE
|
| 243 |
+
):
|
| 244 |
+
self._state = HTTPConnectionState.IDLE
|
| 245 |
+
self._h11_state.start_next_cycle()
|
| 246 |
+
if self._keepalive_expiry is not None:
|
| 247 |
+
now = time.monotonic()
|
| 248 |
+
self._expire_at = now + self._keepalive_expiry
|
| 249 |
+
else:
|
| 250 |
+
await self.aclose()
|
| 251 |
+
|
| 252 |
+
# Once the connection is no longer required...
|
| 253 |
+
|
| 254 |
+
async def aclose(self) -> None:
|
| 255 |
+
# Note that this method unilaterally closes the connection, and does
|
| 256 |
+
# not have any kind of locking in place around it.
|
| 257 |
+
self._state = HTTPConnectionState.CLOSED
|
| 258 |
+
await self._network_stream.aclose()
|
| 259 |
+
|
| 260 |
+
# The AsyncConnectionInterface methods provide information about the state of
|
| 261 |
+
# the connection, allowing for a connection pooling implementation to
|
| 262 |
+
# determine when to reuse and when to close the connection...
|
| 263 |
+
|
| 264 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 265 |
+
return origin == self._origin
|
| 266 |
+
|
| 267 |
+
def is_available(self) -> bool:
|
| 268 |
+
# Note that HTTP/1.1 connections in the "NEW" state are not treated as
|
| 269 |
+
# being "available". The control flow which created the connection will
|
| 270 |
+
# be able to send an outgoing request, but the connection will not be
|
| 271 |
+
# acquired from the connection pool for any other request.
|
| 272 |
+
return self._state == HTTPConnectionState.IDLE
|
| 273 |
+
|
| 274 |
+
def has_expired(self) -> bool:
|
| 275 |
+
now = time.monotonic()
|
| 276 |
+
keepalive_expired = self._expire_at is not None and now > self._expire_at
|
| 277 |
+
|
| 278 |
+
# If the HTTP connection is idle but the socket is readable, then the
|
| 279 |
+
# only valid state is that the socket is about to return b"", indicating
|
| 280 |
+
# a server-initiated disconnect.
|
| 281 |
+
server_disconnected = (
|
| 282 |
+
self._state == HTTPConnectionState.IDLE
|
| 283 |
+
and self._network_stream.get_extra_info("is_readable")
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
return keepalive_expired or server_disconnected
|
| 287 |
+
|
| 288 |
+
def is_idle(self) -> bool:
|
| 289 |
+
return self._state == HTTPConnectionState.IDLE
|
| 290 |
+
|
| 291 |
+
def is_closed(self) -> bool:
|
| 292 |
+
return self._state == HTTPConnectionState.CLOSED
|
| 293 |
+
|
| 294 |
+
def info(self) -> str:
|
| 295 |
+
origin = str(self._origin)
|
| 296 |
+
return (
|
| 297 |
+
f"{origin!r}, HTTP/1.1, {self._state.name}, "
|
| 298 |
+
f"Request Count: {self._request_count}"
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
def __repr__(self) -> str:
|
| 302 |
+
class_name = self.__class__.__name__
|
| 303 |
+
origin = str(self._origin)
|
| 304 |
+
return (
|
| 305 |
+
f"<{class_name} [{origin!r}, {self._state.name}, "
|
| 306 |
+
f"Request Count: {self._request_count}]>"
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# These context managers are not used in the standard flow, but are
|
| 310 |
+
# useful for testing or working with connection instances directly.
|
| 311 |
+
|
| 312 |
+
async def __aenter__(self) -> AsyncHTTP11Connection:
|
| 313 |
+
return self
|
| 314 |
+
|
| 315 |
+
async def __aexit__(
|
| 316 |
+
self,
|
| 317 |
+
exc_type: type[BaseException] | None = None,
|
| 318 |
+
exc_value: BaseException | None = None,
|
| 319 |
+
traceback: types.TracebackType | None = None,
|
| 320 |
+
) -> None:
|
| 321 |
+
await self.aclose()
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class HTTP11ConnectionByteStream:
|
| 325 |
+
def __init__(self, connection: AsyncHTTP11Connection, request: Request) -> None:
|
| 326 |
+
self._connection = connection
|
| 327 |
+
self._request = request
|
| 328 |
+
self._closed = False
|
| 329 |
+
|
| 330 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 331 |
+
kwargs = {"request": self._request}
|
| 332 |
+
try:
|
| 333 |
+
async with Trace("receive_response_body", logger, self._request, kwargs):
|
| 334 |
+
async for chunk in self._connection._receive_response_body(**kwargs):
|
| 335 |
+
yield chunk
|
| 336 |
+
except BaseException as exc:
|
| 337 |
+
# If we get an exception while streaming the response,
|
| 338 |
+
# we want to close the response (and possibly the connection)
|
| 339 |
+
# before raising that exception.
|
| 340 |
+
with AsyncShieldCancellation():
|
| 341 |
+
await self.aclose()
|
| 342 |
+
raise exc
|
| 343 |
+
|
| 344 |
+
async def aclose(self) -> None:
|
| 345 |
+
if not self._closed:
|
| 346 |
+
self._closed = True
|
| 347 |
+
async with Trace("response_closed", logger, self._request):
|
| 348 |
+
await self._connection._response_closed()
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
class AsyncHTTP11UpgradeStream(AsyncNetworkStream):
|
| 352 |
+
def __init__(self, stream: AsyncNetworkStream, leading_data: bytes) -> None:
|
| 353 |
+
self._stream = stream
|
| 354 |
+
self._leading_data = leading_data
|
| 355 |
+
|
| 356 |
+
async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 357 |
+
if self._leading_data:
|
| 358 |
+
buffer = self._leading_data[:max_bytes]
|
| 359 |
+
self._leading_data = self._leading_data[max_bytes:]
|
| 360 |
+
return buffer
|
| 361 |
+
else:
|
| 362 |
+
return await self._stream.read(max_bytes, timeout)
|
| 363 |
+
|
| 364 |
+
async def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 365 |
+
await self._stream.write(buffer, timeout)
|
| 366 |
+
|
| 367 |
+
async def aclose(self) -> None:
|
| 368 |
+
await self._stream.aclose()
|
| 369 |
+
|
| 370 |
+
async def start_tls(
|
| 371 |
+
self,
|
| 372 |
+
ssl_context: ssl.SSLContext,
|
| 373 |
+
server_hostname: str | None = None,
|
| 374 |
+
timeout: float | None = None,
|
| 375 |
+
) -> AsyncNetworkStream:
|
| 376 |
+
return await self._stream.start_tls(ssl_context, server_hostname, timeout)
|
| 377 |
+
|
| 378 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 379 |
+
return self._stream.get_extra_info(info)
|
lib/python3.12/site-packages/httpcore/_async/http2.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
import types
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
import h2.config
|
| 10 |
+
import h2.connection
|
| 11 |
+
import h2.events
|
| 12 |
+
import h2.exceptions
|
| 13 |
+
import h2.settings
|
| 14 |
+
|
| 15 |
+
from .._backends.base import AsyncNetworkStream
|
| 16 |
+
from .._exceptions import (
|
| 17 |
+
ConnectionNotAvailable,
|
| 18 |
+
LocalProtocolError,
|
| 19 |
+
RemoteProtocolError,
|
| 20 |
+
)
|
| 21 |
+
from .._models import Origin, Request, Response
|
| 22 |
+
from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation
|
| 23 |
+
from .._trace import Trace
|
| 24 |
+
from .interfaces import AsyncConnectionInterface
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger("httpcore.http2")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def has_body_headers(request: Request) -> bool:
|
| 30 |
+
return any(
|
| 31 |
+
k.lower() == b"content-length" or k.lower() == b"transfer-encoding"
|
| 32 |
+
for k, v in request.headers
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HTTPConnectionState(enum.IntEnum):
|
| 37 |
+
ACTIVE = 1
|
| 38 |
+
IDLE = 2
|
| 39 |
+
CLOSED = 3
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class AsyncHTTP2Connection(AsyncConnectionInterface):
|
| 43 |
+
READ_NUM_BYTES = 64 * 1024
|
| 44 |
+
CONFIG = h2.config.H2Configuration(validate_inbound_headers=False)
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
origin: Origin,
|
| 49 |
+
stream: AsyncNetworkStream,
|
| 50 |
+
keepalive_expiry: float | None = None,
|
| 51 |
+
):
|
| 52 |
+
self._origin = origin
|
| 53 |
+
self._network_stream = stream
|
| 54 |
+
self._keepalive_expiry: float | None = keepalive_expiry
|
| 55 |
+
self._h2_state = h2.connection.H2Connection(config=self.CONFIG)
|
| 56 |
+
self._state = HTTPConnectionState.IDLE
|
| 57 |
+
self._expire_at: float | None = None
|
| 58 |
+
self._request_count = 0
|
| 59 |
+
self._init_lock = AsyncLock()
|
| 60 |
+
self._state_lock = AsyncLock()
|
| 61 |
+
self._read_lock = AsyncLock()
|
| 62 |
+
self._write_lock = AsyncLock()
|
| 63 |
+
self._sent_connection_init = False
|
| 64 |
+
self._used_all_stream_ids = False
|
| 65 |
+
self._connection_error = False
|
| 66 |
+
|
| 67 |
+
# Mapping from stream ID to response stream events.
|
| 68 |
+
self._events: dict[
|
| 69 |
+
int,
|
| 70 |
+
list[
|
| 71 |
+
h2.events.ResponseReceived
|
| 72 |
+
| h2.events.DataReceived
|
| 73 |
+
| h2.events.StreamEnded
|
| 74 |
+
| h2.events.StreamReset,
|
| 75 |
+
],
|
| 76 |
+
] = {}
|
| 77 |
+
|
| 78 |
+
# Connection terminated events are stored as state since
|
| 79 |
+
# we need to handle them for all streams.
|
| 80 |
+
self._connection_terminated: h2.events.ConnectionTerminated | None = None
|
| 81 |
+
|
| 82 |
+
self._read_exception: Exception | None = None
|
| 83 |
+
self._write_exception: Exception | None = None
|
| 84 |
+
|
| 85 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 86 |
+
if not self.can_handle_request(request.url.origin):
|
| 87 |
+
# This cannot occur in normal operation, since the connection pool
|
| 88 |
+
# will only send requests on connections that handle them.
|
| 89 |
+
# It's in place simply for resilience as a guard against incorrect
|
| 90 |
+
# usage, for anyone working directly with httpcore connections.
|
| 91 |
+
raise RuntimeError(
|
| 92 |
+
f"Attempted to send request to {request.url.origin} on connection "
|
| 93 |
+
f"to {self._origin}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
async with self._state_lock:
|
| 97 |
+
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
|
| 98 |
+
self._request_count += 1
|
| 99 |
+
self._expire_at = None
|
| 100 |
+
self._state = HTTPConnectionState.ACTIVE
|
| 101 |
+
else:
|
| 102 |
+
raise ConnectionNotAvailable()
|
| 103 |
+
|
| 104 |
+
async with self._init_lock:
|
| 105 |
+
if not self._sent_connection_init:
|
| 106 |
+
try:
|
| 107 |
+
sci_kwargs = {"request": request}
|
| 108 |
+
async with Trace(
|
| 109 |
+
"send_connection_init", logger, request, sci_kwargs
|
| 110 |
+
):
|
| 111 |
+
await self._send_connection_init(**sci_kwargs)
|
| 112 |
+
except BaseException as exc:
|
| 113 |
+
with AsyncShieldCancellation():
|
| 114 |
+
await self.aclose()
|
| 115 |
+
raise exc
|
| 116 |
+
|
| 117 |
+
self._sent_connection_init = True
|
| 118 |
+
|
| 119 |
+
# Initially start with just 1 until the remote server provides
|
| 120 |
+
# its max_concurrent_streams value
|
| 121 |
+
self._max_streams = 1
|
| 122 |
+
|
| 123 |
+
local_settings_max_streams = (
|
| 124 |
+
self._h2_state.local_settings.max_concurrent_streams
|
| 125 |
+
)
|
| 126 |
+
self._max_streams_semaphore = AsyncSemaphore(local_settings_max_streams)
|
| 127 |
+
|
| 128 |
+
for _ in range(local_settings_max_streams - self._max_streams):
|
| 129 |
+
await self._max_streams_semaphore.acquire()
|
| 130 |
+
|
| 131 |
+
await self._max_streams_semaphore.acquire()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
stream_id = self._h2_state.get_next_available_stream_id()
|
| 135 |
+
self._events[stream_id] = []
|
| 136 |
+
except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover
|
| 137 |
+
self._used_all_stream_ids = True
|
| 138 |
+
self._request_count -= 1
|
| 139 |
+
raise ConnectionNotAvailable()
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
kwargs = {"request": request, "stream_id": stream_id}
|
| 143 |
+
async with Trace("send_request_headers", logger, request, kwargs):
|
| 144 |
+
await self._send_request_headers(request=request, stream_id=stream_id)
|
| 145 |
+
async with Trace("send_request_body", logger, request, kwargs):
|
| 146 |
+
await self._send_request_body(request=request, stream_id=stream_id)
|
| 147 |
+
async with Trace(
|
| 148 |
+
"receive_response_headers", logger, request, kwargs
|
| 149 |
+
) as trace:
|
| 150 |
+
status, headers = await self._receive_response(
|
| 151 |
+
request=request, stream_id=stream_id
|
| 152 |
+
)
|
| 153 |
+
trace.return_value = (status, headers)
|
| 154 |
+
|
| 155 |
+
return Response(
|
| 156 |
+
status=status,
|
| 157 |
+
headers=headers,
|
| 158 |
+
content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id),
|
| 159 |
+
extensions={
|
| 160 |
+
"http_version": b"HTTP/2",
|
| 161 |
+
"network_stream": self._network_stream,
|
| 162 |
+
"stream_id": stream_id,
|
| 163 |
+
},
|
| 164 |
+
)
|
| 165 |
+
except BaseException as exc: # noqa: PIE786
|
| 166 |
+
with AsyncShieldCancellation():
|
| 167 |
+
kwargs = {"stream_id": stream_id}
|
| 168 |
+
async with Trace("response_closed", logger, request, kwargs):
|
| 169 |
+
await self._response_closed(stream_id=stream_id)
|
| 170 |
+
|
| 171 |
+
if isinstance(exc, h2.exceptions.ProtocolError):
|
| 172 |
+
# One case where h2 can raise a protocol error is when a
|
| 173 |
+
# closed frame has been seen by the state machine.
|
| 174 |
+
#
|
| 175 |
+
# This happens when one stream is reading, and encounters
|
| 176 |
+
# a GOAWAY event. Other flows of control may then raise
|
| 177 |
+
# a protocol error at any point they interact with the 'h2_state'.
|
| 178 |
+
#
|
| 179 |
+
# In this case we'll have stored the event, and should raise
|
| 180 |
+
# it as a RemoteProtocolError.
|
| 181 |
+
if self._connection_terminated: # pragma: nocover
|
| 182 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 183 |
+
# If h2 raises a protocol error in some other state then we
|
| 184 |
+
# must somehow have made a protocol violation.
|
| 185 |
+
raise LocalProtocolError(exc) # pragma: nocover
|
| 186 |
+
|
| 187 |
+
raise exc
|
| 188 |
+
|
| 189 |
+
async def _send_connection_init(self, request: Request) -> None:
|
| 190 |
+
"""
|
| 191 |
+
The HTTP/2 connection requires some initial setup before we can start
|
| 192 |
+
using individual request/response streams on it.
|
| 193 |
+
"""
|
| 194 |
+
# Need to set these manually here instead of manipulating via
|
| 195 |
+
# __setitem__() otherwise the H2Connection will emit SettingsUpdate
|
| 196 |
+
# frames in addition to sending the undesired defaults.
|
| 197 |
+
self._h2_state.local_settings = h2.settings.Settings(
|
| 198 |
+
client=True,
|
| 199 |
+
initial_values={
|
| 200 |
+
# Disable PUSH_PROMISE frames from the server since we don't do anything
|
| 201 |
+
# with them for now. Maybe when we support caching?
|
| 202 |
+
h2.settings.SettingCodes.ENABLE_PUSH: 0,
|
| 203 |
+
# These two are taken from h2 for safe defaults
|
| 204 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
|
| 205 |
+
h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
|
| 206 |
+
},
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
# Some websites (*cough* Yahoo *cough*) balk at this setting being
|
| 210 |
+
# present in the initial handshake since it's not defined in the original
|
| 211 |
+
# RFC despite the RFC mandating ignoring settings you don't know about.
|
| 212 |
+
del self._h2_state.local_settings[
|
| 213 |
+
h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
self._h2_state.initiate_connection()
|
| 217 |
+
self._h2_state.increment_flow_control_window(2**24)
|
| 218 |
+
await self._write_outgoing_data(request)
|
| 219 |
+
|
| 220 |
+
# Sending the request...
|
| 221 |
+
|
| 222 |
+
async def _send_request_headers(self, request: Request, stream_id: int) -> None:
|
| 223 |
+
"""
|
| 224 |
+
Send the request headers to a given stream ID.
|
| 225 |
+
"""
|
| 226 |
+
end_stream = not has_body_headers(request)
|
| 227 |
+
|
| 228 |
+
# In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'.
|
| 229 |
+
# In order to gracefully handle HTTP/1.1 and HTTP/2 we always require
|
| 230 |
+
# HTTP/1.1 style headers, and map them appropriately if we end up on
|
| 231 |
+
# an HTTP/2 connection.
|
| 232 |
+
authority = [v for k, v in request.headers if k.lower() == b"host"][0]
|
| 233 |
+
|
| 234 |
+
headers = [
|
| 235 |
+
(b":method", request.method),
|
| 236 |
+
(b":authority", authority),
|
| 237 |
+
(b":scheme", request.url.scheme),
|
| 238 |
+
(b":path", request.url.target),
|
| 239 |
+
] + [
|
| 240 |
+
(k.lower(), v)
|
| 241 |
+
for k, v in request.headers
|
| 242 |
+
if k.lower()
|
| 243 |
+
not in (
|
| 244 |
+
b"host",
|
| 245 |
+
b"transfer-encoding",
|
| 246 |
+
)
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
|
| 250 |
+
self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id)
|
| 251 |
+
await self._write_outgoing_data(request)
|
| 252 |
+
|
| 253 |
+
async def _send_request_body(self, request: Request, stream_id: int) -> None:
|
| 254 |
+
"""
|
| 255 |
+
Iterate over the request body sending it to a given stream ID.
|
| 256 |
+
"""
|
| 257 |
+
if not has_body_headers(request):
|
| 258 |
+
return
|
| 259 |
+
|
| 260 |
+
assert isinstance(request.stream, typing.AsyncIterable)
|
| 261 |
+
async for data in request.stream:
|
| 262 |
+
await self._send_stream_data(request, stream_id, data)
|
| 263 |
+
await self._send_end_stream(request, stream_id)
|
| 264 |
+
|
| 265 |
+
async def _send_stream_data(
|
| 266 |
+
self, request: Request, stream_id: int, data: bytes
|
| 267 |
+
) -> None:
|
| 268 |
+
"""
|
| 269 |
+
Send a single chunk of data in one or more data frames.
|
| 270 |
+
"""
|
| 271 |
+
while data:
|
| 272 |
+
max_flow = await self._wait_for_outgoing_flow(request, stream_id)
|
| 273 |
+
chunk_size = min(len(data), max_flow)
|
| 274 |
+
chunk, data = data[:chunk_size], data[chunk_size:]
|
| 275 |
+
self._h2_state.send_data(stream_id, chunk)
|
| 276 |
+
await self._write_outgoing_data(request)
|
| 277 |
+
|
| 278 |
+
async def _send_end_stream(self, request: Request, stream_id: int) -> None:
|
| 279 |
+
"""
|
| 280 |
+
Send an empty data frame on on a given stream ID with the END_STREAM flag set.
|
| 281 |
+
"""
|
| 282 |
+
self._h2_state.end_stream(stream_id)
|
| 283 |
+
await self._write_outgoing_data(request)
|
| 284 |
+
|
| 285 |
+
# Receiving the response...
|
| 286 |
+
|
| 287 |
+
async def _receive_response(
|
| 288 |
+
self, request: Request, stream_id: int
|
| 289 |
+
) -> tuple[int, list[tuple[bytes, bytes]]]:
|
| 290 |
+
"""
|
| 291 |
+
Return the response status code and headers for a given stream ID.
|
| 292 |
+
"""
|
| 293 |
+
while True:
|
| 294 |
+
event = await self._receive_stream_event(request, stream_id)
|
| 295 |
+
if isinstance(event, h2.events.ResponseReceived):
|
| 296 |
+
break
|
| 297 |
+
|
| 298 |
+
status_code = 200
|
| 299 |
+
headers = []
|
| 300 |
+
assert event.headers is not None
|
| 301 |
+
for k, v in event.headers:
|
| 302 |
+
if k == b":status":
|
| 303 |
+
status_code = int(v.decode("ascii", errors="ignore"))
|
| 304 |
+
elif not k.startswith(b":"):
|
| 305 |
+
headers.append((k, v))
|
| 306 |
+
|
| 307 |
+
return (status_code, headers)
|
| 308 |
+
|
| 309 |
+
async def _receive_response_body(
|
| 310 |
+
self, request: Request, stream_id: int
|
| 311 |
+
) -> typing.AsyncIterator[bytes]:
|
| 312 |
+
"""
|
| 313 |
+
Iterator that returns the bytes of the response body for a given stream ID.
|
| 314 |
+
"""
|
| 315 |
+
while True:
|
| 316 |
+
event = await self._receive_stream_event(request, stream_id)
|
| 317 |
+
if isinstance(event, h2.events.DataReceived):
|
| 318 |
+
assert event.flow_controlled_length is not None
|
| 319 |
+
assert event.data is not None
|
| 320 |
+
amount = event.flow_controlled_length
|
| 321 |
+
self._h2_state.acknowledge_received_data(amount, stream_id)
|
| 322 |
+
await self._write_outgoing_data(request)
|
| 323 |
+
yield event.data
|
| 324 |
+
elif isinstance(event, h2.events.StreamEnded):
|
| 325 |
+
break
|
| 326 |
+
|
| 327 |
+
async def _receive_stream_event(
|
| 328 |
+
self, request: Request, stream_id: int
|
| 329 |
+
) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded:
|
| 330 |
+
"""
|
| 331 |
+
Return the next available event for a given stream ID.
|
| 332 |
+
|
| 333 |
+
Will read more data from the network if required.
|
| 334 |
+
"""
|
| 335 |
+
while not self._events.get(stream_id):
|
| 336 |
+
await self._receive_events(request, stream_id)
|
| 337 |
+
event = self._events[stream_id].pop(0)
|
| 338 |
+
if isinstance(event, h2.events.StreamReset):
|
| 339 |
+
raise RemoteProtocolError(event)
|
| 340 |
+
return event
|
| 341 |
+
|
| 342 |
+
async def _receive_events(
|
| 343 |
+
self, request: Request, stream_id: int | None = None
|
| 344 |
+
) -> None:
|
| 345 |
+
"""
|
| 346 |
+
Read some data from the network until we see one or more events
|
| 347 |
+
for a given stream ID.
|
| 348 |
+
"""
|
| 349 |
+
async with self._read_lock:
|
| 350 |
+
if self._connection_terminated is not None:
|
| 351 |
+
last_stream_id = self._connection_terminated.last_stream_id
|
| 352 |
+
if stream_id and last_stream_id and stream_id > last_stream_id:
|
| 353 |
+
self._request_count -= 1
|
| 354 |
+
raise ConnectionNotAvailable()
|
| 355 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 356 |
+
|
| 357 |
+
# This conditional is a bit icky. We don't want to block reading if we've
|
| 358 |
+
# actually got an event to return for a given stream. We need to do that
|
| 359 |
+
# check *within* the atomic read lock. Though it also need to be optional,
|
| 360 |
+
# because when we call it from `_wait_for_outgoing_flow` we *do* want to
|
| 361 |
+
# block until we've available flow control, event when we have events
|
| 362 |
+
# pending for the stream ID we're attempting to send on.
|
| 363 |
+
if stream_id is None or not self._events.get(stream_id):
|
| 364 |
+
events = await self._read_incoming_data(request)
|
| 365 |
+
for event in events:
|
| 366 |
+
if isinstance(event, h2.events.RemoteSettingsChanged):
|
| 367 |
+
async with Trace(
|
| 368 |
+
"receive_remote_settings", logger, request
|
| 369 |
+
) as trace:
|
| 370 |
+
await self._receive_remote_settings_change(event)
|
| 371 |
+
trace.return_value = event
|
| 372 |
+
|
| 373 |
+
elif isinstance(
|
| 374 |
+
event,
|
| 375 |
+
(
|
| 376 |
+
h2.events.ResponseReceived,
|
| 377 |
+
h2.events.DataReceived,
|
| 378 |
+
h2.events.StreamEnded,
|
| 379 |
+
h2.events.StreamReset,
|
| 380 |
+
),
|
| 381 |
+
):
|
| 382 |
+
if event.stream_id in self._events:
|
| 383 |
+
self._events[event.stream_id].append(event)
|
| 384 |
+
|
| 385 |
+
elif isinstance(event, h2.events.ConnectionTerminated):
|
| 386 |
+
self._connection_terminated = event
|
| 387 |
+
|
| 388 |
+
await self._write_outgoing_data(request)
|
| 389 |
+
|
| 390 |
+
async def _receive_remote_settings_change(
|
| 391 |
+
self, event: h2.events.RemoteSettingsChanged
|
| 392 |
+
) -> None:
|
| 393 |
+
max_concurrent_streams = event.changed_settings.get(
|
| 394 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS
|
| 395 |
+
)
|
| 396 |
+
if max_concurrent_streams:
|
| 397 |
+
new_max_streams = min(
|
| 398 |
+
max_concurrent_streams.new_value,
|
| 399 |
+
self._h2_state.local_settings.max_concurrent_streams,
|
| 400 |
+
)
|
| 401 |
+
if new_max_streams and new_max_streams != self._max_streams:
|
| 402 |
+
while new_max_streams > self._max_streams:
|
| 403 |
+
await self._max_streams_semaphore.release()
|
| 404 |
+
self._max_streams += 1
|
| 405 |
+
while new_max_streams < self._max_streams:
|
| 406 |
+
await self._max_streams_semaphore.acquire()
|
| 407 |
+
self._max_streams -= 1
|
| 408 |
+
|
| 409 |
+
async def _response_closed(self, stream_id: int) -> None:
|
| 410 |
+
await self._max_streams_semaphore.release()
|
| 411 |
+
del self._events[stream_id]
|
| 412 |
+
async with self._state_lock:
|
| 413 |
+
if self._connection_terminated and not self._events:
|
| 414 |
+
await self.aclose()
|
| 415 |
+
|
| 416 |
+
elif self._state == HTTPConnectionState.ACTIVE and not self._events:
|
| 417 |
+
self._state = HTTPConnectionState.IDLE
|
| 418 |
+
if self._keepalive_expiry is not None:
|
| 419 |
+
now = time.monotonic()
|
| 420 |
+
self._expire_at = now + self._keepalive_expiry
|
| 421 |
+
if self._used_all_stream_ids: # pragma: nocover
|
| 422 |
+
await self.aclose()
|
| 423 |
+
|
| 424 |
+
async def aclose(self) -> None:
|
| 425 |
+
# Note that this method unilaterally closes the connection, and does
|
| 426 |
+
# not have any kind of locking in place around it.
|
| 427 |
+
self._h2_state.close_connection()
|
| 428 |
+
self._state = HTTPConnectionState.CLOSED
|
| 429 |
+
await self._network_stream.aclose()
|
| 430 |
+
|
| 431 |
+
# Wrappers around network read/write operations...
|
| 432 |
+
|
| 433 |
+
async def _read_incoming_data(self, request: Request) -> list[h2.events.Event]:
|
| 434 |
+
timeouts = request.extensions.get("timeout", {})
|
| 435 |
+
timeout = timeouts.get("read", None)
|
| 436 |
+
|
| 437 |
+
if self._read_exception is not None:
|
| 438 |
+
raise self._read_exception # pragma: nocover
|
| 439 |
+
|
| 440 |
+
try:
|
| 441 |
+
data = await self._network_stream.read(self.READ_NUM_BYTES, timeout)
|
| 442 |
+
if data == b"":
|
| 443 |
+
raise RemoteProtocolError("Server disconnected")
|
| 444 |
+
except Exception as exc:
|
| 445 |
+
# If we get a network error we should:
|
| 446 |
+
#
|
| 447 |
+
# 1. Save the exception and just raise it immediately on any future reads.
|
| 448 |
+
# (For example, this means that a single read timeout or disconnect will
|
| 449 |
+
# immediately close all pending streams. Without requiring multiple
|
| 450 |
+
# sequential timeouts.)
|
| 451 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 452 |
+
# incoming requests.
|
| 453 |
+
self._read_exception = exc
|
| 454 |
+
self._connection_error = True
|
| 455 |
+
raise exc
|
| 456 |
+
|
| 457 |
+
events: list[h2.events.Event] = self._h2_state.receive_data(data)
|
| 458 |
+
|
| 459 |
+
return events
|
| 460 |
+
|
| 461 |
+
async def _write_outgoing_data(self, request: Request) -> None:
|
| 462 |
+
timeouts = request.extensions.get("timeout", {})
|
| 463 |
+
timeout = timeouts.get("write", None)
|
| 464 |
+
|
| 465 |
+
async with self._write_lock:
|
| 466 |
+
data_to_send = self._h2_state.data_to_send()
|
| 467 |
+
|
| 468 |
+
if self._write_exception is not None:
|
| 469 |
+
raise self._write_exception # pragma: nocover
|
| 470 |
+
|
| 471 |
+
try:
|
| 472 |
+
await self._network_stream.write(data_to_send, timeout)
|
| 473 |
+
except Exception as exc: # pragma: nocover
|
| 474 |
+
# If we get a network error we should:
|
| 475 |
+
#
|
| 476 |
+
# 1. Save the exception and just raise it immediately on any future write.
|
| 477 |
+
# (For example, this means that a single write timeout or disconnect will
|
| 478 |
+
# immediately close all pending streams. Without requiring multiple
|
| 479 |
+
# sequential timeouts.)
|
| 480 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 481 |
+
# incoming requests.
|
| 482 |
+
self._write_exception = exc
|
| 483 |
+
self._connection_error = True
|
| 484 |
+
raise exc
|
| 485 |
+
|
| 486 |
+
# Flow control...
|
| 487 |
+
|
| 488 |
+
async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int:
|
| 489 |
+
"""
|
| 490 |
+
Returns the maximum allowable outgoing flow for a given stream.
|
| 491 |
+
|
| 492 |
+
If the allowable flow is zero, then waits on the network until
|
| 493 |
+
WindowUpdated frames have increased the flow rate.
|
| 494 |
+
https://tools.ietf.org/html/rfc7540#section-6.9
|
| 495 |
+
"""
|
| 496 |
+
local_flow: int = self._h2_state.local_flow_control_window(stream_id)
|
| 497 |
+
max_frame_size: int = self._h2_state.max_outbound_frame_size
|
| 498 |
+
flow = min(local_flow, max_frame_size)
|
| 499 |
+
while flow == 0:
|
| 500 |
+
await self._receive_events(request)
|
| 501 |
+
local_flow = self._h2_state.local_flow_control_window(stream_id)
|
| 502 |
+
max_frame_size = self._h2_state.max_outbound_frame_size
|
| 503 |
+
flow = min(local_flow, max_frame_size)
|
| 504 |
+
return flow
|
| 505 |
+
|
| 506 |
+
# Interface for connection pooling...
|
| 507 |
+
|
| 508 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 509 |
+
return origin == self._origin
|
| 510 |
+
|
| 511 |
+
def is_available(self) -> bool:
|
| 512 |
+
return (
|
| 513 |
+
self._state != HTTPConnectionState.CLOSED
|
| 514 |
+
and not self._connection_error
|
| 515 |
+
and not self._used_all_stream_ids
|
| 516 |
+
and not (
|
| 517 |
+
self._h2_state.state_machine.state
|
| 518 |
+
== h2.connection.ConnectionState.CLOSED
|
| 519 |
+
)
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
def has_expired(self) -> bool:
|
| 523 |
+
now = time.monotonic()
|
| 524 |
+
return self._expire_at is not None and now > self._expire_at
|
| 525 |
+
|
| 526 |
+
def is_idle(self) -> bool:
|
| 527 |
+
return self._state == HTTPConnectionState.IDLE
|
| 528 |
+
|
| 529 |
+
def is_closed(self) -> bool:
|
| 530 |
+
return self._state == HTTPConnectionState.CLOSED
|
| 531 |
+
|
| 532 |
+
def info(self) -> str:
|
| 533 |
+
origin = str(self._origin)
|
| 534 |
+
return (
|
| 535 |
+
f"{origin!r}, HTTP/2, {self._state.name}, "
|
| 536 |
+
f"Request Count: {self._request_count}"
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def __repr__(self) -> str:
|
| 540 |
+
class_name = self.__class__.__name__
|
| 541 |
+
origin = str(self._origin)
|
| 542 |
+
return (
|
| 543 |
+
f"<{class_name} [{origin!r}, {self._state.name}, "
|
| 544 |
+
f"Request Count: {self._request_count}]>"
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
# These context managers are not used in the standard flow, but are
|
| 548 |
+
# useful for testing or working with connection instances directly.
|
| 549 |
+
|
| 550 |
+
async def __aenter__(self) -> AsyncHTTP2Connection:
|
| 551 |
+
return self
|
| 552 |
+
|
| 553 |
+
async def __aexit__(
|
| 554 |
+
self,
|
| 555 |
+
exc_type: type[BaseException] | None = None,
|
| 556 |
+
exc_value: BaseException | None = None,
|
| 557 |
+
traceback: types.TracebackType | None = None,
|
| 558 |
+
) -> None:
|
| 559 |
+
await self.aclose()
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
class HTTP2ConnectionByteStream:
|
| 563 |
+
def __init__(
|
| 564 |
+
self, connection: AsyncHTTP2Connection, request: Request, stream_id: int
|
| 565 |
+
) -> None:
|
| 566 |
+
self._connection = connection
|
| 567 |
+
self._request = request
|
| 568 |
+
self._stream_id = stream_id
|
| 569 |
+
self._closed = False
|
| 570 |
+
|
| 571 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 572 |
+
kwargs = {"request": self._request, "stream_id": self._stream_id}
|
| 573 |
+
try:
|
| 574 |
+
async with Trace("receive_response_body", logger, self._request, kwargs):
|
| 575 |
+
async for chunk in self._connection._receive_response_body(
|
| 576 |
+
request=self._request, stream_id=self._stream_id
|
| 577 |
+
):
|
| 578 |
+
yield chunk
|
| 579 |
+
except BaseException as exc:
|
| 580 |
+
# If we get an exception while streaming the response,
|
| 581 |
+
# we want to close the response (and possibly the connection)
|
| 582 |
+
# before raising that exception.
|
| 583 |
+
with AsyncShieldCancellation():
|
| 584 |
+
await self.aclose()
|
| 585 |
+
raise exc
|
| 586 |
+
|
| 587 |
+
async def aclose(self) -> None:
|
| 588 |
+
if not self._closed:
|
| 589 |
+
self._closed = True
|
| 590 |
+
kwargs = {"stream_id": self._stream_id}
|
| 591 |
+
async with Trace("response_closed", logger, self._request, kwargs):
|
| 592 |
+
await self._connection._response_closed(stream_id=self._stream_id)
|
lib/python3.12/site-packages/httpcore/_async/http_proxy.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
|
| 9 |
+
from .._exceptions import ProxyError
|
| 10 |
+
from .._models import (
|
| 11 |
+
URL,
|
| 12 |
+
Origin,
|
| 13 |
+
Request,
|
| 14 |
+
Response,
|
| 15 |
+
enforce_bytes,
|
| 16 |
+
enforce_headers,
|
| 17 |
+
enforce_url,
|
| 18 |
+
)
|
| 19 |
+
from .._ssl import default_ssl_context
|
| 20 |
+
from .._synchronization import AsyncLock
|
| 21 |
+
from .._trace import Trace
|
| 22 |
+
from .connection import AsyncHTTPConnection
|
| 23 |
+
from .connection_pool import AsyncConnectionPool
|
| 24 |
+
from .http11 import AsyncHTTP11Connection
|
| 25 |
+
from .interfaces import AsyncConnectionInterface
|
| 26 |
+
|
| 27 |
+
ByteOrStr = typing.Union[bytes, str]
|
| 28 |
+
HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
|
| 29 |
+
HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger("httpcore.proxy")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def merge_headers(
|
| 36 |
+
default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 37 |
+
override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 38 |
+
) -> list[tuple[bytes, bytes]]:
|
| 39 |
+
"""
|
| 40 |
+
Append default_headers and override_headers, de-duplicating if a key exists
|
| 41 |
+
in both cases.
|
| 42 |
+
"""
|
| 43 |
+
default_headers = [] if default_headers is None else list(default_headers)
|
| 44 |
+
override_headers = [] if override_headers is None else list(override_headers)
|
| 45 |
+
has_override = set(key.lower() for key, value in override_headers)
|
| 46 |
+
default_headers = [
|
| 47 |
+
(key, value)
|
| 48 |
+
for key, value in default_headers
|
| 49 |
+
if key.lower() not in has_override
|
| 50 |
+
]
|
| 51 |
+
return default_headers + override_headers
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class AsyncHTTPProxy(AsyncConnectionPool): # pragma: nocover
|
| 55 |
+
"""
|
| 56 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
proxy_url: URL | bytes | str,
|
| 62 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 63 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 64 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 65 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 66 |
+
max_connections: int | None = 10,
|
| 67 |
+
max_keepalive_connections: int | None = None,
|
| 68 |
+
keepalive_expiry: float | None = None,
|
| 69 |
+
http1: bool = True,
|
| 70 |
+
http2: bool = False,
|
| 71 |
+
retries: int = 0,
|
| 72 |
+
local_address: str | None = None,
|
| 73 |
+
uds: str | None = None,
|
| 74 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 75 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 76 |
+
) -> None:
|
| 77 |
+
"""
|
| 78 |
+
A connection pool for making HTTP requests.
|
| 79 |
+
|
| 80 |
+
Parameters:
|
| 81 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 82 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 83 |
+
proxy_auth: Any proxy authentication as a two-tuple of
|
| 84 |
+
(username, password). May be either bytes or ascii-only str.
|
| 85 |
+
proxy_headers: Any HTTP headers to use for the proxy requests.
|
| 86 |
+
For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
|
| 87 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 88 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 89 |
+
will be used.
|
| 90 |
+
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
|
| 91 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 92 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 93 |
+
would exceed this amount will block until a connection is available.
|
| 94 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 95 |
+
that will be maintained in the pool.
|
| 96 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 97 |
+
may be maintained for before being expired from the pool.
|
| 98 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 99 |
+
by the connection pool. Defaults to True.
|
| 100 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 101 |
+
the connection pool. Defaults to False.
|
| 102 |
+
retries: The maximum number of retries when trying to establish
|
| 103 |
+
a connection.
|
| 104 |
+
local_address: Local address to connect from. Can also be used to
|
| 105 |
+
connect using a particular address family. Using
|
| 106 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 107 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 108 |
+
`AF_INET6` address (IPv6).
|
| 109 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 110 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 111 |
+
"""
|
| 112 |
+
super().__init__(
|
| 113 |
+
ssl_context=ssl_context,
|
| 114 |
+
max_connections=max_connections,
|
| 115 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 116 |
+
keepalive_expiry=keepalive_expiry,
|
| 117 |
+
http1=http1,
|
| 118 |
+
http2=http2,
|
| 119 |
+
network_backend=network_backend,
|
| 120 |
+
retries=retries,
|
| 121 |
+
local_address=local_address,
|
| 122 |
+
uds=uds,
|
| 123 |
+
socket_options=socket_options,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 127 |
+
if (
|
| 128 |
+
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
|
| 129 |
+
): # pragma: no cover
|
| 130 |
+
raise RuntimeError(
|
| 131 |
+
"The `proxy_ssl_context` argument is not allowed for the http scheme"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
self._ssl_context = ssl_context
|
| 135 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 136 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 137 |
+
if proxy_auth is not None:
|
| 138 |
+
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
|
| 139 |
+
password = enforce_bytes(proxy_auth[1], name="proxy_auth")
|
| 140 |
+
userpass = username + b":" + password
|
| 141 |
+
authorization = b"Basic " + base64.b64encode(userpass)
|
| 142 |
+
self._proxy_headers = [
|
| 143 |
+
(b"Proxy-Authorization", authorization)
|
| 144 |
+
] + self._proxy_headers
|
| 145 |
+
|
| 146 |
+
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
|
| 147 |
+
if origin.scheme == b"http":
|
| 148 |
+
return AsyncForwardHTTPConnection(
|
| 149 |
+
proxy_origin=self._proxy_url.origin,
|
| 150 |
+
proxy_headers=self._proxy_headers,
|
| 151 |
+
remote_origin=origin,
|
| 152 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 153 |
+
network_backend=self._network_backend,
|
| 154 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 155 |
+
)
|
| 156 |
+
return AsyncTunnelHTTPConnection(
|
| 157 |
+
proxy_origin=self._proxy_url.origin,
|
| 158 |
+
proxy_headers=self._proxy_headers,
|
| 159 |
+
remote_origin=origin,
|
| 160 |
+
ssl_context=self._ssl_context,
|
| 161 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 162 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 163 |
+
http1=self._http1,
|
| 164 |
+
http2=self._http2,
|
| 165 |
+
network_backend=self._network_backend,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class AsyncForwardHTTPConnection(AsyncConnectionInterface):
|
| 170 |
+
def __init__(
|
| 171 |
+
self,
|
| 172 |
+
proxy_origin: Origin,
|
| 173 |
+
remote_origin: Origin,
|
| 174 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 175 |
+
keepalive_expiry: float | None = None,
|
| 176 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 177 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 178 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 179 |
+
) -> None:
|
| 180 |
+
self._connection = AsyncHTTPConnection(
|
| 181 |
+
origin=proxy_origin,
|
| 182 |
+
keepalive_expiry=keepalive_expiry,
|
| 183 |
+
network_backend=network_backend,
|
| 184 |
+
socket_options=socket_options,
|
| 185 |
+
ssl_context=proxy_ssl_context,
|
| 186 |
+
)
|
| 187 |
+
self._proxy_origin = proxy_origin
|
| 188 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 189 |
+
self._remote_origin = remote_origin
|
| 190 |
+
|
| 191 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 192 |
+
headers = merge_headers(self._proxy_headers, request.headers)
|
| 193 |
+
url = URL(
|
| 194 |
+
scheme=self._proxy_origin.scheme,
|
| 195 |
+
host=self._proxy_origin.host,
|
| 196 |
+
port=self._proxy_origin.port,
|
| 197 |
+
target=bytes(request.url),
|
| 198 |
+
)
|
| 199 |
+
proxy_request = Request(
|
| 200 |
+
method=request.method,
|
| 201 |
+
url=url,
|
| 202 |
+
headers=headers,
|
| 203 |
+
content=request.stream,
|
| 204 |
+
extensions=request.extensions,
|
| 205 |
+
)
|
| 206 |
+
return await self._connection.handle_async_request(proxy_request)
|
| 207 |
+
|
| 208 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 209 |
+
return origin == self._remote_origin
|
| 210 |
+
|
| 211 |
+
async def aclose(self) -> None:
|
| 212 |
+
await self._connection.aclose()
|
| 213 |
+
|
| 214 |
+
def info(self) -> str:
|
| 215 |
+
return self._connection.info()
|
| 216 |
+
|
| 217 |
+
def is_available(self) -> bool:
|
| 218 |
+
return self._connection.is_available()
|
| 219 |
+
|
| 220 |
+
def has_expired(self) -> bool:
|
| 221 |
+
return self._connection.has_expired()
|
| 222 |
+
|
| 223 |
+
def is_idle(self) -> bool:
|
| 224 |
+
return self._connection.is_idle()
|
| 225 |
+
|
| 226 |
+
def is_closed(self) -> bool:
|
| 227 |
+
return self._connection.is_closed()
|
| 228 |
+
|
| 229 |
+
def __repr__(self) -> str:
|
| 230 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class AsyncTunnelHTTPConnection(AsyncConnectionInterface):
|
| 234 |
+
def __init__(
|
| 235 |
+
self,
|
| 236 |
+
proxy_origin: Origin,
|
| 237 |
+
remote_origin: Origin,
|
| 238 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 239 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 240 |
+
proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 241 |
+
keepalive_expiry: float | None = None,
|
| 242 |
+
http1: bool = True,
|
| 243 |
+
http2: bool = False,
|
| 244 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 245 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 246 |
+
) -> None:
|
| 247 |
+
self._connection: AsyncConnectionInterface = AsyncHTTPConnection(
|
| 248 |
+
origin=proxy_origin,
|
| 249 |
+
keepalive_expiry=keepalive_expiry,
|
| 250 |
+
network_backend=network_backend,
|
| 251 |
+
socket_options=socket_options,
|
| 252 |
+
ssl_context=proxy_ssl_context,
|
| 253 |
+
)
|
| 254 |
+
self._proxy_origin = proxy_origin
|
| 255 |
+
self._remote_origin = remote_origin
|
| 256 |
+
self._ssl_context = ssl_context
|
| 257 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 258 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 259 |
+
self._keepalive_expiry = keepalive_expiry
|
| 260 |
+
self._http1 = http1
|
| 261 |
+
self._http2 = http2
|
| 262 |
+
self._connect_lock = AsyncLock()
|
| 263 |
+
self._connected = False
|
| 264 |
+
|
| 265 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 266 |
+
timeouts = request.extensions.get("timeout", {})
|
| 267 |
+
timeout = timeouts.get("connect", None)
|
| 268 |
+
|
| 269 |
+
async with self._connect_lock:
|
| 270 |
+
if not self._connected:
|
| 271 |
+
target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port)
|
| 272 |
+
|
| 273 |
+
connect_url = URL(
|
| 274 |
+
scheme=self._proxy_origin.scheme,
|
| 275 |
+
host=self._proxy_origin.host,
|
| 276 |
+
port=self._proxy_origin.port,
|
| 277 |
+
target=target,
|
| 278 |
+
)
|
| 279 |
+
connect_headers = merge_headers(
|
| 280 |
+
[(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers
|
| 281 |
+
)
|
| 282 |
+
connect_request = Request(
|
| 283 |
+
method=b"CONNECT",
|
| 284 |
+
url=connect_url,
|
| 285 |
+
headers=connect_headers,
|
| 286 |
+
extensions=request.extensions,
|
| 287 |
+
)
|
| 288 |
+
connect_response = await self._connection.handle_async_request(
|
| 289 |
+
connect_request
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
if connect_response.status < 200 or connect_response.status > 299:
|
| 293 |
+
reason_bytes = connect_response.extensions.get("reason_phrase", b"")
|
| 294 |
+
reason_str = reason_bytes.decode("ascii", errors="ignore")
|
| 295 |
+
msg = "%d %s" % (connect_response.status, reason_str)
|
| 296 |
+
await self._connection.aclose()
|
| 297 |
+
raise ProxyError(msg)
|
| 298 |
+
|
| 299 |
+
stream = connect_response.extensions["network_stream"]
|
| 300 |
+
|
| 301 |
+
# Upgrade the stream to SSL
|
| 302 |
+
ssl_context = (
|
| 303 |
+
default_ssl_context()
|
| 304 |
+
if self._ssl_context is None
|
| 305 |
+
else self._ssl_context
|
| 306 |
+
)
|
| 307 |
+
alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 308 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 309 |
+
|
| 310 |
+
kwargs = {
|
| 311 |
+
"ssl_context": ssl_context,
|
| 312 |
+
"server_hostname": self._remote_origin.host.decode("ascii"),
|
| 313 |
+
"timeout": timeout,
|
| 314 |
+
}
|
| 315 |
+
async with Trace("start_tls", logger, request, kwargs) as trace:
|
| 316 |
+
stream = await stream.start_tls(**kwargs)
|
| 317 |
+
trace.return_value = stream
|
| 318 |
+
|
| 319 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 320 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 321 |
+
http2_negotiated = (
|
| 322 |
+
ssl_object is not None
|
| 323 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 327 |
+
if http2_negotiated or (self._http2 and not self._http1):
|
| 328 |
+
from .http2 import AsyncHTTP2Connection
|
| 329 |
+
|
| 330 |
+
self._connection = AsyncHTTP2Connection(
|
| 331 |
+
origin=self._remote_origin,
|
| 332 |
+
stream=stream,
|
| 333 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 334 |
+
)
|
| 335 |
+
else:
|
| 336 |
+
self._connection = AsyncHTTP11Connection(
|
| 337 |
+
origin=self._remote_origin,
|
| 338 |
+
stream=stream,
|
| 339 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
self._connected = True
|
| 343 |
+
return await self._connection.handle_async_request(request)
|
| 344 |
+
|
| 345 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 346 |
+
return origin == self._remote_origin
|
| 347 |
+
|
| 348 |
+
async def aclose(self) -> None:
|
| 349 |
+
await self._connection.aclose()
|
| 350 |
+
|
| 351 |
+
def info(self) -> str:
|
| 352 |
+
return self._connection.info()
|
| 353 |
+
|
| 354 |
+
def is_available(self) -> bool:
|
| 355 |
+
return self._connection.is_available()
|
| 356 |
+
|
| 357 |
+
def has_expired(self) -> bool:
|
| 358 |
+
return self._connection.has_expired()
|
| 359 |
+
|
| 360 |
+
def is_idle(self) -> bool:
|
| 361 |
+
return self._connection.is_idle()
|
| 362 |
+
|
| 363 |
+
def is_closed(self) -> bool:
|
| 364 |
+
return self._connection.is_closed()
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
lib/python3.12/site-packages/httpcore/_async/interfaces.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from .._models import (
|
| 7 |
+
URL,
|
| 8 |
+
Extensions,
|
| 9 |
+
HeaderTypes,
|
| 10 |
+
Origin,
|
| 11 |
+
Request,
|
| 12 |
+
Response,
|
| 13 |
+
enforce_bytes,
|
| 14 |
+
enforce_headers,
|
| 15 |
+
enforce_url,
|
| 16 |
+
include_request_headers,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AsyncRequestInterface:
|
| 21 |
+
async def request(
|
| 22 |
+
self,
|
| 23 |
+
method: bytes | str,
|
| 24 |
+
url: URL | bytes | str,
|
| 25 |
+
*,
|
| 26 |
+
headers: HeaderTypes = None,
|
| 27 |
+
content: bytes | typing.AsyncIterator[bytes] | None = None,
|
| 28 |
+
extensions: Extensions | None = None,
|
| 29 |
+
) -> Response:
|
| 30 |
+
# Strict type checking on our parameters.
|
| 31 |
+
method = enforce_bytes(method, name="method")
|
| 32 |
+
url = enforce_url(url, name="url")
|
| 33 |
+
headers = enforce_headers(headers, name="headers")
|
| 34 |
+
|
| 35 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 36 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 37 |
+
|
| 38 |
+
request = Request(
|
| 39 |
+
method=method,
|
| 40 |
+
url=url,
|
| 41 |
+
headers=headers,
|
| 42 |
+
content=content,
|
| 43 |
+
extensions=extensions,
|
| 44 |
+
)
|
| 45 |
+
response = await self.handle_async_request(request)
|
| 46 |
+
try:
|
| 47 |
+
await response.aread()
|
| 48 |
+
finally:
|
| 49 |
+
await response.aclose()
|
| 50 |
+
return response
|
| 51 |
+
|
| 52 |
+
@contextlib.asynccontextmanager
|
| 53 |
+
async def stream(
|
| 54 |
+
self,
|
| 55 |
+
method: bytes | str,
|
| 56 |
+
url: URL | bytes | str,
|
| 57 |
+
*,
|
| 58 |
+
headers: HeaderTypes = None,
|
| 59 |
+
content: bytes | typing.AsyncIterator[bytes] | None = None,
|
| 60 |
+
extensions: Extensions | None = None,
|
| 61 |
+
) -> typing.AsyncIterator[Response]:
|
| 62 |
+
# Strict type checking on our parameters.
|
| 63 |
+
method = enforce_bytes(method, name="method")
|
| 64 |
+
url = enforce_url(url, name="url")
|
| 65 |
+
headers = enforce_headers(headers, name="headers")
|
| 66 |
+
|
| 67 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 68 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 69 |
+
|
| 70 |
+
request = Request(
|
| 71 |
+
method=method,
|
| 72 |
+
url=url,
|
| 73 |
+
headers=headers,
|
| 74 |
+
content=content,
|
| 75 |
+
extensions=extensions,
|
| 76 |
+
)
|
| 77 |
+
response = await self.handle_async_request(request)
|
| 78 |
+
try:
|
| 79 |
+
yield response
|
| 80 |
+
finally:
|
| 81 |
+
await response.aclose()
|
| 82 |
+
|
| 83 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 84 |
+
raise NotImplementedError() # pragma: nocover
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class AsyncConnectionInterface(AsyncRequestInterface):
|
| 88 |
+
async def aclose(self) -> None:
|
| 89 |
+
raise NotImplementedError() # pragma: nocover
|
| 90 |
+
|
| 91 |
+
def info(self) -> str:
|
| 92 |
+
raise NotImplementedError() # pragma: nocover
|
| 93 |
+
|
| 94 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 95 |
+
raise NotImplementedError() # pragma: nocover
|
| 96 |
+
|
| 97 |
+
def is_available(self) -> bool:
|
| 98 |
+
"""
|
| 99 |
+
Return `True` if the connection is currently able to accept an
|
| 100 |
+
outgoing request.
|
| 101 |
+
|
| 102 |
+
An HTTP/1.1 connection will only be available if it is currently idle.
|
| 103 |
+
|
| 104 |
+
An HTTP/2 connection will be available so long as the stream ID space is
|
| 105 |
+
not yet exhausted, and the connection is not in an error state.
|
| 106 |
+
|
| 107 |
+
While the connection is being established we may not yet know if it is going
|
| 108 |
+
to result in an HTTP/1.1 or HTTP/2 connection. The connection should be
|
| 109 |
+
treated as being available, but might ultimately raise `NewConnectionRequired`
|
| 110 |
+
required exceptions if multiple requests are attempted over a connection
|
| 111 |
+
that ends up being established as HTTP/1.1.
|
| 112 |
+
"""
|
| 113 |
+
raise NotImplementedError() # pragma: nocover
|
| 114 |
+
|
| 115 |
+
def has_expired(self) -> bool:
|
| 116 |
+
"""
|
| 117 |
+
Return `True` if the connection is in a state where it should be closed.
|
| 118 |
+
|
| 119 |
+
This either means that the connection is idle and it has passed the
|
| 120 |
+
expiry time on its keep-alive, or that server has sent an EOF.
|
| 121 |
+
"""
|
| 122 |
+
raise NotImplementedError() # pragma: nocover
|
| 123 |
+
|
| 124 |
+
def is_idle(self) -> bool:
|
| 125 |
+
"""
|
| 126 |
+
Return `True` if the connection is currently idle.
|
| 127 |
+
"""
|
| 128 |
+
raise NotImplementedError() # pragma: nocover
|
| 129 |
+
|
| 130 |
+
def is_closed(self) -> bool:
|
| 131 |
+
"""
|
| 132 |
+
Return `True` if the connection has been closed.
|
| 133 |
+
|
| 134 |
+
Used when a response is closed to determine if the connection may be
|
| 135 |
+
returned to the connection pool or not.
|
| 136 |
+
"""
|
| 137 |
+
raise NotImplementedError() # pragma: nocover
|
lib/python3.12/site-packages/httpcore/_async/socks_proxy.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import ssl
|
| 5 |
+
|
| 6 |
+
import socksio
|
| 7 |
+
|
| 8 |
+
from .._backends.auto import AutoBackend
|
| 9 |
+
from .._backends.base import AsyncNetworkBackend, AsyncNetworkStream
|
| 10 |
+
from .._exceptions import ConnectionNotAvailable, ProxyError
|
| 11 |
+
from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url
|
| 12 |
+
from .._ssl import default_ssl_context
|
| 13 |
+
from .._synchronization import AsyncLock
|
| 14 |
+
from .._trace import Trace
|
| 15 |
+
from .connection_pool import AsyncConnectionPool
|
| 16 |
+
from .http11 import AsyncHTTP11Connection
|
| 17 |
+
from .interfaces import AsyncConnectionInterface
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger("httpcore.socks")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
AUTH_METHODS = {
|
| 23 |
+
b"\x00": "NO AUTHENTICATION REQUIRED",
|
| 24 |
+
b"\x01": "GSSAPI",
|
| 25 |
+
b"\x02": "USERNAME/PASSWORD",
|
| 26 |
+
b"\xff": "NO ACCEPTABLE METHODS",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
REPLY_CODES = {
|
| 30 |
+
b"\x00": "Succeeded",
|
| 31 |
+
b"\x01": "General SOCKS server failure",
|
| 32 |
+
b"\x02": "Connection not allowed by ruleset",
|
| 33 |
+
b"\x03": "Network unreachable",
|
| 34 |
+
b"\x04": "Host unreachable",
|
| 35 |
+
b"\x05": "Connection refused",
|
| 36 |
+
b"\x06": "TTL expired",
|
| 37 |
+
b"\x07": "Command not supported",
|
| 38 |
+
b"\x08": "Address type not supported",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def _init_socks5_connection(
|
| 43 |
+
stream: AsyncNetworkStream,
|
| 44 |
+
*,
|
| 45 |
+
host: bytes,
|
| 46 |
+
port: int,
|
| 47 |
+
auth: tuple[bytes, bytes] | None = None,
|
| 48 |
+
) -> None:
|
| 49 |
+
conn = socksio.socks5.SOCKS5Connection()
|
| 50 |
+
|
| 51 |
+
# Auth method request
|
| 52 |
+
auth_method = (
|
| 53 |
+
socksio.socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED
|
| 54 |
+
if auth is None
|
| 55 |
+
else socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD
|
| 56 |
+
)
|
| 57 |
+
conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
|
| 58 |
+
outgoing_bytes = conn.data_to_send()
|
| 59 |
+
await stream.write(outgoing_bytes)
|
| 60 |
+
|
| 61 |
+
# Auth method response
|
| 62 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 63 |
+
response = conn.receive_data(incoming_bytes)
|
| 64 |
+
assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
|
| 65 |
+
if response.method != auth_method:
|
| 66 |
+
requested = AUTH_METHODS.get(auth_method, "UNKNOWN")
|
| 67 |
+
responded = AUTH_METHODS.get(response.method, "UNKNOWN")
|
| 68 |
+
raise ProxyError(
|
| 69 |
+
f"Requested {requested} from proxy server, but got {responded}."
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if response.method == socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD:
|
| 73 |
+
# Username/password request
|
| 74 |
+
assert auth is not None
|
| 75 |
+
username, password = auth
|
| 76 |
+
conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password))
|
| 77 |
+
outgoing_bytes = conn.data_to_send()
|
| 78 |
+
await stream.write(outgoing_bytes)
|
| 79 |
+
|
| 80 |
+
# Username/password response
|
| 81 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 82 |
+
response = conn.receive_data(incoming_bytes)
|
| 83 |
+
assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
|
| 84 |
+
if not response.success:
|
| 85 |
+
raise ProxyError("Invalid username/password")
|
| 86 |
+
|
| 87 |
+
# Connect request
|
| 88 |
+
conn.send(
|
| 89 |
+
socksio.socks5.SOCKS5CommandRequest.from_address(
|
| 90 |
+
socksio.socks5.SOCKS5Command.CONNECT, (host, port)
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
outgoing_bytes = conn.data_to_send()
|
| 94 |
+
await stream.write(outgoing_bytes)
|
| 95 |
+
|
| 96 |
+
# Connect response
|
| 97 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 98 |
+
response = conn.receive_data(incoming_bytes)
|
| 99 |
+
assert isinstance(response, socksio.socks5.SOCKS5Reply)
|
| 100 |
+
if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
|
| 101 |
+
reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN")
|
| 102 |
+
raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class AsyncSOCKSProxy(AsyncConnectionPool): # pragma: nocover
|
| 106 |
+
"""
|
| 107 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
proxy_url: URL | bytes | str,
|
| 113 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 114 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 115 |
+
max_connections: int | None = 10,
|
| 116 |
+
max_keepalive_connections: int | None = None,
|
| 117 |
+
keepalive_expiry: float | None = None,
|
| 118 |
+
http1: bool = True,
|
| 119 |
+
http2: bool = False,
|
| 120 |
+
retries: int = 0,
|
| 121 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 122 |
+
) -> None:
|
| 123 |
+
"""
|
| 124 |
+
A connection pool for making HTTP requests.
|
| 125 |
+
|
| 126 |
+
Parameters:
|
| 127 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 128 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 129 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 130 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 131 |
+
will be used.
|
| 132 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 133 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 134 |
+
would exceed this amount will block until a connection is available.
|
| 135 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 136 |
+
that will be maintained in the pool.
|
| 137 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 138 |
+
may be maintained for before being expired from the pool.
|
| 139 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 140 |
+
by the connection pool. Defaults to True.
|
| 141 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 142 |
+
the connection pool. Defaults to False.
|
| 143 |
+
retries: The maximum number of retries when trying to establish
|
| 144 |
+
a connection.
|
| 145 |
+
local_address: Local address to connect from. Can also be used to
|
| 146 |
+
connect using a particular address family. Using
|
| 147 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 148 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 149 |
+
`AF_INET6` address (IPv6).
|
| 150 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 151 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 152 |
+
"""
|
| 153 |
+
super().__init__(
|
| 154 |
+
ssl_context=ssl_context,
|
| 155 |
+
max_connections=max_connections,
|
| 156 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 157 |
+
keepalive_expiry=keepalive_expiry,
|
| 158 |
+
http1=http1,
|
| 159 |
+
http2=http2,
|
| 160 |
+
network_backend=network_backend,
|
| 161 |
+
retries=retries,
|
| 162 |
+
)
|
| 163 |
+
self._ssl_context = ssl_context
|
| 164 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 165 |
+
if proxy_auth is not None:
|
| 166 |
+
username, password = proxy_auth
|
| 167 |
+
username_bytes = enforce_bytes(username, name="proxy_auth")
|
| 168 |
+
password_bytes = enforce_bytes(password, name="proxy_auth")
|
| 169 |
+
self._proxy_auth: tuple[bytes, bytes] | None = (
|
| 170 |
+
username_bytes,
|
| 171 |
+
password_bytes,
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
self._proxy_auth = None
|
| 175 |
+
|
| 176 |
+
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
|
| 177 |
+
return AsyncSocks5Connection(
|
| 178 |
+
proxy_origin=self._proxy_url.origin,
|
| 179 |
+
remote_origin=origin,
|
| 180 |
+
proxy_auth=self._proxy_auth,
|
| 181 |
+
ssl_context=self._ssl_context,
|
| 182 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 183 |
+
http1=self._http1,
|
| 184 |
+
http2=self._http2,
|
| 185 |
+
network_backend=self._network_backend,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class AsyncSocks5Connection(AsyncConnectionInterface):
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
proxy_origin: Origin,
|
| 193 |
+
remote_origin: Origin,
|
| 194 |
+
proxy_auth: tuple[bytes, bytes] | None = None,
|
| 195 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 196 |
+
keepalive_expiry: float | None = None,
|
| 197 |
+
http1: bool = True,
|
| 198 |
+
http2: bool = False,
|
| 199 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 200 |
+
) -> None:
|
| 201 |
+
self._proxy_origin = proxy_origin
|
| 202 |
+
self._remote_origin = remote_origin
|
| 203 |
+
self._proxy_auth = proxy_auth
|
| 204 |
+
self._ssl_context = ssl_context
|
| 205 |
+
self._keepalive_expiry = keepalive_expiry
|
| 206 |
+
self._http1 = http1
|
| 207 |
+
self._http2 = http2
|
| 208 |
+
|
| 209 |
+
self._network_backend: AsyncNetworkBackend = (
|
| 210 |
+
AutoBackend() if network_backend is None else network_backend
|
| 211 |
+
)
|
| 212 |
+
self._connect_lock = AsyncLock()
|
| 213 |
+
self._connection: AsyncConnectionInterface | None = None
|
| 214 |
+
self._connect_failed = False
|
| 215 |
+
|
| 216 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 217 |
+
timeouts = request.extensions.get("timeout", {})
|
| 218 |
+
sni_hostname = request.extensions.get("sni_hostname", None)
|
| 219 |
+
timeout = timeouts.get("connect", None)
|
| 220 |
+
|
| 221 |
+
async with self._connect_lock:
|
| 222 |
+
if self._connection is None:
|
| 223 |
+
try:
|
| 224 |
+
# Connect to the proxy
|
| 225 |
+
kwargs = {
|
| 226 |
+
"host": self._proxy_origin.host.decode("ascii"),
|
| 227 |
+
"port": self._proxy_origin.port,
|
| 228 |
+
"timeout": timeout,
|
| 229 |
+
}
|
| 230 |
+
async with Trace("connect_tcp", logger, request, kwargs) as trace:
|
| 231 |
+
stream = await self._network_backend.connect_tcp(**kwargs)
|
| 232 |
+
trace.return_value = stream
|
| 233 |
+
|
| 234 |
+
# Connect to the remote host using socks5
|
| 235 |
+
kwargs = {
|
| 236 |
+
"stream": stream,
|
| 237 |
+
"host": self._remote_origin.host.decode("ascii"),
|
| 238 |
+
"port": self._remote_origin.port,
|
| 239 |
+
"auth": self._proxy_auth,
|
| 240 |
+
}
|
| 241 |
+
async with Trace(
|
| 242 |
+
"setup_socks5_connection", logger, request, kwargs
|
| 243 |
+
) as trace:
|
| 244 |
+
await _init_socks5_connection(**kwargs)
|
| 245 |
+
trace.return_value = stream
|
| 246 |
+
|
| 247 |
+
# Upgrade the stream to SSL
|
| 248 |
+
if self._remote_origin.scheme == b"https":
|
| 249 |
+
ssl_context = (
|
| 250 |
+
default_ssl_context()
|
| 251 |
+
if self._ssl_context is None
|
| 252 |
+
else self._ssl_context
|
| 253 |
+
)
|
| 254 |
+
alpn_protocols = (
|
| 255 |
+
["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 256 |
+
)
|
| 257 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 258 |
+
|
| 259 |
+
kwargs = {
|
| 260 |
+
"ssl_context": ssl_context,
|
| 261 |
+
"server_hostname": sni_hostname
|
| 262 |
+
or self._remote_origin.host.decode("ascii"),
|
| 263 |
+
"timeout": timeout,
|
| 264 |
+
}
|
| 265 |
+
async with Trace("start_tls", logger, request, kwargs) as trace:
|
| 266 |
+
stream = await stream.start_tls(**kwargs)
|
| 267 |
+
trace.return_value = stream
|
| 268 |
+
|
| 269 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 270 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 271 |
+
http2_negotiated = (
|
| 272 |
+
ssl_object is not None
|
| 273 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 277 |
+
if http2_negotiated or (
|
| 278 |
+
self._http2 and not self._http1
|
| 279 |
+
): # pragma: nocover
|
| 280 |
+
from .http2 import AsyncHTTP2Connection
|
| 281 |
+
|
| 282 |
+
self._connection = AsyncHTTP2Connection(
|
| 283 |
+
origin=self._remote_origin,
|
| 284 |
+
stream=stream,
|
| 285 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
self._connection = AsyncHTTP11Connection(
|
| 289 |
+
origin=self._remote_origin,
|
| 290 |
+
stream=stream,
|
| 291 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 292 |
+
)
|
| 293 |
+
except Exception as exc:
|
| 294 |
+
self._connect_failed = True
|
| 295 |
+
raise exc
|
| 296 |
+
elif not self._connection.is_available(): # pragma: nocover
|
| 297 |
+
raise ConnectionNotAvailable()
|
| 298 |
+
|
| 299 |
+
return await self._connection.handle_async_request(request)
|
| 300 |
+
|
| 301 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 302 |
+
return origin == self._remote_origin
|
| 303 |
+
|
| 304 |
+
async def aclose(self) -> None:
|
| 305 |
+
if self._connection is not None:
|
| 306 |
+
await self._connection.aclose()
|
| 307 |
+
|
| 308 |
+
def is_available(self) -> bool:
|
| 309 |
+
if self._connection is None: # pragma: nocover
|
| 310 |
+
# If HTTP/2 support is enabled, and the resulting connection could
|
| 311 |
+
# end up as HTTP/2 then we should indicate the connection as being
|
| 312 |
+
# available to service multiple requests.
|
| 313 |
+
return (
|
| 314 |
+
self._http2
|
| 315 |
+
and (self._remote_origin.scheme == b"https" or not self._http1)
|
| 316 |
+
and not self._connect_failed
|
| 317 |
+
)
|
| 318 |
+
return self._connection.is_available()
|
| 319 |
+
|
| 320 |
+
def has_expired(self) -> bool:
|
| 321 |
+
if self._connection is None: # pragma: nocover
|
| 322 |
+
return self._connect_failed
|
| 323 |
+
return self._connection.has_expired()
|
| 324 |
+
|
| 325 |
+
def is_idle(self) -> bool:
|
| 326 |
+
if self._connection is None: # pragma: nocover
|
| 327 |
+
return self._connect_failed
|
| 328 |
+
return self._connection.is_idle()
|
| 329 |
+
|
| 330 |
+
def is_closed(self) -> bool:
|
| 331 |
+
if self._connection is None: # pragma: nocover
|
| 332 |
+
return self._connect_failed
|
| 333 |
+
return self._connection.is_closed()
|
| 334 |
+
|
| 335 |
+
def info(self) -> str:
|
| 336 |
+
if self._connection is None: # pragma: nocover
|
| 337 |
+
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
|
| 338 |
+
return self._connection.info()
|
| 339 |
+
|
| 340 |
+
def __repr__(self) -> str:
|
| 341 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
lib/python3.12/site-packages/httpcore/_backends/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (197 Bytes). View file
|
|
|
lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc
ADDED
|
Binary file (8.54 kB). View file
|
|
|
lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc
ADDED
|
Binary file (2.69 kB). View file
|
|
|