Skip to content

Commit 15bf57b

Browse files
Abel Milashclaude
andcommitted
Wire operation_context into async client User-Agent header
Mirrors sync _ODataClient: reads config.operation_context in __init__ and appends user_agent_context as a parenthesized comment to the User-Agent header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ad9297e commit 15bf57b

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

src/PowerPlatform/Dataverse/aio/data/_async_odata.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ def __init__(
7979
session=session,
8080
logger=self._http_logger,
8181
)
82+
ctx_obj = self.config.operation_context
83+
self._operation_context = ctx_obj.user_agent_context if ctx_obj else None
8284
# Prevents concurrent coroutines from racing through the picklist TTL check
8385
# and issuing redundant metadata fetches.
8486
self._picklist_cache_lock: asyncio.Lock = asyncio.Lock()
@@ -97,13 +99,14 @@ async def _headers(self) -> Dict[str, str]:
9799
"""Build standard OData headers with bearer auth."""
98100
scope = f"{self.base_url}/.default"
99101
token = (await self.auth._acquire_token(scope)).access_token
102+
ua = f"{_USER_AGENT} ({self._operation_context})" if self._operation_context else _USER_AGENT
100103
return {
101104
"Authorization": f"Bearer {token}",
102105
"Accept": "application/json",
103106
"Content-Type": "application/json",
104107
"OData-MaxVersion": "4.0",
105108
"OData-Version": "4.0",
106-
"User-Agent": _USER_AGENT,
109+
"User-Agent": ua,
107110
}
108111

109112
async def _raw_request(self, method: str, url: str, **kwargs) -> aiohttp.ClientResponse:

tests/unit/aio/data/test_async_odata_internal.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,3 +1793,33 @@ async def test_build_list_with_count(self):
17931793
_seed_cache(client)
17941794
req = await client._build_list("account", count=True)
17951795
assert "$count=true" in req.url
1796+
1797+
1798+
class TestAsyncOperationContextUserAgent:
1799+
"""User-Agent header reflects operation_context on the async client."""
1800+
1801+
async def test_default_user_agent_unchanged(self):
1802+
from PowerPlatform.Dataverse.aio.data._async_odata import _USER_AGENT
1803+
client = _make_client()
1804+
headers = await client._headers()
1805+
assert headers["User-Agent"] == _USER_AGENT
1806+
1807+
async def test_operation_context_appended(self):
1808+
from PowerPlatform.Dataverse.aio.data._async_odata import _USER_AGENT
1809+
from PowerPlatform.Dataverse.core.config import DataverseConfig, OperationContext
1810+
ctx_str = "app=dataverse-skills/1.2.1;agent=claude-code"
1811+
config = DataverseConfig(operation_context=OperationContext(user_agent_context=ctx_str))
1812+
auth = MagicMock()
1813+
auth._acquire_token = AsyncMock(return_value=MagicMock(access_token="test-token"))
1814+
client = _AsyncODataClient(auth, "https://example.crm.dynamics.com", config=config)
1815+
headers = await client._headers()
1816+
assert headers["User-Agent"] == f"{_USER_AGENT} ({ctx_str})"
1817+
1818+
async def test_none_context_no_parentheses(self):
1819+
from PowerPlatform.Dataverse.core.config import DataverseConfig
1820+
config = DataverseConfig(operation_context=None)
1821+
auth = MagicMock()
1822+
auth._acquire_token = AsyncMock(return_value=MagicMock(access_token="test-token"))
1823+
client = _AsyncODataClient(auth, "https://example.crm.dynamics.com", config=config)
1824+
headers = await client._headers()
1825+
assert "(" not in headers["User-Agent"]

0 commit comments

Comments
 (0)