diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5841a..eb6fdea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Notable changes to the DIDWW Verification SDK for Python. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Deprecated + +- **`user_agent`** on `VerificationClient` and `AsyncVerificationClient` is deprecated and + ignored. The SDK always sends its own `User-Agent`; passing this parameter now emits a + `DeprecationWarning` and will be removed in the next major version. + ## [1.1.0] — 2026-10 ### Added diff --git a/src/didww_verification/async_client.py b/src/didww_verification/async_client.py index e5dde95..a906f53 100644 --- a/src/didww_verification/async_client.py +++ b/src/didww_verification/async_client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import warnings from collections.abc import Callable from types import TracebackType from typing import Any, TypeVar @@ -47,19 +48,29 @@ def __init__( base_url: str | None = None, timeout: float = 30.0, retry: RetryPolicy | None = None, - user_agent: str = DEFAULT_USER_AGENT, + user_agent: str | None = None, keep_raw_payload: bool = False, http_client: httpx2.AsyncClient | None = None, ) -> None: """ :param base_url: an origin, overriding ``environment``. Must carry no path. :param retry: read-retry policy. Writes are never retried. + :param user_agent: deprecated and ignored. The SDK always sends + ``DEFAULT_USER_AGENT``; this parameter will be removed in the next major + version. :param keep_raw_payload: retain the decoded envelope on each Verification. Off by default; it keeps the destination in memory and is not covered by semantic versioning. :param http_client: bring your own client for proxies, TLS or connection limits. It is *not* closed by this client, since you own its lifetime. """ + if user_agent is not None: + warnings.warn( + "user_agent is ignored; the SDK always sends its own User-Agent, and " + "this parameter will be removed in the next major version.", + DeprecationWarning, + stacklevel=2, + ) self._auth = auth self._base_url = validate_base_url(base_url or environment.value) self._retry = retry or RetryPolicy() @@ -67,7 +78,7 @@ def __init__( self._owns_client = http_client is None self._http = http_client or httpx2.AsyncClient(timeout=timeout) self._http.base_url = httpx2.URL(self._base_url) - self._http.headers["user-agent"] = user_agent + self._http.headers["user-agent"] = DEFAULT_USER_AGENT async def aclose(self) -> None: """Close the underlying connection pool, unless you supplied the client.""" diff --git a/src/didww_verification/client.py b/src/didww_verification/client.py index 115f982..d2d1857 100644 --- a/src/didww_verification/client.py +++ b/src/didww_verification/client.py @@ -3,6 +3,7 @@ from __future__ import annotations import time +import warnings from collections.abc import Callable from types import TracebackType from typing import Any, TypeVar @@ -45,19 +46,29 @@ def __init__( base_url: str | None = None, timeout: float = 30.0, retry: RetryPolicy | None = None, - user_agent: str = DEFAULT_USER_AGENT, + user_agent: str | None = None, keep_raw_payload: bool = False, http_client: httpx2.Client | None = None, ) -> None: """ :param base_url: an origin, overriding ``environment``. Must carry no path. :param retry: read-retry policy. Writes are never retried. + :param user_agent: deprecated and ignored. The SDK always sends + ``DEFAULT_USER_AGENT``; this parameter will be removed in the next major + version. :param keep_raw_payload: retain the decoded envelope on each Verification. Off by default; it keeps the destination in memory and is not covered by semantic versioning. :param http_client: bring your own client for proxies, TLS or connection limits. It is *not* closed by this client, since you own its lifetime. """ + if user_agent is not None: + warnings.warn( + "user_agent is ignored; the SDK always sends its own User-Agent, and " + "this parameter will be removed in the next major version.", + DeprecationWarning, + stacklevel=2, + ) self._auth = auth self._base_url = validate_base_url(base_url or environment.value) self._retry = retry or RetryPolicy() @@ -65,7 +76,7 @@ def __init__( self._owns_client = http_client is None self._http = http_client or httpx2.Client(timeout=timeout) self._http.base_url = httpx2.URL(self._base_url) - self._http.headers["user-agent"] = user_agent + self._http.headers["user-agent"] = DEFAULT_USER_AGENT def close(self) -> None: """Close the underlying connection pool, unless you supplied the client.""" diff --git a/tests/test_user_agent.py b/tests/test_user_agent.py new file mode 100644 index 0000000..cf77c92 --- /dev/null +++ b/tests/test_user_agent.py @@ -0,0 +1,83 @@ +"""The User-Agent header is fixed, and the deprecated parameter cannot change it. + +``user_agent`` shipped as a constructor parameter before the SDK settled on a single +identifying string; it is kept only so old callers do not break on upgrade. +""" + +from __future__ import annotations + +import anyio +import httpx2 +import pytest + +from didww_verification import ApplicationAuth, AsyncVerificationClient, VerificationClient +from didww_verification.config import DEFAULT_USER_AGENT +from tests.conftest import SECRET, recording_transport + + +def test_sync_client_sends_the_default_user_agent() -> None: + transport, seen = recording_transport() + VerificationClient( + ApplicationAuth("k", SECRET), + base_url="https://v.test", + http_client=httpx2.Client(transport=transport), + ).get_verification("abc") + assert seen[0].headers["user-agent"] == DEFAULT_USER_AGENT + + +def test_async_client_sends_the_default_user_agent() -> None: + transport, seen = recording_transport() + + async def run() -> None: + async with AsyncVerificationClient( + ApplicationAuth("k", SECRET), + base_url="https://v.test", + http_client=httpx2.AsyncClient(transport=transport), + ) as client: + await client.get_verification("abc") + + anyio.run(run) + assert seen[0].headers["user-agent"] == DEFAULT_USER_AGENT + + +def test_sync_client_warns_and_still_sends_the_default_when_user_agent_is_passed() -> None: + transport, seen = recording_transport() + with pytest.deprecated_call(): + client = VerificationClient( + ApplicationAuth("k", SECRET), + base_url="https://v.test", + http_client=httpx2.Client(transport=transport), + user_agent="some/custom-agent", + ) + client.get_verification("abc") + assert seen[0].headers["user-agent"] == DEFAULT_USER_AGENT + + +def test_async_client_warns_and_still_sends_the_default_when_user_agent_is_passed() -> None: + transport, seen = recording_transport() + + async def run() -> None: + with pytest.deprecated_call(): + client = AsyncVerificationClient( + ApplicationAuth("k", SECRET), + base_url="https://v.test", + http_client=httpx2.AsyncClient(transport=transport), + user_agent="some/custom-agent", + ) + async with client: + await client.get_verification("abc") + + anyio.run(run) + assert seen[0].headers["user-agent"] == DEFAULT_USER_AGENT + + +def test_a_supplied_http_client_with_its_own_default_is_overridden() -> None: + """The negative control: a caller-supplied client's own default must not survive.""" + transport, seen = recording_transport() + http = httpx2.Client(transport=transport, headers={"user-agent": "caller/1.0"}) + VerificationClient( + ApplicationAuth("k", SECRET), + base_url="https://v.test", + http_client=http, + ).get_verification("abc") + assert seen[0].headers["user-agent"] == DEFAULT_USER_AGENT