From e9de9f2cba427fde14fef5de50d994cd9f5c2252 Mon Sep 17 00:00:00 2001 From: Danylo_Kriachkov Date: Wed, 8 Jul 2026 15:42:38 +0300 Subject: [PATCH] feat: return FileItem from files.upload() #118 The upload response is item-shaped, not a folder listing, so upload() now returns FileItem instead of FileMetadata. Introduce a shared ResourceItemMetadata base (created_at, updated_at, etag, author) that mirrors ai-dial-core's hierarchy: FileItem extends it and adds content_length/content_type, while FileMetadata stays a separate folder-listing branch off BaseMetadata. --- README.md | 26 +++++++++++++++++++++++--- aidial_client/resources/files.py | 10 +++++----- aidial_client/types/metadata.py | 3 +++ tests/integration/test_async_files.py | 4 ++-- tests/integration/test_sync_files.py | 4 ++-- tests/resources/files/test_upload.py | 24 +++++++++++++++++++++--- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f13d61d..465966e 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,25 @@ sync_client.files.upload( ) ``` +`upload()` returns a `FileItem` describing the stored file: + +```python +FileItem( + name="my-file.txt", + parent_path="some-relative-path", + bucket="my-bucket", + url="files/my-bucket/some-relative-path/my-file.txt", + node_type="ITEM", + resource_type="FILE", + content_length=12, + content_type="text/plain", + etag="9749fad13d6e7092a6337c4af9d83764", + created_at=1724836229736, + updated_at=1724836248936, + author="user@example.com", +) +``` + #### Downloading Files Use `download()` to download files from your storage bucket: @@ -664,11 +683,12 @@ FileMetadata( resource_type="FILE", content_length=12, content_type="application/octet-stream", + etag="9749fad13d6e7092a6337c4af9d83764", + created_at=1724836229736, + updated_at=1724836248936, + author="user@example.com", next_token=None, items=None, - updatedAt=1724836248936, - etag="9749fad13d6e7092a6337c4af9d83764", - createdAt=1724836229736, ) ``` diff --git a/aidial_client/resources/files.py b/aidial_client/resources/files.py index fb01e71..ce3de02 100644 --- a/aidial_client/resources/files.py +++ b/aidial_client/resources/files.py @@ -22,7 +22,7 @@ from aidial_client.resources.base import AsyncResource, Resource from aidial_client.resources.metadata import AsyncMetadata, Metadata from aidial_client.types.file import FileDownloadResponse -from aidial_client.types.metadata import FileMetadata +from aidial_client.types.metadata import FileItem, FileMetadata def _files_error_processor( @@ -49,9 +49,9 @@ def upload( file: FileTypes, etag_if_match: str | None = None, etag_if_none_match: Literal["*"] | None = None, - ) -> FileMetadata: + ) -> FileItem: return self.http_client.request( - cast_to=FileMetadata, + cast_to=FileItem, options=FinalRequestOptions( method="PUT", url=urljoin(API_PREFIX, self.get_api_path(str(url))), @@ -163,9 +163,9 @@ async def upload( file: FileTypes, etag_if_match: str | None = None, etag_if_none_match: Literal["*"] | None = None, - ) -> FileMetadata: + ) -> FileItem: return await self.http_client.request( - cast_to=FileMetadata, + cast_to=FileItem, options=FinalRequestOptions( method="PUT", url=urljoin(API_PREFIX, self.get_api_path(str(url))), diff --git a/aidial_client/types/metadata.py b/aidial_client/types/metadata.py index f78e04c..ff033a7 100644 --- a/aidial_client/types/metadata.py +++ b/aidial_client/types/metadata.py @@ -30,7 +30,10 @@ class FileItem(BaseMetadata): resource_type: Literal["FILE"] content_length: int | None = None content_type: str | None = None + created_at: int | None = None updated_at: int | None = None + etag: str | None = None + author: str | None = None class FileMetadata(BaseMetadata): diff --git a/tests/integration/test_async_files.py b/tests/integration/test_async_files.py index c5e62d3..1b5f3b1 100644 --- a/tests/integration/test_async_files.py +++ b/tests/integration/test_async_files.py @@ -4,7 +4,7 @@ from aidial_client import AsyncDial, DialException from aidial_client._exception import EtagMismatchError -from aidial_client.types.metadata import FileMetadata +from aidial_client.types.metadata import FileItem from tests.integration.fixtures import * # type: ignore # noqa current_file_path = os.path.abspath(__file__) @@ -20,7 +20,7 @@ async def test_upload(async_client: AsyncDial): upload_result = await async_client.files.upload( url=await async_client.my_files_home() / file_path, file=file ) - assert isinstance(upload_result, FileMetadata) + assert isinstance(upload_result, FileItem) assert upload_result.bucket == await async_client.my_bucket() assert upload_result.node_type == "ITEM" assert upload_result.name == file_name diff --git a/tests/integration/test_sync_files.py b/tests/integration/test_sync_files.py index 050f7db..03b0177 100644 --- a/tests/integration/test_sync_files.py +++ b/tests/integration/test_sync_files.py @@ -4,7 +4,7 @@ from aidial_client import Dial, DialException from aidial_client._exception import EtagMismatchError -from aidial_client.types.metadata import FileMetadata +from aidial_client.types.metadata import FileItem from tests.integration.fixtures import * # type: ignore # noqa current_file_path = os.path.abspath(__file__) @@ -21,7 +21,7 @@ def test_upload(sync_client: Dial): upload_result = sync_client.files.upload( url=sync_client.my_files_home() / file_path, file=file ) - assert isinstance(upload_result, FileMetadata) + assert isinstance(upload_result, FileItem) assert upload_result.bucket == sync_client.my_bucket() assert upload_result.node_type == "ITEM" assert upload_result.name == file_name diff --git a/tests/resources/files/test_upload.py b/tests/resources/files/test_upload.py index cdd9c74..28158a2 100644 --- a/tests/resources/files/test_upload.py +++ b/tests/resources/files/test_upload.py @@ -4,7 +4,7 @@ import pytest from aidial_client._exception import InvalidDialURLError -from aidial_client.types.metadata import FileMetadata +from aidial_client.types.metadata import FileItem from tests.client_mock import get_async_client_mock, get_client_mock UPLOAD_RESPONSE_MOCK = { @@ -16,6 +16,10 @@ "resourceType": "FILE", "contentLength": 128630, "contentType": "image/png", + "etag": "9749fad13d6e7092a6337c4af9d83764", + "createdAt": 1724836229736, + "updatedAt": 1724836248936, + "author": "user@example.com", } current_file_path = os.path.abspath(__file__) @@ -35,10 +39,17 @@ def test_upload_file_object(): file=file, ) for r in [valid_response, valid_response_using_default_bucket]: - assert isinstance(r, FileMetadata) + assert isinstance(r, FileItem) assert r.bucket == "test-bucket" assert r.name == "file.png" assert r.parent_path == "folder1/folder2" + assert r.node_type == "ITEM" + assert r.content_length == 128630 + assert r.content_type == "image/png" + assert r.etag == "9749fad13d6e7092a6337c4af9d83764" + assert r.created_at == 1724836229736 + assert r.updated_at == 1724836248936 + assert r.author == "user@example.com" @pytest.mark.asyncio @@ -66,7 +77,14 @@ async def test_upload_file_object_async(): file=file, ) for r in [valid_response, valid_response_with_files_home]: - assert isinstance(r, FileMetadata) + assert isinstance(r, FileItem) assert r.bucket == "test-bucket" assert r.name == "file.png" assert r.parent_path == "folder1/folder2" + assert r.node_type == "ITEM" + assert r.content_length == 128630 + assert r.content_type == "image/png" + assert r.etag == "9749fad13d6e7092a6337c4af9d83764" + assert r.created_at == 1724836229736 + assert r.updated_at == 1724836248936 + assert r.author == "user@example.com"