diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index d9f0ced811..9ae6f489c2 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -361,6 +361,11 @@ def _retry_hook(retry_state: RetryCallState) -> None: rest_catalog._refresh_token() # pylint: disable=protected-access +def _user_agent() -> str: + """Return the User-Agent recommended by the REST catalog spec, for example `pyiceberg/0.11.0 (cpython 3.11.4)`.""" + return f"pyiceberg/{__version__} ({platform.python_implementation().lower()} {platform.python_version()})" + + _RETRY_ARGS = { "retry": retry_if_exception_type((AuthorizationExpiredError, UnauthorizedError)), "stop": stop_after_attempt(2), @@ -1232,9 +1237,7 @@ def _config_headers(self, session: Session) -> None: header_properties = get_header_properties(self.properties) session.headers.update(header_properties) session.headers["Content-type"] = "application/json" - session.headers["User-Agent"] = ( - f"pyiceberg/{__version__} ({platform.python_implementation().lower()} {platform.python_version()})" - ) + session.headers["User-Agent"] = _user_agent() session.headers["X-Client-Version"] = f"PyIceberg {__version__}" session.headers.setdefault("X-Iceberg-Access-Delegation", ACCESS_DELEGATION_DEFAULT) diff --git a/pyiceberg/io/fsspec.py b/pyiceberg/io/fsspec.py index 49f10b1310..e27dacdd2c 100644 --- a/pyiceberg/io/fsspec.py +++ b/pyiceberg/io/fsspec.py @@ -38,6 +38,7 @@ from typing_extensions import override from pyiceberg.catalog import TOKEN, URI +from pyiceberg.catalog.rest import _user_agent from pyiceberg.catalog.rest.auth import AUTH_MANAGER from pyiceberg.exceptions import SignError from pyiceberg.io import ( @@ -141,6 +142,7 @@ def __call__(self, request: "AWSRequest", operation_name: str | None = None, **_ signer_headers["Authorization"] = auth_header signer_headers.update(get_header_properties(self.properties)) + signer_headers["User-Agent"] = _user_agent() signer_body = { "method": request.method, diff --git a/tests/io/test_fsspec.py b/tests/io/test_fsspec.py index 66bb319d49..e7b2c82687 100644 --- a/tests/io/test_fsspec.py +++ b/tests/io/test_fsspec.py @@ -32,6 +32,7 @@ from fsspec.spec import AbstractFileSystem from requests_mock import Mocker +from pyiceberg.catalog.rest import _user_agent from pyiceberg.catalog.rest.auth import AUTH_MANAGER from pyiceberg.exceptions import SignError from pyiceberg.io import fsspec @@ -1028,6 +1029,7 @@ def test_s3v4_rest_signer(requests_mock: Mocker) -> None: "x-amz-content-sha256": "UNSIGNED-PAYLOAD", "X-Custom-Header": "value", } + assert requests_mock.last_request.headers["User-Agent"] == _user_agent() @pytest.mark.s3