lib/chronicle/etl/cli/jobs.rb in chronicle-etl-0.4.1 vs lib/chronicle/etl/cli/jobs.rb in chronicle-etl-0.4.2

- old
+ new

@@ -1,14 +1,15 @@ require 'pp' +require 'tty-prompt' module Chronicle module ETL module CLI # CLI commands for working with ETL jobs class Jobs < SubcommandBase default_task "start" - namespace :jobs + namespace :jobs class_option :name, aliases: '-j', desc: 'Job configuration name' class_option :extractor, aliases: '-e', desc: "Extractor class. Default: stdin", banner: 'NAME' class_option :'extractor-opts', desc: 'Extractor options', type: :hash, default: {} @@ -23,20 +24,15 @@ class_option :until, desc: "Load records UNTIL this date", banner: 'DATE' class_option :limit, desc: "Only extract the first LIMIT records", banner: 'N' class_option :output, aliases: '-o', desc: 'Output filename', type: 'string' class_option :fields, desc: 'Output only these fields', type: 'array', banner: 'field1 field2 ...' + class_option :header_row, desc: 'Output the header row of tabular output', type: 'boolean' - class_option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info' - class_option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean - class_option :silent, desc: 'Silence all output', type: :boolean - # Thor doesn't like `run` as a command name map run: :start desc "run", "Start a job" - option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info' - option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean option :dry_run, desc: 'Only run the extraction and transform steps, not the loading', type: :boolean long_desc <<-LONG_DESC This will run an ETL job. Each job needs three parts: 1. #{'Extractor'.underline}: pulls data from an external source. By default, this is stdout. Other common options including pulling data from an API or reading JSON from a file. @@ -47,29 +43,44 @@ If you do not want to use the command line flags, you can also configure a job with a .yml config file. You can either specify the path to this file or use the filename and place the file in ~/.config/chronicle/etl/jobs/NAME.yml and call it with `--job NAME` LONG_DESC # Run an ETL job def start - setup_log_level - job_definition = build_job_definition(options) - job = Chronicle::ETL::Job.new(job_definition) - runner = Chronicle::ETL::Runner.new(job) - runner.run! + run_job(options) + rescue Chronicle::ETL::JobDefinitionError => e + missing_plugins = e.job_definition.errors + .select { |error| error.is_a?(Chronicle::ETL::PluginLoadError) } + .map(&:name) + .uniq + + install_missing_plugins(missing_plugins) + run_job(options) end desc "create", "Create a job" # Create an ETL job def create job_definition = build_job_definition(options) + job_definition.validate! + path = File.join('chronicle', 'etl', 'jobs', options[:name]) Chronicle::ETL::Config.write(path, job_definition.definition) + rescue Chronicle::ETL::JobDefinitionError => e + Chronicle::ETL::Logger.debug(e.full_message) + Chronicle::ETL::Logger.fatal("Job definition error".red) end desc "show", "Show details about a job" # Show an ETL job def show - puts Chronicle::ETL::Job.new(build_job_definition(options)) + job_definition = build_job_definition(options) + job_definition.validate! + puts Chronicle::ETL::Job.new(job_definition) + rescue Chronicle::ETL::JobDefinitionError => e + Chronicle::ETL::Logger.debug(e.full_message) + Chronicle::ETL::Logger.fatal("Job definition error".red) + exit 1 end desc "list", "List all available jobs" # List available ETL jobs def list @@ -85,25 +96,47 @@ [job, extractor, transformer, loader] end headers = ['name', 'extractor', 'transformer', 'loader'].map { |h| h.upcase.bold } + puts "Available jobs:" table = TTY::Table.new(headers, job_details) puts table.render(indent: 0, padding: [0, 2]) + rescue Chronicle::ETL::ConfigError => e + Chronicle::ETL::Logger.debug(e.full_message) + Chronicle::ETL::Logger.fatal("Error reading config. #{e.message}".red) + exit 1 end private - def setup_log_level - if options[:silent] - Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::SILENT - elsif options[:verbose] - Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::DEBUG - elsif options[:log_level] - level = Chronicle::ETL::Logger.const_get(options[:log_level].upcase) - Chronicle::ETL::Logger.log_level = level + def run_job(options) + job_definition = build_job_definition(options) + job = Chronicle::ETL::Job.new(job_definition) + runner = Chronicle::ETL::Runner.new(job) + runner.run! + end + + # TODO: probably could merge this with something in cli/plugin + def install_missing_plugins(missing_plugins) + prompt = TTY::Prompt.new + message = "Plugin#{'s' if missing_plugins.count > 1} specified by job not installed.\n" + message += "Do you want to install " + message += missing_plugins.map { |name| "chronicle-#{name}".bold}.join(", ") + message += " and start the job?" + install = prompt.yes?(message) + return unless install + + spinner = TTY::Spinner.new("[:spinner] Installing plugins...", format: :dots_2) + spinner.auto_spin + missing_plugins.each do |plugin| + Chronicle::ETL::Registry::PluginRegistry.install(plugin) end + spinner.success("(#{'successful'.green})") + rescue Chronicle::ETL::PluginNotAvailableError => e + spinner.error("Error".red) + Chronicle::ETL::Logger.fatal("Plugin '#{e.name}' could not be installed".red) end # Create job definition by reading config file and then overwriting with flag options def build_job_definition(options) definition = Chronicle::ETL::JobDefinition.new @@ -127,9 +160,10 @@ transformer_options = options[:'transformer-opts'] loader_options = options[:'loader-opts'].merge({ output: options[:output], + header_row: options[:header_row], fields: options[:fields] }.compact) { dry_run: options[:dry_run],