From e6ac83fad119f7f904eafa5ff562ee5b330d6a1e Mon Sep 17 00:00:00 2001 From: GruffElixir Date: Mon, 7 Sep 2026 01:40:49 +0530 Subject: [PATCH 1/2] feat: allow caller data in request user agents --- docs/configuration.md | 9 +++++++++ src/poetry/publishing/uploader.py | 6 ++---- src/poetry/utils/authenticator.py | 5 ++--- src/poetry/utils/user_agent.py | 17 +++++++++++++++++ tests/publishing/test_uploader.py | 9 +++++++++ tests/utils/test_authenticator.py | 8 ++++++++ 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 src/poetry/utils/user_agent.py 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..f8ec92b426b --- /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 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: From 4d59b927f85a88649557e669df805f90ff5b11d0 Mon Sep 17 00:00:00 2001 From: GruffElixir Date: Mon, 7 Sep 2026 02:40:14 +0530 Subject: [PATCH 2/2] Fix user agent return type --- src/poetry/utils/user_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poetry/utils/user_agent.py b/src/poetry/utils/user_agent.py index f8ec92b426b..8b071311fdc 100644 --- a/src/poetry/utils/user_agent.py +++ b/src/poetry/utils/user_agent.py @@ -14,4 +14,4 @@ def get_user_agent() -> str: if user_data is not None: extras.append(("user_data", user_data)) - return requests_user_agent("poetry", __version__, extras=extras) + return str(requests_user_agent("poetry", __version__, extras=extras))