All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cognite.sdk.scala.v1.Client.scala Maven / Gradle / Ivy

The newest version!
// Copyright 2020 Cognite AS
// SPDX-License-Identifier: Apache-2.0

package com.cognite.sdk.scala.v1

import cats.implicits._
import cats.{Id, MonadError => CMonadError}
import com.cognite.scala_sdk.BuildInfo
import com.cognite.sdk.scala.common._
import com.cognite.sdk.scala.v1.GenericClient.parseResponse
import com.cognite.sdk.scala.v1.resources._
import com.cognite.sdk.scala.v1.resources.fdm.containers.Containers
import com.cognite.sdk.scala.v1.resources.fdm.datamodels.{DataModels => DataModelsV3}
import com.cognite.sdk.scala.v1.resources.fdm.instances.Instances
import com.cognite.sdk.scala.v1.resources.fdm.views.Views
import io.circe.Decoder
import io.circe.generic.semiauto.deriveDecoder
import natchez.Trace
import sttp.capabilities.Effect
import sttp.client3._
import sttp.client3.circe.asJsonEither
import sttp.model.{Header, StatusCode, Uri}
import sttp.monad.MonadError

import java.net.{InetAddress, UnknownHostException}
import scala.concurrent.duration._
import scala.util.control.NonFatal

class TraceSttpBackend[F[_]: Trace, +P](delegate: SttpBackend[F, P]) extends SttpBackend[F, P] {

  def sendImpl[T, R >: P with Effect[F]](
      request: Request[T, R]
  )(implicit monad: MonadError[F]): F[Response[T]] =
    Trace[F].span("sttp-client-request") {
      import sttp.monad.syntax._
      for {
        knl <- Trace[F].kernel
        _ <- Trace[F].put(
          "client.http.uri" -> request.uri.toString(),
          "client.http.method" -> request.method.toString
        )
        response <- delegate.send(
          request.headers(
            knl.toHeaders.map { case (k, v) => Header(k.toString, v) }.toSeq: _*
          )
        )
        _ <- Trace[F].put("client.http.status_code" -> response.code.toString())
      } yield response
    }
  override def send[T, R >: P with Effect[F]](request: Request[T, R]): F[Response[T]] =
    sendImpl(request)(responseMonad)

  override def close(): F[Unit] = delegate.close()

  override def responseMonad: MonadError[F] = delegate.responseMonad
}

class AuthSttpBackend[F[_], +P](delegate: SttpBackend[F, P], authProvider: AuthProvider[F])
    extends SttpBackend[F, P] {
  override def send[T, R >: P with Effect[F]](request: Request[T, R]): F[Response[T]] =
    responseMonad.flatMap(authProvider.getAuth) { (auth: Auth) =>
      delegate.send(auth.auth(request))
    }

  override def close(): F[Unit] = delegate.close()

  override def responseMonad: MonadError[F] = delegate.responseMonad
}

class RequestSessionImplicits[F[_]](
    implicit val FMonad: CMonadError[F, Throwable],
    val FTrace: Trace[F]
)
final case class RequestSession[F[_]: Trace](
    applicationName: String,
    baseUrl: Uri,
    baseSttpBackend: SttpBackend[F, _],
    auth: AuthProvider[F],
    clientTag: Option[String] = None,
    cdfVersion: Option[String] = None,
    tags: Map[String, Any] = Map.empty
)(implicit F: CMonadError[F, Throwable]) {
  val implicits: RequestSessionImplicits[F] = new RequestSessionImplicits[F]
  def withResourceType(resourceType: GenericClient.RESOURCE_TYPE): RequestSession[F] =
    this.copy(tags = this.tags + (GenericClient.RESOURCE_TYPE_TAG -> resourceType))

  val sttpBackend: SttpBackend[F, _] =
    new AuthSttpBackend(new TraceSttpBackend(baseSttpBackend), auth)

  def send[R](
      r: RequestT[Empty, Either[String, String], Any] => RequestT[Id, R, Any]
  ): F[Response[R]] =
    r(emptyRequest.readTimeout(90.seconds)).send(sttpBackend)

  private val sttpRequest = {
    val baseRequest = basicRequest
      .followRedirects(false)
      .header("x-cdp-sdk", s"CogniteScalaSDK:${BuildInfo.version}")
      .header("x-cdp-app", applicationName)
      .readTimeout(90.seconds)
      .headers(
        Seq(
          clientTag.map(Header("x-cdp-clienttag", _)),
          cdfVersion.map(Header("cdf-version", _))
        ).flatMap(_.toList): _*
      )
    tags.foldLeft(baseRequest)((req, tag) => req.tag(tag._1, tag._2))
  }

  def get[R, T](
      uri: Uri,
      mapResult: T => R,
      contentType: String = "application/json",
      accept: String = "application/json"
  )(implicit decoder: Decoder[T]): F[R] =
    sttpRequest
      .contentType(contentType)
      .header("accept", accept)
      .get(uri)
      .response(parseResponse(uri, mapResult))
      .send(sttpBackend)
      .flatMap(r => F.fromEither(r.body))

  def postEmptyBody[R, T](
      uri: Uri,
      mapResult: T => R,
      contentType: String = "application/json",
      accept: String = "application/json"
  )(implicit decoder: Decoder[T]): F[R] =
    sttpRequest
      .contentType(contentType)
      .header("accept", accept)
      .header("cdf-version", "alpha")
      .post(uri)
      .response(parseResponse(uri, mapResult))
      .send(sttpBackend)
      .flatMap(r => F.fromEither(r.body))

  def post[R, T, I](
      body: I,
      uri: Uri,
      mapResult: T => R,
      contentType: String = "application/json",
      accept: String = "application/json"
  )(implicit serializer: BodySerializer[I], decoder: Decoder[T]): F[R] =
    sttpRequest
      .contentType(contentType)
      .header("accept", accept)
      .post(uri)
      .body(body)
      .response(parseResponse(uri, mapResult))
      .send(sttpBackend)
      .flatMap(r => F.fromEither(r.body))

  def head(
      uri: Uri,
      overrideHeaders: Seq[Header] = Seq()
  ): F[Seq[Header]] =
    sttpRequest
      .headers(overrideHeaders: _*)
      .head(uri)
      .send(sttpBackend)
      .map(_.headers)

  def sendCdf[R](
      r: RequestT[Empty, Either[String, String], Any] => RequestT[Id, R, Any],
      contentType: String = "application/json",
      accept: String = "application/json"
  ): F[R] =
    r(
      sttpRequest
        .contentType(contentType)
        .header("accept", accept)
    )
      .send(sttpBackend)
      .map(_.body)
}

class GenericClient[F[_]: Trace](
    applicationName: String,
    val projectName: String,
    baseUrl: String,
    authProvider: AuthProvider[F],
    apiVersion: Option[String],
    clientTag: Option[String],
    cdfVersion: Option[String]
)(implicit monad: CMonadError[F, Throwable], sttpBackend: SttpBackend[F, Any]) {
  def this(
      applicationName: String,
      projectName: String,
      baseUrl: String = GenericClient.defaultBaseUrl,
      auth: Auth,
      apiVersion: Option[String] = None,
      clientTag: Option[String] = None,
      cdfVersion: Option[String] = None
  )(implicit monad: CMonadError[F, Throwable], sttpBackend: SttpBackend[F, Any]) =
    this(
      applicationName,
      projectName,
      baseUrl,
      AuthProvider[F](auth),
      apiVersion,
      clientTag,
      cdfVersion
    )

  import GenericClient._

  val uri: Uri = parseBaseUrlOrThrow(baseUrl)

  lazy val requestSession: RequestSession[F] =
    RequestSession(
      applicationName,
      uri"$uri/api/${apiVersion.getOrElse("v1")}/projects/$projectName",
      sttpBackend,
      authProvider,
      clientTag,
      cdfVersion
    )
  lazy val token =
    new Token[F](RequestSession(applicationName, uri, sttpBackend, authProvider, clientTag))
  lazy val assets = new Assets[F](requestSession.withResourceType(ASSETS))
  lazy val events = new Events[F](requestSession.withResourceType(EVENTS))
  lazy val files = new Files[F](requestSession.withResourceType(FILES))
  lazy val timeSeries =
    new TimeSeriesResource[F](requestSession.withResourceType(TIMESERIES))
  lazy val dataPoints =
    new DataPointsResource[F](requestSession.withResourceType(DATAPOINTS))
  lazy val sequences =
    new SequencesResource[F](requestSession.withResourceType(SEQUENCES))
  lazy val sequenceRows =
    new SequenceRows[F](requestSession.withResourceType(SEQUENCES_ROWS))
  lazy val dataSets = new DataSets[F](requestSession.withResourceType(DATASETS))
  lazy val labels = new Labels[F](requestSession.withResourceType(LABELS))
  lazy val relationships =
    new Relationships[F](requestSession.withResourceType(RELATIONSHIPS))

  lazy val rawDatabases =
    new RawDatabases[F](requestSession.withResourceType(RAW_METADATA))
  def rawTables(database: String): RawTables[F] =
    new RawTables(requestSession.withResourceType(RAW_METADATA), database)
  def rawRows(database: String, table: String, filterNullFields: Boolean = false): RawRows[F] =
    new RawRows(requestSession.withResourceType(RAW_ROWS), database, table, filterNullFields)

  lazy val threeDModels = new ThreeDModels[F](requestSession.withResourceType(THREED))
  def threeDRevisions(modelId: Long): ThreeDRevisions[F] =
    new ThreeDRevisions(requestSession.withResourceType(THREED), modelId)
  def threeDAssetMappings(modelId: Long, revisionId: Long): ThreeDAssetMappings[F] =
    new ThreeDAssetMappings(
      requestSession.withResourceType(THREED),
      modelId,
      revisionId
    )
  def threeDNodes(modelId: Long, revisionId: Long): ThreeDNodes[F] =
    new ThreeDNodes(requestSession.withResourceType(THREED), modelId, revisionId)

  lazy val functions = new Functions[F](requestSession.withResourceType(FUNCTIONS))
  def functionCalls(functionId: Long): FunctionCalls[F] =
    new FunctionCalls(requestSession.withResourceType(FUNCTIONS), functionId)
  lazy val functionSchedules =
    new FunctionSchedules[F](requestSession.withResourceType(FUNCTIONS))

  lazy val sessions = new Sessions[F](requestSession.withResourceType(SESSIONS))
  lazy val transformations =
    new Transformations[F](requestSession.withResourceType(TRANSFORMATIONS))
  lazy val containers =
    new Containers[F](requestSession.withResourceType(DATAMODELS))
  lazy val instances =
    new Instances[F](requestSession.withResourceType(DATAMODELS))
  lazy val views = new Views[F](requestSession.withResourceType(DATAMODELS))
  lazy val spacesv3 = new SpacesV3[F](requestSession.withResourceType(DATAMODELS))
  lazy val dataModelsV3 =
    new DataModelsV3[F](requestSession.withResourceType(DATAMODELS))

  def project: F[Project] =
    requestSession
      .withResourceType(PROJECT)
      .get[Project, Project](
        requestSession.baseUrl,
        value => value
      )
  lazy val groups = new Groups[F](requestSession.withResourceType(GROUPS))
  lazy val securityCategories =
    new SecurityCategories[F](requestSession.withResourceType(SECURITY_CATEGORIES))
}

object GenericClient {
  val RESOURCE_TYPE_TAG = "cdf-resource-type"

  sealed trait RESOURCE_TYPE
  case object ASSETS extends RESOURCE_TYPE
  case object EVENTS extends RESOURCE_TYPE
  case object FILES extends RESOURCE_TYPE
  case object TIMESERIES extends RESOURCE_TYPE
  case object DATAPOINTS extends RESOURCE_TYPE
  case object SEQUENCES extends RESOURCE_TYPE
  case object SEQUENCES_ROWS extends RESOURCE_TYPE
  case object DATASETS extends RESOURCE_TYPE
  case object LABELS extends RESOURCE_TYPE
  case object RELATIONSHIPS extends RESOURCE_TYPE
  case object RAW_METADATA extends RESOURCE_TYPE
  case object RAW_ROWS extends RESOURCE_TYPE
  case object THREED extends RESOURCE_TYPE
  case object FUNCTIONS extends RESOURCE_TYPE
  case object SESSIONS extends RESOURCE_TYPE
  case object TRANSFORMATIONS extends RESOURCE_TYPE
  case object DATAMODELS extends RESOURCE_TYPE
  case object PROJECT extends RESOURCE_TYPE
  case object GROUPS extends RESOURCE_TYPE
  case object SECURITY_CATEGORIES extends RESOURCE_TYPE

  implicit val projectAuthenticationDecoder: Decoder[ProjectAuthentication] =
    deriveDecoder[ProjectAuthentication]
  @SuppressWarnings(Array("org.wartremover.warts.JavaSerializable"))
  implicit val projectDecoder: Decoder[Project] = deriveDecoder[Project]

  val defaultBaseUrl: String = Option(System.getenv("COGNITE_BASE_URL"))
    .getOrElse("https://api.cognitedata.com")

  def apply[F[_]: Trace](
      applicationName: String,
      projectName: String,
      baseUrl: String,
      auth: Auth
  )(implicit F: CMonadError[F, Throwable], sttpBackend: SttpBackend[F, Any]): GenericClient[F] =
    new GenericClient(applicationName, projectName, baseUrl, auth)

  def parseBaseUrlOrThrow(baseUrl: String): Uri =
    try {
      // sttp allows this, but we don't.
      if (baseUrl.isEmpty) {
        throw new IllegalArgumentException()
      }
      val uri = uri"$baseUrl"
      val uriWithScheme = if (uri.scheme.isEmpty) {
        uri"https://$baseUrl"
      } else {
        uri
      }
      uriWithScheme.host match {
        case Some(host) =>
          // Validate that this is a valid hostname.
          val _ = InetAddress.getByName(host)
          uriWithScheme
        case None => throw new UnknownHostException(s"Unknown host in baseUrl: $baseUrl")
      }
    } catch {
      case e: UnknownHostException => throw e
      case NonFatal(_) =>
        throw new IllegalArgumentException(
          s"Unable to parse this baseUrl as a valid URI: $baseUrl"
        )
    }

  def forAuth[F[_]: Trace](
      applicationName: String,
      projectName: String,
      auth: Auth,
      baseUrl: String = defaultBaseUrl,
      apiVersion: Option[String] = None,
      clientTag: Option[String] = None,
      cdfVersion: Option[String] = None
  )(implicit F: CMonadError[F, Throwable], sttpBackend: SttpBackend[F, Any]): F[GenericClient[F]] =
    forAuthProvider(
      applicationName,
      projectName,
      AuthProvider(auth),
      baseUrl,
      apiVersion,
      clientTag,
      cdfVersion
    )

  def forAuthProvider[F[_]: Trace](
      applicationName: String,
      projectName: String,
      authProvider: AuthProvider[F],
      baseUrl: String = defaultBaseUrl,
      apiVersion: Option[String] = None,
      clientTag: Option[String] = None,
      cdfVersion: Option[String] = None
  )(implicit F: CMonadError[F, Throwable], sttpBackend: SttpBackend[F, Any]): F[GenericClient[F]] =
    if (projectName.isEmpty) {
      F.raiseError(InvalidAuthentication())
    } else {
      F.pure(
        new GenericClient[F](
          applicationName,
          projectName,
          baseUrl,
          authProvider,
          apiVersion,
          clientTag,
          cdfVersion
        )
      )
    }

  def parseResponse[T, R](uri: Uri, mapResult: T => R)(
      implicit decoder: Decoder[T]
  ): ResponseAs[Either[Throwable, R], Any] =
    asJsonEither[CdpApiError, T].mapWithMetadata((response, metadata) =>
      response
        .leftMap[Throwable] {
          case DeserializationException(_, _)
              if metadata.code.code === StatusCode.TooManyRequests.code =>
            CdpApiException(
              url = uri"$uri",
              code = StatusCode.TooManyRequests.code,
              missing = None,
              duplicated = None,
              missingFields = None,
              message = "Too many requests.",
              requestId = metadata.header("x-request-id")
            )
          case DeserializationException(_, error) =>
            SdkException(
              s"Failed to parse response, reason: ${error.getMessage}",
              Some(uri),
              metadata.header("x-request-id"),
              Some(metadata.code.code)
            )
          case HttpError(cdpApiError, _) =>
            cdpApiError.asException(uri"$uri", metadata.header("x-request-id"))
        }
        .map(mapResult)
    )
}

class Client(
    applicationName: String,
    override val projectName: String,
    baseUrl: String =
      Option(System.getenv("COGNITE_BASE_URL")).getOrElse("https://api.cognitedata.com"),
    auth: Auth
)(implicit trace: Trace[OrError], sttpBackend: SttpBackend[OrError, Any])
    extends GenericClient[OrError](applicationName, projectName, baseUrl, auth)

object Client {
  def apply(
      applicationName: String,
      projectName: String,
      baseUrl: String,
      auth: Auth
  )(
      implicit trace: Trace[OrError],
      sttpBackend: SttpBackend[OrError, Any]
  ): Client = new Client(applicationName, projectName, baseUrl, auth)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy