diff --git a/src/AnthropicClient/AnthropicApiClient.cs b/src/AnthropicClient/AnthropicApiClient.cs index bcfb50a..1870fbd 100644 --- a/src/AnthropicClient/AnthropicApiClient.cs +++ b/src/AnthropicClient/AnthropicApiClient.cs @@ -385,6 +385,14 @@ public class AnthropicApiClient : IAnthropicApiClient public async Task> CreateFileAsync(CreateFileRequest request, CancellationToken cancellationToken = default) { var response = await SendFileRequestAsync(FilesEndpoint, request, cancellationToken); + + if (response.IsSuccessStatusCode is false) + { + var content = await response.Content.ReadAsStringAsync(); + var error = Deserialize(content) ?? new AnthropicError(); + return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); + } + return await CreateResultAsync(response); } @@ -394,6 +402,14 @@ public class AnthropicApiClient : IAnthropicApiClient var pagingRequest = request ?? new PagingRequest(); var endpoint = $"{FilesEndpoint}?{pagingRequest.ToQueryParameters()}"; var response = await SendRequestAsync(endpoint, cancellationToken: cancellationToken); + + if (response.IsSuccessStatusCode is false) + { + var content = await response.Content.ReadAsStringAsync(); + var error = Deserialize(content) ?? new AnthropicError(); + return AnthropicResult>.Failure(error, new AnthropicHeaders(response.Headers)); + } + return await CreateResultAsync>(response); } @@ -406,6 +422,55 @@ public class AnthropicApiClient : IAnthropicApiClient } } + /// + public async Task> 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(content) ?? new AnthropicError(); + return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); + } + + return await CreateResultAsync(response); + } + + /// + public async Task> 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(content) ?? new AnthropicError(); + return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); + } + + var stream = await response.Content.ReadAsStreamAsync(); + return AnthropicResult.Success(stream, new AnthropicHeaders(response.Headers)); + } + + /// + public async Task> 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(content) ?? new AnthropicError(); + return AnthropicResult.Failure(error, new AnthropicHeaders(response.Headers)); + } + + return await CreateResultAsync(response); + } + 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 547879f..89e3f7c 100644 --- a/src/AnthropicClient/IAnthropicApiClient.cs +++ b/src/AnthropicClient/IAnthropicApiClient.cs @@ -135,4 +135,29 @@ public interface IAnthropicApiClient /// 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); + + /// + /// Gets a file's metadata by its ID asynchronously. + /// + /// The ID of the file to get. + /// 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 . + Task> GetFileInfoAsync(string fileId, CancellationToken cancellationToken = default); + + /// + /// Gets a file's content by its ID asynchronously. + /// + /// The ID of the file to get the content for. + /// 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 a stream containing the file content. + Task> GetFileAsync(string fileId, CancellationToken cancellationToken = default); + + + /// + /// Deletes a file by its ID asynchronously. + /// + /// The ID of the file to delete. + /// 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 . + Task> DeleteFileAsync(string fileId, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/AnthropicClient/Models/AnthropicFileDeleteResponse.cs b/src/AnthropicClient/Models/AnthropicFileDeleteResponse.cs new file mode 100644 index 0000000..346d00b --- /dev/null +++ b/src/AnthropicClient/Models/AnthropicFileDeleteResponse.cs @@ -0,0 +1,17 @@ +namespace AnthropicClient.Models; + +/// +/// Represents the response from deleting a file in the Anthropic API. +/// +public class AnthropicFileDeleteResponse +{ + /// + /// Gets or sets the ID of the file that was deleted. + /// + public string Id { get; init; } = string.Empty; + + /// + /// Gets or sets the response type + /// + public string Type { get; init; } = string.Empty; +} \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs index d86fc4a..da6a61f 100644 --- a/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/EndToEnd/AnthropicApiClientTests.cs @@ -744,4 +744,40 @@ public class AnthropicApiClientTests(ConfigurationFixture configFixture) : EndTo responses.Select(r => r.Value).SelectMany(p => p.Data) .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(); + 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(); + result.Value.Id.Should().Be(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 d9197ac..66ea05e 100644 --- a/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs +++ b/tests/AnthropicClient.Tests/Integration/AnthropicApiClientTests.cs @@ -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(); + + 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(); + result.Value.Id.Should().Be(fileId); + result.Value.Type.Should().Be("file_deleted"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs index 19c5a78..dcc339b 100644 --- a/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs +++ b/tests/AnthropicClient.Tests/Integration/IntegrationTest.cs @@ -116,4 +116,22 @@ public static class MockHttpMessageHandlerExtensions return mockHttpMessageHandler .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}"); + } } \ No newline at end of file diff --git a/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileDeleteResponseTests.cs b/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileDeleteResponseTests.cs new file mode 100644 index 0000000..750279b --- /dev/null +++ b/tests/AnthropicClient.Tests/Unit/Models/AnthropicFileDeleteResponseTests.cs @@ -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(_testJson, JsonSerializationOptions.DefaultOptions); + + response.Should().NotBeNull(); + response.Id.Should().Be("file-12345"); + response.Type.Should().Be("file_deleted"); + } +} \ No newline at end of file