
com.azavea.stac4s.api.client.SttpStacClientF.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of client_sjs1_2.13 Show documentation
Show all versions of client_sjs1_2.13 Show documentation
stac4s is a scala library with primitives to build applications using the SpatioTemporal Asset Catalogs specification
The newest version!
package com.azavea.stac4s.api.client
import com.azavea.stac4s.api.client.util.syntax._
import com.azavea.stac4s.{StacCollection, StacItem, StacLink, StacLinkType}
import cats.MonadThrow
import cats.syntax.apply._
import cats.syntax.either._
import cats.syntax.flatMap._
import cats.syntax.functor._
import cats.syntax.nested._
import cats.syntax.option._
import eu.timepit.refined.types.string.NonEmptyString
import fs2.Stream
import io.circe.syntax._
import io.circe.{Encoder, Json, JsonObject}
import sttp.client3.circe.asJson
import sttp.client3.{Response, SttpBackend, UriContext, basicRequest}
import sttp.model.{MediaType, Uri}
case class SttpStacClientF[F[_]: MonadThrow, S: Encoder](
client: SttpBackend[F, Any],
baseUri: Uri
) extends StreamingStacClientF[F, Stream[F, *], S] {
import SttpStacClientF._
def search: Stream[F, StacItem] = search(None)
def search(filter: S): Stream[F, StacItem] = search(filter.some)
def search(filter: Option[S]): Stream[F, StacItem] = {
// the initial filter may contain the paginationBody that is used for the initial query
val initialBody = filter.map(_.asJson.deepDropNullValues).getOrElse(JsonObject.empty.asJson)
Stream
.unfoldLoopEval((baseUri.addPath("search"), initialBody)) { case (link, request) =>
client
.send(
basicRequest
.post(link)
.contentType(MediaType.ApplicationJson)
.body(request.noSpaces)
.response(asJson[Json])
)
.flatMap { response =>
val items = response.stacItems
val next = response.nextPage(request)
(items, next).tupled
}
}
.flatMap(Stream.emits)
}
def collections: Stream[F, StacCollection] =
Stream
.unfoldLoopEval(baseUri.addPath("collections")) { link =>
client
.send(basicRequest.get(link).response(asJson[Json]))
.flatMap { response =>
val items = response.stacCollections
val nextLink = response.nextLink
(items, nextLink).tupled
}
}
.flatMap(Stream.emits)
def collection(collectionId: NonEmptyString): F[StacCollection] =
client
.send(
basicRequest
.get(baseUri.addPath("collections", collectionId.value))
.response(asJson[StacCollection])
)
.flatMap(_.body.liftTo[F])
def collectionCreate(collection: StacCollection): F[StacCollection] =
client
.send(
basicRequest
.post(baseUri.addPath("collections"))
.contentType(MediaType.ApplicationJson)
.body(collection.asJson.noSpaces)
.response(asJson[StacCollection])
)
.flatMap(_.body.liftTo[F])
def items(collectionId: NonEmptyString): Stream[F, StacItem] =
Stream
.unfoldLoopEval(baseUri.addPath("collections", collectionId.value, "items")) { link =>
client
.send(basicRequest.get(link).response(asJson[Json]))
.flatMap { response =>
val items = response.stacItems
val nextLink = response.nextLink
(items, nextLink).tupled
}
}
.flatMap(Stream.emits)
def item(collectionId: NonEmptyString, itemId: NonEmptyString): F[ETag[StacItem]] =
client
.send(
basicRequest
.get(baseUri.addPath("collections", collectionId.value, "items", itemId.value))
.response(asJson[StacItem])
)
.flatMap(_.bodyETag.liftTo[F])
def itemCreate(collectionId: NonEmptyString, item: StacItem): F[ETag[StacItem]] =
client
.send(
basicRequest
.post(baseUri.addPath("collections", collectionId.value, "items"))
.contentType(MediaType.ApplicationJson)
.body(item.asJson.noSpaces)
.response(asJson[StacItem])
)
.flatMap(_.bodyETag.liftTo[F])
def itemUpdate(collectionId: NonEmptyString, item: ETag[StacItem]): F[ETag[StacItem]] =
client
.send(
basicRequest
.put(baseUri.addPath("collections", collectionId.value, "items", item.entity.id))
.headerIfMatch(item.tag)
.body(item.entity.asJson.noSpaces)
.response(asJson[StacItem])
)
.flatMap(_.bodyETag.liftTo[F])
def itemPatch(collectionId: NonEmptyString, itemId: NonEmptyString, patch: ETag[Json]): F[ETag[StacItem]] =
client
.send(
basicRequest
.patch(baseUri.addPath("collections", collectionId.value, "items", itemId.value))
.headerIfMatch(patch.tag)
.body(patch.entity.noSpaces)
.response(asJson[StacItem])
)
.flatMap(_.bodyETag.liftTo[F])
def itemDelete(collectionId: NonEmptyString, itemId: NonEmptyString): F[Either[String, String]] =
client
.send(basicRequest.delete(baseUri.addPath("collections", collectionId.value, "items", itemId.value)))
.map(_.body)
}
object SttpStacClientF {
implicit class ResponseEitherJsonOps[E <: Exception](val self: Response[Either[E, Json]]) extends AnyVal {
/** Get the next page Uri from the retrieved Json body and the next pagination body. */
def nextPage[F[_]: MonadThrow]: F[Option[(Uri, Option[Json])]] =
self.body
.flatMap {
_.hcursor
.downField("links")
.as[Option[List[StacLink]]]
.map(_.flatMap(_.collectFirst {
case l if l.rel == StacLinkType.Next =>
// The STAC API server may return the next page token as a part of the extensionFields,
// it is just a string that should be used in the next page request body.
// Some STAC API implementations (i.e. Franklin)
// encode pagination into the next page Uri (put it into the l.href):
// in this case, the pagination token is always set to None and only Uri is used for the pagination purposes.
// to make the case described above more generic, we can take the entire body
// and pass it forward by merging with the body (SearchFilters in a form of Json)
// with the paginationBody
// see https://github.com/azavea/stac4s/pull/496 and https://github.com/azavea/stac4s/pull/502 for details
val paginationBody: Option[Json] = l.extensionFields("body").map(_.deepDropNullValues)
uri"${l.href}" -> paginationBody
}))
}
.liftTo[F]
/** Get the next page Uri and the next page Json request body (that has a correctly set next page token). */
def nextPage[F[_]: MonadThrow](filter: Json): F[Option[(Uri, Json)]] =
nextPage.nested.map { case (uri, body) => (uri, filter.setPaginationBody(body)) }.value
/** Get the next page Uri and drop the next page token / body. Useful for get requests with no POST pagination
* support.
*/
def nextLink[F[_]: MonadThrow]: F[Option[Uri]] = nextPage.nested.map(_._1).value
/** Decode List of StacItem from the retrieved Json body. */
def stacItems[F[_]: MonadThrow]: F[List[StacItem]] =
self.body.flatMap(_.hcursor.downField("features").as[List[StacItem]]).liftTo[F]
/** Decode List of StacCollection from the retrieved Json body. */
def stacCollections[F[_]: MonadThrow]: F[List[StacCollection]] =
self.body.flatMap(_.hcursor.downField("collections").as[List[StacCollection]]).liftTo[F]
}
implicit class JsonOps(val self: Json) extends AnyVal {
def setPaginationBody(body: Option[Json]): Json = {
val selfNotNull = self.deepDropNullValues
val bodyNotNull = body.map(_.deepDropNullValues).getOrElse(JsonObject.empty.asJson)
val filter = selfNotNull.deepMerge(bodyNotNull)
// bbox and intersection can't be present at the same time
if (filter.hcursor.downField("bbox").succeeded && filter.hcursor.downField("intersects").succeeded) {
// let's see which field is present in the nextPageBody
val field =
if (bodyNotNull.hcursor.downField("bbox").succeeded && bodyNotNull.hcursor.downField("intersects").failed)
filter.hcursor.downField("intersects")
else
filter.hcursor.downField("bbox")
field.delete.top.getOrElse(filter)
} else filter
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy