diff --git a/docs/configuration.md b/docs/configuration.md index f1fac414dbe..73450c204a4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -119,6 +119,15 @@ This also works for secret settings, like credentials: export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret ``` +### Add caller data to the User-Agent + +Set `POETRY_USER_AGENT_USER_DATA` to append caller-provided context to the +User-Agent on requests made by Poetry, including package downloads and uploads: + +```bash +export POETRY_USER_AGENT_USER_DATA=build/42 +``` + ## Configuration sources When a setting is set in multiple places, Poetry applies the following precedence diff --git a/src/poetry/publishing/uploader.py b/src/poetry/publishing/uploader.py index edc67e40080..b055baba6af 100644 --- a/src/poetry/publishing/uploader.py +++ b/src/poetry/publishing/uploader.py @@ -17,14 +17,13 @@ from packaging.metadata import parse_email from poetry.core.constraints.version import Version from poetry.core.masonry.utils.helpers import distribution_name -from requests_toolbelt import user_agent from requests_toolbelt.multipart import MultipartEncoder from requests_toolbelt.multipart import MultipartEncoderMonitor -from poetry.__version__ import __version__ from poetry.publishing.hash_manager import HashManager from poetry.utils.constants import REQUESTS_TIMEOUT from poetry.utils.patterns import wheel_file_re +from poetry.utils.user_agent import get_user_agent if TYPE_CHECKING: @@ -48,8 +47,7 @@ def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None @property def user_agent(self) -> str: - agent: str = user_agent("poetry", __version__) - return agent + return get_user_agent() @property def default_dist_dir(self) -> Path: diff --git a/src/poetry/utils/authenticator.py b/src/poetry/utils/authenticator.py index 4c23be1849c..741793f4c61 100644 --- a/src/poetry/utils/authenticator.py +++ b/src/poetry/utils/authenticator.py @@ -18,9 +18,7 @@ from cachecontrol import CacheControlAdapter from cachecontrol.caches import SeparateBodyFileCache -from requests_toolbelt import user_agent -from poetry.__version__ import __version__ from poetry.config.config import Config from poetry.console.exceptions import ConsoleMessage from poetry.console.exceptions import PoetryRuntimeError @@ -30,6 +28,7 @@ from poetry.utils.constants import STATUS_FORCELIST from poetry.utils.password_manager import HTTPAuthCredential from poetry.utils.password_manager import PasswordManager +from poetry.utils.user_agent import get_user_agent if TYPE_CHECKING: @@ -133,7 +132,7 @@ def __init__( self._get_repository_config_for_url ) self._pool_size = pool_size - self._user_agent = user_agent("poetry", __version__) + self._user_agent = get_user_agent() def create_session(self) -> requests.Session: session = requests.Session() diff --git a/src/poetry/utils/user_agent.py b/src/poetry/utils/user_agent.py new file mode 100644 index 00000000000..8b071311fdc --- /dev/null +++ b/src/poetry/utils/user_agent.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import os + +from requests_toolbelt import user_agent as requests_user_agent + +from poetry.__version__ import __version__ + + +def get_user_agent() -> str: + """Build Poetry's User-Agent, including optional caller context.""" + extras: list[tuple[str, str]] = [] + user_data = os.environ.get("POETRY_USER_AGENT_USER_DATA") + if user_data is not None: + extras.append(("user_data", user_data)) + + return str(requests_user_agent("poetry", __version__, extras=extras)) diff --git a/tests/publishing/test_uploader.py b/tests/publishing/test_uploader.py index eb694bdd035..930171493db 100644 --- a/tests/publishing/test_uploader.py +++ b/tests/publishing/test_uploader.py @@ -18,6 +18,7 @@ import responses + from pytest import MonkeyPatch from pytest_mock import MockerFixture from poetry.poetry import Poetry @@ -34,6 +35,14 @@ def uploader(poetry: Poetry) -> Uploader: return Uploader(poetry, NullIO()) +def test_user_agent_includes_user_data( + uploader: Uploader, monkeypatch: MonkeyPatch +) -> None: + monkeypatch.setenv("POETRY_USER_AGENT_USER_DATA", "build/42") + + assert "user_data/build/42" in uploader.user_agent + + @pytest.mark.parametrize( ("files", "expected_files", "expected_version"), [ diff --git a/tests/utils/test_authenticator.py b/tests/utils/test_authenticator.py index 3fe048b518f..5a309663dee 100644 --- a/tests/utils/test_authenticator.py +++ b/tests/utils/test_authenticator.py @@ -134,6 +134,14 @@ def test_authenticator_ignores_failing_keyring( assert spy_get_credential.call_count == spy_get_password.call_count == 0 +def test_authenticator_user_agent_includes_user_data(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("POETRY_USER_AGENT_USER_DATA", "build/42") + + authenticator = Authenticator(disable_cache=True) + + assert "user_data/build/42" in authenticator.create_session().headers["User-Agent"] + + def test_authenticator_uses_password_only_credentials( mock_config: Config, mock_remote: None, http: responses.RequestsMock ) -> None: