From 9cb8443f94d2086192700aeb1f2dc977a86cb752 Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:33:11 -0500 Subject: [PATCH] feat: implement list all files method --- src/AnthropicClient/AnthropicApiClient.cs | 9 ++ src/AnthropicClient/IAnthropicApiClient.cs | 8 ++ .../EndToEnd/AnthropicApiClientTests.cs | 18 ++++ .../Integration/AnthropicApiClientTests.cs | 99 +++++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index 95d7e01..bcfb50a 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -397,6 +397,15 @@ public class AnthropicApiClient : IAnthropicApiClient return await CreateResultAsync>(response); } + /// + public async IAsyncEnumerable>> ListAllFilesAsync(int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var result in GetAllPagesAsync(FilesEndpoint, limit, cancellationToken)) + { + yield return result; + } + } + private async IAsyncEnumerable>> GetAllPagesAsync(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var pagingRequest = new PagingRequest(limit: limit); diff --git a/src/AnthropicClient/IAnthropicApiClient.cs b/src/AnthropicClient/IAnthropicApiClient.cs index bae3b70..547879f 100644 --- a/src/AnthropicClient/IAnthropicApiClient.cs +++ b/src/AnthropicClient/IAnthropicApiClient.cs @@ -127,4 +127,12 @@ public interface IAnthropicApiClient /// A token to cancel the asynchronous operation. /// A task that represents the asynchronous operation. The task result contains the response as an where T is where T is . Task>> ListFilesAsync(PagingRequest? request = null, CancellationToken cancellationToken = default); + + /// + /// Lists all files asynchronously, returning every page of results. + /// + /// The maximum number of files to return in each page. + /// A token to cancel the asynchronous operation. + /// An asynchronous enumerable that yields the response as an where T is where T is . + IAsyncEnumerable>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index 568bd64..d86fc4a 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -726,4 +726,22 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo result.Value.Should().BeOfType>(); result.Value.Data.Should().ContainSingle(f => f.Id == createdFile.Value.Id); } + + [Fact] + public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles() + { + var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("anthropic-beta", "files-api-2025-04-14"); + var client = CreateClient(httpClient); + + var fileBytes = await File.ReadAllBytesAsync(TestFileHelper.GetTestFilePath("story.txt")); + var createFileRequest = new CreateFileRequest(fileBytes, "story.txt", "text/plain"); + var createdFile = await client.CreateFileAsync(createFileRequest); + + var responses = await client.ListAllFilesAsync(limit: 1).ToListAsync(); + + responses.Should().HaveCountGreaterThan(0); + responses.Select(r => r.Value).SelectMany(p => p.Data) + .Should().ContainSingle(f => f.Id == createdFile.Value.Id); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs index 97a1ef7..d9197ac 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -2068,4 +2068,103 @@ public class AnthropicApiClientTests : IntegrationTest } }); } + + [Fact] + public async Task ListAllFilesAsync_WhenCalled_ItShouldReturnAllFiles() + { + _mockHttpMessageHandler + .WhenListFilesRequest() + .WithExactQueryString(new Dictionary() + { + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""created_at"": ""2023-11-07T05:31:56Z"", + ""downloadable"": false, + ""filename"": ""example.txt"", + ""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"", + ""mime_type"": ""text/plain"", + ""size_bytes"": 1234, + ""type"": ""file"" + } + ], + ""has_more"": true, + ""first_id"": ""1"", + ""last_id"": ""1"" + }" + ); + + _mockHttpMessageHandler + .WhenListFilesRequest() + .WithExactQueryString(new Dictionary() + { + { "after_id", "1" }, + { "limit", "20" }, + }) + .Respond( + HttpStatusCode.OK, + "application/json", + @"{ + ""data"": [ + { + ""created_at"": ""2023-11-07T05:31:56Z"", + ""downloadable"": false, + ""filename"": ""example.txt"", + ""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"", + ""mime_type"": ""text/plain"", + ""size_bytes"": 1234, + ""type"": ""file"" + } + ], + ""has_more"": false, + ""first_id"": ""2"", + ""last_id"": ""2"" + }" + ); + + var pageResponses = Client.ListAllFilesAsync(); + var collectedPages = new List>(); + + await foreach (var response in pageResponses) + { + response.IsSuccess.Should().BeTrue(); + response.Value.Should().BeOfType>(); + collectedPages.Add(response.Value); + } + + var expectedFile = new AnthropicFile() + { + CreatedAt = DateTimeOffset.Parse("2023-11-07T05:31:56Z"), + Downloadable = false, + Name = "example.txt", + Id = "file_013Zva2CMHLNnXjNJJKqJ2EF", + MimeType = "text/plain", + Size = 1234, + Type = "file" + }; + + collectedPages.Should().HaveCount(2); + collectedPages.Should().BeEquivalentTo(new List>() + { + new() + { + Data = [expectedFile], + FirstId = "1", + LastId = "1", + HasMore = true + }, + new() + { + Data = [expectedFile], + FirstId = "2", + LastId = "2", + HasMore = false + } + }); + } } \ No newline at end of file