summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/pip/_internal/network
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.11/site-packages/pip/_internal/network')
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py2
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pycbin0 -> 270 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pycbin0 -> 23997 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pycbin0 -> 7938 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pycbin0 -> 9548 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pycbin0 -> 13031 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pycbin0 -> 21447 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pycbin0 -> 2419 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pycbin0 -> 3257 bytes
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/auth.py561
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/cache.py106
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/download.py186
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py210
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/session.py520
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/utils.py96
-rw-r--r--venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py62
16 files changed, 1743 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py b/venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py
new file mode 100644
index 0000000..b51bde9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__init__.py
@@ -0,0 +1,2 @@
+"""Contains purely network-related utilities.
+"""
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..ec24904
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/__init__.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc
new file mode 100644
index 0000000..133e9b5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/auth.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc
new file mode 100644
index 0000000..81014c1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/cache.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc
new file mode 100644
index 0000000..3123483
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/download.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc
new file mode 100644
index 0000000..17052b7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/lazy_wheel.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc
new file mode 100644
index 0000000..e5dda57
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/session.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc
new file mode 100644
index 0000000..4ede2f9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/utils.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pyc b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pyc
new file mode 100644
index 0000000..2bc7f9e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/__pycache__/xmlrpc.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/auth.py b/venv/lib/python3.11/site-packages/pip/_internal/network/auth.py
new file mode 100644
index 0000000..94a82fa
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/auth.py
@@ -0,0 +1,561 @@
+"""Network Authentication Helpers
+
+Contains interface (MultiDomainBasicAuth) and associated glue code for
+providing credentials in the context of network requests.
+"""
+import logging
+import os
+import shutil
+import subprocess
+import sysconfig
+import typing
+import urllib.parse
+from abc import ABC, abstractmethod
+from functools import lru_cache
+from os.path import commonprefix
+from pathlib import Path
+from typing import Any, Dict, List, NamedTuple, Optional, Tuple
+
+from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
+from pip._vendor.requests.models import Request, Response
+from pip._vendor.requests.utils import get_netrc_auth
+
+from pip._internal.utils.logging import getLogger
+from pip._internal.utils.misc import (
+ ask,
+ ask_input,
+ ask_password,
+ remove_auth_from_url,
+ split_auth_netloc_from_url,
+)
+from pip._internal.vcs.versioncontrol import AuthInfo
+
+logger = getLogger(__name__)
+
+KEYRING_DISABLED = False
+
+
+class Credentials(NamedTuple):
+ url: str
+ username: str
+ password: str
+
+
+class KeyRingBaseProvider(ABC):
+ """Keyring base provider interface"""
+
+ has_keyring: bool
+
+ @abstractmethod
+ def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
+ ...
+
+ @abstractmethod
+ def save_auth_info(self, url: str, username: str, password: str) -> None:
+ ...
+
+
+class KeyRingNullProvider(KeyRingBaseProvider):
+ """Keyring null provider"""
+
+ has_keyring = False
+
+ def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
+ return None
+
+ def save_auth_info(self, url: str, username: str, password: str) -> None:
+ return None
+
+
+class KeyRingPythonProvider(KeyRingBaseProvider):
+ """Keyring interface which uses locally imported `keyring`"""
+
+ has_keyring = True
+
+ def __init__(self) -> None:
+ import keyring
+
+ self.keyring = keyring
+
+ def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
+ # Support keyring's get_credential interface which supports getting
+ # credentials without a username. This is only available for
+ # keyring>=15.2.0.
+ if hasattr(self.keyring, "get_credential"):
+ logger.debug("Getting credentials from keyring for %s", url)
+ cred = self.keyring.get_credential(url, username)
+ if cred is not None:
+ return cred.username, cred.password
+ return None
+
+ if username is not None:
+ logger.debug("Getting password from keyring for %s", url)
+ password = self.keyring.get_password(url, username)
+ if password:
+ return username, password
+ return None
+
+ def save_auth_info(self, url: str, username: str, password: str) -> None:
+ self.keyring.set_password(url, username, password)
+
+
+class KeyRingCliProvider(KeyRingBaseProvider):
+ """Provider which uses `keyring` cli
+
+ Instead of calling the keyring package installed alongside pip
+ we call keyring on the command line which will enable pip to
+ use which ever installation of keyring is available first in
+ PATH.
+ """
+
+ has_keyring = True
+
+ def __init__(self, cmd: str) -> None:
+ self.keyring = cmd
+
+ def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
+ # This is the default implementation of keyring.get_credential
+ # https://github.com/jaraco/keyring/blob/97689324abcf01bd1793d49063e7ca01e03d7d07/keyring/backend.py#L134-L139
+ if username is not None:
+ password = self._get_password(url, username)
+ if password is not None:
+ return username, password
+ return None
+
+ def save_auth_info(self, url: str, username: str, password: str) -> None:
+ return self._set_password(url, username, password)
+
+ def _get_password(self, service_name: str, username: str) -> Optional[str]:
+ """Mirror the implementation of keyring.get_password using cli"""
+ if self.keyring is None:
+ return None
+
+ cmd = [self.keyring, "get", service_name, username]
+ env = os.environ.copy()
+ env["PYTHONIOENCODING"] = "utf-8"
+ res = subprocess.run(
+ cmd,
+ stdin=subprocess.DEVNULL,
+ stdout=subprocess.PIPE,
+ env=env,
+ )
+ if res.returncode:
+ return None
+ return res.stdout.decode("utf-8").strip(os.linesep)
+
+ def _set_password(self, service_name: str, username: str, password: str) -> None:
+ """Mirror the implementation of keyring.set_password using cli"""
+ if self.keyring is None:
+ return None
+ env = os.environ.copy()
+ env["PYTHONIOENCODING"] = "utf-8"
+ subprocess.run(
+ [self.keyring, "set", service_name, username],
+ input=f"{password}{os.linesep}".encode("utf-8"),
+ env=env,
+ check=True,
+ )
+ return None
+
+
+@lru_cache(maxsize=None)
+def get_keyring_provider(provider: str) -> KeyRingBaseProvider:
+ logger.verbose("Keyring provider requested: %s", provider)
+
+ # keyring has previously failed and been disabled
+ if KEYRING_DISABLED:
+ provider = "disabled"
+ if provider in ["import", "auto"]:
+ try:
+ impl = KeyRingPythonProvider()
+ logger.verbose("Keyring provider set: import")
+ return impl
+ except ImportError:
+ pass
+ except Exception as exc:
+ # In the event of an unexpected exception
+ # we should warn the user
+ msg = "Installed copy of keyring fails with exception %s"
+ if provider == "auto":
+ msg = msg + ", trying to find a keyring executable as a fallback"
+ logger.warning(msg, exc, exc_info=logger.isEnabledFor(logging.DEBUG))
+ if provider in ["subprocess", "auto"]:
+ cli = shutil.which("keyring")
+ if cli and cli.startswith(sysconfig.get_path("scripts")):
+ # all code within this function is stolen from shutil.which implementation
+ @typing.no_type_check
+ def PATH_as_shutil_which_determines_it() -> str:
+ path = os.environ.get("PATH", None)
+ if path is None:
+ try:
+ path = os.confstr("CS_PATH")
+ except (AttributeError, ValueError):
+ # os.confstr() or CS_PATH is not available
+ path = os.defpath
+ # bpo-35755: Don't use os.defpath if the PATH environment variable is
+ # set to an empty string
+
+ return path
+
+ scripts = Path(sysconfig.get_path("scripts"))
+
+ paths = []
+ for path in PATH_as_shutil_which_determines_it().split(os.pathsep):
+ p = Path(path)
+ try:
+ if not p.samefile(scripts):
+ paths.append(path)
+ except FileNotFoundError:
+ pass
+
+ path = os.pathsep.join(paths)
+
+ cli = shutil.which("keyring", path=path)
+
+ if cli:
+ logger.verbose("Keyring provider set: subprocess with executable %s", cli)
+ return KeyRingCliProvider(cli)
+
+ logger.verbose("Keyring provider set: disabled")
+ return KeyRingNullProvider()
+
+
+class MultiDomainBasicAuth(AuthBase):
+ def __init__(
+ self,
+ prompting: bool = True,
+ index_urls: Optional[List[str]] = None,
+ keyring_provider: str = "auto",
+ ) -> None:
+ self.prompting = prompting
+ self.index_urls = index_urls
+ self.keyring_provider = keyring_provider # type: ignore[assignment]
+ self.passwords: Dict[str, AuthInfo] = {}
+ # When the user is prompted to enter credentials and keyring is
+ # available, we will offer to save them. If the user accepts,
+ # this value is set to the credentials they entered. After the
+ # request authenticates, the caller should call
+ # ``save_credentials`` to save these.
+ self._credentials_to_save: Optional[Credentials] = None
+
+ @property
+ def keyring_provider(self) -> KeyRingBaseProvider:
+ return get_keyring_provider(self._keyring_provider)
+
+ @keyring_provider.setter
+ def keyring_provider(self, provider: str) -> None:
+ # The free function get_keyring_provider has been decorated with
+ # functools.cache. If an exception occurs in get_keyring_auth that
+ # cache will be cleared and keyring disabled, take that into account
+ # if you want to remove this indirection.
+ self._keyring_provider = provider
+
+ @property
+ def use_keyring(self) -> bool:
+ # We won't use keyring when --no-input is passed unless
+ # a specific provider is requested because it might require
+ # user interaction
+ return self.prompting or self._keyring_provider not in ["auto", "disabled"]
+
+ def _get_keyring_auth(
+ self,
+ url: Optional[str],
+ username: Optional[str],
+ ) -> Optional[AuthInfo]:
+ """Return the tuple auth for a given url from keyring."""
+ # Do nothing if no url was provided
+ if not url:
+ return None
+
+ try:
+ return self.keyring_provider.get_auth_info(url, username)
+ except Exception as exc:
+ logger.warning(
+ "Keyring is skipped due to an exception: %s",
+ str(exc),
+ )
+ global KEYRING_DISABLED
+ KEYRING_DISABLED = True
+ get_keyring_provider.cache_clear()
+ return None
+
+ def _get_index_url(self, url: str) -> Optional[str]:
+ """Return the original index URL matching the requested URL.
+
+ Cached or dynamically generated credentials may work against
+ the original index URL rather than just the netloc.
+
+ The provided url should have had its username and password
+ removed already. If the original index url had credentials then
+ they will be included in the return value.
+
+ Returns None if no matching index was found, or if --no-index
+ was specified by the user.
+ """
+ if not url or not self.index_urls:
+ return None
+
+ url = remove_auth_from_url(url).rstrip("/") + "/"
+ parsed_url = urllib.parse.urlsplit(url)
+
+ candidates = []
+
+ for index in self.index_urls:
+ index = index.rstrip("/") + "/"
+ parsed_index = urllib.parse.urlsplit(remove_auth_from_url(index))
+ if parsed_url == parsed_index:
+ return index
+
+ if parsed_url.netloc != parsed_index.netloc:
+ continue
+
+ candidate = urllib.parse.urlsplit(index)
+ candidates.append(candidate)
+
+ if not candidates:
+ return None
+
+ candidates.sort(
+ reverse=True,
+ key=lambda candidate: commonprefix(
+ [
+ parsed_url.path,
+ candidate.path,
+ ]
+ ).rfind("/"),
+ )
+
+ return urllib.parse.urlunsplit(candidates[0])
+
+ def _get_new_credentials(
+ self,
+ original_url: str,
+ *,
+ allow_netrc: bool = True,
+ allow_keyring: bool = False,
+ ) -> AuthInfo:
+ """Find and return credentials for the specified URL."""
+ # Split the credentials and netloc from the url.
+ url, netloc, url_user_password = split_auth_netloc_from_url(
+ original_url,
+ )
+
+ # Start with the credentials embedded in the url
+ username, password = url_user_password
+ if username is not None and password is not None:
+ logger.debug("Found credentials in url for %s", netloc)
+ return url_user_password
+
+ # Find a matching index url for this request
+ index_url = self._get_index_url(url)
+ if index_url:
+ # Split the credentials from the url.
+ index_info = split_auth_netloc_from_url(index_url)
+ if index_info:
+ index_url, _, index_url_user_password = index_info
+ logger.debug("Found index url %s", index_url)
+
+ # If an index URL was found, try its embedded credentials
+ if index_url and index_url_user_password[0] is not None:
+ username, password = index_url_user_password
+ if username is not None and password is not None:
+ logger.debug("Found credentials in index url for %s", netloc)
+ return index_url_user_password
+
+ # Get creds from netrc if we still don't have them
+ if allow_netrc:
+ netrc_auth = get_netrc_auth(original_url)
+ if netrc_auth:
+ logger.debug("Found credentials in netrc for %s", netloc)
+ return netrc_auth
+
+ # If we don't have a password and keyring is available, use it.
+ if allow_keyring:
+ # The index url is more specific than the netloc, so try it first
+ # fmt: off
+ kr_auth = (
+ self._get_keyring_auth(index_url, username) or
+ self._get_keyring_auth(netloc, username)
+ )
+ # fmt: on
+ if kr_auth:
+ logger.debug("Found credentials in keyring for %s", netloc)
+ return kr_auth
+
+ return username, password
+
+ def _get_url_and_credentials(
+ self, original_url: str
+ ) -> Tuple[str, Optional[str], Optional[str]]:
+ """Return the credentials to use for the provided URL.
+
+ If allowed, netrc and keyring may be used to obtain the
+ correct credentials.
+
+ Returns (url_without_credentials, username, password). Note
+ that even if the original URL contains credentials, this
+ function may return a different username and password.
+ """
+ url, netloc, _ = split_auth_netloc_from_url(original_url)
+
+ # Try to get credentials from original url
+ username, password = self._get_new_credentials(original_url)
+
+ # If credentials not found, use any stored credentials for this netloc.
+ # Do this if either the username or the password is missing.
+ # This accounts for the situation in which the user has specified
+ # the username in the index url, but the password comes from keyring.
+ if (username is None or password is None) and netloc in self.passwords:
+ un, pw = self.passwords[netloc]
+ # It is possible that the cached credentials are for a different username,
+ # in which case the cache should be ignored.
+ if username is None or username == un:
+ username, password = un, pw
+
+ if username is not None or password is not None:
+ # Convert the username and password if they're None, so that
+ # this netloc will show up as "cached" in the conditional above.
+ # Further, HTTPBasicAuth doesn't accept None, so it makes sense to
+ # cache the value that is going to be used.
+ username = username or ""
+ password = password or ""
+
+ # Store any acquired credentials.
+ self.passwords[netloc] = (username, password)
+
+ assert (
+ # Credentials were found
+ (username is not None and password is not None)
+ # Credentials were not found
+ or (username is None and password is None)
+ ), f"Could not load credentials from url: {original_url}"
+
+ return url, username, password
+
+ def __call__(self, req: Request) -> Request:
+ # Get credentials for this request
+ url, username, password = self._get_url_and_credentials(req.url)
+
+ # Set the url of the request to the url without any credentials
+ req.url = url
+
+ if username is not None and password is not None:
+ # Send the basic auth with this request
+ req = HTTPBasicAuth(username, password)(req)
+
+ # Attach a hook to handle 401 responses
+ req.register_hook("response", self.handle_401)
+
+ return req
+
+ # Factored out to allow for easy patching in tests
+ def _prompt_for_password(
+ self, netloc: str
+ ) -> Tuple[Optional[str], Optional[str], bool]:
+ username = ask_input(f"User for {netloc}: ") if self.prompting else None
+ if not username:
+ return None, None, False
+ if self.use_keyring:
+ auth = self._get_keyring_auth(netloc, username)
+ if auth and auth[0] is not None and auth[1] is not None:
+ return auth[0], auth[1], False
+ password = ask_password("Password: ")
+ return username, password, True
+
+ # Factored out to allow for easy patching in tests
+ def _should_save_password_to_keyring(self) -> bool:
+ if (
+ not self.prompting
+ or not self.use_keyring
+ or not self.keyring_provider.has_keyring
+ ):
+ return False
+ return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
+
+ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
+ # We only care about 401 responses, anything else we want to just
+ # pass through the actual response
+ if resp.status_code != 401:
+ return resp
+
+ username, password = None, None
+
+ # Query the keyring for credentials:
+ if self.use_keyring:
+ username, password = self._get_new_credentials(
+ resp.url,
+ allow_netrc=False,
+ allow_keyring=True,
+ )
+
+ # We are not able to prompt the user so simply return the response
+ if not self.prompting and not username and not password:
+ return resp
+
+ parsed = urllib.parse.urlparse(resp.url)
+
+ # Prompt the user for a new username and password
+ save = False
+ if not username and not password:
+ username, password, save = self._prompt_for_password(parsed.netloc)
+
+ # Store the new username and password to use for future requests
+ self._credentials_to_save = None
+ if username is not None and password is not None:
+ self.passwords[parsed.netloc] = (username, password)
+
+ # Prompt to save the password to keyring
+ if save and self._should_save_password_to_keyring():
+ self._credentials_to_save = Credentials(
+ url=parsed.netloc,
+ username=username,
+ password=password,
+ )
+
+ # Consume content and release the original connection to allow our new
+ # request to reuse the same one.
+ # The result of the assignment isn't used, it's just needed to consume
+ # the content.
+ _ = resp.content
+ resp.raw.release_conn()
+
+ # Add our new username and password to the request
+ req = HTTPBasicAuth(username or "", password or "")(resp.request)
+ req.register_hook("response", self.warn_on_401)
+
+ # On successful request, save the credentials that were used to
+ # keyring. (Note that if the user responded "no" above, this member
+ # is not set and nothing will be saved.)
+ if self._credentials_to_save:
+ req.register_hook("response", self.save_credentials)
+
+ # Send our new request
+ new_resp = resp.connection.send(req, **kwargs)
+ new_resp.history.append(resp)
+
+ return new_resp
+
+ def warn_on_401(self, resp: Response, **kwargs: Any) -> None:
+ """Response callback to warn about incorrect credentials."""
+ if resp.status_code == 401:
+ logger.warning(
+ "401 Error, Credentials not correct for %s",
+ resp.request.url,
+ )
+
+ def save_credentials(self, resp: Response, **kwargs: Any) -> None:
+ """Response callback to save credentials on success."""
+ assert (
+ self.keyring_provider.has_keyring
+ ), "should never reach here without keyring"
+
+ creds = self._credentials_to_save
+ self._credentials_to_save = None
+ if creds and resp.status_code < 400:
+ try:
+ logger.info("Saving credentials to keyring")
+ self.keyring_provider.save_auth_info(
+ creds.url, creds.username, creds.password
+ )
+ except Exception:
+ logger.exception("Failed to save credentials")
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/cache.py b/venv/lib/python3.11/site-packages/pip/_internal/network/cache.py
new file mode 100644
index 0000000..4d0fb54
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/cache.py
@@ -0,0 +1,106 @@
+"""HTTP cache implementation.
+"""
+
+import os
+from contextlib import contextmanager
+from datetime import datetime
+from typing import BinaryIO, Generator, Optional, Union
+
+from pip._vendor.cachecontrol.cache import SeparateBodyBaseCache
+from pip._vendor.cachecontrol.caches import SeparateBodyFileCache
+from pip._vendor.requests.models import Response
+
+from pip._internal.utils.filesystem import adjacent_tmp_file, replace
+from pip._internal.utils.misc import ensure_dir
+
+
+def is_from_cache(response: Response) -> bool:
+ return getattr(response, "from_cache", False)
+
+
+@contextmanager
+def suppressed_cache_errors() -> Generator[None, None, None]:
+ """If we can't access the cache then we can just skip caching and process
+ requests as if caching wasn't enabled.
+ """
+ try:
+ yield
+ except OSError:
+ pass
+
+
+class SafeFileCache(SeparateBodyBaseCache):
+ """
+ A file based cache which is safe to use even when the target directory may
+ not be accessible or writable.
+
+ There is a race condition when two processes try to write and/or read the
+ same entry at the same time, since each entry consists of two separate
+ files (https://github.com/psf/cachecontrol/issues/324). We therefore have
+ additional logic that makes sure that both files to be present before
+ returning an entry; this fixes the read side of the race condition.
+
+ For the write side, we assume that the server will only ever return the
+ same data for the same URL, which ought to be the case for files pip is
+ downloading. PyPI does not have a mechanism to swap out a wheel for
+ another wheel, for example. If this assumption is not true, the
+ CacheControl issue will need to be fixed.
+ """
+
+ def __init__(self, directory: str) -> None:
+ assert directory is not None, "Cache directory must not be None."
+ super().__init__()
+ self.directory = directory
+
+ def _get_cache_path(self, name: str) -> str:
+ # From cachecontrol.caches.file_cache.FileCache._fn, brought into our
+ # class for backwards-compatibility and to avoid using a non-public
+ # method.
+ hashed = SeparateBodyFileCache.encode(name)
+ parts = list(hashed[:5]) + [hashed]
+ return os.path.join(self.directory, *parts)
+
+ def get(self, key: str) -> Optional[bytes]:
+ # The cache entry is only valid if both metadata and body exist.
+ metadata_path = self._get_cache_path(key)
+ body_path = metadata_path + ".body"
+ if not (os.path.exists(metadata_path) and os.path.exists(body_path)):
+ return None
+ with suppressed_cache_errors():
+ with open(metadata_path, "rb") as f:
+ return f.read()
+
+ def _write(self, path: str, data: bytes) -> None:
+ with suppressed_cache_errors():
+ ensure_dir(os.path.dirname(path))
+
+ with adjacent_tmp_file(path) as f:
+ f.write(data)
+
+ replace(f.name, path)
+
+ def set(
+ self, key: str, value: bytes, expires: Union[int, datetime, None] = None
+ ) -> None:
+ path = self._get_cache_path(key)
+ self._write(path, value)
+
+ def delete(self, key: str) -> None:
+ path = self._get_cache_path(key)
+ with suppressed_cache_errors():
+ os.remove(path)
+ with suppressed_cache_errors():
+ os.remove(path + ".body")
+
+ def get_body(self, key: str) -> Optional[BinaryIO]:
+ # The cache entry is only valid if both metadata and body exist.
+ metadata_path = self._get_cache_path(key)
+ body_path = metadata_path + ".body"
+ if not (os.path.exists(metadata_path) and os.path.exists(body_path)):
+ return None
+ with suppressed_cache_errors():
+ return open(body_path, "rb")
+
+ def set_body(self, key: str, body: bytes) -> None:
+ path = self._get_cache_path(key) + ".body"
+ self._write(path, body)
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/download.py b/venv/lib/python3.11/site-packages/pip/_internal/network/download.py
new file mode 100644
index 0000000..d1d4354
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/download.py
@@ -0,0 +1,186 @@
+"""Download files with progress indicators.
+"""
+import email.message
+import logging
+import mimetypes
+import os
+from typing import Iterable, Optional, Tuple
+
+from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
+
+from pip._internal.cli.progress_bars import get_download_progress_renderer
+from pip._internal.exceptions import NetworkConnectionError
+from pip._internal.models.index import PyPI
+from pip._internal.models.link import Link
+from pip._internal.network.cache import is_from_cache
+from pip._internal.network.session import PipSession
+from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
+from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext
+
+logger = logging.getLogger(__name__)
+
+
+def _get_http_response_size(resp: Response) -> Optional[int]:
+ try:
+ return int(resp.headers["content-length"])
+ except (ValueError, KeyError, TypeError):
+ return None
+
+
+def _prepare_download(
+ resp: Response,
+ link: Link,
+ progress_bar: str,
+) -> Iterable[bytes]:
+ total_length = _get_http_response_size(resp)
+
+ if link.netloc == PyPI.file_storage_domain:
+ url = link.show_url
+ else:
+ url = link.url_without_fragment
+
+ logged_url = redact_auth_from_url(url)
+
+ if total_length:
+ logged_url = f"{logged_url} ({format_size(total_length)})"
+
+ if is_from_cache(resp):
+ logger.info("Using cached %s", logged_url)
+ else:
+ logger.info("Downloading %s", logged_url)
+
+ if logger.getEffectiveLevel() > logging.INFO:
+ show_progress = False
+ elif is_from_cache(resp):
+ show_progress = False
+ elif not total_length:
+ show_progress = True
+ elif total_length > (40 * 1000):
+ show_progress = True
+ else:
+ show_progress = False
+
+ chunks = response_chunks(resp, CONTENT_CHUNK_SIZE)
+
+ if not show_progress:
+ return chunks
+
+ renderer = get_download_progress_renderer(bar_type=progress_bar, size=total_length)
+ return renderer(chunks)
+
+
+def sanitize_content_filename(filename: str) -> str:
+ """
+ Sanitize the "filename" value from a Content-Disposition header.
+ """
+ return os.path.basename(filename)
+
+
+def parse_content_disposition(content_disposition: str, default_filename: str) -> str:
+ """
+ Parse the "filename" value from a Content-Disposition header, and
+ return the default filename if the result is empty.
+ """
+ m = email.message.Message()
+ m["content-type"] = content_disposition
+ filename = m.get_param("filename")
+ if filename:
+ # We need to sanitize the filename to prevent directory traversal
+ # in case the filename contains ".." path parts.
+ filename = sanitize_content_filename(str(filename))
+ return filename or default_filename
+
+
+def _get_http_response_filename(resp: Response, link: Link) -> str:
+ """Get an ideal filename from the given HTTP response, falling back to
+ the link filename if not provided.
+ """
+ filename = link.filename # fallback
+ # Have a look at the Content-Disposition header for a better guess
+ content_disposition = resp.headers.get("content-disposition")
+ if content_disposition:
+ filename = parse_content_disposition(content_disposition, filename)
+ ext: Optional[str] = splitext(filename)[1]
+ if not ext:
+ ext = mimetypes.guess_extension(resp.headers.get("content-type", ""))
+ if ext:
+ filename += ext
+ if not ext and link.url != resp.url:
+ ext = os.path.splitext(resp.url)[1]
+ if ext:
+ filename += ext
+ return filename
+
+
+def _http_get_download(session: PipSession, link: Link) -> Response:
+ target_url = link.url.split("#", 1)[0]
+ resp = session.get(target_url, headers=HEADERS, stream=True)
+ raise_for_status(resp)
+ return resp
+
+
+class Downloader:
+ def __init__(
+ self,
+ session: PipSession,
+ progress_bar: str,
+ ) -> None:
+ self._session = session
+ self._progress_bar = progress_bar
+
+ def __call__(self, link: Link, location: str) -> Tuple[str, str]:
+ """Download the file given by link into location."""
+ try:
+ resp = _http_get_download(self._session, link)
+ except NetworkConnectionError as e:
+ assert e.response is not None
+ logger.critical(
+ "HTTP error %s while getting %s", e.response.status_code, link
+ )
+ raise
+
+ filename = _get_http_response_filename(resp, link)
+ filepath = os.path.join(location, filename)
+
+ chunks = _prepare_download(resp, link, self._progress_bar)
+ with open(filepath, "wb") as content_file:
+ for chunk in chunks:
+ content_file.write(chunk)
+ content_type = resp.headers.get("Content-Type", "")
+ return filepath, content_type
+
+
+class BatchDownloader:
+ def __init__(
+ self,
+ session: PipSession,
+ progress_bar: str,
+ ) -> None:
+ self._session = session
+ self._progress_bar = progress_bar
+
+ def __call__(
+ self, links: Iterable[Link], location: str
+ ) -> Iterable[Tuple[Link, Tuple[str, str]]]:
+ """Download the files given by links into location."""
+ for link in links:
+ try:
+ resp = _http_get_download(self._session, link)
+ except NetworkConnectionError as e:
+ assert e.response is not None
+ logger.critical(
+ "HTTP error %s while getting %s",
+ e.response.status_code,
+ link,
+ )
+ raise
+
+ filename = _get_http_response_filename(resp, link)
+ filepath = os.path.join(location, filename)
+
+ chunks = _prepare_download(resp, link, self._progress_bar)
+ with open(filepath, "wb") as content_file:
+ for chunk in chunks:
+ content_file.write(chunk)
+ content_type = resp.headers.get("Content-Type", "")
+ yield link, (filepath, content_type)
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py b/venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py
new file mode 100644
index 0000000..82ec50d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py
@@ -0,0 +1,210 @@
+"""Lazy ZIP over HTTP"""
+
+__all__ = ["HTTPRangeRequestUnsupported", "dist_from_wheel_url"]
+
+from bisect import bisect_left, bisect_right
+from contextlib import contextmanager
+from tempfile import NamedTemporaryFile
+from typing import Any, Dict, Generator, List, Optional, Tuple
+from zipfile import BadZipFile, ZipFile
+
+from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
+
+from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
+from pip._internal.network.session import PipSession
+from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
+
+
+class HTTPRangeRequestUnsupported(Exception):
+ pass
+
+
+def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
+ """Return a distribution object from the given wheel URL.
+
+ This uses HTTP range requests to only fetch the portion of the wheel
+ containing metadata, just enough for the object to be constructed.
+ If such requests are not supported, HTTPRangeRequestUnsupported
+ is raised.
+ """
+ with LazyZipOverHTTP(url, session) as zf:
+ # For read-only ZIP files, ZipFile only needs methods read,
+ # seek, seekable and tell, not the whole IO protocol.
+ wheel = MemoryWheel(zf.name, zf) # type: ignore
+ # After context manager exit, wheel.name
+ # is an invalid file by intention.
+ return get_wheel_distribution(wheel, canonicalize_name(name))
+
+
+class LazyZipOverHTTP:
+ """File-like object mapped to a ZIP file over HTTP.
+
+ This uses HTTP range requests to lazily fetch the file's content,
+ which is supposed to be fed to ZipFile. If such requests are not
+ supported by the server, raise HTTPRangeRequestUnsupported
+ during initialization.
+ """
+
+ def __init__(
+ self, url: str, session: PipSession, chunk_size: int = CONTENT_CHUNK_SIZE
+ ) -> None:
+ head = session.head(url, headers=HEADERS)
+ raise_for_status(head)
+ assert head.status_code == 200
+ self._session, self._url, self._chunk_size = session, url, chunk_size
+ self._length = int(head.headers["Content-Length"])
+ self._file = NamedTemporaryFile()
+ self.truncate(self._length)
+ self._left: List[int] = []
+ self._right: List[int] = []
+ if "bytes" not in head.headers.get("Accept-Ranges", "none"):
+ raise HTTPRangeRequestUnsupported("range request is not supported")
+ self._check_zip()
+
+ @property
+ def mode(self) -> str:
+ """Opening mode, which is always rb."""
+ return "rb"
+
+ @property
+ def name(self) -> str:
+ """Path to the underlying file."""
+ return self._file.name
+
+ def seekable(self) -> bool:
+ """Return whether random access is supported, which is True."""
+ return True
+
+ def close(self) -> None:
+ """Close the file."""
+ self._file.close()
+
+ @property
+ def closed(self) -> bool:
+ """Whether the file is closed."""
+ return self._file.closed
+
+ def read(self, size: int = -1) -> bytes:
+ """Read up to size bytes from the object and return them.
+
+ As a convenience, if size is unspecified or -1,
+ all bytes until EOF are returned. Fewer than
+ size bytes may be returned if EOF is reached.
+ """
+ download_size = max(size, self._chunk_size)
+ start, length = self.tell(), self._length
+ stop = length if size < 0 else min(start + download_size, length)
+ start = max(0, stop - download_size)
+ self._download(start, stop - 1)
+ return self._file.read(size)
+
+ def readable(self) -> bool:
+ """Return whether the file is readable, which is True."""
+ return True
+
+ def seek(self, offset: int, whence: int = 0) -> int:
+ """Change stream position and return the new absolute position.
+
+ Seek to offset relative position indicated by whence:
+ * 0: Start of stream (the default). pos should be >= 0;
+ * 1: Current position - pos may be negative;
+ * 2: End of stream - pos usually negative.
+ """
+ return self._file.seek(offset, whence)
+
+ def tell(self) -> int:
+ """Return the current position."""
+ return self._file.tell()
+
+ def truncate(self, size: Optional[int] = None) -> int:
+ """Resize the stream to the given size in bytes.
+
+ If size is unspecified resize to the current position.
+ The current stream position isn't changed.
+
+ Return the new file size.
+ """
+ return self._file.truncate(size)
+
+ def writable(self) -> bool:
+ """Return False."""
+ return False
+
+ def __enter__(self) -> "LazyZipOverHTTP":
+ self._file.__enter__()
+ return self
+
+ def __exit__(self, *exc: Any) -> None:
+ self._file.__exit__(*exc)
+
+ @contextmanager
+ def _stay(self) -> Generator[None, None, None]:
+ """Return a context manager keeping the position.
+
+ At the end of the block, seek back to original position.
+ """
+ pos = self.tell()
+ try:
+ yield
+ finally:
+ self.seek(pos)
+
+ def _check_zip(self) -> None:
+ """Check and download until the file is a valid ZIP."""
+ end = self._length - 1
+ for start in reversed(range(0, end, self._chunk_size)):
+ self._download(start, end)
+ with self._stay():
+ try:
+ # For read-only ZIP files, ZipFile only needs
+ # methods read, seek, seekable and tell.
+ ZipFile(self) # type: ignore
+ except BadZipFile:
+ pass
+ else:
+ break
+
+ def _stream_response(
+ self, start: int, end: int, base_headers: Dict[str, str] = HEADERS
+ ) -> Response:
+ """Return HTTP response to a range request from start to end."""
+ headers = base_headers.copy()
+ headers["Range"] = f"bytes={start}-{end}"
+ # TODO: Get range requests to be correctly cached
+ headers["Cache-Control"] = "no-cache"
+ return self._session.get(self._url, headers=headers, stream=True)
+
+ def _merge(
+ self, start: int, end: int, left: int, right: int
+ ) -> Generator[Tuple[int, int], None, None]:
+ """Return a generator of intervals to be fetched.
+
+ Args:
+ start (int): Start of needed interval
+ end (int): End of needed interval
+ left (int): Index of first overlapping downloaded data
+ right (int): Index after last overlapping downloaded data
+ """
+ lslice, rslice = self._left[left:right], self._right[left:right]
+ i = start = min([start] + lslice[:1])
+ end = max([end] + rslice[-1:])
+ for j, k in zip(lslice, rslice):
+ if j > i:
+ yield i, j - 1
+ i = k + 1
+ if i <= end:
+ yield i, end
+ self._left[left:right], self._right[left:right] = [start], [end]
+
+ def _download(self, start: int, end: int) -> None:
+ """Download bytes from start to end inclusively."""
+ with self._stay():
+ left = bisect_left(self._right, start)
+ right = bisect_right(self._left, end)
+ for start, end in self._merge(start, end, left, right):
+ response = self._stream_response(start, end)
+ response.raise_for_status()
+ self.seek(start)
+ for chunk in response_chunks(response, self._chunk_size):
+ self._file.write(chunk)
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/session.py b/venv/lib/python3.11/site-packages/pip/_internal/network/session.py
new file mode 100644
index 0000000..f17efc5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/session.py
@@ -0,0 +1,520 @@
+"""PipSession and supporting code, containing all pip-specific
+network request configuration and behavior.
+"""
+
+import email.utils
+import io
+import ipaddress
+import json
+import logging
+import mimetypes
+import os
+import platform
+import shutil
+import subprocess
+import sys
+import urllib.parse
+import warnings
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Dict,
+ Generator,
+ List,
+ Mapping,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+)
+
+from pip._vendor import requests, urllib3
+from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter
+from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter
+from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter
+from pip._vendor.requests.models import PreparedRequest, Response
+from pip._vendor.requests.structures import CaseInsensitiveDict
+from pip._vendor.urllib3.connectionpool import ConnectionPool
+from pip._vendor.urllib3.exceptions import InsecureRequestWarning
+
+from pip import __version__
+from pip._internal.metadata import get_default_environment
+from pip._internal.models.link import Link
+from pip._internal.network.auth import MultiDomainBasicAuth
+from pip._internal.network.cache import SafeFileCache
+
+# Import ssl from compat so the initial import occurs in only one place.
+from pip._internal.utils.compat import has_tls
+from pip._internal.utils.glibc import libc_ver
+from pip._internal.utils.misc import build_url_from_netloc, parse_netloc
+from pip._internal.utils.urls import url_to_path
+
+if TYPE_CHECKING:
+ from ssl import SSLContext
+
+ from pip._vendor.urllib3.poolmanager import PoolManager
+
+
+logger = logging.getLogger(__name__)
+
+SecureOrigin = Tuple[str, str, Optional[Union[int, str]]]
+
+
+# Ignore warning raised when using --trusted-host.
+warnings.filterwarnings("ignore", category=InsecureRequestWarning)
+
+
+SECURE_ORIGINS: List[SecureOrigin] = [
+ # protocol, hostname, port
+ # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC)
+ ("https", "*", "*"),
+ ("*", "localhost", "*"),
+ ("*", "127.0.0.0/8", "*"),
+ ("*", "::1/128", "*"),
+ ("file", "*", None),
+ # ssh is always secure.
+ ("ssh", "*", "*"),
+]
+
+
+# These are environment variables present when running under various
+# CI systems. For each variable, some CI systems that use the variable
+# are indicated. The collection was chosen so that for each of a number
+# of popular systems, at least one of the environment variables is used.
+# This list is used to provide some indication of and lower bound for
+# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive.
+# For more background, see: https://github.com/pypa/pip/issues/5499
+CI_ENVIRONMENT_VARIABLES = (
+ # Azure Pipelines
+ "BUILD_BUILDID",
+ # Jenkins
+ "BUILD_ID",
+ # AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
+ "CI",
+ # Explicit environment variable.
+ "PIP_IS_CI",
+)
+
+
+def looks_like_ci() -> bool:
+ """
+ Return whether it looks like pip is running under CI.
+ """
+ # We don't use the method of checking for a tty (e.g. using isatty())
+ # because some CI systems mimic a tty (e.g. Travis CI). Thus that
+ # method doesn't provide definitive information in either direction.
+ return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)
+
+
+def user_agent() -> str:
+ """
+ Return a string representing the user agent.
+ """
+ data: Dict[str, Any] = {
+ "installer": {"name": "pip", "version": __version__},
+ "python": platform.python_version(),
+ "implementation": {
+ "name": platform.python_implementation(),
+ },
+ }
+
+ if data["implementation"]["name"] == "CPython":
+ data["implementation"]["version"] = platform.python_version()
+ elif data["implementation"]["name"] == "PyPy":
+ pypy_version_info = sys.pypy_version_info # type: ignore
+ if pypy_version_info.releaselevel == "final":
+ pypy_version_info = pypy_version_info[:3]
+ data["implementation"]["version"] = ".".join(
+ [str(x) for x in pypy_version_info]
+ )
+ elif data["implementation"]["name"] == "Jython":
+ # Complete Guess
+ data["implementation"]["version"] = platform.python_version()
+ elif data["implementation"]["name"] == "IronPython":
+ # Complete Guess
+ data["implementation"]["version"] = platform.python_version()
+
+ if sys.platform.startswith("linux"):
+ from pip._vendor import distro
+
+ linux_distribution = distro.name(), distro.version(), distro.codename()
+ distro_infos: Dict[str, Any] = dict(
+ filter(
+ lambda x: x[1],
+ zip(["name", "version", "id"], linux_distribution),
+ )
+ )
+ libc = dict(
+ filter(
+ lambda x: x[1],
+ zip(["lib", "version"], libc_ver()),
+ )
+ )
+ if libc:
+ distro_infos["libc"] = libc
+ if distro_infos:
+ data["distro"] = distro_infos
+
+ if sys.platform.startswith("darwin") and platform.mac_ver()[0]:
+ data["distro"] = {"name": "macOS", "version": platform.mac_ver()[0]}
+
+ if platform.system():
+ data.setdefault("system", {})["name"] = platform.system()
+
+ if platform.release():
+ data.setdefault("system", {})["release"] = platform.release()
+
+ if platform.machine():
+ data["cpu"] = platform.machine()
+
+ if has_tls():
+ import _ssl as ssl
+
+ data["openssl_version"] = ssl.OPENSSL_VERSION
+
+ setuptools_dist = get_default_environment().get_distribution("setuptools")
+ if setuptools_dist is not None:
+ data["setuptools_version"] = str(setuptools_dist.version)
+
+ if shutil.which("rustc") is not None:
+ # If for any reason `rustc --version` fails, silently ignore it
+ try:
+ rustc_output = subprocess.check_output(
+ ["rustc", "--version"], stderr=subprocess.STDOUT, timeout=0.5
+ )
+ except Exception:
+ pass
+ else:
+ if rustc_output.startswith(b"rustc "):
+ # The format of `rustc --version` is:
+ # `b'rustc 1.52.1 (9bc8c42bb 2021-05-09)\n'`
+ # We extract just the middle (1.52.1) part
+ data["rustc_version"] = rustc_output.split(b" ")[1].decode()
+
+ # Use None rather than False so as not to give the impression that
+ # pip knows it is not being run under CI. Rather, it is a null or
+ # inconclusive result. Also, we include some value rather than no
+ # value to make it easier to know that the check has been run.
+ data["ci"] = True if looks_like_ci() else None
+
+ user_data = os.environ.get("PIP_USER_AGENT_USER_DATA")
+ if user_data is not None:
+ data["user_data"] = user_data
+
+ return "{data[installer][name]}/{data[installer][version]} {json}".format(
+ data=data,
+ json=json.dumps(data, separators=(",", ":"), sort_keys=True),
+ )
+
+
+class LocalFSAdapter(BaseAdapter):
+ def send(
+ self,
+ request: PreparedRequest,
+ stream: bool = False,
+ timeout: Optional[Union[float, Tuple[float, float]]] = None,
+ verify: Union[bool, str] = True,
+ cert: Optional[Union[str, Tuple[str, str]]] = None,
+ proxies: Optional[Mapping[str, str]] = None,
+ ) -> Response:
+ pathname = url_to_path(request.url)
+
+ resp = Response()
+ resp.status_code = 200
+ resp.url = request.url
+
+ try:
+ stats = os.stat(pathname)
+ except OSError as exc:
+ # format the exception raised as a io.BytesIO object,
+ # to return a better error message:
+ resp.status_code = 404
+ resp.reason = type(exc).__name__
+ resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
+ else:
+ modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
+ content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
+ resp.headers = CaseInsensitiveDict(
+ {
+ "Content-Type": content_type,
+ "Content-Length": stats.st_size,
+ "Last-Modified": modified,
+ }
+ )
+
+ resp.raw = open(pathname, "rb")
+ resp.close = resp.raw.close
+
+ return resp
+
+ def close(self) -> None:
+ pass
+
+
+class _SSLContextAdapterMixin:
+ """Mixin to add the ``ssl_context`` constructor argument to HTTP adapters.
+
+ The additional argument is forwarded directly to the pool manager. This allows us
+ to dynamically decide what SSL store to use at runtime, which is used to implement
+ the optional ``truststore`` backend.
+ """
+
+ def __init__(
+ self,
+ *,
+ ssl_context: Optional["SSLContext"] = None,
+ **kwargs: Any,
+ ) -> None:
+ self._ssl_context = ssl_context
+ super().__init__(**kwargs)
+
+ def init_poolmanager(
+ self,
+ connections: int,
+ maxsize: int,
+ block: bool = DEFAULT_POOLBLOCK,
+ **pool_kwargs: Any,
+ ) -> "PoolManager":
+ if self._ssl_context is not None:
+ pool_kwargs.setdefault("ssl_context", self._ssl_context)
+ return super().init_poolmanager( # type: ignore[misc]
+ connections=connections,
+ maxsize=maxsize,
+ block=block,
+ **pool_kwargs,
+ )
+
+
+class HTTPAdapter(_SSLContextAdapterMixin, _BaseHTTPAdapter):
+ pass
+
+
+class CacheControlAdapter(_SSLContextAdapterMixin, _BaseCacheControlAdapter):
+ pass
+
+
+class InsecureHTTPAdapter(HTTPAdapter):
+ def cert_verify(
+ self,
+ conn: ConnectionPool,
+ url: str,
+ verify: Union[bool, str],
+ cert: Optional[Union[str, Tuple[str, str]]],
+ ) -> None:
+ super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
+
+
+class InsecureCacheControlAdapter(CacheControlAdapter):
+ def cert_verify(
+ self,
+ conn: ConnectionPool,
+ url: str,
+ verify: Union[bool, str],
+ cert: Optional[Union[str, Tuple[str, str]]],
+ ) -> None:
+ super().cert_verify(conn=conn, url=url, verify=False, cert=cert)
+
+
+class PipSession(requests.Session):
+ timeout: Optional[int] = None
+
+ def __init__(
+ self,
+ *args: Any,
+ retries: int = 0,
+ cache: Optional[str] = None,
+ trusted_hosts: Sequence[str] = (),
+ index_urls: Optional[List[str]] = None,
+ ssl_context: Optional["SSLContext"] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param trusted_hosts: Domains not to emit warnings for when not using
+ HTTPS.
+ """
+ super().__init__(*args, **kwargs)
+
+ # Namespace the attribute with "pip_" just in case to prevent
+ # possible conflicts with the base class.
+ self.pip_trusted_origins: List[Tuple[str, Optional[int]]] = []
+
+ # Attach our User Agent to the request
+ self.headers["User-Agent"] = user_agent()
+
+ # Attach our Authentication handler to the session
+ self.auth = MultiDomainBasicAuth(index_urls=index_urls)
+
+ # Create our urllib3.Retry instance which will allow us to customize
+ # how we handle retries.
+ retries = urllib3.Retry(
+ # Set the total number of retries that a particular request can
+ # have.
+ total=retries,
+ # A 503 error from PyPI typically means that the Fastly -> Origin
+ # connection got interrupted in some way. A 503 error in general
+ # is typically considered a transient error so we'll go ahead and
+ # retry it.
+ # A 500 may indicate transient error in Amazon S3
+ # A 502 may be a transient error from a CDN like CloudFlare or CloudFront
+ # A 520 or 527 - may indicate transient error in CloudFlare
+ status_forcelist=[500, 502, 503, 520, 527],
+ # Add a small amount of back off between failed requests in
+ # order to prevent hammering the service.
+ backoff_factor=0.25,
+ ) # type: ignore
+
+ # Our Insecure HTTPAdapter disables HTTPS validation. It does not
+ # support caching so we'll use it for all http:// URLs.
+ # If caching is disabled, we will also use it for
+ # https:// hosts that we've marked as ignoring
+ # TLS errors for (trusted-hosts).
+ insecure_adapter = InsecureHTTPAdapter(max_retries=retries)
+
+ # We want to _only_ cache responses on securely fetched origins or when
+ # the host is specified as trusted. We do this because
+ # we can't validate the response of an insecurely/untrusted fetched
+ # origin, and we don't want someone to be able to poison the cache and
+ # require manual eviction from the cache to fix it.
+ if cache:
+ secure_adapter = CacheControlAdapter(
+ cache=SafeFileCache(cache),
+ max_retries=retries,
+ ssl_context=ssl_context,
+ )
+ self._trusted_host_adapter = InsecureCacheControlAdapter(
+ cache=SafeFileCache(cache),
+ max_retries=retries,
+ )
+ else:
+ secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context)
+ self._trusted_host_adapter = insecure_adapter
+
+ self.mount("https://", secure_adapter)
+ self.mount("http://", insecure_adapter)
+
+ # Enable file:// urls
+ self.mount("file://", LocalFSAdapter())
+
+ for host in trusted_hosts:
+ self.add_trusted_host(host, suppress_logging=True)
+
+ def update_index_urls(self, new_index_urls: List[str]) -> None:
+ """
+ :param new_index_urls: New index urls to update the authentication
+ handler with.
+ """
+ self.auth.index_urls = new_index_urls
+
+ def add_trusted_host(
+ self, host: str, source: Optional[str] = None, suppress_logging: bool = False
+ ) -> None:
+ """
+ :param host: It is okay to provide a host that has previously been
+ added.
+ :param source: An optional source string, for logging where the host
+ string came from.
+ """
+ if not suppress_logging:
+ msg = f"adding trusted host: {host!r}"
+ if source is not None:
+ msg += f" (from {source})"
+ logger.info(msg)
+
+ parsed_host, parsed_port = parse_netloc(host)
+ if parsed_host is None:
+ raise ValueError(f"Trusted host URL must include a host part: {host!r}")
+ if (parsed_host, parsed_port) not in self.pip_trusted_origins:
+ self.pip_trusted_origins.append((parsed_host, parsed_port))
+
+ self.mount(
+ build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
+ )
+ self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
+ if not parsed_port:
+ self.mount(
+ build_url_from_netloc(host, scheme="http") + ":",
+ self._trusted_host_adapter,
+ )
+ # Mount wildcard ports for the same host.
+ self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
+
+ def iter_secure_origins(self) -> Generator[SecureOrigin, None, None]:
+ yield from SECURE_ORIGINS
+ for host, port in self.pip_trusted_origins:
+ yield ("*", host, "*" if port is None else port)
+
+ def is_secure_origin(self, location: Link) -> bool:
+ # Determine if this url used a secure transport mechanism
+ parsed = urllib.parse.urlparse(str(location))
+ origin_protocol, origin_host, origin_port = (
+ parsed.scheme,
+ parsed.hostname,
+ parsed.port,
+ )
+
+ # The protocol to use to see if the protocol matches.
+ # Don't count the repository type as part of the protocol: in
+ # cases such as "git+ssh", only use "ssh". (I.e., Only verify against
+ # the last scheme.)
+ origin_protocol = origin_protocol.rsplit("+", 1)[-1]
+
+ # Determine if our origin is a secure origin by looking through our
+ # hardcoded list of secure origins, as well as any additional ones
+ # configured on this PackageFinder instance.
+ for secure_origin in self.iter_secure_origins():
+ secure_protocol, secure_host, secure_port = secure_origin
+ if origin_protocol != secure_protocol and secure_protocol != "*":
+ continue
+
+ try:
+ addr = ipaddress.ip_address(origin_host or "")
+ network = ipaddress.ip_network(secure_host)
+ except ValueError:
+ # We don't have both a valid address or a valid network, so
+ # we'll check this origin against hostnames.
+ if (
+ origin_host
+ and origin_host.lower() != secure_host.lower()
+ and secure_host != "*"
+ ):
+ continue
+ else:
+ # We have a valid address and network, so see if the address
+ # is contained within the network.
+ if addr not in network:
+ continue
+
+ # Check to see if the port matches.
+ if (
+ origin_port != secure_port
+ and secure_port != "*"
+ and secure_port is not None
+ ):
+ continue
+
+ # If we've gotten here, then this origin matches the current
+ # secure origin and we should return True
+ return True
+
+ # If we've gotten to this point, then the origin isn't secure and we
+ # will not accept it as a valid location to search. We will however
+ # log a warning that we are ignoring it.
+ logger.warning(
+ "The repository located at %s is not a trusted or secure host and "
+ "is being ignored. If this repository is available via HTTPS we "
+ "recommend you use HTTPS instead, otherwise you may silence "
+ "this warning and allow it anyway with '--trusted-host %s'.",
+ origin_host,
+ origin_host,
+ )
+
+ return False
+
+ def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
+ # Allow setting a default timeout on a session
+ kwargs.setdefault("timeout", self.timeout)
+ # Allow setting a default proxies on a session
+ kwargs.setdefault("proxies", self.proxies)
+
+ # Dispatch the actual request
+ return super().request(method, url, *args, **kwargs)
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/utils.py b/venv/lib/python3.11/site-packages/pip/_internal/network/utils.py
new file mode 100644
index 0000000..134848a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/utils.py
@@ -0,0 +1,96 @@
+from typing import Dict, Generator
+
+from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
+
+from pip._internal.exceptions import NetworkConnectionError
+
+# The following comments and HTTP headers were originally added by
+# Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03.
+#
+# We use Accept-Encoding: identity here because requests defaults to
+# accepting compressed responses. This breaks in a variety of ways
+# depending on how the server is configured.
+# - Some servers will notice that the file isn't a compressible file
+# and will leave the file alone and with an empty Content-Encoding
+# - Some servers will notice that the file is already compressed and
+# will leave the file alone, adding a Content-Encoding: gzip header
+# - Some servers won't notice anything at all and will take a file
+# that's already been compressed and compress it again, and set
+# the Content-Encoding: gzip header
+# By setting this to request only the identity encoding we're hoping
+# to eliminate the third case. Hopefully there does not exist a server
+# which when given a file will notice it is already compressed and that
+# you're not asking for a compressed file and will then decompress it
+# before sending because if that's the case I don't think it'll ever be
+# possible to make this work.
+HEADERS: Dict[str, str] = {"Accept-Encoding": "identity"}
+
+
+def raise_for_status(resp: Response) -> None:
+ http_error_msg = ""
+ if isinstance(resp.reason, bytes):
+ # We attempt to decode utf-8 first because some servers
+ # choose to localize their reason strings. If the string
+ # isn't utf-8, we fall back to iso-8859-1 for all other
+ # encodings.
+ try:
+ reason = resp.reason.decode("utf-8")
+ except UnicodeDecodeError:
+ reason = resp.reason.decode("iso-8859-1")
+ else:
+ reason = resp.reason
+
+ if 400 <= resp.status_code < 500:
+ http_error_msg = (
+ f"{resp.status_code} Client Error: {reason} for url: {resp.url}"
+ )
+
+ elif 500 <= resp.status_code < 600:
+ http_error_msg = (
+ f"{resp.status_code} Server Error: {reason} for url: {resp.url}"
+ )
+
+ if http_error_msg:
+ raise NetworkConnectionError(http_error_msg, response=resp)
+
+
+def response_chunks(
+ response: Response, chunk_size: int = CONTENT_CHUNK_SIZE
+) -> Generator[bytes, None, None]:
+ """Given a requests Response, provide the data chunks."""
+ try:
+ # Special case for urllib3.
+ for chunk in response.raw.stream(
+ chunk_size,
+ # We use decode_content=False here because we don't
+ # want urllib3 to mess with the raw bytes we get
+ # from the server. If we decompress inside of
+ # urllib3 then we cannot verify the checksum
+ # because the checksum will be of the compressed
+ # file. This breakage will only occur if the
+ # server adds a Content-Encoding header, which
+ # depends on how the server was configured:
+ # - Some servers will notice that the file isn't a
+ # compressible file and will leave the file alone
+ # and with an empty Content-Encoding
+ # - Some servers will notice that the file is
+ # already compressed and will leave the file
+ # alone and will add a Content-Encoding: gzip
+ # header
+ # - Some servers won't notice anything at all and
+ # will take a file that's already been compressed
+ # and compress it again and set the
+ # Content-Encoding: gzip header
+ #
+ # By setting this not to decode automatically we
+ # hope to eliminate problems with the second case.
+ decode_content=False,
+ ):
+ yield chunk
+ except AttributeError:
+ # Standard file-like object.
+ while True:
+ chunk = response.raw.read(chunk_size)
+ if not chunk:
+ break
+ yield chunk
diff --git a/venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py b/venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py
new file mode 100644
index 0000000..22ec8d2
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py
@@ -0,0 +1,62 @@
+"""xmlrpclib.Transport implementation
+"""
+
+import logging
+import urllib.parse
+import xmlrpc.client
+from typing import TYPE_CHECKING, Tuple
+
+from pip._internal.exceptions import NetworkConnectionError
+from pip._internal.network.session import PipSession
+from pip._internal.network.utils import raise_for_status
+
+if TYPE_CHECKING:
+ from xmlrpc.client import _HostType, _Marshallable
+
+ from _typeshed import SizedBuffer
+
+logger = logging.getLogger(__name__)
+
+
+class PipXmlrpcTransport(xmlrpc.client.Transport):
+ """Provide a `xmlrpclib.Transport` implementation via a `PipSession`
+ object.
+ """
+
+ def __init__(
+ self, index_url: str, session: PipSession, use_datetime: bool = False
+ ) -> None:
+ super().__init__(use_datetime)
+ index_parts = urllib.parse.urlparse(index_url)
+ self._scheme = index_parts.scheme
+ self._session = session
+
+ def request(
+ self,
+ host: "_HostType",
+ handler: str,
+ request_body: "SizedBuffer",
+ verbose: bool = False,
+ ) -> Tuple["_Marshallable", ...]:
+ assert isinstance(host, str)
+ parts = (self._scheme, host, handler, None, None, None)
+ url = urllib.parse.urlunparse(parts)
+ try:
+ headers = {"Content-Type": "text/xml"}
+ response = self._session.post(
+ url,
+ data=request_body,
+ headers=headers,
+ stream=True,
+ )
+ raise_for_status(response)
+ self.verbose = verbose
+ return self.parse_response(response.raw)
+ except NetworkConnectionError as exc:
+ assert exc.response
+ logger.critical(
+ "HTTP error %s while getting %s",
+ exc.response.status_code,
+ url,
+ )
+ raise