Skip to content

Commit 994f3b0

Browse files
committed
feat(tokens): support optional metadata on client token creation
Add keyword-only metadata parameter to tokens.create(). Always sends application/json, includes metadata key only when provided. Includes 2 new tests verifying metadata forwarding and empty-body behavior.
1 parent 96878ab commit 994f3b0

File tree

2 files changed

+72
-6
lines changed

2 files changed

+72
-6
lines changed

decart/tokens/client.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING
1+
from typing import TYPE_CHECKING, Any
22

33
import aiohttp
44

@@ -20,6 +20,9 @@ class TokensClient:
2020
client = DecartClient(api_key=os.getenv("DECART_API_KEY"))
2121
token = await client.tokens.create()
2222
# Returns: CreateTokenResponse(api_key="ek_...", expires_at="...")
23+
24+
# With metadata:
25+
token = await client.tokens.create(metadata={"role": "viewer"})
2326
```
2427
"""
2528

@@ -29,17 +32,27 @@ def __init__(self, parent: "DecartClient") -> None:
2932
async def _get_session(self) -> aiohttp.ClientSession:
3033
return await self._parent._get_session()
3134

32-
async def create(self) -> CreateTokenResponse:
35+
async def create(
36+
self,
37+
*,
38+
metadata: dict[str, Any] | None = None,
39+
) -> CreateTokenResponse:
3340
"""
3441
Create a client token.
3542
43+
Args:
44+
metadata: Optional custom key-value pairs to attach to the token.
45+
3646
Returns:
3747
A short-lived API key safe for client-side use.
3848
3949
Example:
4050
```python
4151
token = await client.tokens.create()
4252
# Returns: CreateTokenResponse(api_key="ek_...", expires_at="...")
53+
54+
# With metadata:
55+
token = await client.tokens.create(metadata={"role": "viewer"})
4356
```
4457
4558
Raises:
@@ -48,12 +61,17 @@ async def create(self) -> CreateTokenResponse:
4861
session = await self._get_session()
4962
endpoint = f"{self._parent.base_url}/v1/client/tokens"
5063

64+
headers = {
65+
"X-API-KEY": self._parent.api_key,
66+
"User-Agent": build_user_agent(self._parent.integration),
67+
}
68+
69+
body = {"metadata": metadata} if metadata is not None else {}
70+
5171
async with session.post(
5272
endpoint,
53-
headers={
54-
"X-API-KEY": self._parent.api_key,
55-
"User-Agent": build_user_agent(self._parent.integration),
56-
},
73+
headers=headers,
74+
json=body,
5775
) as response:
5876
if not response.ok:
5977
error_text = await response.text()

tests/test_tokens.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,51 @@ async def test_create_token_403_error() -> None:
6666
with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)):
6767
with pytest.raises(TokenCreateError, match="Failed to create token"):
6868
await client.tokens.create()
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_create_token_with_metadata() -> None:
73+
"""Sends metadata as JSON body when provided."""
74+
client = DecartClient(api_key="test-api-key")
75+
76+
mock_response = AsyncMock()
77+
mock_response.ok = True
78+
mock_response.json = AsyncMock(
79+
return_value={"apiKey": "ek_test123", "expiresAt": "2024-12-15T12:10:00Z"}
80+
)
81+
82+
mock_session = MagicMock()
83+
mock_session.post = MagicMock(
84+
return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_response))
85+
)
86+
87+
with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)):
88+
result = await client.tokens.create(metadata={"role": "viewer"})
89+
90+
assert result.api_key == "ek_test123"
91+
assert result.expires_at == "2024-12-15T12:10:00Z"
92+
call_kwargs = mock_session.post.call_args
93+
assert call_kwargs.kwargs["json"] == {"metadata": {"role": "viewer"}}
94+
95+
96+
@pytest.mark.asyncio
97+
async def test_create_token_without_metadata_sends_null() -> None:
98+
"""Sends JSON body with null metadata when none provided."""
99+
client = DecartClient(api_key="test-api-key")
100+
101+
mock_response = AsyncMock()
102+
mock_response.ok = True
103+
mock_response.json = AsyncMock(
104+
return_value={"apiKey": "ek_test123", "expiresAt": "2024-12-15T12:10:00Z"}
105+
)
106+
107+
mock_session = MagicMock()
108+
mock_session.post = MagicMock(
109+
return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_response))
110+
)
111+
112+
with patch.object(client, "_get_session", AsyncMock(return_value=mock_session)):
113+
await client.tokens.create()
114+
115+
call_kwargs = mock_session.post.call_args
116+
assert call_kwargs.kwargs["json"] == {}

0 commit comments

Comments
 (0)