Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
connection_pool.py421 linesDownload Raw Back to _sync
1from __future__ import annotations2 3import ssl4import sys5import types6import typing7 8from .._backends.sync import SyncBackend9from .._backends.base import SOCKET_OPTION, NetworkBackend10from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol11from .._models import Origin, Proxy, Request, Response12from .._synchronization import Event, ShieldCancellation, ThreadLock13from .connection import HTTPConnection14from .interfaces import ConnectionInterface, RequestInterface15 16 17class PoolRequest:18    def __init__(self, request: Request) -> None:19        self.request = request20        self.connection: ConnectionInterface | None = None21        self._connection_acquired = Event()22 23    def assign_to_connection(self, connection: ConnectionInterface | None) -> None:24        self.connection = connection25        self._connection_acquired.set()26 27    def clear_connection(self) -> None:28        self.connection = None29        self._connection_acquired = Event()30 31    def wait_for_connection(32        self, timeout: float | None = None33    ) -> ConnectionInterface:34        if self.connection is None:35            self._connection_acquired.wait(timeout=timeout)36        assert self.connection is not None37        return self.connection38 39    def is_queued(self) -> bool:40        return self.connection is None41 42 43class ConnectionPool(RequestInterface):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: NetworkBackend | 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 that71                the pool should allow. Any attempt to send a request on a pool that72                would exceed this amount will block until a connection is available.73            max_keepalive_connections: The maximum number of idle HTTP connections74                that will be maintained in the pool.75            keepalive_expiry: The duration in seconds that an idle HTTP connection76                may be maintained for before being expired from the pool.77            http1: A boolean indicating if HTTP/1.1 requests should be supported78                by the connection pool. Defaults to True.79            http2: A boolean indicating if HTTP/2 requests should be supported by80                the connection pool. Defaults to False.81            retries: The maximum number of retries when trying to establish a82                connection.83            local_address: Local address to connect from. Can also be used to connect84                using a particular address family. Using `local_address="0.0.0.0"`85                will connect using an `AF_INET` address (IPv4), while using86                `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 included90             in the TCP socket when the connection was established.91        """92        self._ssl_context = ssl_context93        self._proxy = proxy94        self._max_connections = (95            sys.maxsize if max_connections is None else max_connections96        )97        self._max_keepalive_connections = (98            sys.maxsize99            if max_keepalive_connections is None100            else max_keepalive_connections101        )102        self._max_keepalive_connections = min(103            self._max_connections, self._max_keepalive_connections104        )105 106        self._keepalive_expiry = keepalive_expiry107        self._http1 = http1108        self._http2 = http2109        self._retries = retries110        self._local_address = local_address111        self._uds = uds112 113        self._network_backend = (114            SyncBackend() if network_backend is None else network_backend115        )116        self._socket_options = socket_options117 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[ConnectionInterface] = []121        self._requests: list[PoolRequest] = []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 = ThreadLock()127 128    def create_connection(self, origin: Origin) -> ConnectionInterface:129        if self._proxy is not None:130            if self._proxy.url.scheme in (b"socks5", b"socks5h"):131                from .socks_proxy import Socks5Connection132 133                return Socks5Connection(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 ForwardHTTPConnection145 146                return ForwardHTTPConnection(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 TunnelHTTPConnection155 156            return TunnelHTTPConnection(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 HTTPConnection(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    @property182    def connections(self) -> list[ConnectionInterface]:183        """184        Return a list of the connections currently in the pool.185 186        For example:187 188        ```python189        >>> pool.connections190        [191            <HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 6]>,192            <HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 9]> ,193            <HTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>,194        ]195        ```196        """197        return list(self._connections)198 199    def handle_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 = PoolRequest(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                self._close_connections(closing)230 231                # Wait until this request has an assigned connection.232                connection = pool_request.wait_for_connection(timeout=timeout)233 234                try:235                    # Send the request on the assigned connection.236                    response = connection.handle_request(237                        pool_request.request238                    )239                except ConnectionNotAvailable:240                    # In some cases a connection may initially be available to241                    # 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: nocover247 248        except BaseException as exc:249            with self._optional_thread_lock:250                # For any exception or cancellation we remove the request from251                # the queue, and then re-assign requests to connections.252                self._requests.remove(pool_request)253                closing = self._assign_requests_to_connections()254 255            self._close_connections(closing)256            raise exc from None257 258        # Return the response. Note that in this case we still have to manage259        # the point at which the response is closed.260        assert isinstance(response.stream, typing.Iterable)261        return Response(262            status=response.status,263            headers=response.headers,264            content=PoolByteStream(265                stream=response.stream, pool_request=pool_request, pool=self266            ),267            extensions=response.extensions,268        )269 270    def _assign_requests_to_connections(self) -> list[ConnectionInterface]:271        """272        Manage the state of the connection pool, assigning incoming273        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 closing278        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_connections296            ):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.origin305            available_connections = [306                connection307                for connection in self._connections308                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 connection319            #    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_connections340 341    def _close_connections(self, closing: list[ConnectionInterface]) -> None:342        # Close connections which have been removed from the pool.343        with ShieldCancellation():344            for connection in closing:345                connection.close()346 347    def close(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        self._close_connections(closing_connections)354 355    def __enter__(self) -> ConnectionPool:356        return self357 358    def __exit__(359        self,360        exc_type: type[BaseException] | None = None,361        exc_value: BaseException | None = None,362        traceback: types.TracebackType | None = None,363    ) -> None:364        self.close()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._connections372            ]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 389class PoolByteStream:390    def __init__(391        self,392        stream: typing.Iterable[bytes],393        pool_request: PoolRequest,394        pool: ConnectionPool,395    ) -> None:396        self._stream = stream397        self._pool_request = pool_request398        self._pool = pool399        self._closed = False400 401    def __iter__(self) -> typing.Iterator[bytes]:402        try:403            for part in self._stream:404                yield part405        except BaseException as exc:406            self.close()407            raise exc from None408 409    def close(self) -> None:410        if not self._closed:411            self._closed = True412            with ShieldCancellation():413                if hasattr(self._stream, "close"):414                    self._stream.close()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            self._pool._close_connections(closing)421 
codekingpro/portable-devtools · Team Ai