chronicle-etl 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +15 -4
- data/chronicle-etl.gemspec +3 -0
- data/lib/chronicle/etl/cli/connectors.rb +6 -11
- data/lib/chronicle/etl/cli/jobs.rb +55 -21
- data/lib/chronicle/etl/cli/main.rb +28 -0
- data/lib/chronicle/etl/cli/plugins.rb +62 -0
- data/lib/chronicle/etl/cli.rb +2 -0
- data/lib/chronicle/etl/config.rb +7 -4
- data/lib/chronicle/etl/configurable.rb +7 -2
- data/lib/chronicle/etl/exceptions.rb +23 -1
- data/lib/chronicle/etl/extractors/extractor.rb +1 -1
- data/lib/chronicle/etl/job.rb +7 -1
- data/lib/chronicle/etl/job_definition.rb +19 -4
- data/lib/chronicle/etl/loaders/csv_loader.rb +35 -8
- data/lib/chronicle/etl/loaders/helpers/encoding_helper.rb +18 -0
- data/lib/chronicle/etl/loaders/json_loader.rb +1 -1
- data/lib/chronicle/etl/loaders/loader.rb +23 -0
- data/lib/chronicle/etl/loaders/table_loader.rb +4 -20
- data/lib/chronicle/etl/logger.rb +5 -2
- data/lib/chronicle/etl/registry/connector_registration.rb +5 -0
- data/lib/chronicle/etl/registry/plugin_registry.rb +70 -0
- data/lib/chronicle/etl/registry/registry.rb +27 -14
- data/lib/chronicle/etl/runner.rb +34 -17
- data/lib/chronicle/etl/version.rb +1 -1
- metadata +48 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f041e90fc6019ecbc2424e8e75e7f21fa5ba715c2cdbde73d61c298e9ca82e07
|
4
|
+
data.tar.gz: 2feda7669f8fefc7ad80fe7918530a86ad2ced431f75e70ad42653c871b67d90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4a40c72dcb6514037c6e53214dc0af3bfba20c272b959c3e83496658b4b8dc3f841d7399aa215a5fcc5c5cd494278223666b8b881f645ed2c61b667351ccde94
|
7
|
+
data.tar.gz: 5b5450b76a8c03d7cb8405888b2e059390c1585a66524dd7403b458c828a1936422e03a9654ec6d270b634bf6bfe17d6dafda54e05aca8a8732207366d03ffd2
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
## A CLI toolkit for extracting and working with your digital history
|
2
2
|
|
3
|
+
![chronicle-etl-banner](https://user-images.githubusercontent.com/6291/157330518-0f934c9a-9ec4-43d9-9cc2-12f156d09b37.png)
|
4
|
+
|
3
5
|
[![Gem Version](https://badge.fury.io/rb/chronicle-etl.svg)](https://badge.fury.io/rb/chronicle-etl) [![Ruby](https://github.com/chronicle-app/chronicle-etl/actions/workflows/ruby.yml/badge.svg)](https://github.com/chronicle-app/chronicle-etl/actions/workflows/ruby.yml)
|
4
6
|
|
5
7
|
Are you trying to archive your digital history or incorporate it into your own projects? You’ve probably discovered how frustrating it is to get machine-readable access to your own data. While [building a memex](https://hyfen.net/memex/), I learned first-hand what great efforts must be made before you can begin using the data in interesting ways.
|
6
8
|
|
7
9
|
If you don’t want to spend all your time writing scrapers, reverse-engineering APIs, or parsing takeout data, this project is for you! (*If you do enjoy these things, please see the [open issues](https://github.com/chronicle-app/chronicle-etl/issues).*)
|
8
10
|
|
9
|
-
|
11
|
+
**`chronicle-etl` is a CLI tool that gives you a unified interface for accessing your personal data.** It uses the ETL pattern to *extract* it from a source (e.g. your local browser history, a directory of images, goodreads.com reading history), *transform* it (into a given schema), and *load* it to a source (e.g. a CSV file, JSON, external API).
|
10
12
|
|
11
13
|
## What does `chronicle-etl` give you?
|
12
14
|
* **CLI tool for working with personal data**. You can monitor progress of exports, manipulate the output, set up recurring jobs, manage credentials, and more.
|
@@ -86,7 +88,16 @@ Plugins provide access to data from third-party platforms, services, or formats.
|
|
86
88
|
|
87
89
|
```bash
|
88
90
|
# Install a plugin
|
89
|
-
$ chronicle-etl
|
91
|
+
$ chronicle-etl plugins:install NAME
|
92
|
+
|
93
|
+
# Install the imessage plugin
|
94
|
+
$ chronicle-etl plugins:install imessage
|
95
|
+
|
96
|
+
# List installed plugins
|
97
|
+
$ chronicle-etl plugins:list
|
98
|
+
|
99
|
+
# Uninstall a plugin
|
100
|
+
$ chronicle-etl plugins:uninstall NAME
|
90
101
|
```
|
91
102
|
|
92
103
|
A few dozen importers exist [in my Memex project](https://hyfen.net/memex/) and they’re being ported over to the Chronicle system. This table shows what’s available now and what’s coming. Rows are sorted in very rough order of priority.
|
@@ -99,8 +110,8 @@ If you want to work together on a connector, please [get in touch](#get-in-touch
|
|
99
110
|
| [shell](https://github.com/chronicle-app/chronicle-shell) | Shell command history | Available (zsh support pending) |
|
100
111
|
| [email](https://github.com/chronicle-app/chronicle-email) | Emails and attachments from IMAP or .mbox files | Available (imap support pending) |
|
101
112
|
| [pinboard](https://github.com/chronicle-app/chronicle-email) | Bookmarks and tags | Available |
|
113
|
+
| [safari](https://github.com/chronicle-app/chronicle-safari) | Browser history from local sqlite db | Available |
|
102
114
|
| github | Github user and repo activity | In progress |
|
103
|
-
| safari | Browser history from local sqlite db | Needs porting |
|
104
115
|
| chrome | Browser history from local sqlite db | Needs porting |
|
105
116
|
| whatsapp | Messaging history (via individual chat exports) or reverse-engineered local desktop install | Unstarted |
|
106
117
|
| anki | Studying and card creation history | Needs porting |
|
@@ -186,4 +197,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/chroni
|
|
186
197
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
187
198
|
|
188
199
|
## Code of Conduct
|
189
|
-
Everyone interacting in the Chronicle::ETL project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/chronicle-app/chronicle-etl/blob/
|
200
|
+
Everyone interacting in the Chronicle::ETL project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/chronicle-app/chronicle-etl/blob/main/CODE_OF_CONDUCT.md).
|
data/chronicle-etl.gemspec
CHANGED
@@ -47,8 +47,11 @@ Gem::Specification.new do |spec|
|
|
47
47
|
spec.add_dependency "sequel", "~> 5.35"
|
48
48
|
spec.add_dependency "sqlite3", "~> 1.4"
|
49
49
|
spec.add_dependency "thor", "~> 1.2"
|
50
|
+
spec.add_dependency "thor-hollaback", "~> 0.2"
|
50
51
|
spec.add_dependency "tty-progressbar", "~> 0.17"
|
52
|
+
spec.add_dependency "tty-spinner"
|
51
53
|
spec.add_dependency "tty-table", "~> 0.11"
|
54
|
+
spec.add_dependency "tty-prompt", "~> 0.23"
|
52
55
|
|
53
56
|
spec.add_development_dependency "bundler", "~> 2.1"
|
54
57
|
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
@@ -8,11 +8,6 @@ module Chronicle
|
|
8
8
|
default_task 'list'
|
9
9
|
namespace :connectors
|
10
10
|
|
11
|
-
desc "install NAME", "Installs connector NAME"
|
12
|
-
def install(name)
|
13
|
-
Chronicle::ETL::Registry.install_connector(name)
|
14
|
-
end
|
15
|
-
|
16
11
|
desc "list", "Lists available connectors"
|
17
12
|
# Display all available connectors that chronicle-etl has access to
|
18
13
|
def list
|
@@ -44,21 +39,21 @@ module Chronicle
|
|
44
39
|
desc "show PHASE IDENTIFIER", "Show information about a connector"
|
45
40
|
def show(phase, identifier)
|
46
41
|
unless ['extractor', 'transformer', 'loader'].include?(phase)
|
47
|
-
|
48
|
-
|
42
|
+
Chronicle::ETL::Logger.fatal("Phase argument must be one of: [extractor, transformer, loader]")
|
43
|
+
exit 1
|
49
44
|
end
|
50
45
|
|
51
46
|
begin
|
52
47
|
connector = Chronicle::ETL::Registry.find_by_phase_and_identifier(phase.to_sym, identifier)
|
53
|
-
rescue Chronicle::ETL::ConnectorNotAvailableError
|
54
|
-
|
55
|
-
|
48
|
+
rescue Chronicle::ETL::ConnectorNotAvailableError, Chronicle::ETL::PluginError
|
49
|
+
Chronicle::ETL::Logger.fatal("Could not find #{phase} #{identifier}")
|
50
|
+
exit 1
|
56
51
|
end
|
57
52
|
|
58
53
|
puts connector.klass.to_s.bold
|
59
54
|
puts " #{connector.descriptive_phrase}"
|
60
55
|
puts
|
61
|
-
puts "
|
56
|
+
puts "Settings:"
|
62
57
|
|
63
58
|
headers = ['name', 'default', 'required'].map{ |h| h.to_s.upcase.bold }
|
64
59
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'pp'
|
2
|
+
require 'tty-prompt'
|
2
3
|
|
3
4
|
module Chronicle
|
4
5
|
module ETL
|
@@ -6,7 +7,7 @@ module Chronicle
|
|
6
7
|
# CLI commands for working with ETL jobs
|
7
8
|
class Jobs < SubcommandBase
|
8
9
|
default_task "start"
|
9
|
-
namespace :jobs
|
10
|
+
namespace :jobs
|
10
11
|
|
11
12
|
class_option :name, aliases: '-j', desc: 'Job configuration name'
|
12
13
|
|
@@ -25,16 +26,11 @@ module Chronicle
|
|
25
26
|
|
26
27
|
class_option :output, aliases: '-o', desc: 'Output filename', type: 'string'
|
27
28
|
class_option :fields, desc: 'Output only these fields', type: 'array', banner: 'field1 field2 ...'
|
28
|
-
|
29
|
-
class_option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info'
|
30
|
-
class_option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
|
31
|
-
class_option :silent, desc: 'Silence all output', type: :boolean
|
29
|
+
class_option :header_row, desc: 'Output the header row of tabular output', type: 'boolean'
|
32
30
|
|
33
31
|
# Thor doesn't like `run` as a command name
|
34
32
|
map run: :start
|
35
33
|
desc "run", "Start a job"
|
36
|
-
option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info'
|
37
|
-
option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
|
38
34
|
option :dry_run, desc: 'Only run the extraction and transform steps, not the loading', type: :boolean
|
39
35
|
long_desc <<-LONG_DESC
|
40
36
|
This will run an ETL job. Each job needs three parts:
|
@@ -49,25 +45,40 @@ module Chronicle
|
|
49
45
|
LONG_DESC
|
50
46
|
# Run an ETL job
|
51
47
|
def start
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
48
|
+
run_job(options)
|
49
|
+
rescue Chronicle::ETL::JobDefinitionError => e
|
50
|
+
missing_plugins = e.job_definition.errors
|
51
|
+
.select { |error| error.is_a?(Chronicle::ETL::PluginLoadError) }
|
52
|
+
.map(&:name)
|
53
|
+
.uniq
|
54
|
+
|
55
|
+
install_missing_plugins(missing_plugins)
|
56
|
+
run_job(options)
|
57
57
|
end
|
58
58
|
|
59
59
|
desc "create", "Create a job"
|
60
60
|
# Create an ETL job
|
61
61
|
def create
|
62
62
|
job_definition = build_job_definition(options)
|
63
|
+
job_definition.validate!
|
64
|
+
|
63
65
|
path = File.join('chronicle', 'etl', 'jobs', options[:name])
|
64
66
|
Chronicle::ETL::Config.write(path, job_definition.definition)
|
67
|
+
rescue Chronicle::ETL::JobDefinitionError => e
|
68
|
+
Chronicle::ETL::Logger.debug(e.full_message)
|
69
|
+
Chronicle::ETL::Logger.fatal("Job definition error".red)
|
65
70
|
end
|
66
71
|
|
67
72
|
desc "show", "Show details about a job"
|
68
73
|
# Show an ETL job
|
69
74
|
def show
|
70
|
-
|
75
|
+
job_definition = build_job_definition(options)
|
76
|
+
job_definition.validate!
|
77
|
+
puts Chronicle::ETL::Job.new(job_definition)
|
78
|
+
rescue Chronicle::ETL::JobDefinitionError => e
|
79
|
+
Chronicle::ETL::Logger.debug(e.full_message)
|
80
|
+
Chronicle::ETL::Logger.fatal("Job definition error".red)
|
81
|
+
exit 1
|
71
82
|
end
|
72
83
|
|
73
84
|
desc "list", "List all available jobs"
|
@@ -87,21 +98,43 @@ LONG_DESC
|
|
87
98
|
|
88
99
|
headers = ['name', 'extractor', 'transformer', 'loader'].map { |h| h.upcase.bold }
|
89
100
|
|
101
|
+
puts "Available jobs:"
|
90
102
|
table = TTY::Table.new(headers, job_details)
|
91
103
|
puts table.render(indent: 0, padding: [0, 2])
|
104
|
+
rescue Chronicle::ETL::ConfigError => e
|
105
|
+
Chronicle::ETL::Logger.debug(e.full_message)
|
106
|
+
Chronicle::ETL::Logger.fatal("Error reading config. #{e.message}".red)
|
107
|
+
exit 1
|
92
108
|
end
|
93
109
|
|
94
110
|
private
|
95
111
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
112
|
+
def run_job(options)
|
113
|
+
job_definition = build_job_definition(options)
|
114
|
+
job = Chronicle::ETL::Job.new(job_definition)
|
115
|
+
runner = Chronicle::ETL::Runner.new(job)
|
116
|
+
runner.run!
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO: probably could merge this with something in cli/plugin
|
120
|
+
def install_missing_plugins(missing_plugins)
|
121
|
+
prompt = TTY::Prompt.new
|
122
|
+
message = "Plugin#{'s' if missing_plugins.count > 1} specified by job not installed.\n"
|
123
|
+
message += "Do you want to install "
|
124
|
+
message += missing_plugins.map { |name| "chronicle-#{name}".bold}.join(", ")
|
125
|
+
message += " and start the job?"
|
126
|
+
install = prompt.yes?(message)
|
127
|
+
return unless install
|
128
|
+
|
129
|
+
spinner = TTY::Spinner.new("[:spinner] Installing plugins...", format: :dots_2)
|
130
|
+
spinner.auto_spin
|
131
|
+
missing_plugins.each do |plugin|
|
132
|
+
Chronicle::ETL::Registry::PluginRegistry.install(plugin)
|
104
133
|
end
|
134
|
+
spinner.success("(#{'successful'.green})")
|
135
|
+
rescue Chronicle::ETL::PluginNotAvailableError => e
|
136
|
+
spinner.error("Error".red)
|
137
|
+
Chronicle::ETL::Logger.fatal("Plugin '#{e.name}' could not be installed".red)
|
105
138
|
end
|
106
139
|
|
107
140
|
# Create job definition by reading config file and then overwriting with flag options
|
@@ -129,6 +162,7 @@ LONG_DESC
|
|
129
162
|
|
130
163
|
loader_options = options[:'loader-opts'].merge({
|
131
164
|
output: options[:output],
|
165
|
+
header_row: options[:header_row],
|
132
166
|
fields: options[:fields]
|
133
167
|
}.compact)
|
134
168
|
|
@@ -5,6 +5,14 @@ module Chronicle
|
|
5
5
|
module CLI
|
6
6
|
# Main entrypoint for CLI app
|
7
7
|
class Main < ::Thor
|
8
|
+
class_before :set_log_level
|
9
|
+
class_before :set_color_output
|
10
|
+
|
11
|
+
class_option :log_level, desc: 'Log level (debug, info, warn, error, fatal, silent)', default: 'info'
|
12
|
+
class_option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
|
13
|
+
class_option :silent, desc: 'Silence all output', type: :boolean
|
14
|
+
class_option :'no-color', desc: 'Disable colour output', type: :boolean
|
15
|
+
|
8
16
|
default_task "jobs"
|
9
17
|
|
10
18
|
desc 'connectors:COMMAND', 'Connectors available for ETL jobs', hide: true
|
@@ -13,6 +21,9 @@ module Chronicle
|
|
13
21
|
desc 'jobs:COMMAND', 'Configure and run jobs', hide: true
|
14
22
|
subcommand 'jobs', Jobs
|
15
23
|
|
24
|
+
desc 'plugins:COMMAND', 'Configure plugins', hide: true
|
25
|
+
subcommand 'plugins', Plugins
|
26
|
+
|
16
27
|
# Entrypoint for the CLI
|
17
28
|
def self.start(given_args = ARGV, config = {})
|
18
29
|
# take a subcommand:command and splits them so Thor knows how to hand off to the subcommand class
|
@@ -79,6 +90,23 @@ module Chronicle
|
|
79
90
|
shell.say
|
80
91
|
end
|
81
92
|
end
|
93
|
+
|
94
|
+
no_commands do
|
95
|
+
def set_color_output
|
96
|
+
String.disable_colorization true if options[:'no-color'] || ENV['NO_COLOR']
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_log_level
|
100
|
+
if options[:silent]
|
101
|
+
Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::SILENT
|
102
|
+
elsif options[:verbose]
|
103
|
+
Chronicle::ETL::Logger.log_level = Chronicle::ETL::Logger::DEBUG
|
104
|
+
elsif options[:log_level]
|
105
|
+
level = Chronicle::ETL::Logger.const_get(options[:log_level].upcase)
|
106
|
+
Chronicle::ETL::Logger.log_level = level
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
82
110
|
end
|
83
111
|
end
|
84
112
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "tty-prompt"
|
4
|
+
require "tty-spinner"
|
5
|
+
|
6
|
+
|
7
|
+
module Chronicle
|
8
|
+
module ETL
|
9
|
+
module CLI
|
10
|
+
# CLI commands for working with ETL plugins
|
11
|
+
class Plugins < SubcommandBase
|
12
|
+
default_task 'list'
|
13
|
+
namespace :plugins
|
14
|
+
|
15
|
+
desc "install", "Install a plugin"
|
16
|
+
def install(name)
|
17
|
+
spinner = TTY::Spinner.new("[:spinner] Installing plugin #{name}...", format: :dots_2)
|
18
|
+
spinner.auto_spin
|
19
|
+
Chronicle::ETL::Registry::PluginRegistry.install(name)
|
20
|
+
spinner.success("(#{'successful'.green})")
|
21
|
+
rescue Chronicle::ETL::PluginError => e
|
22
|
+
spinner.error("Error".red)
|
23
|
+
Chronicle::ETL::Logger.debug(e.full_message)
|
24
|
+
Chronicle::ETL::Logger.fatal("Plugin '#{name}' could not be installed".red)
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "uninstall", "Unintall a plugin"
|
29
|
+
def uninstall(name)
|
30
|
+
spinner = TTY::Spinner.new("[:spinner] Uninstalling plugin #{name}...", format: :dots_2)
|
31
|
+
spinner.auto_spin
|
32
|
+
Chronicle::ETL::Registry::PluginRegistry.uninstall(name)
|
33
|
+
spinner.success("(#{'successful'.green})")
|
34
|
+
rescue Chronicle::ETL::PluginError => e
|
35
|
+
spinner.error("Error".red)
|
36
|
+
Chronicle::ETL::Logger.debug(e.full_message)
|
37
|
+
Chronicle::ETL::Logger.fatal("Plugin '#{name}' could not be uninstalled (was it installed?)".red)
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "list", "Lists available plugins"
|
42
|
+
# Display all available plugins that chronicle-etl has access to
|
43
|
+
def list
|
44
|
+
plugins = Chronicle::ETL::Registry::PluginRegistry.all_installed_latest
|
45
|
+
|
46
|
+
info = plugins.map do |plugin|
|
47
|
+
{
|
48
|
+
name: plugin.name.sub("chronicle-", ""),
|
49
|
+
description: plugin.description,
|
50
|
+
version: plugin.version
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
headers = ['name', 'description', 'latest version'].map{ |h| h.to_s.upcase.bold }
|
55
|
+
table = TTY::Table.new(headers, info.map(&:values))
|
56
|
+
puts "Installed plugins:"
|
57
|
+
puts table.render(indent: 2, padding: [0, 0])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/chronicle/etl/cli.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'thor'
|
2
|
+
require 'thor/hollaback'
|
2
3
|
require 'chronicle/etl'
|
3
4
|
|
4
5
|
require 'chronicle/etl/cli/subcommand_base'
|
5
6
|
require 'chronicle/etl/cli/connectors'
|
6
7
|
require 'chronicle/etl/cli/jobs'
|
8
|
+
require 'chronicle/etl/cli/plugins'
|
7
9
|
require 'chronicle/etl/cli/main'
|
data/lib/chronicle/etl/config.rb
CHANGED
@@ -24,16 +24,14 @@ module Chronicle
|
|
24
24
|
|
25
25
|
# Returns all jobs available in ~/.config/chronicle/etl/jobs/*.yml
|
26
26
|
def available_jobs
|
27
|
-
|
28
|
-
Dir.glob(File.join(job_directory, "*.yml")).map do |filename|
|
27
|
+
Dir.glob(File.join(config_directory("jobs"), "*.yml")).map do |filename|
|
29
28
|
File.basename(filename, ".*")
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
33
32
|
# Returns all available credentials available in ~/.config/chronicle/etl/credentials/*.yml
|
34
33
|
def available_credentials
|
35
|
-
|
36
|
-
Dir.glob(File.join(job_directory, "*.yml")).map do |filename|
|
34
|
+
Dir.glob(File.join(config_directory("credentials"), "*.yml")).map do |filename|
|
37
35
|
File.basename(filename, ".*")
|
38
36
|
end
|
39
37
|
end
|
@@ -48,6 +46,11 @@ module Chronicle
|
|
48
46
|
def load_credentials(name)
|
49
47
|
config = self.load("chronicle/etl/credentials/#{name}.yml")
|
50
48
|
end
|
49
|
+
|
50
|
+
def config_directory(type)
|
51
|
+
path = "chronicle/etl/#{type}"
|
52
|
+
Runcom::Config.new(path).current || raise(Chronicle::ETL::ConfigError, "Could not access config directory (#{path})")
|
53
|
+
end
|
51
54
|
end
|
52
55
|
end
|
53
56
|
end
|
@@ -57,7 +57,7 @@ module Chronicle
|
|
57
57
|
|
58
58
|
options.each do |name, value|
|
59
59
|
setting = self.class.all_settings[name]
|
60
|
-
raise(Chronicle::ETL::
|
60
|
+
raise(Chronicle::ETL::ConnectorConfigurationError, "Unrecognized setting: #{name}") unless setting
|
61
61
|
|
62
62
|
@config[name] = coerced_value(setting, value)
|
63
63
|
end
|
@@ -78,7 +78,7 @@ module Chronicle
|
|
78
78
|
|
79
79
|
def validate_config
|
80
80
|
missing = (self.class.all_required_settings.keys - @config.compacted_h.keys)
|
81
|
-
raise Chronicle::ETL::
|
81
|
+
raise Chronicle::ETL::ConnectorConfigurationError, "Missing options: #{missing}" if missing.count.positive?
|
82
82
|
end
|
83
83
|
|
84
84
|
def coerced_value(setting, value)
|
@@ -89,6 +89,11 @@ module Chronicle
|
|
89
89
|
value.to_s
|
90
90
|
end
|
91
91
|
|
92
|
+
# TODO: think about whether to split up float, integer
|
93
|
+
def coerce_numeric(value)
|
94
|
+
value.to_f
|
95
|
+
end
|
96
|
+
|
92
97
|
def coerce_boolean(value)
|
93
98
|
if value.is_a?(String)
|
94
99
|
value.downcase == "true"
|
@@ -2,10 +2,32 @@ module Chronicle
|
|
2
2
|
module ETL
|
3
3
|
class Error < StandardError; end
|
4
4
|
|
5
|
-
class
|
5
|
+
class ConfigError < Error; end
|
6
6
|
|
7
7
|
class RunnerTypeError < Error; end
|
8
8
|
|
9
|
+
class JobDefinitionError < Error
|
10
|
+
attr_reader :job_definition
|
11
|
+
|
12
|
+
def initialize(job_definition)
|
13
|
+
@job_definition = job_definition
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class PluginError < Error
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
def initialize(name)
|
22
|
+
@name = name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class PluginNotAvailableError < PluginError; end
|
27
|
+
class PluginLoadError < PluginError; end
|
28
|
+
|
29
|
+
class ConnectorConfigurationError < Error; end
|
30
|
+
|
9
31
|
class ConnectorNotAvailableError < Error
|
10
32
|
def initialize(message, provider: nil, name: nil)
|
11
33
|
super(message)
|
data/lib/chronicle/etl/job.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'forwardable'
|
2
|
+
|
2
3
|
module Chronicle
|
3
4
|
module ETL
|
5
|
+
# A runner job
|
6
|
+
#
|
7
|
+
# TODO: this can probably be merged with JobDefinition. Not clear
|
8
|
+
# where the boundaries are
|
4
9
|
class Job
|
5
10
|
extend Forwardable
|
6
11
|
|
@@ -12,7 +17,8 @@ module Chronicle
|
|
12
17
|
:transformer_klass,
|
13
18
|
:transformer_options,
|
14
19
|
:loader_klass,
|
15
|
-
:loader_options
|
20
|
+
:loader_options,
|
21
|
+
:job_definition
|
16
22
|
|
17
23
|
# TODO: build a proper id system
|
18
24
|
alias id name
|
@@ -19,12 +19,31 @@ module Chronicle
|
|
19
19
|
}
|
20
20
|
}.freeze
|
21
21
|
|
22
|
+
attr_reader :errors
|
22
23
|
attr_accessor :definition
|
23
24
|
|
24
25
|
def initialize()
|
25
26
|
@definition = SKELETON_DEFINITION
|
26
27
|
end
|
27
28
|
|
29
|
+
def validate
|
30
|
+
@errors = []
|
31
|
+
|
32
|
+
Chronicle::ETL::Registry::PHASES.each do |phase|
|
33
|
+
__send__("#{phase}_klass".to_sym)
|
34
|
+
rescue Chronicle::ETL::PluginError => e
|
35
|
+
@errors << e
|
36
|
+
end
|
37
|
+
|
38
|
+
@errors.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate!
|
42
|
+
raise(Chronicle::ETL::JobDefinitionError.new(self), "Job definition is invalid") unless validate
|
43
|
+
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
28
47
|
# Add config hash to this definition
|
29
48
|
def add_config(config = {})
|
30
49
|
@definition = @definition.deep_merge(config)
|
@@ -80,10 +99,6 @@ module Chronicle
|
|
80
99
|
end
|
81
100
|
end
|
82
101
|
end
|
83
|
-
|
84
|
-
def validate
|
85
|
-
return true # TODO
|
86
|
-
end
|
87
102
|
end
|
88
103
|
end
|
89
104
|
end
|
@@ -7,22 +7,49 @@ module Chronicle
|
|
7
7
|
r.description = 'CSV'
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
setting :output, default: $stdout
|
11
|
+
setting :headers, default: true
|
12
|
+
setting :header_row, default: true
|
13
|
+
|
14
|
+
def records
|
15
|
+
@records ||= []
|
13
16
|
end
|
14
17
|
|
15
18
|
def load(record)
|
16
|
-
|
19
|
+
records << record.to_h_flattened
|
17
20
|
end
|
18
21
|
|
19
22
|
def finish
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
23
|
+
return unless records.any?
|
24
|
+
|
25
|
+
headers = build_headers(records)
|
26
|
+
|
27
|
+
csv_options = {}
|
28
|
+
if @config.headers
|
29
|
+
csv_options[:write_headers] = @config.header_row
|
30
|
+
csv_options[:headers] = headers
|
31
|
+
end
|
32
|
+
|
33
|
+
if @config.output.is_a?(IO)
|
34
|
+
# This might seem like a duplication of the default value ($stdout)
|
35
|
+
# but it's because rspec overwrites $stdout (in helper #capture) to
|
36
|
+
# capture output.
|
37
|
+
io = $stdout.dup
|
38
|
+
else
|
39
|
+
io = File.open(@config.output, "w+")
|
40
|
+
end
|
41
|
+
|
42
|
+
output = CSV.generate(**csv_options) do |csv|
|
43
|
+
records.each do |record|
|
44
|
+
csv << record
|
45
|
+
.transform_keys(&:to_sym)
|
46
|
+
.values_at(*headers)
|
47
|
+
.map { |value| force_utf8(value) }
|
24
48
|
end
|
25
49
|
end
|
50
|
+
|
51
|
+
io.write(output)
|
52
|
+
io.close
|
26
53
|
end
|
27
54
|
end
|
28
55
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Chronicle
|
4
|
+
module ETL
|
5
|
+
module Loaders
|
6
|
+
module Helpers
|
7
|
+
module EncodingHelper
|
8
|
+
# Mostly useful for handling loading with binary data from a raw extraction
|
9
|
+
def force_utf8(value)
|
10
|
+
return value unless value.is_a?(String)
|
11
|
+
|
12
|
+
value.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,11 +1,17 @@
|
|
1
|
+
require_relative 'helpers/encoding_helper'
|
2
|
+
|
1
3
|
module Chronicle
|
2
4
|
module ETL
|
3
5
|
# Abstract class representing a Loader for an ETL job
|
4
6
|
class Loader
|
5
7
|
extend Chronicle::ETL::Registry::SelfRegistering
|
6
8
|
include Chronicle::ETL::Configurable
|
9
|
+
include Chronicle::ETL::Loaders::Helpers::EncodingHelper
|
7
10
|
|
8
11
|
setting :output
|
12
|
+
setting :fields
|
13
|
+
setting :fields_limit, default: nil
|
14
|
+
setting :fields_exclude
|
9
15
|
|
10
16
|
# Construct a new instance of this loader. Options are passed in from a Runner
|
11
17
|
# == Parameters:
|
@@ -25,6 +31,23 @@ module Chronicle
|
|
25
31
|
|
26
32
|
# Called once there are no more records to process
|
27
33
|
def finish; end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_headers(records)
|
38
|
+
headers =
|
39
|
+
if @config.fields && @config.fields.any?
|
40
|
+
Set[*@config.fields]
|
41
|
+
else
|
42
|
+
# use all the keys of the flattened record hash
|
43
|
+
Set[*records.map(&:keys).flatten.map(&:to_s).uniq]
|
44
|
+
end
|
45
|
+
|
46
|
+
headers = headers.delete_if { |header| header.end_with?(*@config.fields_exclude) }
|
47
|
+
headers = headers.first(@config.fields_limit) if @config.fields_limit
|
48
|
+
|
49
|
+
headers.to_a.map(&:to_sym)
|
50
|
+
end
|
28
51
|
end
|
29
52
|
end
|
30
53
|
end
|
@@ -9,11 +9,10 @@ module Chronicle
|
|
9
9
|
r.description = 'an ASCII table'
|
10
10
|
end
|
11
11
|
|
12
|
-
setting :fields_limit, default: nil
|
13
|
-
setting :fields_exclude, default: ['lids', 'type']
|
14
|
-
setting :fields, default: []
|
15
12
|
setting :truncate_values_at, default: 40
|
16
13
|
setting :table_renderer, default: :basic
|
14
|
+
setting :fields_exclude, default: ['lids', 'type']
|
15
|
+
setting :header_row, default: true
|
17
16
|
|
18
17
|
def load(record)
|
19
18
|
records << record.to_h_flattened
|
@@ -25,7 +24,7 @@ module Chronicle
|
|
25
24
|
headers = build_headers(records)
|
26
25
|
rows = build_rows(records, headers)
|
27
26
|
|
28
|
-
@table = TTY::Table.new(header: headers, rows: rows)
|
27
|
+
@table = TTY::Table.new(header: (headers if @config.header_row), rows: rows)
|
29
28
|
puts @table.render(
|
30
29
|
@config.table_renderer.to_sym,
|
31
30
|
padding: [0, 2, 0, 0]
|
@@ -38,25 +37,10 @@ module Chronicle
|
|
38
37
|
|
39
38
|
private
|
40
39
|
|
41
|
-
def build_headers(records)
|
42
|
-
headers =
|
43
|
-
if @config.fields.any?
|
44
|
-
Set[*@config.fields]
|
45
|
-
else
|
46
|
-
# use all the keys of the flattened record hash
|
47
|
-
Set[*records.map(&:keys).flatten.map(&:to_s).uniq]
|
48
|
-
end
|
49
|
-
|
50
|
-
headers = headers.delete_if { |header| header.end_with?(*@config.fields_exclude) } if @config.fields_exclude.any?
|
51
|
-
headers = headers.first(@config.fields_limit) if @config.fields_limit
|
52
|
-
|
53
|
-
headers.to_a.map(&:to_sym)
|
54
|
-
end
|
55
|
-
|
56
40
|
def build_rows(records, headers)
|
57
41
|
records.map do |record|
|
58
42
|
values = record.transform_keys(&:to_sym).values_at(*headers).map{|value| value.to_s }
|
59
|
-
|
43
|
+
values = values.map { |value| force_utf8(value) }
|
60
44
|
if @config.truncate_values_at
|
61
45
|
values = values.map{ |value| value.truncate(@config.truncate_values_at) }
|
62
46
|
end
|
data/lib/chronicle/etl/logger.rb
CHANGED
@@ -13,7 +13,6 @@ module Chronicle
|
|
13
13
|
attr_accessor :log_level
|
14
14
|
|
15
15
|
@log_level = INFO
|
16
|
-
@destination = $stderr
|
17
16
|
|
18
17
|
def output message, level
|
19
18
|
return unless level >= @log_level
|
@@ -21,10 +20,14 @@ module Chronicle
|
|
21
20
|
if @progress_bar
|
22
21
|
@progress_bar.log(message)
|
23
22
|
else
|
24
|
-
|
23
|
+
$stderr.puts(message)
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
27
|
+
def fatal(message)
|
28
|
+
output(message, FATAL)
|
29
|
+
end
|
30
|
+
|
28
31
|
def error(message)
|
29
32
|
output(message, ERROR)
|
30
33
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rubygems/command'
|
3
|
+
require 'rubygems/commands/install_command'
|
4
|
+
require 'rubygems/uninstaller'
|
5
|
+
|
6
|
+
module Chronicle
|
7
|
+
module ETL
|
8
|
+
module Registry
|
9
|
+
# Responsible for managing plugins available to chronicle-etl
|
10
|
+
#
|
11
|
+
# @todo Better validation for whether a gem is actually a plugin
|
12
|
+
# @todo Add ways to load a plugin that don't require a gem on rubygems.org
|
13
|
+
module PluginRegistry
|
14
|
+
# Does this plugin exist?
|
15
|
+
def self.exists?(name)
|
16
|
+
# TODO: implement this. Could query rubygems.org or have a
|
17
|
+
# hardcoded approved list
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
# All versions of all plugins currently installed
|
22
|
+
def self.all_installed
|
23
|
+
# TODO: add check for chronicle-etl dependency
|
24
|
+
Gem::Specification.filter { |s| s.name.match(/^chronicle-/) && s.name != "chronicle-etl" }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Latest version of each installed plugin
|
28
|
+
def self.all_installed_latest
|
29
|
+
all_installed.group_by(&:name)
|
30
|
+
.transform_values { |versions| versions.sort_by(&:version).reverse.first }
|
31
|
+
.values
|
32
|
+
end
|
33
|
+
|
34
|
+
# Activate a plugin with given name by `require`ing it
|
35
|
+
def self.activate(name)
|
36
|
+
# By default, activates the latest available version of a gem
|
37
|
+
# so don't have to run Kernel#gem separately
|
38
|
+
require "chronicle/#{name}"
|
39
|
+
rescue LoadError
|
40
|
+
raise Chronicle::ETL::PluginLoadError.new(name), "Plugin #{name} couldn't be loaded" if exists?(name)
|
41
|
+
|
42
|
+
raise Chronicle::ETL::PluginNotAvailableError.new(name), "Plugin #{name} doesn't exist"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Install a plugin to local gems
|
46
|
+
def self.install(name)
|
47
|
+
gem_name = "chronicle-#{name}"
|
48
|
+
raise(Chronicle::ETL::PluginNotAvailableError.new(gem_name), "Plugin #{name} doesn't exist") unless exists?(gem_name)
|
49
|
+
|
50
|
+
Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
|
51
|
+
Gem.install(gem_name)
|
52
|
+
rescue Gem::UnsatisfiableDependencyError
|
53
|
+
# TODO: we need to catch a lot more than this here
|
54
|
+
raise Chronicle::ETL::PluginNotAvailableError.new(name), "Plugin #{name} doesn't exist"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Uninstall a plugin
|
58
|
+
def self.uninstall(name)
|
59
|
+
gem_name = "chronicle-#{name}"
|
60
|
+
Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
|
61
|
+
uninstaller = Gem::Uninstaller.new(gem_name)
|
62
|
+
uninstaller.uninstall
|
63
|
+
rescue Gem::InstallError
|
64
|
+
# TODO: strengthen this exception handling
|
65
|
+
raise(Chronicle::ETL::PluginError.new(name), "Plugin #{name} wasn't uninstalled")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -20,28 +20,40 @@ module Chronicle
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
24
|
-
|
25
|
-
Gem.install(gem_name)
|
23
|
+
def register connector
|
24
|
+
connectors << connector
|
26
25
|
end
|
27
26
|
|
28
|
-
def
|
27
|
+
def connectors
|
29
28
|
@connectors ||= []
|
30
|
-
@connectors << connector
|
31
29
|
end
|
32
30
|
|
33
31
|
def find_by_phase_and_identifier(phase, identifier)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
32
|
+
# Simple case: built in connector
|
33
|
+
connector = connectors.find { |c| c.phase == phase && c.identifier == identifier }
|
34
|
+
return connector if connector
|
35
|
+
|
36
|
+
# if not available in built-in connectors, try to activate a
|
37
|
+
# relevant plugin and try again
|
38
|
+
if identifier.include?(":")
|
39
|
+
plugin, name = identifier.split(":")
|
40
|
+
else
|
41
|
+
# This case handles the case where the identifier is a
|
42
|
+
# shorthand (ie `imessage`) because there's only one default
|
43
|
+
# connector.
|
44
|
+
plugin = identifier
|
39
45
|
end
|
40
|
-
connector || raise(ConnectorNotAvailableError.new("Connector '#{identifier}' not found"))
|
41
|
-
end
|
42
46
|
|
43
|
-
|
44
|
-
|
47
|
+
PluginRegistry.activate(plugin)
|
48
|
+
|
49
|
+
candidates = connectors.select { |c| c.phase == phase && c.plugin == plugin }
|
50
|
+
# if no name given, just use first connector with right phase/plugin
|
51
|
+
# TODO: set up a property for connectors to specify that they're the
|
52
|
+
# default connector for the plugin
|
53
|
+
candidates = candidates.select { |c| c.identifier == name } if name
|
54
|
+
connector = candidates.first
|
55
|
+
|
56
|
+
connector || raise(ConnectorNotAvailableError, "Connector '#{identifier}' not found")
|
45
57
|
end
|
46
58
|
end
|
47
59
|
end
|
@@ -50,3 +62,4 @@ end
|
|
50
62
|
|
51
63
|
require_relative 'self_registering'
|
52
64
|
require_relative 'connector_registration'
|
65
|
+
require_relative 'plugin_registry'
|
data/lib/chronicle/etl/runner.rb
CHANGED
@@ -8,19 +8,41 @@ class Chronicle::ETL::Runner
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def run!
|
11
|
-
|
12
|
-
|
11
|
+
validate_job
|
12
|
+
instantiate_connectors
|
13
|
+
prepare_job
|
14
|
+
prepare_ui
|
15
|
+
run_extraction
|
16
|
+
finish_job
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_job
|
22
|
+
@job.job_definition.validate!
|
23
|
+
end
|
24
|
+
|
25
|
+
def instantiate_connectors
|
26
|
+
@extractor = @job.instantiate_extractor
|
27
|
+
@loader = @job.instantiate_loader
|
28
|
+
end
|
13
29
|
|
30
|
+
def prepare_job
|
31
|
+
Chronicle::ETL::Logger.info(tty_log_job_start)
|
14
32
|
@job_logger.start
|
15
|
-
loader.start
|
33
|
+
@loader.start
|
34
|
+
@extractor.prepare
|
35
|
+
end
|
16
36
|
|
17
|
-
|
18
|
-
total = extractor.results_count
|
37
|
+
def prepare_ui
|
38
|
+
total = @extractor.results_count
|
19
39
|
@progress_bar = Chronicle::ETL::Utils::ProgressBar.new(title: 'Running job', total: total)
|
20
40
|
Chronicle::ETL::Logger.attach_to_progress_bar(@progress_bar)
|
41
|
+
end
|
21
42
|
|
22
|
-
|
23
|
-
|
43
|
+
# TODO: refactor this further
|
44
|
+
def run_extraction
|
45
|
+
@extractor.extract do |extraction|
|
24
46
|
unless extraction.is_a?(Chronicle::ETL::Extraction)
|
25
47
|
raise Chronicle::ETL::RunnerTypeError, "Extracted should be a Chronicle::ETL::Extraction"
|
26
48
|
end
|
@@ -28,15 +50,10 @@ class Chronicle::ETL::Runner
|
|
28
50
|
transformer = @job.instantiate_transformer(extraction)
|
29
51
|
record = transformer.transform
|
30
52
|
|
31
|
-
# TODO: rethink this
|
32
|
-
# unless record.is_a?(Chronicle::ETL::Models)
|
33
|
-
# raise Chronicle::ETL::RunnerTypeError, "Transformed data should be a type of Chronicle::ETL::Models"
|
34
|
-
# end
|
35
|
-
|
36
53
|
Chronicle::ETL::Logger.info(tty_log_transformation(transformer))
|
37
54
|
@job_logger.log_transformation(transformer)
|
38
55
|
|
39
|
-
loader.load(record) unless @job.dry_run?
|
56
|
+
@loader.load(record) unless @job.dry_run?
|
40
57
|
rescue Chronicle::ETL::TransformationError => e
|
41
58
|
Chronicle::ETL::Logger.error(tty_log_transformation_failure(e))
|
42
59
|
ensure
|
@@ -44,22 +61,22 @@ class Chronicle::ETL::Runner
|
|
44
61
|
end
|
45
62
|
|
46
63
|
@progress_bar.finish
|
47
|
-
loader.finish
|
64
|
+
@loader.finish
|
48
65
|
@job_logger.finish
|
49
66
|
rescue Interrupt
|
50
67
|
Chronicle::ETL::Logger.error("\n#{'Job interrupted'.red}")
|
51
68
|
@job_logger.error
|
52
69
|
rescue StandardError => e
|
53
70
|
raise e
|
54
|
-
|
71
|
+
end
|
72
|
+
|
73
|
+
def finish_job
|
55
74
|
@job_logger.save
|
56
75
|
@progress_bar&.finish
|
57
76
|
Chronicle::ETL::Logger.detach_from_progress_bar
|
58
77
|
Chronicle::ETL::Logger.info(tty_log_completion)
|
59
78
|
end
|
60
79
|
|
61
|
-
private
|
62
|
-
|
63
80
|
def tty_log_job_start
|
64
81
|
output = "Beginning job "
|
65
82
|
output += "'#{@job.name}'".bold if @job.name
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chronicle-etl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Louis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-03-
|
11
|
+
date: 2022-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '1.2'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: thor-hollaback
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.2'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.2'
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
168
|
name: tty-progressbar
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +178,20 @@ dependencies:
|
|
164
178
|
- - "~>"
|
165
179
|
- !ruby/object:Gem::Version
|
166
180
|
version: '0.17'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: tty-spinner
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - ">="
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0'
|
188
|
+
type: :runtime
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
167
195
|
- !ruby/object:Gem::Dependency
|
168
196
|
name: tty-table
|
169
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,6 +206,20 @@ dependencies:
|
|
178
206
|
- - "~>"
|
179
207
|
- !ruby/object:Gem::Version
|
180
208
|
version: '0.11'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: tty-prompt
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0.23'
|
216
|
+
type: :runtime
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0.23'
|
181
223
|
- !ruby/object:Gem::Dependency
|
182
224
|
name: bundler
|
183
225
|
requirement: !ruby/object:Gem::Requirement
|
@@ -320,6 +362,7 @@ files:
|
|
320
362
|
- lib/chronicle/etl/cli/connectors.rb
|
321
363
|
- lib/chronicle/etl/cli/jobs.rb
|
322
364
|
- lib/chronicle/etl/cli/main.rb
|
365
|
+
- lib/chronicle/etl/cli/plugins.rb
|
323
366
|
- lib/chronicle/etl/cli/subcommand_base.rb
|
324
367
|
- lib/chronicle/etl/config.rb
|
325
368
|
- lib/chronicle/etl/configurable.rb
|
@@ -336,6 +379,7 @@ files:
|
|
336
379
|
- lib/chronicle/etl/job_log.rb
|
337
380
|
- lib/chronicle/etl/job_logger.rb
|
338
381
|
- lib/chronicle/etl/loaders/csv_loader.rb
|
382
|
+
- lib/chronicle/etl/loaders/helpers/encoding_helper.rb
|
339
383
|
- lib/chronicle/etl/loaders/json_loader.rb
|
340
384
|
- lib/chronicle/etl/loaders/loader.rb
|
341
385
|
- lib/chronicle/etl/loaders/rest_loader.rb
|
@@ -347,6 +391,7 @@ files:
|
|
347
391
|
- lib/chronicle/etl/models/entity.rb
|
348
392
|
- lib/chronicle/etl/models/raw.rb
|
349
393
|
- lib/chronicle/etl/registry/connector_registration.rb
|
394
|
+
- lib/chronicle/etl/registry/plugin_registry.rb
|
350
395
|
- lib/chronicle/etl/registry/registry.rb
|
351
396
|
- lib/chronicle/etl/registry/self_registering.rb
|
352
397
|
- lib/chronicle/etl/runner.rb
|
@@ -384,7 +429,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
384
429
|
- !ruby/object:Gem::Version
|
385
430
|
version: '0'
|
386
431
|
requirements: []
|
387
|
-
rubygems_version: 3.
|
432
|
+
rubygems_version: 3.3.9
|
388
433
|
signing_key:
|
389
434
|
specification_version: 4
|
390
435
|
summary: ETL tool for personal data
|