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

no.nrk.bigquery.client.http4s.Http4sBigQueryClient.scala Maven / Gradle / Ivy

/*
 * Copyright 2020 NRK
 *
 * SPDX-License-Identifier: MIT
 */

package no.nrk.bigquery.client.http4s

import cats.effect.{Async, Resource}
import cats.syntax.all.*
import com.permutive.gcp.auth.TokenProvider
import fs2.io.file.{Files, Path}
import googleapis.bigquery.{Routine, Table}
import io.circe.Encoder
import no.nrk.bigquery.*
import no.nrk.bigquery.BQPersistentRoutine.Unknown
import org.apache.avro.Schema
import org.apache.avro.generic.GenericRecord
import org.http4s.client.Client
import org.http4s.client.middleware.{Retry, RetryPolicy as HttpRetryPolicy}
import org.typelevel.log4cats.LoggerFactory
import retry.RetryPolicies.constantDelay

import scala.concurrent.duration.*

class Http4sBigQueryClient[F[_]: Async: LoggerFactory](
    client: Client[F],
    defaults: BQClientDefaults,
    pollConfig: QueryClient.PollConfig)
    extends QueryClient[F]
    with BQAdminClientWithUnderlying[F, Routine, Table] {
  val queryClient = Http4sQueryClient.fromClient(client, defaults, pollConfig)
  val adminClient = Http4sAdminClient.fromClient(client)

  override type Job = queryClient.Job

  override protected[bigquery] def synchronousQueryExecute(
      jobId: BQJobId,
      query: BQSqlFrag,
      legacySql: Boolean,
      logStream: Boolean): Resource[F, (Schema, fs2.Stream[F, GenericRecord])] =
    queryClient.synchronousQueryExecute(jobId, query, legacySql, logStream)

  override def loadToHashedPartition[A](
      jobId: BQJobId,
      table: BQTableDef.Table[Long],
      stream: fs2.Stream[F, A],
      logStream: Boolean,
      chunkSize: Int)(implicit hashedEncoder: HashedPartitionEncoder[A]): F[Option[BQJobStatistics.Load]] =
    queryClient.loadToHashedPartition(jobId, table, stream, logStream, chunkSize)

  /** @return
    *   None, if `chunkedStream` is empty
    */
  override def loadJson[A: Encoder, P: TableOps](
      jobId: BQJobId,
      table: BQTableDef.Table[P],
      partition: P,
      stream: fs2.Stream[F, A],
      writeDisposition: WriteDisposition,
      logStream: Boolean,
      chunkSize: Int): F[Option[BQJobStatistics.Load]] =
    queryClient.loadJson(jobId, table, partition, stream, writeDisposition, logStream, chunkSize)

  override def submitQuery[P](
      id: BQJobId,
      query: BQSqlFrag,
      destination: Option[BQPartitionId[P]],
      writeDisposition: Option[WriteDisposition]): F[JobWithStats[Job]] =
    queryClient.submitQuery(id, query, destination, writeDisposition)

  override def extract(id: BQJobId, extract: BQTableExtract): F[BQJobStatistics.Extract] =
    queryClient.extract(id, extract)

  override def dryRun(id: BQJobId, query: BQSqlFrag): F[BQJobStatistics.Query] =
    queryClient.dryRun(id, query)

  override def getTableWithUnderlying(id: BQTableId): F[Option[ExistingTable[Table]]] =
    adminClient.getTableWithUnderlying(id)

  override def updateTableWithExisting(existing: ExistingTable[Table], table: BQTableDef[Any]): F[BQTableDef[Any]] =
    adminClient.updateTableWithExisting(existing, table)

  override def getRoutineWithUnderlying(id: BQPersistentRoutine.Id): F[Option[ExistingRoutine[Routine]]] =
    adminClient.getRoutineWithUnderlying(id)

  override def updateRoutineWithExisting(existing: ExistingRoutine[Routine], routine: Unknown): F[Unknown] =
    adminClient.updateRoutineWithExisting(existing, routine)

  override def createDataset(dataset: BQDataset): F[BQDataset] =
    adminClient.createDataset(dataset)

  override def deleteDataset(dataset: BQDataset.Ref): F[Boolean] =
    adminClient.deleteDataset(dataset)

  override def datasetsInProject(project: ProjectId): F[Vector[BQDataset]] =
    adminClient.datasetsInProject(project)

  override def getDataset(dataset: BQDataset.Ref): F[Option[BQDataset]] =
    adminClient.getDataset(dataset)

  override def getTable(tableId: BQTableId): F[Option[BQTableDef[Any]]] =
    adminClient.getTable(tableId)

  override def createTable(table: BQTableDef[Any]): F[BQTableDef[Any]] =
    adminClient.createTable(table)

  override def updateTable(table: BQTableDef[Any]): F[BQTableDef[Any]] =
    adminClient.updateTable(table)

  override def deleteTable(tableId: BQTableId): F[Boolean] =
    adminClient.deleteTable(tableId)

  override def tablesInDataset(dataset: BQDataset.Ref): F[Vector[BQTableRef[Any]]] =
    adminClient.tablesInDataset(dataset)

  override def getRoutine(id: BQPersistentRoutine.Id): F[Option[Unknown]] =
    adminClient.getRoutine(id)

  override def createRoutine(routine: Unknown): F[Unknown] =
    adminClient.createRoutine(routine)

  override def updateRoutine(routine: Unknown): F[Unknown] =
    adminClient.updateRoutine(routine)

  override def deleteRoutine(id: BQPersistentRoutine.Id): F[Boolean] =
    adminClient.deleteRoutine(id)

  override def routinesInDataset(dataset: BQDataset.Ref): F[Vector[Unknown]] =
    adminClient.routinesInDataset(dataset)

  override def createTempTable[Param](
      table: BQTableDef.Table[Param],
      tmpDataset: BQDataset.Ref,
      expirationDuration: Option[FiniteDuration]): F[BQTableDef.Table[Param]] =
    queryClient.createTempTable(table, tmpDataset, expirationDuration)

  override def createTempTableResource[Param](
      table: BQTableDef.Table[Param],
      tmpDataset: BQDataset.Ref): Resource[F, BQTableDef.Table[Param]] =
    queryClient.createTempTableResource(table, tmpDataset)
}

object Http4sBigQueryClient {
  val DEFAULT_SCOPES = "https://www.googleapis.com/auth/bigquery" :: Nil

  def serviceAccount[F[_]: Async: Files](path: Path, client: Client[F]): F[TokenProvider[F]] =
    TokenProvider.serviceAccount(path, DEFAULT_SCOPES, client)

  def serviceAccountFromString[F[_]: Async: Files](input: String, client: Client[F]): F[TokenProvider[F]] =
    Files[F].tempFile.use { path =>
      fs2.Stream.emit(input).through(Files[F].writeUtf8(path)).compile.drain >>
        serviceAccount[F](path, client)
    }

  def withMiddlewares[F[_]: Async](
      client: Client[F],
      retry: HttpRetryPolicy[F] = HttpRetryPolicy[F](backoff = x => if (x < 3) Some((x * 3).seconds) else None),
      authentication: TokenProvider[F]): Client[F] =
    Retry(retry)(authentication.clientMiddleware(client))

  def defaultCached[F[_]: Async] = TokenProvider
    .cached[F]
    .safetyPeriod(4.seconds)
    .onRefreshFailure { case (_, _) => Async[F].unit }
    .onExhaustedRetries(_ => Async[F].unit)
    .onNewToken { case (_, _) => Async[F].unit }
    .retryPolicy(constantDelay[F](200.millis))

  def resource[F[_]: Async: LoggerFactory](
      client: Client[F],
      defaults: BQClientDefaults,
      authentication: TokenProvider[F],
      pollConfig: QueryClient.PollConfig = QueryClient.PollConfig(),
      retry: HttpRetryPolicy[F] = HttpRetryPolicy[F](backoff = x => if (x < 3) Some((x * 3).seconds) else None),
      configureAuth: Option[(TokenProvider.CachedBuilder[F]) => TokenProvider.CachedBuilder[F]] = None
  ): Resource[F, Http4sBigQueryClient[F]] =
    for {
      auth <- configureAuth
        .getOrElse(identity[TokenProvider.CachedBuilder[F]])(defaultCached)
        .build(authentication)
      middle = withMiddlewares(client, retry, auth)
    } yield new Http4sBigQueryClient(middle, defaults, pollConfig)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy