about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/aiohappyeyeballs
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/aiohappyeyeballs')
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/__init__.py14
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py207
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py259
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py17
-rw-r--r--.venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py97
6 files changed, 594 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/__init__.py b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/__init__.py
new file mode 100644
index 00000000..71c689cc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/__init__.py
@@ -0,0 +1,14 @@
+__version__ = "2.6.1"
+
+from .impl import start_connection
+from .types import AddrInfoType, SocketFactoryType
+from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos
+
+__all__ = (
+    "AddrInfoType",
+    "SocketFactoryType",
+    "addr_to_addr_infos",
+    "pop_addr_infos_interleave",
+    "remove_addr_infos",
+    "start_connection",
+)
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py
new file mode 100644
index 00000000..9a4ba720
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/_staggered.py
@@ -0,0 +1,207 @@
+import asyncio
+import contextlib
+
+# PY3.9: Import Callable from typing until we drop Python 3.9 support
+# https://github.com/python/cpython/issues/87131
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Awaitable,
+    Callable,
+    Iterable,
+    List,
+    Optional,
+    Set,
+    Tuple,
+    TypeVar,
+    Union,
+)
+
+_T = TypeVar("_T")
+
+RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt)
+
+
+def _set_result(wait_next: "asyncio.Future[None]") -> None:
+    """Set the result of a future if it is not already done."""
+    if not wait_next.done():
+        wait_next.set_result(None)
+
+
+async def _wait_one(
+    futures: "Iterable[asyncio.Future[Any]]",
+    loop: asyncio.AbstractEventLoop,
+) -> _T:
+    """Wait for the first future to complete."""
+    wait_next = loop.create_future()
+
+    def _on_completion(fut: "asyncio.Future[Any]") -> None:
+        if not wait_next.done():
+            wait_next.set_result(fut)
+
+    for f in futures:
+        f.add_done_callback(_on_completion)
+
+    try:
+        return await wait_next
+    finally:
+        for f in futures:
+            f.remove_done_callback(_on_completion)
+
+
+async def staggered_race(
+    coro_fns: Iterable[Callable[[], Awaitable[_T]]],
+    delay: Optional[float],
+    *,
+    loop: Optional[asyncio.AbstractEventLoop] = None,
+) -> Tuple[Optional[_T], Optional[int], List[Optional[BaseException]]]:
+    """
+    Run coroutines with staggered start times and take the first to finish.
+
+    This method takes an iterable of coroutine functions. The first one is
+    started immediately. From then on, whenever the immediately preceding one
+    fails (raises an exception), or when *delay* seconds has passed, the next
+    coroutine is started. This continues until one of the coroutines complete
+    successfully, in which case all others are cancelled, or until all
+    coroutines fail.
+
+    The coroutines provided should be well-behaved in the following way:
+
+    * They should only ``return`` if completed successfully.
+
+    * They should always raise an exception if they did not complete
+      successfully. In particular, if they handle cancellation, they should
+      probably reraise, like this::
+
+        try:
+            # do work
+        except asyncio.CancelledError:
+            # undo partially completed work
+            raise
+
+    Args:
+    ----
+        coro_fns: an iterable of coroutine functions, i.e. callables that
+            return a coroutine object when called. Use ``functools.partial`` or
+            lambdas to pass arguments.
+
+        delay: amount of time, in seconds, between starting coroutines. If
+            ``None``, the coroutines will run sequentially.
+
+        loop: the event loop to use. If ``None``, the running loop is used.
+
+    Returns:
+    -------
+        tuple *(winner_result, winner_index, exceptions)* where
+
+        - *winner_result*: the result of the winning coroutine, or ``None``
+          if no coroutines won.
+
+        - *winner_index*: the index of the winning coroutine in
+          ``coro_fns``, or ``None`` if no coroutines won. If the winning
+          coroutine may return None on success, *winner_index* can be used
+          to definitively determine whether any coroutine won.
+
+        - *exceptions*: list of exceptions returned by the coroutines.
+          ``len(exceptions)`` is equal to the number of coroutines actually
+          started, and the order is the same as in ``coro_fns``. The winning
+          coroutine's entry is ``None``.
+
+    """
+    loop = loop or asyncio.get_running_loop()
+    exceptions: List[Optional[BaseException]] = []
+    tasks: Set[asyncio.Task[Optional[Tuple[_T, int]]]] = set()
+
+    async def run_one_coro(
+        coro_fn: Callable[[], Awaitable[_T]],
+        this_index: int,
+        start_next: "asyncio.Future[None]",
+    ) -> Optional[Tuple[_T, int]]:
+        """
+        Run a single coroutine.
+
+        If the coroutine fails, set the exception in the exceptions list and
+        start the next coroutine by setting the result of the start_next.
+
+        If the coroutine succeeds, return the result and the index of the
+        coroutine in the coro_fns list.
+
+        If SystemExit or KeyboardInterrupt is raised, re-raise it.
+        """
+        try:
+            result = await coro_fn()
+        except RE_RAISE_EXCEPTIONS:
+            raise
+        except BaseException as e:
+            exceptions[this_index] = e
+            _set_result(start_next)  # Kickstart the next coroutine
+            return None
+
+        return result, this_index
+
+    start_next_timer: Optional[asyncio.TimerHandle] = None
+    start_next: Optional[asyncio.Future[None]]
+    task: asyncio.Task[Optional[Tuple[_T, int]]]
+    done: Union[asyncio.Future[None], asyncio.Task[Optional[Tuple[_T, int]]]]
+    coro_iter = iter(coro_fns)
+    this_index = -1
+    try:
+        while True:
+            if coro_fn := next(coro_iter, None):
+                this_index += 1
+                exceptions.append(None)
+                start_next = loop.create_future()
+                task = loop.create_task(run_one_coro(coro_fn, this_index, start_next))
+                tasks.add(task)
+                start_next_timer = (
+                    loop.call_later(delay, _set_result, start_next) if delay else None
+                )
+            elif not tasks:
+                # We exhausted the coro_fns list and no tasks are running
+                # so we have no winner and all coroutines failed.
+                break
+
+            while tasks or start_next:
+                done = await _wait_one(
+                    (*tasks, start_next) if start_next else tasks, loop
+                )
+                if done is start_next:
+                    # The current task has failed or the timer has expired
+                    # so we need to start the next task.
+                    start_next = None
+                    if start_next_timer:
+                        start_next_timer.cancel()
+                        start_next_timer = None
+
+                    # Break out of the task waiting loop to start the next
+                    # task.
+                    break
+
+                if TYPE_CHECKING:
+                    assert isinstance(done, asyncio.Task)
+
+                tasks.remove(done)
+                if winner := done.result():
+                    return *winner, exceptions
+    finally:
+        # We either have:
+        #  - a winner
+        #  - all tasks failed
+        #  - a KeyboardInterrupt or SystemExit.
+
+        #
+        # If the timer is still running, cancel it.
+        #
+        if start_next_timer:
+            start_next_timer.cancel()
+
+        #
+        # If there are any tasks left, cancel them and than
+        # wait them so they fill the exceptions list.
+        #
+        for task in tasks:
+            task.cancel()
+            with contextlib.suppress(asyncio.CancelledError):
+                await task
+
+    return None, None, exceptions
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py
new file mode 100644
index 00000000..8f3919a0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/impl.py
@@ -0,0 +1,259 @@
+"""Base implementation."""
+
+import asyncio
+import collections
+import contextlib
+import functools
+import itertools
+import socket
+from typing import List, Optional, Sequence, Set, Union
+
+from . import _staggered
+from .types import AddrInfoType, SocketFactoryType
+
+
+async def start_connection(
+    addr_infos: Sequence[AddrInfoType],
+    *,
+    local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
+    happy_eyeballs_delay: Optional[float] = None,
+    interleave: Optional[int] = None,
+    loop: Optional[asyncio.AbstractEventLoop] = None,
+    socket_factory: Optional[SocketFactoryType] = None,
+) -> socket.socket:
+    """
+    Connect to a TCP server.
+
+    Create a socket connection to a specified destination.  The
+    destination is specified as a list of AddrInfoType tuples as
+    returned from getaddrinfo().
+
+    The arguments are, in order:
+
+    * ``family``: the address family, e.g. ``socket.AF_INET`` or
+        ``socket.AF_INET6``.
+    * ``type``: the socket type, e.g. ``socket.SOCK_STREAM`` or
+        ``socket.SOCK_DGRAM``.
+    * ``proto``: the protocol, e.g. ``socket.IPPROTO_TCP`` or
+        ``socket.IPPROTO_UDP``.
+    * ``canonname``: the canonical name of the address, e.g.
+        ``"www.python.org"``.
+    * ``sockaddr``: the socket address
+
+    This method is a coroutine which will try to establish the connection
+    in the background. When successful, the coroutine returns a
+    socket.
+
+    The expected use case is to use this method in conjunction with
+    loop.create_connection() to establish a connection to a server::
+
+            socket = await start_connection(addr_infos)
+            transport, protocol = await loop.create_connection(
+                MyProtocol, sock=socket, ...)
+    """
+    if not (current_loop := loop):
+        current_loop = asyncio.get_running_loop()
+
+    single_addr_info = len(addr_infos) == 1
+
+    if happy_eyeballs_delay is not None and interleave is None:
+        # If using happy eyeballs, default to interleave addresses by family
+        interleave = 1
+
+    if interleave and not single_addr_info:
+        addr_infos = _interleave_addrinfos(addr_infos, interleave)
+
+    sock: Optional[socket.socket] = None
+    # uvloop can raise RuntimeError instead of OSError
+    exceptions: List[List[Union[OSError, RuntimeError]]] = []
+    if happy_eyeballs_delay is None or single_addr_info:
+        # not using happy eyeballs
+        for addrinfo in addr_infos:
+            try:
+                sock = await _connect_sock(
+                    current_loop,
+                    exceptions,
+                    addrinfo,
+                    local_addr_infos,
+                    None,
+                    socket_factory,
+                )
+                break
+            except (RuntimeError, OSError):
+                continue
+    else:  # using happy eyeballs
+        open_sockets: Set[socket.socket] = set()
+        try:
+            sock, _, _ = await _staggered.staggered_race(
+                (
+                    functools.partial(
+                        _connect_sock,
+                        current_loop,
+                        exceptions,
+                        addrinfo,
+                        local_addr_infos,
+                        open_sockets,
+                        socket_factory,
+                    )
+                    for addrinfo in addr_infos
+                ),
+                happy_eyeballs_delay,
+            )
+        finally:
+            # If we have a winner, staggered_race will
+            # cancel the other tasks, however there is a
+            # small race window where any of the other tasks
+            # can be done before they are cancelled which
+            # will leave the socket open. To avoid this problem
+            # we pass a set to _connect_sock to keep track of
+            # the open sockets and close them here if there
+            # are any "runner up" sockets.
+            for s in open_sockets:
+                if s is not sock:
+                    with contextlib.suppress(OSError):
+                        s.close()
+            open_sockets = None  # type: ignore[assignment]
+
+    if sock is None:
+        all_exceptions = [exc for sub in exceptions for exc in sub]
+        try:
+            first_exception = all_exceptions[0]
+            if len(all_exceptions) == 1:
+                raise first_exception
+            else:
+                # If they all have the same str(), raise one.
+                model = str(first_exception)
+                if all(str(exc) == model for exc in all_exceptions):
+                    raise first_exception
+                # Raise a combined exception so the user can see all
+                # the various error messages.
+                msg = "Multiple exceptions: {}".format(
+                    ", ".join(str(exc) for exc in all_exceptions)
+                )
+                # If the errno is the same for all exceptions, raise
+                # an OSError with that errno.
+                if isinstance(first_exception, OSError):
+                    first_errno = first_exception.errno
+                    if all(
+                        isinstance(exc, OSError) and exc.errno == first_errno
+                        for exc in all_exceptions
+                    ):
+                        raise OSError(first_errno, msg)
+                elif isinstance(first_exception, RuntimeError) and all(
+                    isinstance(exc, RuntimeError) for exc in all_exceptions
+                ):
+                    raise RuntimeError(msg)
+                # We have a mix of OSError and RuntimeError
+                # so we have to pick which one to raise.
+                # and we raise OSError for compatibility
+                raise OSError(msg)
+        finally:
+            all_exceptions = None  # type: ignore[assignment]
+            exceptions = None  # type: ignore[assignment]
+
+    return sock
+
+
+async def _connect_sock(
+    loop: asyncio.AbstractEventLoop,
+    exceptions: List[List[Union[OSError, RuntimeError]]],
+    addr_info: AddrInfoType,
+    local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
+    open_sockets: Optional[Set[socket.socket]] = None,
+    socket_factory: Optional[SocketFactoryType] = None,
+) -> socket.socket:
+    """
+    Create, bind and connect one socket.
+
+    If open_sockets is passed, add the socket to the set of open sockets.
+    Any failure caught here will remove the socket from the set and close it.
+
+    Callers can use this set to close any sockets that are not the winner
+    of all staggered tasks in the result there are runner up sockets aka
+    multiple winners.
+    """
+    my_exceptions: List[Union[OSError, RuntimeError]] = []
+    exceptions.append(my_exceptions)
+    family, type_, proto, _, address = addr_info
+    sock = None
+    try:
+        if socket_factory is not None:
+            sock = socket_factory(addr_info)
+        else:
+            sock = socket.socket(family=family, type=type_, proto=proto)
+        if open_sockets is not None:
+            open_sockets.add(sock)
+        sock.setblocking(False)
+        if local_addr_infos is not None:
+            for lfamily, _, _, _, laddr in local_addr_infos:
+                # skip local addresses of different family
+                if lfamily != family:
+                    continue
+                try:
+                    sock.bind(laddr)
+                    break
+                except OSError as exc:
+                    msg = (
+                        f"error while attempting to bind on "
+                        f"address {laddr!r}: "
+                        f"{(exc.strerror or '').lower()}"
+                    )
+                    exc = OSError(exc.errno, msg)
+                    my_exceptions.append(exc)
+            else:  # all bind attempts failed
+                if my_exceptions:
+                    raise my_exceptions.pop()
+                else:
+                    raise OSError(f"no matching local address with {family=} found")
+        await loop.sock_connect(sock, address)
+        return sock
+    except (RuntimeError, OSError) as exc:
+        my_exceptions.append(exc)
+        if sock is not None:
+            if open_sockets is not None:
+                open_sockets.remove(sock)
+            try:
+                sock.close()
+            except OSError as e:
+                my_exceptions.append(e)
+                raise
+        raise
+    except:
+        if sock is not None:
+            if open_sockets is not None:
+                open_sockets.remove(sock)
+            try:
+                sock.close()
+            except OSError as e:
+                my_exceptions.append(e)
+                raise
+        raise
+    finally:
+        exceptions = my_exceptions = None  # type: ignore[assignment]
+
+
+def _interleave_addrinfos(
+    addrinfos: Sequence[AddrInfoType], first_address_family_count: int = 1
+) -> List[AddrInfoType]:
+    """Interleave list of addrinfo tuples by family."""
+    # Group addresses by family
+    addrinfos_by_family: collections.OrderedDict[int, List[AddrInfoType]] = (
+        collections.OrderedDict()
+    )
+    for addr in addrinfos:
+        family = addr[0]
+        if family not in addrinfos_by_family:
+            addrinfos_by_family[family] = []
+        addrinfos_by_family[family].append(addr)
+    addrinfos_lists = list(addrinfos_by_family.values())
+
+    reordered: List[AddrInfoType] = []
+    if first_address_family_count > 1:
+        reordered.extend(addrinfos_lists[0][: first_address_family_count - 1])
+        del addrinfos_lists[0][: first_address_family_count - 1]
+    reordered.extend(
+        a
+        for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists))
+        if a is not None
+    )
+    return reordered
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/py.typed b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/py.typed
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py
new file mode 100644
index 00000000..e8c75074
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/types.py
@@ -0,0 +1,17 @@
+"""Types for aiohappyeyeballs."""
+
+import socket
+
+# PY3.9: Import Callable from typing until we drop Python 3.9 support
+# https://github.com/python/cpython/issues/87131
+from typing import Callable, Tuple, Union
+
+AddrInfoType = Tuple[
+    Union[int, socket.AddressFamily],
+    Union[int, socket.SocketKind],
+    int,
+    str,
+    Tuple,  # type: ignore[type-arg]
+]
+
+SocketFactoryType = Callable[[AddrInfoType], socket.socket]
diff --git a/.venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py
new file mode 100644
index 00000000..ea29adb9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/aiohappyeyeballs/utils.py
@@ -0,0 +1,97 @@
+"""Utility functions for aiohappyeyeballs."""
+
+import ipaddress
+import socket
+from typing import Dict, List, Optional, Tuple, Union
+
+from .types import AddrInfoType
+
+
+def addr_to_addr_infos(
+    addr: Optional[
+        Union[Tuple[str, int, int, int], Tuple[str, int, int], Tuple[str, int]]
+    ],
+) -> Optional[List[AddrInfoType]]:
+    """Convert an address tuple to a list of addr_info tuples."""
+    if addr is None:
+        return None
+    host = addr[0]
+    port = addr[1]
+    is_ipv6 = ":" in host
+    if is_ipv6:
+        flowinfo = 0
+        scopeid = 0
+        addr_len = len(addr)
+        if addr_len >= 4:
+            scopeid = addr[3]  # type: ignore[misc]
+        if addr_len >= 3:
+            flowinfo = addr[2]  # type: ignore[misc]
+        addr = (host, port, flowinfo, scopeid)
+        family = socket.AF_INET6
+    else:
+        addr = (host, port)
+        family = socket.AF_INET
+    return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)]
+
+
+def pop_addr_infos_interleave(
+    addr_infos: List[AddrInfoType], interleave: Optional[int] = None
+) -> None:
+    """
+    Pop addr_info from the list of addr_infos by family up to interleave times.
+
+    The interleave parameter is used to know how many addr_infos for
+    each family should be popped of the top of the list.
+    """
+    seen: Dict[int, int] = {}
+    if interleave is None:
+        interleave = 1
+    to_remove: List[AddrInfoType] = []
+    for addr_info in addr_infos:
+        family = addr_info[0]
+        if family not in seen:
+            seen[family] = 0
+        if seen[family] < interleave:
+            to_remove.append(addr_info)
+        seen[family] += 1
+    for addr_info in to_remove:
+        addr_infos.remove(addr_info)
+
+
+def _addr_tuple_to_ip_address(
+    addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
+) -> Union[
+    Tuple[ipaddress.IPv4Address, int], Tuple[ipaddress.IPv6Address, int, int, int]
+]:
+    """Convert an address tuple to an IPv4Address."""
+    return (ipaddress.ip_address(addr[0]), *addr[1:])
+
+
+def remove_addr_infos(
+    addr_infos: List[AddrInfoType],
+    addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
+) -> None:
+    """
+    Remove an address from the list of addr_infos.
+
+    The addr value is typically the return value of
+    sock.getpeername().
+    """
+    bad_addrs_infos: List[AddrInfoType] = []
+    for addr_info in addr_infos:
+        if addr_info[-1] == addr:
+            bad_addrs_infos.append(addr_info)
+    if bad_addrs_infos:
+        for bad_addr_info in bad_addrs_infos:
+            addr_infos.remove(bad_addr_info)
+        return
+    # Slow path in case addr is formatted differently
+    match_addr = _addr_tuple_to_ip_address(addr)
+    for addr_info in addr_infos:
+        if match_addr == _addr_tuple_to_ip_address(addr_info[-1]):
+            bad_addrs_infos.append(addr_info)
+    if bad_addrs_infos:
+        for bad_addr_info in bad_addrs_infos:
+            addr_infos.remove(bad_addr_info)
+        return
+    raise ValueError(f"Address {addr} not found in addr_infos")