Repository: softwaremill/sttp-openai (scala)

Issue:
List files
Given uri:
`https://api.openai.com/v1/files`

Create sttp GET endpoint + tests

Docs reference
https://platform.openai.com/docs/api-reference/files/list

Pull request description:
Closes #12

Patch:
diff --git a/core/src/main/scala/sttp/openai/OpenAi.scala b/core/src/main/scala/sttp/openai/OpenAi.scala
index d727eada..c1ab3f31 100644
--- a/core/src/main/scala/sttp/openai/OpenAi.scala
+++ b/core/src/main/scala/sttp/openai/OpenAi.scala
@@ -4,6 +4,7 @@ import sttp.client4._
 import sttp.model.Uri
 import sttp.openai.requests.models.ModelsGetResponseData.ModelsResponse
 import sttp.openai.json.SttpUpickleApiExtension.asJsonSnake
+import sttp.openai.requests.files.FilesResponseData._
 
 class OpenAi(authToken: String) {
 
@@ -13,10 +14,17 @@ class OpenAi(authToken: String) {
       .get(OpenAIEndpoints.ModelEndpoint)
       .response(asJsonSnake[ModelsResponse])
 
+  /** Fetches all files that belong to the user's organization from [[https://platform.openai.com/docs/api-reference/files]] */
+  def getFiles: Request[Either[ResponseException[String, Exception], FilesResponse]] =
+    openApiAuthRequest
+      .get(OpenAIEndpoints.FilesEndpoint)
+      .response(asJsonSnake[FilesResponse])
+
   private val openApiAuthRequest: PartialRequest[Either[String, String]] = basicRequest.auth
     .bearer(authToken)
 }
 
 private object OpenAIEndpoints {
   val ModelEndpoint: Uri = uri"https://api.openai.com/v1/models"
+  val FilesEndpoint: Uri = uri"https://api.openai.com/v1/files"
 }
diff --git a/core/src/main/scala/sttp/openai/requests/files/FilesResponseData.scala b/core/src/main/scala/sttp/openai/requests/files/FilesResponseData.scala
new file mode 100644
index 00000000..2cf54c3e
--- /dev/null
+++ b/core/src/main/scala/sttp/openai/requests/files/FilesResponseData.scala
@@ -0,0 +1,29 @@
+package sttp.openai.requests.files
+
+import sttp.openai.json.SnakePickle
+
+object FilesResponseData {
+  case class FileData(
+      `object`: String,
+      id: String,
+      purpose: String,
+      filename: String,
+      bytes: Int,
+      createdAt: Int,
+      status: String,
+      statusDetails: Option[String]
+  )
+
+  object FileData {
+    implicit val fileInfoReadWriter: SnakePickle.ReadWriter[FileData] = SnakePickle.macroRW[FileData]
+  }
+
+  case class FilesResponse(
+      `object`: String,
+      data: Seq[FileData]
+  )
+
+  object FilesResponse {
+    implicit val filesResponseReadWriter: SnakePickle.ReadWriter[FilesResponse] = SnakePickle.macroRW[FilesResponse]
+  }
+}


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/FilesResponse.scala b/core/src/test/scala/sttp/openai/fixtures/FilesResponse.scala
new file mode 100644
index 00000000..69fe9dbc
--- /dev/null
+++ b/core/src/test/scala/sttp/openai/fixtures/FilesResponse.scala
@@ -0,0 +1,19 @@
+package sttp.openai.fixtures
+
+object FilesResponse {
+  val listFilesJsonResponse: String = """{
+                                |  "object": "list",
+                                |  "data": [
+                                |    {
+                                |      "object": "file",
+                                |      "id": "file-tralala",
+                                |      "purpose": "fine-tune",
+                                |      "filename": "example.jsonl",
+                                |      "bytes": 44,
+                                |      "created_at": 1681375533,
+                                |      "status": "processed",
+                                |      "status_details": null
+                                |    }
+                                |  ]
+                                |}""".stripMargin
+}
diff --git a/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala b/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala
new file mode 100644
index 00000000..35141e35
--- /dev/null
+++ b/core/src/test/scala/sttp/openai/requests/files/FilesResponseDataSpec.scala
@@ -0,0 +1,37 @@
+package sttp.openai.requests.files
+
+import org.scalatest.EitherValues
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import sttp.openai.fixtures
+import sttp.openai.json.SttpUpickleApiExtension
+import sttp.openai.requests.files.FilesResponseData.{FileData, FilesResponse}
+import sttp.openai.requests.files.FilesResponseData.FilesResponse._
+
+class FilesResponseDataSpec extends AnyFlatSpec with Matchers with EitherValues {
+  "Given files response as Json" should "be properly deserialized to case class" in {
+    // given
+    val listFilesResponse = fixtures.FilesResponse.listFilesJsonResponse
+    val expectedResponse = FilesResponse(
+      `object` = "list",
+      Seq(
+        FileData(
+          `object` = "file",
+          id = "file-tralala",
+          purpose = "fine-tune",
+          filename = "example.jsonl",
+          bytes = 44,
+          createdAt = 1681375533,
+          status = "processed",
+          statusDetails = None
+        )
+      )
+    )
+
+    // when
+    val givenResponse: Either[Exception, FilesResponse] = SttpUpickleApiExtension.deserializeJsonSnake.apply(listFilesResponse)
+
+    // then
+    givenResponse.value shouldBe expectedResponse
+  }
+}