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

com.spotify.scio.repl.ScioILoop.scala Maven / Gradle / Ivy

There is a newer version: 0.2.6
Show newest version
/*
 * Copyright 2016 Spotify AB.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.spotify.scio.repl

import java.io.BufferedReader

import com.google.cloud.dataflow.sdk.options.{DataflowPipelineOptions, PipelineOptionsFactory}
import com.spotify.scio.bigquery.BigQueryClient
import com.spotify.scio.scioVersion
import com.spotify.scio.util.GCloudConfigUtils

import scala.tools.nsc.GenericRunnerSettings
import scala.tools.nsc.interpreter.{IR, JPrintWriter}

/**
 * ScioILoop - core of Scio REPL.
 * @param scioClassLoader [[ScioReplClassLoader]] used for runtime/in-memory classloading
 * @param args user arguments for Scio REPL
 */
class ScioILoop(scioClassLoader: ScioReplClassLoader,
                args: List[String],
                in: Option[BufferedReader],
                out: JPrintWriter)
  extends ILoopCompat(in, out) {

  def this(scioCL: ScioReplClassLoader, args: List[String]) =
    this(scioCL, args, None, new JPrintWriter(Console.out, true))

  // Fail fast for illegal arguments
  try {
    PipelineOptionsFactory.fromArgs(args.toArray).as(classOf[DataflowPipelineOptions])
  } catch {
    case e: Throwable =>
      echo(e.getMessage)
      sys.exit(1)
  }

  settings = new GenericRunnerSettings(echo)

  override def printWelcome() {}

  override def prompt: String = Console.GREEN + "\nscio> " + Console.RESET

  // Options for creating new Scio contexts
  private var scioOpts: Array[String] = args.toArray

  // =======================================================================
  // Scio REPL magic commands:
  // =======================================================================

  // Hidden magics/helpers for REPL session jars.

  private val createJarCmd = LoopCommand.nullary(
    "createJar", "create a Scio REPL runtime jar",
    () => { Result.resultFromString(scioClassLoader.createReplCodeJar) } )

  private val getNextJarCmd = LoopCommand.nullary(
    "nextJar", "get the path of the next Scio REPL runtime jar",
    () => { Result.resultFromString(scioClassLoader.getNextReplCodeJarPath) } )

  /**
   * REPL magic to get a new Scio context using arguments from the command line or :scioOpts.
   * User may specify a name for the context val, default is `sc`.
   */
  private def newScioCmdImpl(name: String) = {

    val sc = if (name.nonEmpty) name else "sc"
    val rsc = "com.spotify.scio.repl.ReplScioContext"
    val opts = optsFromArgs(scioOpts)
    val nextReplJar = scioClassLoader.getNextReplCodeJarPath
    intp.beQuietDuring {
      intp.interpret(
        s"""val $sc: ScioContext = new $rsc($opts, List("$nextReplJar"), None)
           |$sc.setName("sciorepl")
         """.stripMargin)
    }
    this.echo("Scio context available as '" + sc + "'")
    Result.default
  }

  private val newScioCmd = LoopCommand.cmd(
    "newScio", "<[context-name] | sc>", "get a new Scio context", newScioCmdImpl)

  /**
   * REPL magic to get a new __local__ Scio context.
   * User may specify a name for the context val, default is `sc`.
   */
  private def newLocalScioCmdImpl(name: String) = {
    val sc = if (name.nonEmpty) name else "sc"
    intp.beQuietDuring {
      // TODO: pass BQ settings + non distributed settings
      intp.interpret(s"val $sc = ScioContext()")
    }
    this.echo(s"Local Scio context available as '$sc'")
    Result.default
  }

  private val newLocalScioCmd = LoopCommand.cmd(
    "newLocalScio", "<[context-name] | sc>", "get a new local Scio context", newLocalScioCmdImpl)

  /** REPL magic to show or update Scio options. */
  private def scioOptsCmdImpl(args: String) = {
    if (args.trim.nonEmpty) {
      // update options
      val newOpts = args.split("\\s+")
      val result = intp.beQuietDuring {
        intp.interpret(optsFromArgs(newOpts))
      }
      if (result == IR.Success) {
        scioOpts = newOpts
        echo("Scio options updated. Use :newScio to get a new Scio context.")
      }
    } else {
      // show options
      if (scioOpts.isEmpty) {
        echo("Scio options is empty")
      } else {
        echo("Scio options: " + scioOpts.mkString(" "))
      }
    }
    Result.default
  }

  private val scioOptsCmd = LoopCommand.cmd(
    "scioOpts", "<[opts]>", "show or update Scio options", scioOptsCmdImpl)

  /**
   * REPL magic to run a Scio context.
   * User may specify a name for the context val, default is `sc`.
   * It will take care of dumping in-memory classes to local jar.
   */
  private def runScioCmdImpl(name: String) = {
    scioClassLoader.createReplCodeJar
    val sc = if (name.nonEmpty) name else "sc"
    val scioResult = intp.interpret(s"$sc.close()")
    Result.default
  }

  private val runScioCmd = LoopCommand.cmd(
    "runScio", "<[context-name] | sc>", "run Scio pipeline", runScioCmdImpl)

  private val scioCommands = List(newScioCmd, newLocalScioCmd, scioOptsCmd)

  // TODO: find way to inject those into power commands. For now unused.
  private val scioPowerCommands = List(createJarCmd, getNextJarCmd, runScioCmd)

  override def commands: List[LoopCommand] = super.commands ++ scioCommands

  // =======================================================================
  // Initialization
  // =======================================================================

  private def optsFromArgs(args: Array[String]): String = {
    val ns = "com.google.cloud.dataflow.sdk.options"
    val argsStr = args.mkString("Array(\"", "\", \"", "\")")
    s"""$ns.PipelineOptionsFactory.fromArgs($argsStr).as(classOf[$ns.DataflowPipelineOptions])"""
  }

  private def welcome(): Unit = {
    val ascii = """Welcome to
                  |                 _____
                  |    ________________(_)_____
                  |    __  ___/  ___/_  /_  __ \
                  |    _(__  )/ /__ _  / / /_/ /
                  |    /____/ \___/ /_/  \____/""".stripMargin + "   version " + scioVersion + "\n"
    echo(ascii)

    val p = scala.util.Properties
    val scalaVersion =
      "Using Scala %s (%s, Java %s)".format(p.versionString, p.javaVmName, p.javaVersion)
    echo(scalaVersion)

    echo(
      """
        |Type in expressions to have them evaluated.
        |Type :help for more information.
      """.stripMargin)
  }

  private def addImports(): IR.Result =
    intp.interpret(
      """
        |import com.spotify.scio._
        |import com.spotify.scio.bigquery._
        |import com.spotify.scio.experimental._
        |import com.spotify.scio.repl._
        |import scala.concurrent.ExecutionContext.Implicits.global
      """.stripMargin)

  private def createBigQueryClient(): IR.Result = {
    def create(projectId: String): IR.Result = {
      val r = intp.interpret(s"""val bq = BigQueryClient("$projectId")""")
      echo(s"BigQuery client available as 'bq'")
      r
    }

    val key = BigQueryClient.PROJECT_KEY

    if (sys.props(key) == null) {
      val project = GCloudConfigUtils.getGCloudProjectId
      project match {
        case Some(project_id) =>
          echo(s"Using '$project_id' as your BigQuery project.")
          sys.props(key) = project_id
          create(project_id)
        case None =>
          echo(s"System property '$key' not set. BigQueryClient is not available.")
          echo(s"Set it with '-D$key=' command line argument.")
          IR.Success
      }
    } else {
      create(sys.props(key))
    }
  }

  private def loadIoCommands(): IR.Result = {
    intp.interpret(
      """
        |val _ioCommands = new com.spotify.scio.repl.IoCommands(sc.options)
        |import _ioCommands._
      """.stripMargin)
  }

  override def createInterpreter(): Unit = {
    super.createInterpreter()
    welcome()
    intp.beQuietDuring {
      addImports()
      createBigQueryClient()
      newScioCmdImpl("sc")
      loadIoCommands()
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy