diff --git a/README.md b/README.md index 6e2cb16..bb1fde7 100644 --- a/README.md +++ b/README.md @@ -623,14 +623,32 @@ Both methods return `None` on success. `source` and `destination` must point to #### Accessing Metadata -Use `metadata()` to access metadata of a file: +Use `get_metadata()` to access metadata of a file or folder: ```python -metadata = await async_client.files.metadata( +# Sync client +metadata = sync_client.files.get_metadata( + url=sync_client.my_files_home() / "relative_folder/my-file.txt" +) + +# Async client +metadata = await async_client.files.get_metadata( url=await async_client.my_files_home() / "relative_folder/my-file.txt" ) ``` +Folder metadata can be paginated with `limit` and `token`: + +```python +metadata = await async_client.files.get_metadata( + url=await async_client.my_files_home() / "relative_folder/", + limit=100, + token=next_token, +) +next_token = metadata.next_token +items = metadata.items +``` + Example of metadata: ```python @@ -643,6 +661,7 @@ FileMetadata( resource_type="FILE", content_length=12, content_type="application/octet-stream", + next_token=None, items=None, updatedAt=1724836248936, etag="9749fad13d6e7092a6337c4af9d83764", diff --git a/aidial_client/resources/files.py b/aidial_client/resources/files.py index 5ea97cc..fb01e71 100644 --- a/aidial_client/resources/files.py +++ b/aidial_client/resources/files.py @@ -138,10 +138,18 @@ def copy_to( on_http_error=_files_error_processor, ) - def get_metadata(self, url: str | PurePosixPath) -> FileMetadata: + def get_metadata( + self, + url: str | PurePosixPath, + *, + limit: int | None = None, + token: str | None = None, + ) -> FileMetadata: return self.metadata.get( resource="files", relative_url=self.get_api_path(str(url)), + limit=limit, + token=token, ) @@ -257,8 +265,16 @@ async def copy_to( on_http_error=_files_error_processor, ) - async def get_metadata(self, url: str | PurePosixPath) -> FileMetadata: + async def get_metadata( + self, + url: str | PurePosixPath, + *, + limit: int | None = None, + token: str | None = None, + ) -> FileMetadata: return await self.metadata.get( resource="files", relative_url=self.get_api_path(str(url)), + limit=limit, + token=token, ) diff --git a/aidial_client/resources/metadata.py b/aidial_client/resources/metadata.py index e13d5c2..acf460a 100644 --- a/aidial_client/resources/metadata.py +++ b/aidial_client/resources/metadata.py @@ -5,6 +5,7 @@ from aidial_client._constants import METADATA_PREFIX from aidial_client._internal_types._http_request import FinalRequestOptions +from aidial_client._utils._dict import remove_none from aidial_client.helpers.storage_resource import StorageResourceType from aidial_client.resources.base import AsyncResource, Resource from aidial_client.types.metadata import ( @@ -30,29 +31,48 @@ def _get_cast_to( class Metadata(Resource): @overload def get( - self, resource: Literal["files"], relative_url: str + self, + resource: Literal["files"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> FileMetadata: ... @overload def get( - self, resource: Literal["conversations"], relative_url: str + self, + resource: Literal["conversations"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> ConversationMetadata: ... @overload def get( - self, resource: Literal["prompts"], relative_url: str + self, + resource: Literal["prompts"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> PromptMetadata: ... def get( self, resource: StorageResourceType, relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> FileMetadata | ConversationMetadata | PromptMetadata: return self.http_client.request( cast_to=_get_cast_to(resource), options=FinalRequestOptions( method="GET", url=urljoin(METADATA_PREFIX, relative_url), + params=remove_none({"limit": limit, "token": token}), ), ) @@ -60,28 +80,47 @@ def get( class AsyncMetadata(AsyncResource): @overload async def get( - self, resource: Literal["files"], relative_url: str + self, + resource: Literal["files"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> FileMetadata: ... @overload async def get( - self, resource: Literal["conversations"], relative_url: str + self, + resource: Literal["conversations"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> ConversationMetadata: ... @overload async def get( - self, resource: Literal["prompts"], relative_url: str + self, + resource: Literal["prompts"], + relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> PromptMetadata: ... async def get( self, resource: StorageResourceType, relative_url: str, + *, + limit: int | None = None, + token: str | None = None, ) -> FileMetadata | ConversationMetadata | PromptMetadata: return await self.http_client.request( cast_to=_get_cast_to(resource), options=FinalRequestOptions( method="GET", url=urljoin(METADATA_PREFIX, relative_url), + params=remove_none({"limit": limit, "token": token}), ), ) diff --git a/aidial_client/types/metadata.py b/aidial_client/types/metadata.py index e5a62be..1591a6a 100644 --- a/aidial_client/types/metadata.py +++ b/aidial_client/types/metadata.py @@ -37,6 +37,7 @@ class FileMetadata(BaseMetadata): resource_type: Literal["FILE"] content_length: int | None = None content_type: str | None = None + next_token: str | None = None items: list[FileItem] | None = None etag: str | None = None diff --git a/tests/resources/files/test_metadata.py b/tests/resources/files/test_metadata.py index ff0c49f..8c41751 100644 --- a/tests/resources/files/test_metadata.py +++ b/tests/resources/files/test_metadata.py @@ -1,7 +1,11 @@ +from typing import Any from unittest.mock import AsyncMock, Mock +import httpx import pytest +from aidial_client import Dial +from aidial_client._client import AsyncDial from aidial_client.types.metadata import FileMetadata from tests.client_mock import get_async_client_mock, get_client_mock @@ -24,9 +28,44 @@ "contentType": "image/png", } ], + "nextToken": "next-page-token", } +def _make_capturing_client(captured: list[httpx.Request]) -> Dial: + client = Dial(api_key="dummy", base_url="http://dial.core") + + def send_mock(request: httpx.Request, **_: Any) -> httpx.Response: + captured.append(request) + response = httpx.Response( + status_code=200, request=request, json=METADATA_RESPONSE_MOCK + ) + response.request = request + return response + + client._http_client._internal_http_client.send = send_mock + client._get_my_bucket = Mock(return_value="test-bucket") + return client + + +def _make_async_capturing_client( + captured: list[httpx.Request], +) -> AsyncDial: + client = AsyncDial(api_key="dummy", base_url="http://dial.core") + + async def send_mock(request: httpx.Request, **_: Any) -> httpx.Response: + captured.append(request) + response = httpx.Response( + status_code=200, request=request, json=METADATA_RESPONSE_MOCK + ) + response.request = request + return response + + client._http_client._internal_http_client.send = send_mock + client._get_my_bucket = AsyncMock(return_value="test-bucket") + return client + + def test_get_metadata(): client = get_client_mock(status_code=200, json_mock=METADATA_RESPONSE_MOCK) client._get_my_bucket = Mock(return_value="test-bucket") @@ -44,6 +83,26 @@ def test_get_metadata(): assert r.bucket == "test-bucket" assert r.items and len(r.items) == 1 assert r.items[0].node_type == "ITEM" + assert r.next_token == "next-page-token" # noqa: S105 + + +def test_get_metadata_sends_pagination_params(): + captured: list[httpx.Request] = [] + client = _make_capturing_client(captured) + + result = client.files.get_metadata( + url=client.my_files_home() / "folder1/folder2/", + limit=100, + token="page-token", # noqa: S106 + ) + + assert isinstance(result, FileMetadata) + assert len(captured) == 1 + request = captured[0] + assert request.method == "GET" + assert request.url.path == "/v1/metadata/files/test-bucket/folder1/folder2" + assert request.url.params["limit"] == "100" + assert request.url.params["token"] == "page-token" # noqa: S105 @pytest.mark.asyncio @@ -66,3 +125,24 @@ async def test_get_metadata_async(): assert r.bucket == "test-bucket" assert r.items and len(r.items) == 1 assert r.items[0].node_type == "ITEM" + assert r.next_token == "next-page-token" # noqa: S105 + + +@pytest.mark.asyncio +async def test_get_metadata_async_sends_pagination_params(): + captured: list[httpx.Request] = [] + client = _make_async_capturing_client(captured) + + result = await client.files.get_metadata( + url=await client.my_files_home() / "folder1/folder2/", + limit=100, + token="page-token", # noqa: S106 + ) + + assert isinstance(result, FileMetadata) + assert len(captured) == 1 + request = captured[0] + assert request.method == "GET" + assert request.url.path == "/v1/metadata/files/test-bucket/folder1/folder2" + assert request.url.params["limit"] == "100" + assert request.url.params["token"] == "page-token" # noqa: S105