Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions src/didww_verification/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import warnings
from collections.abc import Callable
from types import TracebackType
from typing import Any, TypeVar
Expand Down Expand Up @@ -47,27 +48,37 @@ 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()
self._keep_raw = keep_raw_payload
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."""
Expand Down
15 changes: 13 additions & 2 deletions src/didww_verification/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -45,27 +46,37 @@ 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()
self._keep_raw = keep_raw_payload
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."""
Expand Down
83 changes: 83 additions & 0 deletions tests/test_user_agent.py
Original file line number Diff line number Diff line change
@@ -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
Loading