feat: implement GetFileInfoAsync, GetFileAsync, and DeleteFileAsync

This commit is contained in:
Stevan Freeborn
2025-07-14 23:30:02 -05:00
parent 9cb8443f94
commit d1e88a52ac
7 changed files with 303 additions and 0 deletions
+65
View File
@@ -385,6 +385,14 @@ public class AnthropicApiClient : IAnthropicApiClient
public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default) public async Task<AnthropicResult<AnthropicFile>> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default)
{ {
var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken); var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken);
if (response.IsSuccessStatusCode is false)
{
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<AnthropicFile>.Failure(error, new AnthropicHeaders(response.Headers));
}
return await CreateResultAsync<AnthropicFile>(response); return await CreateResultAsync<AnthropicFile>(response);
} }
@@ -394,6 +402,14 @@ public class AnthropicApiClient : IAnthropicApiClient
var pagingRequest = request ?? new PagingRequest(); var pagingRequest = request ?? new PagingRequest();
var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}"; var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}";
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode is false)
{
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<Page<AnthropicFile>>.Failure(error, new AnthropicHeaders(response.Headers));
}
return await CreateResultAsync<Page<AnthropicFile>>(response); return await CreateResultAsync<Page<AnthropicFile>>(response);
} }
@@ -406,6 +422,55 @@ public class AnthropicApiClient : IAnthropicApiClient
} }
} }
/// <inheritdoc/>
public async Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default)
{
var endpoint = $"{FilesEndpoint}/{fileId}";
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode is false)
{
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<AnthropicFile>.Failure(error, new AnthropicHeaders(response.Headers));
}
return await CreateResultAsync<AnthropicFile>(response);
}
/// <inheritdoc/>
public async Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default)
{
var endpoint = $"{FilesEndpoint}/{fileId}/content";
var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken);
if (response.IsSuccessStatusCode is false)
{
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<Stream>.Failure(error, new AnthropicHeaders(response.Headers));
}
var stream = await response.Content.ReadAsStreamAsync();
return AnthropicResult<Stream>.Success(stream, new AnthropicHeaders(response.Headers));
}
/// <inheritdoc/>
public async Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default)
{
var endpoint = $"{FilesEndpoint}/{fileId}";
var response = await SendRequestAsync(endpoint, HttpMethod.Delete, cancellationToken);
if (response.IsSuccessStatusCode is false)
{
var content = await response.Content.ReadAsStringAsync();
var error = Deserialize<AnthropicError>(content) ?? new AnthropicError();
return AnthropicResult<AnthropicFileDeleteResponse>.Failure(error, new AnthropicHeaders(response.Headers));
}
return await CreateResultAsync<AnthropicFileDeleteResponse>(response);
}
private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default) private async IAsyncEnumerable<AnthropicResult<Page<T>>> GetAllPagesAsync<T>(string endpoint, int limit = 20, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{ {
var pagingRequest = new PagingRequest(limit: limit); var pagingRequest = new PagingRequest(limit: limit);
@@ -135,4 +135,29 @@ public interface IAnthropicApiClient
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param> /// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicFile"/>.</returns> /// <returns>An asynchronous enumerable that yields the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="Page{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default); IAsyncEnumerable<AnthropicResult<Page<AnthropicFile>>> ListAllFilesAsync(int limit = 20, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a file's metadata by its ID asynchronously.
/// </summary>
/// <param name="fileId">The ID of the file to get.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFile"/>.</returns>
Task<AnthropicResult<AnthropicFile>> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a file's content by its ID asynchronously.
/// </summary>
/// <param name="fileId">The ID of the file to get the content for.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is a stream containing the file content.</returns>
Task<AnthropicResult<Stream>> GetFileAsync(string fileId, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a file by its ID asynchronously.
/// </summary>
/// <param name="fileId">The ID of the file to delete.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the response as an <see cref="AnthropicResult{T}"/> where T is <see cref="AnthropicFileDeleteResponse"/>.</returns>
Task<AnthropicResult<AnthropicFileDeleteResponse>> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default);
} }
@@ -0,0 +1,17 @@
namespace AnthropicClient.Models;
/// <summary>
/// Represents the response from deleting a file in the Anthropic API.
/// </summary>
public class AnthropicFileDeleteResponse
{
/// <summary>
/// Gets or sets the ID of the file that was deleted.
/// </summary>
public string Id { get; init; } = string.Empty;
/// <summary>
/// Gets or sets the response type
/// </summary>
public string Type { get; init; } = string.Empty;
}
@@ -744,4 +744,40 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo
responses.Select(r => r.Value).SelectMany(p => p.Data) responses.Select(r => r.Value).SelectMany(p => p.Data)
.Should().ContainSingle(f => f.Id == createdFile.Value.Id); .Should().ContainSingle(f => f.Id == createdFile.Value.Id);
} }
[Fact]
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
{
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 result = await client.GetFileInfoAsync(createdFile.Value.Id);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFile>();
result.Value.Id.Should().Be(createdFile.Value.Id);
}
[Fact]
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeleteResponse()
{
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 result = await client.DeleteFileAsync(createdFile.Value.Id);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
result.Value.Id.Should().Be(createdFile.Value.Id);
}
} }
@@ -2167,4 +2167,88 @@ public class AnthropicApiClientTests : IntegrationTest
} }
}); });
} }
[Fact]
public async Task GetFileInfoAsync_WhenCalled_ItShouldReturnFile()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenGetFileRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""created_at"": ""2023-11-07T05:31:56Z"",
""downloadable"": false,
""filename"": ""example.txt"",
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""mime_type"": ""text/plain"",
""size_bytes"": 1234,
""type"": ""file""
}"
);
var result = await Client.GetFileInfoAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeEquivalentTo(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"
});
}
[Fact]
public async Task GetFileAsync_WhenCalled_ItShouldReturnFileContent()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
var fileContent = new MemoryStream(Encoding.UTF8.GetBytes("Example file content"));
_mockHttpMessageHandler
.WhenGetFileContentRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/octet-stream",
fileContent
);
var result = await Client.GetFileAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeAssignableTo<Stream>();
using var streamReader = new StreamReader(result.Value);
var content = await streamReader.ReadToEndAsync();
content.Should().Be("Example file content");
}
[Fact]
public async Task DeleteFileAsync_WhenCalled_ItShouldReturnDeletionResponse()
{
var fileId = "file_013Zva2CMHLNnXjNJJKqJ2EF";
_mockHttpMessageHandler
.WhenDeleteFileRequest(fileId)
.Respond(
HttpStatusCode.OK,
"application/json",
@"{
""id"": ""file_013Zva2CMHLNnXjNJJKqJ2EF"",
""type"": ""file_deleted""
}"
);
var result = await Client.DeleteFileAsync(fileId);
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeOfType<AnthropicFileDeleteResponse>();
result.Value.Id.Should().Be(fileId);
result.Value.Type.Should().Be("file_deleted");
}
} }
@@ -116,4 +116,22 @@ public static class MockHttpMessageHandlerExtensions
return mockHttpMessageHandler return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, FilesEndpoint); .SetupBaseRequest(HttpMethod.Get, FilesEndpoint);
} }
public static MockedRequest WhenGetFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
{
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}");
}
public static MockedRequest WhenGetFileContentRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
{
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Get, $"{FilesEndpoint}/{fileId}/content");
}
public static MockedRequest WhenDeleteFileRequest(this MockHttpMessageHandler mockHttpMessageHandler, string fileId)
{
return mockHttpMessageHandler
.SetupBaseRequest(HttpMethod.Delete, $"{FilesEndpoint}/{fileId}");
}
} }
@@ -0,0 +1,58 @@
namespace AnthropicClient.Tests.Unit.Models;
public class AnthropicFileDeleteResponseTests : SerializationTest
{
private readonly string _testJson = @"{
""id"": ""file-12345"",
""type"": ""file_deleted""
}";
[Fact]
public void Constructor_WhenCalled_ItShouldInitializeProperties()
{
var response = new AnthropicFileDeleteResponse();
response.Id.Should().BeEmpty();
response.Type.Should().BeEmpty();
}
[Fact]
public void Constructor_WhenCalledWithValues_ItShouldInitializePropertiesWithValues()
{
var id = "file-12345";
var type = "file_deleted";
var response = new AnthropicFileDeleteResponse
{
Id = id,
Type = type
};
response.Id.Should().Be(id);
response.Type.Should().Be(type);
}
[Fact]
public void JsonSerialization_WhenSerialized_ItShouldMatchExpectedJson()
{
var response = new AnthropicFileDeleteResponse
{
Id = "file-12345",
Type = "file_deleted"
};
var json = JsonSerializer.Serialize(response, JsonSerializationOptions.DefaultOptions);
JsonAssert.Equal(_testJson, json);
}
[Fact]
public void JsonDeserialization_WhenDeserialized_ItShouldMatchExpectedObject()
{
var response = JsonSerializer.Deserialize<AnthropicFileDeleteResponse>(_testJson, JsonSerializationOptions.DefaultOptions);
response.Should().NotBeNull();
response.Id.Should().Be("file-12345");
response.Type.Should().Be("file_deleted");
}
}