From 6d49c6bd82f6591b394595c1bbf029e330331ac5 Mon Sep 17 00:00:00 2001 From: Danylo_Kriachkov Date: Wed, 10 Jun 2026 14:25:18 +0300 Subject: [PATCH 1/3] feat: add user resource and documentation for retrieving authenticated user info --- README.md | 18 +++++++ aidial_client/_client.py | 2 + aidial_client/resources/__init__.py | 3 ++ aidial_client/resources/user.py | 20 ++++++++ tests/resources/test_user.py | 79 +++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 aidial_client/resources/user.py create mode 100644 tests/resources/test_user.py diff --git a/README.md b/README.md index 98ffb61..767be76 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ - [Get Application by Id](#get-application-by-id) - [Models](#models) - [Get Model by Name](#get-model-by-name) + - [User](#user) + - [Get Authenticated User Info](#get-authenticated-user-info) - [Toolsets](#toolsets) - [Get Toolset by Id](#get-toolset-by-id) - [Resource Permissions](#resource-permissions) @@ -799,6 +801,22 @@ ModelInfo( ) ``` +### User + +#### Get Authenticated User Info + +To retrieve information about the currently authenticated user: + +```python +# Sync +user_info = client.user.info() + +# Async +user_info = await async_client.user.info() +``` + +The response shape may evolve depending on DIAL deployment settings, so this method returns a plain `dict[str, Any]`. + ### Toolsets #### Get Toolset by Id diff --git a/aidial_client/_client.py b/aidial_client/_client.py index 5c4907d..67b8ddf 100644 --- a/aidial_client/_client.py +++ b/aidial_client/_client.py @@ -122,6 +122,7 @@ def _init_resources(self) -> None: self.client_channel = resources.ClientChannel( http_client=self._http_client ) + self.user = resources.User(http_client=self._http_client) def _create_http_client(self) -> SyncHTTPClient: return SyncHTTPClient( @@ -213,6 +214,7 @@ def _init_resources(self) -> None: self.client_channel = resources.AsyncClientChannel( http_client=self._http_client ) + self.user = resources.AsyncUser(http_client=self._http_client) def _create_http_client(self) -> AsyncHTTPClient: return AsyncHTTPClient( diff --git a/aidial_client/resources/__init__.py b/aidial_client/resources/__init__.py index ed55b9a..170b700 100644 --- a/aidial_client/resources/__init__.py +++ b/aidial_client/resources/__init__.py @@ -10,6 +10,7 @@ ResourcePermissions, ) from aidial_client.resources.toolset import AsyncToolset, Toolset +from aidial_client.resources.user import AsyncUser, User from .application import Application, AsyncApplication from .bucket import AsyncBucket, Bucket @@ -40,4 +41,6 @@ "AsyncResourcePermissions", "ClientChannel", "AsyncClientChannel", + "User", + "AsyncUser", ] diff --git a/aidial_client/resources/user.py b/aidial_client/resources/user.py new file mode 100644 index 0000000..11e7b8d --- /dev/null +++ b/aidial_client/resources/user.py @@ -0,0 +1,20 @@ +from typing import Any, Dict + +from aidial_client._internal_types._http_request import FinalRequestOptions +from aidial_client.resources.base import AsyncResource, Resource + + +class User(Resource): + def info(self) -> Dict[str, Any]: + return self.http_client.request( + cast_to=dict, + options=FinalRequestOptions(method="GET", url="v1/user/info"), + ) + + +class AsyncUser(AsyncResource): + async def info(self) -> Dict[str, Any]: + return await self.http_client.request( + cast_to=dict, + options=FinalRequestOptions(method="GET", url="v1/user/info"), + ) diff --git a/tests/resources/test_user.py b/tests/resources/test_user.py new file mode 100644 index 0000000..848f320 --- /dev/null +++ b/tests/resources/test_user.py @@ -0,0 +1,79 @@ +import httpx +import pytest + +from aidial_client import AsyncDial, Dial +from aidial_client._exception import DialException +from tests.client_mock import get_async_client_mock, get_client_mock + +BASE_URL = "http://dial.core" + +USER_INFO_MOCK = { + "sub": "user-123", + "email": "user@example.com", + "name": "Test User", +} + + +def test_get_user_info(): + client = get_client_mock(status_code=200, json_mock=USER_INFO_MOCK) + result = client.user.info() + assert result == USER_INFO_MOCK + + +@pytest.mark.asyncio +async def test_async_get_user_info(): + client = get_async_client_mock(status_code=200, json_mock=USER_INFO_MOCK) + result = await client.user.info() + assert result == USER_INFO_MOCK + + +def test_get_user_info_request_method_and_url(): + captured: list[httpx.Request] = [] + client = Dial(api_key="dummy", base_url=BASE_URL) + + def send_mock(request: httpx.Request, **kwargs): + captured.append(request) + return httpx.Response(200, request=request, json=USER_INFO_MOCK) + + client._http_client._internal_http_client.send = send_mock + client.user.info() + + assert len(captured) == 1 + assert captured[0].method == "GET" + assert captured[0].url.path == "/v1/user/info" + + +@pytest.mark.asyncio +async def test_async_get_user_info_request_method_and_url(): + captured: list[httpx.Request] = [] + client = AsyncDial(api_key="dummy", base_url=BASE_URL) + + async def send_mock(request: httpx.Request, **kwargs): + captured.append(request) + return httpx.Response(200, request=request, json=USER_INFO_MOCK) + + client._http_client._internal_http_client.send = send_mock + await client.user.info() + + assert len(captured) == 1 + assert captured[0].method == "GET" + assert captured[0].url.path == "/v1/user/info" + + +def test_get_user_info_http_error(): + client = get_client_mock( + status_code=401, + json_mock={"error": {"message": "Unauthorized", "type": "auth_error"}}, + ) + with pytest.raises(DialException): + client.user.info() + + +@pytest.mark.asyncio +async def test_async_get_user_info_http_error(): + client = get_async_client_mock( + status_code=401, + json_mock={"error": {"message": "Unauthorized", "type": "auth_error"}}, + ) + with pytest.raises(DialException): + await client.user.info() From ead0bfd6c5469ea97ab116553b4d5c33a15774ac Mon Sep 17 00:00:00 2001 From: Danylo_Kriachkov Date: Mon, 22 Jun 2026 16:03:53 +0300 Subject: [PATCH 2/3] refactor: update return type annotations in User and AsyncUser info methods to use dict syntax --- aidial_client/resources/user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aidial_client/resources/user.py b/aidial_client/resources/user.py index 11e7b8d..8d047cc 100644 --- a/aidial_client/resources/user.py +++ b/aidial_client/resources/user.py @@ -1,11 +1,11 @@ -from typing import Any, Dict +from typing import Any from aidial_client._internal_types._http_request import FinalRequestOptions from aidial_client.resources.base import AsyncResource, Resource class User(Resource): - def info(self) -> Dict[str, Any]: + def info(self) -> dict[str, Any]: return self.http_client.request( cast_to=dict, options=FinalRequestOptions(method="GET", url="v1/user/info"), @@ -13,7 +13,7 @@ def info(self) -> Dict[str, Any]: class AsyncUser(AsyncResource): - async def info(self) -> Dict[str, Any]: + async def info(self) -> dict[str, Any]: return await self.http_client.request( cast_to=dict, options=FinalRequestOptions(method="GET", url="v1/user/info"), From 9dc2bd3ef0b72dcb936838f28d354981cdd7b8a9 Mon Sep 17 00:00:00 2001 From: Danylo_Kriachkov Date: Tue, 23 Jun 2026 13:51:16 +0300 Subject: [PATCH 3/3] feat: implement UserInfo type and update user info methods to return UserInfo objects - Added UserInfo class to represent authenticated user information. - Updated info methods in User and AsyncUser classes to return UserInfo instead of dict. - Enhanced README with examples of UserInfo object structure for API key and access token authentication. - Updated tests to validate UserInfo structure and claims handling. --- README.md | 28 ++++++++++++++++++++++++- aidial_client/__init__.py | 2 ++ aidial_client/resources/user.py | 11 +++++----- aidial_client/types/user.py | 11 ++++++++++ tests/resources/test_user.py | 36 ++++++++++++++++++++++++++++----- 5 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 aidial_client/types/user.py diff --git a/README.md b/README.md index 1f968fe..fc7c834 100644 --- a/README.md +++ b/README.md @@ -862,7 +862,33 @@ user_info = client.user.info() user_info = await async_client.user.info() ``` -The response shape may evolve depending on DIAL deployment settings, so this method returns a plain `dict[str, Any]`. +As a result, you will receive a `UserInfo` object. When authenticated with an +API key: + +```python +UserInfo( + roles=["default"], + project="PROJECT-NAME", + userClaims=None, +) +``` + +When authenticated with an access token: + +```python +UserInfo( + roles=["BA"], + project=None, + userClaims={ + "email": ["user_email"], + "sub": ["user_sub"], + }, +) +``` + +`userClaims` is returned as an opaque `dict` because its contents depend on the +identity provider. `UserInfo` also preserves any additional fields the DIAL +deployment may return, so forward compatibility is retained. ### Toolsets diff --git a/aidial_client/__init__.py b/aidial_client/__init__.py index edf195f..43f5f0b 100644 --- a/aidial_client/__init__.py +++ b/aidial_client/__init__.py @@ -12,6 +12,7 @@ from aidial_client.types.client_channel import SigninResult from aidial_client.types.model import ModelInfo, ModelLimits, ModelPricing from aidial_client.types.toolset import ToolsetInfo +from aidial_client.types.user import UserInfo __all__ = [ "Dial", @@ -32,4 +33,5 @@ "ModelPricing", "ModelLimits", "SigninResult", + "UserInfo", ] diff --git a/aidial_client/resources/user.py b/aidial_client/resources/user.py index 8d047cc..47c20b9 100644 --- a/aidial_client/resources/user.py +++ b/aidial_client/resources/user.py @@ -1,20 +1,19 @@ -from typing import Any - from aidial_client._internal_types._http_request import FinalRequestOptions from aidial_client.resources.base import AsyncResource, Resource +from aidial_client.types.user import UserInfo class User(Resource): - def info(self) -> dict[str, Any]: + def info(self) -> UserInfo: return self.http_client.request( - cast_to=dict, + cast_to=UserInfo, options=FinalRequestOptions(method="GET", url="v1/user/info"), ) class AsyncUser(AsyncResource): - async def info(self) -> dict[str, Any]: + async def info(self) -> UserInfo: return await self.http_client.request( - cast_to=dict, + cast_to=UserInfo, options=FinalRequestOptions(method="GET", url="v1/user/info"), ) diff --git a/aidial_client/types/user.py b/aidial_client/types/user.py new file mode 100644 index 0000000..089fc95 --- /dev/null +++ b/aidial_client/types/user.py @@ -0,0 +1,11 @@ +from typing import Any + +from aidial_client._internal_types._model import ExtraAllowModel + + +class UserInfo(ExtraAllowModel): + """Information about the authenticated user or API key.""" + + roles: list[str] + project: str | None = None + userClaims: dict[str, Any] | None = None # depends on the IdP, so opaque diff --git a/tests/resources/test_user.py b/tests/resources/test_user.py index 848f320..3a5520f 100644 --- a/tests/resources/test_user.py +++ b/tests/resources/test_user.py @@ -3,28 +3,54 @@ from aidial_client import AsyncDial, Dial from aidial_client._exception import DialException +from aidial_client.types.user import UserInfo from tests.client_mock import get_async_client_mock, get_client_mock BASE_URL = "http://dial.core" USER_INFO_MOCK = { - "sub": "user-123", - "email": "user@example.com", - "name": "Test User", + "project": "PROJECT-NAME", + "roles": ["default"], +} + +USER_INFO_TOKEN_MOCK = { + "roles": ["BA"], + "userClaims": { + "email": ["user@example.com"], + "sub": ["user-123"], + }, } def test_get_user_info(): client = get_client_mock(status_code=200, json_mock=USER_INFO_MOCK) result = client.user.info() - assert result == USER_INFO_MOCK + assert isinstance(result, UserInfo) + assert result.project == "PROJECT-NAME" + assert result.roles == ["default"] + assert result.userClaims is None @pytest.mark.asyncio async def test_async_get_user_info(): client = get_async_client_mock(status_code=200, json_mock=USER_INFO_MOCK) result = await client.user.info() - assert result == USER_INFO_MOCK + assert isinstance(result, UserInfo) + assert result.project == "PROJECT-NAME" + assert result.roles == ["default"] + assert result.userClaims is None + + +def test_get_user_info_with_token_claims(): + client = get_client_mock(status_code=200, json_mock=USER_INFO_TOKEN_MOCK) + result = client.user.info() + assert isinstance(result, UserInfo) + assert result.project is None + assert result.roles == ["BA"] + assert result.userClaims == { + "email": ["user@example.com"], + "sub": ["user-123"], + } def test_get_user_info_request_method_and_url():