chronicle-etl 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +35 -0
- data/.rubocop.yml +28 -1
- data/Guardfile +7 -0
- data/README.md +2 -2
- data/Rakefile +4 -2
- data/chronicle-etl.gemspec +8 -3
- data/exe/chronicle-etl +1 -1
- data/lib/chronicle/etl/cli/connectors.rb +34 -0
- data/lib/chronicle/etl/cli/jobs.rb +36 -7
- data/lib/chronicle/etl/cli/main.rb +13 -19
- data/lib/chronicle/etl/cli/subcommand_base.rb +2 -2
- data/lib/chronicle/etl/cli.rb +7 -0
- data/lib/chronicle/etl/configurable.rb +150 -0
- data/lib/chronicle/etl/exceptions.rb +2 -0
- data/lib/chronicle/etl/extractors/csv_extractor.rb +6 -12
- data/lib/chronicle/etl/extractors/extractor.rb +15 -14
- data/lib/chronicle/etl/extractors/file_extractor.rb +7 -4
- data/lib/chronicle/etl/extractors/json_extractor.rb +2 -10
- data/lib/chronicle/etl/job.rb +1 -1
- data/lib/chronicle/etl/loaders/csv_loader.rb +1 -1
- data/lib/chronicle/etl/loaders/loader.rb +4 -1
- data/lib/chronicle/etl/loaders/rest_loader.rb +5 -5
- data/lib/chronicle/etl/loaders/table_loader.rb +13 -19
- data/lib/chronicle/etl/registry/connector_registration.rb +1 -0
- data/lib/chronicle/etl/transformers/image_file_transformer.rb +22 -28
- data/lib/chronicle/etl/transformers/transformer.rb +3 -2
- data/lib/chronicle/etl/version.rb +1 -1
- data/lib/chronicle/etl.rb +1 -0
- metadata +74 -15
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fd411a9a41a645b85780230c79b09f361e121d0e8ca7f3270ca8eba55a76ca8
|
4
|
+
data.tar.gz: c09053715910ab4f027fbdc3a5b7d10c042eee962f7fa93c6571ce8359f51009
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c9ec14b6c0a51f1c5ec77ee8d9a7f016d16bdc35db5634f9fa5d38aabc30dec201cd4b8bef06a31b86773a0c1cda2d271d7008dcb247a86d956c094919f3c0f
|
7
|
+
data.tar.gz: 0dca41e1654e5b2b98a148f853492a67126cdac767000b3c5f97c5c8ff88b77464e17a2fab38b72c1f014f3515c911e5f3f391eaf68d64e73dcfcff5d8e6cb6a
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ master ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ master ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
test:
|
18
|
+
|
19
|
+
runs-on: ubuntu-latest
|
20
|
+
strategy:
|
21
|
+
matrix:
|
22
|
+
ruby-version: ['2.7', '3.0']
|
23
|
+
|
24
|
+
steps:
|
25
|
+
- uses: actions/checkout@v2
|
26
|
+
- name: Set up Ruby
|
27
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
28
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
29
|
+
# uses: ruby/setup-ruby@v1
|
30
|
+
uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
|
31
|
+
with:
|
32
|
+
ruby-version: ${{ matrix.ruby-version }}
|
33
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
34
|
+
- name: Run tests
|
35
|
+
run: bundle exec rake
|
data/.rubocop.yml
CHANGED
@@ -1,11 +1,38 @@
|
|
1
1
|
AllCops:
|
2
2
|
EnabledByDefault: true
|
3
|
+
TargetRubyVersion: 2.7
|
4
|
+
|
5
|
+
Style/FrozenStringLiteralComment:
|
6
|
+
SafeAutoCorrect: true
|
3
7
|
|
4
8
|
Style/StringLiterals:
|
5
9
|
Enabled: false
|
6
10
|
|
11
|
+
Layout/MultilineAssignmentLayout:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Layout/RedundantLineBreak:
|
15
|
+
Enabled: false
|
16
|
+
|
7
17
|
Style/MethodCallWithArgsParentheses:
|
8
18
|
Enabled: false
|
9
19
|
|
20
|
+
Style/MethodCalledOnDoEndBlock:
|
21
|
+
Exclude:
|
22
|
+
- 'spec/**/*'
|
23
|
+
|
24
|
+
Style/OpenStructUse:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Style/Copyright:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Style/SymbolArray:
|
31
|
+
EnforcedStyle: brackets
|
32
|
+
|
33
|
+
Style/WordArray:
|
34
|
+
EnforcedStyle: brackets
|
35
|
+
|
10
36
|
Lint/ConstantResolution:
|
11
|
-
Enabled: false
|
37
|
+
Enabled: false
|
38
|
+
|
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Chronicle::ETL
|
2
2
|
|
3
|
-
[![Gem Version](https://badge.fury.io/rb/chronicle-etl.svg)](https://badge.fury.io/rb/chronicle-etl)
|
3
|
+
[![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
4
|
|
5
5
|
Chronicle ETL is a utility that helps you archive and processes personal data. You can *extract* it from a variety of sources, *transform* it, and *load* it to an external API, file, or stdout.
|
6
6
|
|
@@ -57,7 +57,7 @@ Built in connectors:
|
|
57
57
|
In addition to the built-in importers, importers for third-party platforms are available. They are packaged as individual Ruby gems.
|
58
58
|
|
59
59
|
- [email](https://github.com/chronicle-app/chronicle-email). Extractors for `mbox` and other email files
|
60
|
-
- [
|
60
|
+
- [shell](https://github.com/chronicle-app/chronicle-shell). Extract shell history from Bash or Zsh`
|
61
61
|
- [imessage](https://github.com/chronicle-app/chronicle-imessage). Extract iMessage messages from a local macOS installation
|
62
62
|
|
63
63
|
To install any of these, run `gem install chronicle-PROVIDER`.
|
data/Rakefile
CHANGED
data/chronicle-etl.gemspec
CHANGED
@@ -35,17 +35,18 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.bindir = "exe"
|
36
36
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
37
37
|
spec.require_paths = ["lib"]
|
38
|
+
spec.required_ruby_version = ">= 2.7"
|
38
39
|
|
39
|
-
spec.add_dependency "activesupport"
|
40
|
+
spec.add_dependency "activesupport", "~> 7.0"
|
40
41
|
spec.add_dependency "chronic_duration", "~> 0.10.6"
|
41
42
|
spec.add_dependency "colorize", "~> 0.8.1"
|
42
43
|
spec.add_dependency "marcel", "~> 1.0.2"
|
43
44
|
spec.add_dependency "mini_exiftool", "~> 2.10"
|
44
45
|
spec.add_dependency "nokogiri", "~> 1.13"
|
45
|
-
spec.add_dependency "runcom", "
|
46
|
+
spec.add_dependency "runcom", ">= 6.0"
|
46
47
|
spec.add_dependency "sequel", "~> 5.35"
|
47
48
|
spec.add_dependency "sqlite3", "~> 1.4"
|
48
|
-
spec.add_dependency "thor", "~>
|
49
|
+
spec.add_dependency "thor", "~> 1.2"
|
49
50
|
spec.add_dependency "tty-progressbar", "~> 0.17"
|
50
51
|
spec.add_dependency "tty-table", "~> 0.11"
|
51
52
|
|
@@ -53,4 +54,8 @@ Gem::Specification.new do |spec|
|
|
53
54
|
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
54
55
|
spec.add_development_dependency "rake", "~> 13.0"
|
55
56
|
spec.add_development_dependency "rspec", "~> 3.9"
|
57
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
58
|
+
spec.add_development_dependency "guard-rspec", "~> 4.7.3"
|
59
|
+
spec.add_development_dependency "yard", "~> 0.9.7"
|
60
|
+
spec.add_development_dependency "rubocop", "~> 1.25.1"
|
56
61
|
end
|
data/exe/chronicle-etl
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chronicle
|
2
4
|
module ETL
|
3
5
|
module CLI
|
@@ -38,6 +40,38 @@ module Chronicle
|
|
38
40
|
table = TTY::Table.new(headers, connector_info.map(&:values))
|
39
41
|
puts table.render(indent: 0, padding: [0, 2])
|
40
42
|
end
|
43
|
+
|
44
|
+
desc "show PHASE IDENTIFIER", "Show information about a connector"
|
45
|
+
def show(phase, identifier)
|
46
|
+
unless ['extractor', 'transformer', 'loader'].include?(phase)
|
47
|
+
puts "phase argument must be one of: [extractor, transformer, loader]"
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
connector = Chronicle::ETL::Registry.find_by_phase_and_identifier(phase.to_sym, identifier)
|
53
|
+
rescue Chronicle::ETL::ConnectorNotAvailableError
|
54
|
+
puts "Could not find #{phase} #{identifier}"
|
55
|
+
return
|
56
|
+
end
|
57
|
+
|
58
|
+
puts connector.klass.to_s.bold
|
59
|
+
puts " #{connector.descriptive_phrase}"
|
60
|
+
puts
|
61
|
+
puts "OPTIONS"
|
62
|
+
|
63
|
+
headers = ['name', 'default', 'required'].map{ |h| h.to_s.upcase.bold }
|
64
|
+
|
65
|
+
settings = connector.klass.settings.map do |name, setting|
|
66
|
+
[
|
67
|
+
name,
|
68
|
+
setting.default,
|
69
|
+
setting.required ? 'yes' : 'no'
|
70
|
+
]
|
71
|
+
end
|
72
|
+
table = TTY::Table.new(headers, settings)
|
73
|
+
puts table.render(indent: 0, padding: [0, 2])
|
74
|
+
end
|
41
75
|
end
|
42
76
|
end
|
43
77
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'pp'
|
2
|
+
|
2
3
|
module Chronicle
|
3
4
|
module ETL
|
4
5
|
module CLI
|
@@ -7,15 +8,29 @@ module Chronicle
|
|
7
8
|
default_task "start"
|
8
9
|
namespace :jobs
|
9
10
|
|
11
|
+
class_option :name, aliases: '-j', desc: 'Job configuration name'
|
12
|
+
|
10
13
|
class_option :extractor, aliases: '-e', desc: "Extractor class. Default: stdin", banner: 'extractor-name'
|
11
14
|
class_option :'extractor-opts', desc: 'Extractor options', type: :hash, default: {}
|
12
15
|
class_option :transformer, aliases: '-t', desc: 'Transformer class. Default: null', banner: 'transformer-name'
|
13
16
|
class_option :'transformer-opts', desc: 'Transformer options', type: :hash, default: {}
|
14
17
|
class_option :loader, aliases: '-l', desc: 'Loader class. Default: stdout', banner: 'loader-name'
|
15
18
|
class_option :'loader-opts', desc: 'Loader options', type: :hash, default: {}
|
16
|
-
class_option :name, aliases: '-j', desc: 'Job configuration name'
|
17
19
|
|
18
|
-
|
20
|
+
# This is an array to deal with shell globbing
|
21
|
+
class_option :input, aliases: '-i', desc: 'Input filename or directory', default: [], type: 'array', banner: 'FILENAME'
|
22
|
+
class_option :since, desc: "Load records SINCE this date. Overrides job's `load_since` configuration option in extractor's options", banner: 'DATE'
|
23
|
+
class_option :until, desc: "Load records UNTIL this date", banner: 'DATE'
|
24
|
+
class_option :limit, desc: "Only extract the first LIMIT records", banner: 'N'
|
25
|
+
|
26
|
+
class_option :output, aliases: '-o', desc: 'Output filename', type: 'string'
|
27
|
+
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
|
+
|
32
|
+
# Thor doesn't like `run` as a command name
|
33
|
+
map run: :start
|
19
34
|
desc "run", "Start a job"
|
20
35
|
option :log_level, desc: 'Log level (debug, info, warn, error, fatal)', default: 'info'
|
21
36
|
option :verbose, aliases: '-v', desc: 'Set log level to verbose', type: :boolean
|
@@ -69,7 +84,7 @@ LONG_DESC
|
|
69
84
|
[job, extractor, transformer, loader]
|
70
85
|
end
|
71
86
|
|
72
|
-
headers = ['name', 'extractor', 'transformer', 'loader'].map{|h| h.upcase.bold }
|
87
|
+
headers = ['name', 'extractor', 'transformer', 'loader'].map { |h| h.upcase.bold }
|
73
88
|
|
74
89
|
table = TTY::Table.new(headers, job_details)
|
75
90
|
puts table.render(indent: 0, padding: [0, 2])
|
@@ -90,7 +105,7 @@ LONG_DESC
|
|
90
105
|
def build_job_definition(options)
|
91
106
|
definition = Chronicle::ETL::JobDefinition.new
|
92
107
|
definition.add_config(load_job_config(options[:name]))
|
93
|
-
definition.add_config(process_flag_options(options))
|
108
|
+
definition.add_config(process_flag_options(options).transform_keys(&:to_sym))
|
94
109
|
definition
|
95
110
|
end
|
96
111
|
|
@@ -100,19 +115,33 @@ LONG_DESC
|
|
100
115
|
|
101
116
|
# Takes flag options and turns them into a runner config
|
102
117
|
def process_flag_options options
|
118
|
+
extractor_options = options[:'extractor-opts'].merge({
|
119
|
+
filename: (options[:input] if options[:input].any?),
|
120
|
+
since: options[:since],
|
121
|
+
until: options[:until],
|
122
|
+
limit: options[:limit],
|
123
|
+
}.compact)
|
124
|
+
|
125
|
+
transformer_options = options[:'transformer-opts']
|
126
|
+
|
127
|
+
loader_options = options[:'loader-opts'].merge({
|
128
|
+
output: options[:output],
|
129
|
+
fields: options[:fields]
|
130
|
+
}.compact)
|
131
|
+
|
103
132
|
{
|
104
133
|
dry_run: options[:dry_run],
|
105
134
|
extractor: {
|
106
135
|
name: options[:extractor],
|
107
|
-
options:
|
136
|
+
options: extractor_options
|
108
137
|
}.compact,
|
109
138
|
transformer: {
|
110
139
|
name: options[:transformer],
|
111
|
-
options:
|
140
|
+
options: transformer_options
|
112
141
|
}.compact,
|
113
142
|
loader: {
|
114
143
|
name: options[:loader],
|
115
|
-
options:
|
144
|
+
options: loader_options
|
116
145
|
}.compact
|
117
146
|
}
|
118
147
|
end
|
@@ -1,17 +1,10 @@
|
|
1
|
-
require 'thor'
|
2
|
-
require 'chronicle/etl'
|
3
1
|
require 'colorize'
|
4
2
|
|
5
|
-
require 'chronicle/etl/cli/subcommand_base'
|
6
|
-
require 'chronicle/etl/cli/connectors'
|
7
|
-
require 'chronicle/etl/cli/jobs'
|
8
|
-
|
9
3
|
module Chronicle
|
10
4
|
module ETL
|
11
5
|
module CLI
|
12
6
|
# Main entrypoint for CLI app
|
13
|
-
class Main < Thor
|
14
|
-
class_option "verbose", type: :boolean, default: false
|
7
|
+
class Main < ::Thor
|
15
8
|
default_task "jobs"
|
16
9
|
|
17
10
|
desc 'connectors:COMMAND', 'Connectors available for ETL jobs', hide: true
|
@@ -22,15 +15,6 @@ module Chronicle
|
|
22
15
|
|
23
16
|
# Entrypoint for the CLI
|
24
17
|
def self.start(given_args = ARGV, config = {})
|
25
|
-
if given_args[0] == "--version"
|
26
|
-
puts "#{Chronicle::ETL::VERSION}"
|
27
|
-
exit
|
28
|
-
end
|
29
|
-
|
30
|
-
if given_args.none?
|
31
|
-
abort "No command entered or job specified. To see commands, run `chronicle-etl help`".red
|
32
|
-
end
|
33
|
-
|
34
18
|
# take a subcommand:command and splits them so Thor knows how to hand off to the subcommand class
|
35
19
|
if given_args.any? && given_args[0].include?(':')
|
36
20
|
commands = given_args.shift.split(':')
|
@@ -40,10 +24,20 @@ module Chronicle
|
|
40
24
|
super(given_args, config)
|
41
25
|
end
|
42
26
|
|
27
|
+
def self.exit_on_failure?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "version", "Show version"
|
32
|
+
map %w(--version -v) => :version
|
33
|
+
def version
|
34
|
+
shell.say "chronicle-etl #{Chronicle::ETL::VERSION}"
|
35
|
+
end
|
36
|
+
|
43
37
|
# Displays help options for chronicle-etl
|
44
38
|
def help(meth = nil, subcommand = false)
|
45
39
|
if meth && !respond_to?(meth)
|
46
|
-
klass, task = Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}")
|
40
|
+
klass, task = ::Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}")
|
47
41
|
klass.start(['-h', task].compact, shell: shell)
|
48
42
|
else
|
49
43
|
shell.say "ABOUT".bold
|
@@ -64,7 +58,7 @@ module Chronicle
|
|
64
58
|
|
65
59
|
list = []
|
66
60
|
|
67
|
-
Thor::Util.thor_classes_in(Chronicle::ETL::CLI).each do |thor_class|
|
61
|
+
::Thor::Util.thor_classes_in(Chronicle::ETL::CLI).each do |thor_class|
|
68
62
|
list += thor_class.printable_tasks(false)
|
69
63
|
end
|
70
64
|
list.sort! { |a, b| a[0] <=> b[0] }
|
@@ -2,11 +2,11 @@ module Chronicle
|
|
2
2
|
module ETL
|
3
3
|
module CLI
|
4
4
|
# Base class for CLI subcommands. Overrides Thor methods so we can use command:subcommand syntax
|
5
|
-
class SubcommandBase < Thor
|
5
|
+
class SubcommandBase < ::Thor
|
6
6
|
# Print usage instructions for a subcommand
|
7
7
|
def self.help(shell, subcommand = false)
|
8
8
|
list = printable_commands(true, subcommand)
|
9
|
-
Thor::Util.thor_classes_in(self).each do |klass|
|
9
|
+
::Thor::Util.thor_classes_in(self).each do |klass|
|
10
10
|
list += klass.printable_commands(false)
|
11
11
|
end
|
12
12
|
list.sort! { |a, b| a[0] <=> b[0] }
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
module Chronicle
|
6
|
+
module ETL
|
7
|
+
# A mixin that gives a class
|
8
|
+
# a {Chronicle::ETL::Configurable::ClassMethods#setting} macro to define
|
9
|
+
# settings and their properties (require, type, etc)
|
10
|
+
#
|
11
|
+
# @example Basic usage
|
12
|
+
# class Test < Chronicle::ETL::Extractor
|
13
|
+
# include Chronicle::ETL::Configurable
|
14
|
+
# setting :when, type: :date, required: true
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# t = Test.new(when: '2022-02-24')
|
18
|
+
# t.config.when
|
19
|
+
module Configurable
|
20
|
+
# An individual setting for this Configurable
|
21
|
+
Setting = Struct.new(:default, :required, :type)
|
22
|
+
private_constant :Setting
|
23
|
+
|
24
|
+
# Collection of user-supplied options for this Configurable
|
25
|
+
class Config < OpenStruct
|
26
|
+
# Config values that aren't nil, as a hash
|
27
|
+
def compacted_h
|
28
|
+
to_h.compact
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def self.included(klass)
|
34
|
+
klass.extend(ClassMethods)
|
35
|
+
klass.include(InstanceMethods)
|
36
|
+
klass.prepend(Initializer)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initializer method for classes that have Configurable mixed in
|
40
|
+
module Initializer
|
41
|
+
# Make sure this class has a default @config ready to use
|
42
|
+
def initialize(*args)
|
43
|
+
@config = initialize_default_config
|
44
|
+
super
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Instance methods for classes that have Configurable mixed in
|
49
|
+
module InstanceMethods
|
50
|
+
attr_reader :config
|
51
|
+
|
52
|
+
# Take given options and apply them to this class's settings
|
53
|
+
# and make them available in @config and validates that they
|
54
|
+
# conform to setting rules
|
55
|
+
def apply_options(options)
|
56
|
+
options.transform_keys!(&:to_sym)
|
57
|
+
|
58
|
+
options.each do |name, value|
|
59
|
+
setting = self.class.all_settings[name]
|
60
|
+
raise(Chronicle::ETL::ConfigurationError, "Unrecognized setting: #{name}") unless setting
|
61
|
+
|
62
|
+
@config[name] = coerced_value(setting, value)
|
63
|
+
end
|
64
|
+
validate_config
|
65
|
+
options
|
66
|
+
end
|
67
|
+
|
68
|
+
# Name of all settings available to this class
|
69
|
+
def self.settings
|
70
|
+
self.class.all_settings.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def initialize_default_config
|
76
|
+
self.class.config_with_defaults
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_config
|
80
|
+
missing = (self.class.all_required_settings.keys - @config.compacted_h.keys)
|
81
|
+
raise Chronicle::ETL::ConfigurationError, "Missing options: #{missing}" if missing.count.positive?
|
82
|
+
end
|
83
|
+
|
84
|
+
def coerced_value(setting, value)
|
85
|
+
setting.type ? __send__("coerce_#{setting.type}", value) : value
|
86
|
+
end
|
87
|
+
|
88
|
+
def coerce_string(value)
|
89
|
+
value.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
def coerce_time(value)
|
93
|
+
# TODO: handle durations like '3h'
|
94
|
+
if value.is_a?(String)
|
95
|
+
Time.parse(value)
|
96
|
+
else
|
97
|
+
value
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Class methods for classes that have Configurable mixed in
|
103
|
+
module ClassMethods
|
104
|
+
# Macro for creating a setting on a class {::Chronicle::ETL::Configurable}
|
105
|
+
#
|
106
|
+
# @param [String] name Name of the setting
|
107
|
+
# @param [Boolean] required whether setting is required
|
108
|
+
# @param [Object] default Default value
|
109
|
+
# @param [Symbol] type Type
|
110
|
+
#
|
111
|
+
# @example Basic usage
|
112
|
+
# setting :when, type: :date, required: true
|
113
|
+
#
|
114
|
+
# @see ::Chronicle::ETL::Configurable
|
115
|
+
def setting(name, default: nil, required: false, type: nil)
|
116
|
+
s = Setting.new(default, required, type)
|
117
|
+
settings[name] = s
|
118
|
+
end
|
119
|
+
|
120
|
+
# Collect all settings defined on this class and its ancestors (that
|
121
|
+
# have Configurable mixin included)
|
122
|
+
def all_settings
|
123
|
+
if superclass.include?(Chronicle::ETL::Configurable)
|
124
|
+
superclass.all_settings.merge(settings)
|
125
|
+
else
|
126
|
+
settings
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Filters settings to those that are required.
|
131
|
+
def all_required_settings
|
132
|
+
all_settings.select { |_name, setting| setting.required } || {}
|
133
|
+
end
|
134
|
+
|
135
|
+
def settings
|
136
|
+
@settings ||= {}
|
137
|
+
end
|
138
|
+
|
139
|
+
def setting_exists?(name)
|
140
|
+
all_settings.keys.include? name
|
141
|
+
end
|
142
|
+
|
143
|
+
def config_with_defaults
|
144
|
+
s = all_settings.transform_values(&:default)
|
145
|
+
Config.new(s)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -2,21 +2,15 @@ require 'csv'
|
|
2
2
|
|
3
3
|
module Chronicle
|
4
4
|
module ETL
|
5
|
-
class
|
5
|
+
class CSVExtractor < Chronicle::ETL::Extractor
|
6
6
|
include Extractors::Helpers::FilesystemReader
|
7
7
|
|
8
8
|
register_connector do |r|
|
9
9
|
r.description = 'input as CSV'
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
filename: $stdin
|
15
|
-
}.freeze
|
16
|
-
|
17
|
-
def initialize(options = {})
|
18
|
-
super(DEFAULT_OPTIONS.merge(options))
|
19
|
-
end
|
12
|
+
setting :headers, default: true
|
13
|
+
setting :filename, default: $stdin
|
20
14
|
|
21
15
|
def extract
|
22
16
|
csv = initialize_csv
|
@@ -26,20 +20,20 @@ module Chronicle
|
|
26
20
|
end
|
27
21
|
|
28
22
|
def results_count
|
29
|
-
CSV.read(@
|
23
|
+
CSV.read(@config.filename, headers: @config.headers).count unless stdin?(@config.filename)
|
30
24
|
end
|
31
25
|
|
32
26
|
private
|
33
27
|
|
34
28
|
def initialize_csv
|
35
|
-
headers = @
|
29
|
+
headers = @config.headers.is_a?(String) ? @config.headers.split(',') : @config.headers
|
36
30
|
|
37
31
|
csv_options = {
|
38
32
|
headers: headers,
|
39
33
|
converters: :all
|
40
34
|
}
|
41
35
|
|
42
|
-
open_from_filesystem(filename: @
|
36
|
+
open_from_filesystem(filename: @config.filename) do |file|
|
43
37
|
return CSV.new(file, **csv_options)
|
44
38
|
end
|
45
39
|
end
|
@@ -5,15 +5,20 @@ module Chronicle
|
|
5
5
|
# Abstract class representing an Extractor for an ETL job
|
6
6
|
class Extractor
|
7
7
|
extend Chronicle::ETL::Registry::SelfRegistering
|
8
|
+
include Chronicle::ETL::Configurable
|
9
|
+
|
10
|
+
setting :since, type: :date
|
11
|
+
setting :until, type: :date
|
12
|
+
setting :limit
|
13
|
+
setting :load_after_id
|
14
|
+
setting :filename
|
8
15
|
|
9
16
|
# Construct a new instance of this extractor. Options are passed in from a Runner
|
10
|
-
# ==
|
17
|
+
# == Parameters:
|
11
18
|
# options::
|
12
19
|
# Options for configuring this Extractor
|
13
20
|
def initialize(options = {})
|
14
|
-
|
15
|
-
sanitize_options
|
16
|
-
handle_continuation
|
21
|
+
apply_options(options)
|
17
22
|
end
|
18
23
|
|
19
24
|
# Hook called before #extract. Useful for gathering data, initailizing proxies, etc
|
@@ -30,17 +35,13 @@ module Chronicle
|
|
30
35
|
|
31
36
|
private
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
def handle_continuation
|
39
|
-
return unless @options[:continuation]
|
38
|
+
# TODO: reimplemenet this
|
39
|
+
# def handle_continuation
|
40
|
+
# return unless @config.continuation
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
end
|
42
|
+
# @config.since = @config.continuation.highest_timestamp if @config.continuation.highest_timestamp
|
43
|
+
# @config.load_after_id = @config.continuation.last_id if @config.continuation.last_id
|
44
|
+
# end
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
@@ -9,6 +9,9 @@ module Chronicle
|
|
9
9
|
r.description = 'file or directory of files'
|
10
10
|
end
|
11
11
|
|
12
|
+
# TODO: consolidate this with @config.filename
|
13
|
+
setting :dir_glob_pattern
|
14
|
+
|
12
15
|
def extract
|
13
16
|
filenames.each do |filename|
|
14
17
|
yield Chronicle::ETL::Extraction.new(data: filename)
|
@@ -23,10 +26,10 @@ module Chronicle
|
|
23
26
|
|
24
27
|
def filenames
|
25
28
|
@filenames ||= filenames_in_directory(
|
26
|
-
path: @
|
27
|
-
dir_glob_pattern: @
|
28
|
-
load_since: @
|
29
|
-
load_until: @
|
29
|
+
path: @config.filename,
|
30
|
+
dir_glob_pattern: @config.dir_glob_pattern,
|
31
|
+
load_since: @config.since,
|
32
|
+
load_until: @config.until
|
30
33
|
)
|
31
34
|
end
|
32
35
|
end
|
@@ -7,16 +7,8 @@ module Chronicle
|
|
7
7
|
r.description = 'input as JSON'
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# We're expecting line-separated json objects
|
14
|
-
jsonl: true
|
15
|
-
}.freeze
|
16
|
-
|
17
|
-
def initialize(options = {})
|
18
|
-
super(DEFAULT_OPTIONS.merge(options))
|
19
|
-
end
|
10
|
+
setting :filename, default: $stdin
|
11
|
+
setting :jsonl, default: true
|
20
12
|
|
21
13
|
def extract
|
22
14
|
load_input do |input|
|
data/lib/chronicle/etl/job.rb
CHANGED
@@ -35,7 +35,7 @@ module Chronicle
|
|
35
35
|
|
36
36
|
def instantiate_transformer(extraction)
|
37
37
|
@transformer_klass = @job_definition.transformer_klass
|
38
|
-
@transformer_klass.new(@transformer_options
|
38
|
+
@transformer_klass.new(extraction, @transformer_options)
|
39
39
|
end
|
40
40
|
|
41
41
|
def instantiate_loader
|
@@ -3,13 +3,16 @@ module Chronicle
|
|
3
3
|
# Abstract class representing a Loader for an ETL job
|
4
4
|
class Loader
|
5
5
|
extend Chronicle::ETL::Registry::SelfRegistering
|
6
|
+
include Chronicle::ETL::Configurable
|
7
|
+
|
8
|
+
setting :output
|
6
9
|
|
7
10
|
# Construct a new instance of this loader. Options are passed in from a Runner
|
8
11
|
# == Parameters:
|
9
12
|
# options::
|
10
13
|
# Options for configuring this Loader
|
11
14
|
def initialize(options = {})
|
12
|
-
|
15
|
+
apply_options(options)
|
13
16
|
end
|
14
17
|
|
15
18
|
# Called once before processing records
|
@@ -9,19 +9,19 @@ module Chronicle
|
|
9
9
|
r.description = 'a REST endpoint'
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
setting :hostname, required: true
|
13
|
+
setting :endpoint, required: true
|
14
|
+
setting :access_token
|
15
15
|
|
16
16
|
def load(record)
|
17
17
|
payload = Chronicle::ETL::JSONAPISerializer.serialize(record)
|
18
18
|
# have the outer data key that json-api expects
|
19
19
|
payload = { data: payload } unless payload[:data]
|
20
20
|
|
21
|
-
uri = URI.parse("#{@
|
21
|
+
uri = URI.parse("#{@config.hostname}#{@config.endpoint}")
|
22
22
|
|
23
23
|
header = {
|
24
|
-
"Authorization" => "Bearer #{@
|
24
|
+
"Authorization" => "Bearer #{@config.access_token}",
|
25
25
|
"Content-Type": 'application/json'
|
26
26
|
}
|
27
27
|
use_ssl = uri.scheme == 'https'
|
@@ -9,20 +9,14 @@ module Chronicle
|
|
9
9
|
r.description = 'an ASCII table'
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
table_renderer: :basic
|
18
|
-
}.freeze
|
19
|
-
|
20
|
-
def initialize(options={})
|
21
|
-
@options = options.reverse_merge(DEFAULT_OPTIONS)
|
22
|
-
@records = []
|
23
|
-
end
|
12
|
+
setting :fields_limit, default: nil
|
13
|
+
setting :fields_exclude, default: ['lids', 'type']
|
14
|
+
setting :fields_include, default: []
|
15
|
+
setting :truncate_values_at, default: 40
|
16
|
+
setting :table_renderer, default: :basic
|
24
17
|
|
25
18
|
def load(record)
|
19
|
+
@records ||= []
|
26
20
|
@records << record.to_h_flattened
|
27
21
|
end
|
28
22
|
|
@@ -34,7 +28,7 @@ module Chronicle
|
|
34
28
|
|
35
29
|
@table = TTY::Table.new(header: headers, rows: rows)
|
36
30
|
puts @table.render(
|
37
|
-
@
|
31
|
+
@config.table_renderer.to_sym,
|
38
32
|
padding: [0, 2, 0, 0]
|
39
33
|
)
|
40
34
|
end
|
@@ -43,15 +37,15 @@ module Chronicle
|
|
43
37
|
|
44
38
|
def build_headers(records)
|
45
39
|
headers =
|
46
|
-
if @
|
47
|
-
Set[*@
|
40
|
+
if @config.fields_include.any?
|
41
|
+
Set[*@config.fields_include]
|
48
42
|
else
|
49
43
|
# use all the keys of the flattened record hash
|
50
44
|
Set[*records.map(&:keys).flatten.map(&:to_s).uniq]
|
51
45
|
end
|
52
46
|
|
53
|
-
headers = headers.delete_if { |header| header.end_with?(*@
|
54
|
-
headers = headers.first(@
|
47
|
+
headers = headers.delete_if { |header| header.end_with?(*@config.fields_exclude) } if @config.fields_exclude.any?
|
48
|
+
headers = headers.first(@config.fields_limit) if @config.fields_limit
|
55
49
|
|
56
50
|
headers.to_a.map(&:to_sym)
|
57
51
|
end
|
@@ -60,8 +54,8 @@ module Chronicle
|
|
60
54
|
records.map do |record|
|
61
55
|
values = record.values_at(*headers).map{|value| value.to_s }
|
62
56
|
|
63
|
-
if @
|
64
|
-
values = values.map{ |value| value.truncate(@
|
57
|
+
if @config.truncate_values_at
|
58
|
+
values = values.map{ |value| value.truncate(@config.truncate_values_at) }
|
65
59
|
end
|
66
60
|
|
67
61
|
values
|
@@ -3,6 +3,7 @@ module Chronicle
|
|
3
3
|
module Registry
|
4
4
|
# Records details about a connector such as its provider and a description
|
5
5
|
class ConnectorRegistration
|
6
|
+
# FIXME: refactor custom accessor methods later in file
|
6
7
|
attr_accessor :identifier, :provider, :klass, :description
|
7
8
|
|
8
9
|
def initialize(klass)
|
@@ -19,20 +19,14 @@ module Chronicle
|
|
19
19
|
r.description = 'an image file'
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
}.freeze
|
31
|
-
|
32
|
-
def initialize(*args)
|
33
|
-
super(*args)
|
34
|
-
@options = @options.reverse_merge(DEFAULT_OPTIONS)
|
35
|
-
end
|
22
|
+
setting :timestamp_strategy, default: 'file_mtime'
|
23
|
+
setting :id_strategy, default: 'file_hash'
|
24
|
+
setting :verb, default: 'photographed'
|
25
|
+
# EXIF tags often don't have timezones
|
26
|
+
setting :timezone_default, default: 'Eastern Time (US & Canada)'
|
27
|
+
setting :include_image_data, default: true
|
28
|
+
setting :actor
|
29
|
+
setting :involved
|
36
30
|
|
37
31
|
def transform
|
38
32
|
# FIXME: set @filename; use block for reading file when necessary
|
@@ -48,7 +42,7 @@ module Chronicle
|
|
48
42
|
|
49
43
|
def id
|
50
44
|
@id ||= begin
|
51
|
-
id = build_with_strategy(field: :id, strategy: @
|
45
|
+
id = build_with_strategy(field: :id, strategy: @config.id_strategy)
|
52
46
|
raise UntransformableRecordError.new("Could not build id", transformation: self) unless id
|
53
47
|
|
54
48
|
id
|
@@ -57,7 +51,7 @@ module Chronicle
|
|
57
51
|
|
58
52
|
def timestamp
|
59
53
|
@timestamp ||= begin
|
60
|
-
ts = build_with_strategy(field: :timestamp, strategy: @
|
54
|
+
ts = build_with_strategy(field: :timestamp, strategy: @config.timestamp_strategy)
|
61
55
|
raise UntransformableRecordError.new("Could not build timestamp", transformation: self) unless ts
|
62
56
|
|
63
57
|
ts
|
@@ -68,8 +62,8 @@ module Chronicle
|
|
68
62
|
|
69
63
|
def build_created(file)
|
70
64
|
record = ::Chronicle::ETL::Models::Activity.new
|
71
|
-
record.verb = @
|
72
|
-
record.provider = @
|
65
|
+
record.verb = @config.verb
|
66
|
+
record.provider = @config.provider
|
73
67
|
record.provider_id = id
|
74
68
|
record.end_at = timestamp
|
75
69
|
record.dedupe_on = [[:provider_id, :verb, :provider]]
|
@@ -84,24 +78,24 @@ module Chronicle
|
|
84
78
|
def build_actor
|
85
79
|
actor = ::Chronicle::ETL::Models::Entity.new
|
86
80
|
actor.represents = 'identity'
|
87
|
-
actor.provider = @
|
88
|
-
actor.slug = @
|
81
|
+
actor.provider = @config.actor[:provider]
|
82
|
+
actor.slug = @config.actor[:slug]
|
89
83
|
actor.dedupe_on = [[:provider, :slug, :represents]]
|
90
84
|
actor
|
91
85
|
end
|
92
86
|
|
93
87
|
def build_image
|
94
88
|
image = ::Chronicle::ETL::Models::Entity.new
|
95
|
-
image.represents = @
|
89
|
+
image.represents = @config.involved[:represents]
|
96
90
|
image.title = build_title
|
97
91
|
image.body = exif['Description']
|
98
|
-
image.provider = @
|
92
|
+
image.provider = @config.involved[:provider]
|
99
93
|
image.provider_id = id
|
100
94
|
image.assign_attributes(build_gps)
|
101
95
|
image.dedupe_on = [[:provider, :provider_id, :represents]]
|
102
96
|
|
103
|
-
if @
|
104
|
-
ocr_text = build_with_strategy(field: :ocr, strategy: @
|
97
|
+
if @config.ocr_strategy
|
98
|
+
ocr_text = build_with_strategy(field: :ocr, strategy: @config.ocr_strategy)
|
105
99
|
image.metadata[:ocr_text] = ocr_text if ocr_text
|
106
100
|
end
|
107
101
|
|
@@ -111,7 +105,7 @@ module Chronicle
|
|
111
105
|
image.depicts = build_people_depicted(names)
|
112
106
|
image.abouts = build_keywords(tags)
|
113
107
|
|
114
|
-
if @
|
108
|
+
if @config.include_image_data
|
115
109
|
attachment = ::Chronicle::ETL::Models::Attachment.new
|
116
110
|
attachment.data = build_image_data
|
117
111
|
image.attachments = [attachment]
|
@@ -124,7 +118,7 @@ module Chronicle
|
|
124
118
|
topics.map do |topic|
|
125
119
|
t = ::Chronicle::ETL::Models::Entity.new
|
126
120
|
t.represents = 'topic'
|
127
|
-
t.provider = @
|
121
|
+
t.provider = @config.involved[:provider]
|
128
122
|
t.title = topic
|
129
123
|
t.slug = topic.parameterize
|
130
124
|
t.dedupe_on = [[:provider, :represents, :slug]]
|
@@ -136,7 +130,7 @@ module Chronicle
|
|
136
130
|
names.map do |name|
|
137
131
|
identity = ::Chronicle::ETL::Models::Entity.new
|
138
132
|
identity.represents = 'identity'
|
139
|
-
identity.provider = @
|
133
|
+
identity.provider = @config.involved[:provider]
|
140
134
|
identity.slug = name.parameterize
|
141
135
|
identity.title = name
|
142
136
|
identity.dedupe_on = [[:provider, :represents, :slug]]
|
@@ -199,7 +193,7 @@ module Chronicle
|
|
199
193
|
elsif false
|
200
194
|
# TODO: support option of using GPS coordinates to determine timezone
|
201
195
|
else
|
202
|
-
zone = ActiveSupport::TimeZone.new(@
|
196
|
+
zone = ActiveSupport::TimeZone.new(@config.timezone_default)
|
203
197
|
timestamp = zone.parse(timestamp.asctime)
|
204
198
|
end
|
205
199
|
|
@@ -3,14 +3,15 @@ module Chronicle
|
|
3
3
|
# Abstract class representing an Transformer for an ETL job
|
4
4
|
class Transformer
|
5
5
|
extend Chronicle::ETL::Registry::SelfRegistering
|
6
|
+
include Chronicle::ETL::Configurable
|
6
7
|
|
7
8
|
# Construct a new instance of this transformer. Options are passed in from a Runner
|
8
9
|
# == Parameters:
|
9
10
|
# options::
|
10
11
|
# Options for configuring this Transformer
|
11
|
-
def initialize(options = {}
|
12
|
-
@options = options
|
12
|
+
def initialize(extraction, options = {})
|
13
13
|
@extraction = extraction
|
14
|
+
apply_options(options)
|
14
15
|
end
|
15
16
|
|
16
17
|
# @abstract Subclass is expected to implement #transform
|
data/lib/chronicle/etl.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chronicle-etl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
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-02-
|
11
|
+
date: 2022-02-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
19
|
+
version: '7.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
26
|
+
version: '7.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: chronic_duration
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: runcom
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '6.
|
103
|
+
version: '6.0'
|
104
104
|
type: :runtime
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- - "
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '6.
|
110
|
+
version: '6.0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: sequel
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,14 +142,14 @@ dependencies:
|
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
145
|
+
version: '1.2'
|
146
146
|
type: :runtime
|
147
147
|
prerelease: false
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
149
149
|
requirements:
|
150
150
|
- - "~>"
|
151
151
|
- !ruby/object:Gem::Version
|
152
|
-
version: '
|
152
|
+
version: '1.2'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
154
|
name: tty-progressbar
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -234,6 +234,62 @@ dependencies:
|
|
234
234
|
- - "~>"
|
235
235
|
- !ruby/object:Gem::Version
|
236
236
|
version: '3.9'
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: simplecov
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - "~>"
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0.21'
|
244
|
+
type: :development
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - "~>"
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: '0.21'
|
251
|
+
- !ruby/object:Gem::Dependency
|
252
|
+
name: guard-rspec
|
253
|
+
requirement: !ruby/object:Gem::Requirement
|
254
|
+
requirements:
|
255
|
+
- - "~>"
|
256
|
+
- !ruby/object:Gem::Version
|
257
|
+
version: 4.7.3
|
258
|
+
type: :development
|
259
|
+
prerelease: false
|
260
|
+
version_requirements: !ruby/object:Gem::Requirement
|
261
|
+
requirements:
|
262
|
+
- - "~>"
|
263
|
+
- !ruby/object:Gem::Version
|
264
|
+
version: 4.7.3
|
265
|
+
- !ruby/object:Gem::Dependency
|
266
|
+
name: yard
|
267
|
+
requirement: !ruby/object:Gem::Requirement
|
268
|
+
requirements:
|
269
|
+
- - "~>"
|
270
|
+
- !ruby/object:Gem::Version
|
271
|
+
version: 0.9.7
|
272
|
+
type: :development
|
273
|
+
prerelease: false
|
274
|
+
version_requirements: !ruby/object:Gem::Requirement
|
275
|
+
requirements:
|
276
|
+
- - "~>"
|
277
|
+
- !ruby/object:Gem::Version
|
278
|
+
version: 0.9.7
|
279
|
+
- !ruby/object:Gem::Dependency
|
280
|
+
name: rubocop
|
281
|
+
requirement: !ruby/object:Gem::Requirement
|
282
|
+
requirements:
|
283
|
+
- - "~>"
|
284
|
+
- !ruby/object:Gem::Version
|
285
|
+
version: 1.25.1
|
286
|
+
type: :development
|
287
|
+
prerelease: false
|
288
|
+
version_requirements: !ruby/object:Gem::Requirement
|
289
|
+
requirements:
|
290
|
+
- - "~>"
|
291
|
+
- !ruby/object:Gem::Version
|
292
|
+
version: 1.25.1
|
237
293
|
description: Chronicle-ETL allows you to extract personal data from a variety of services,
|
238
294
|
transformer it, and load it.
|
239
295
|
email:
|
@@ -243,14 +299,15 @@ executables:
|
|
243
299
|
extensions: []
|
244
300
|
extra_rdoc_files: []
|
245
301
|
files:
|
302
|
+
- ".github/workflows/ruby.yml"
|
246
303
|
- ".gitignore"
|
247
304
|
- ".rspec"
|
248
305
|
- ".rubocop.yml"
|
249
|
-
- ".ruby-version"
|
250
306
|
- ".travis.yml"
|
251
307
|
- ".yardopts"
|
252
308
|
- CODE_OF_CONDUCT.md
|
253
309
|
- Gemfile
|
310
|
+
- Guardfile
|
254
311
|
- LICENSE.txt
|
255
312
|
- README.md
|
256
313
|
- Rakefile
|
@@ -259,11 +316,13 @@ files:
|
|
259
316
|
- chronicle-etl.gemspec
|
260
317
|
- exe/chronicle-etl
|
261
318
|
- lib/chronicle/etl.rb
|
319
|
+
- lib/chronicle/etl/cli.rb
|
262
320
|
- lib/chronicle/etl/cli/connectors.rb
|
263
321
|
- lib/chronicle/etl/cli/jobs.rb
|
264
322
|
- lib/chronicle/etl/cli/main.rb
|
265
323
|
- lib/chronicle/etl/cli/subcommand_base.rb
|
266
324
|
- lib/chronicle/etl/config.rb
|
325
|
+
- lib/chronicle/etl/configurable.rb
|
267
326
|
- lib/chronicle/etl/exceptions.rb
|
268
327
|
- lib/chronicle/etl/extraction.rb
|
269
328
|
- lib/chronicle/etl/extractors/csv_extractor.rb
|
@@ -317,14 +376,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
317
376
|
requirements:
|
318
377
|
- - ">="
|
319
378
|
- !ruby/object:Gem::Version
|
320
|
-
version: '
|
379
|
+
version: '2.7'
|
321
380
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
322
381
|
requirements:
|
323
382
|
- - ">="
|
324
383
|
- !ruby/object:Gem::Version
|
325
384
|
version: '0'
|
326
385
|
requirements: []
|
327
|
-
rubygems_version: 3.1.
|
386
|
+
rubygems_version: 3.1.6
|
328
387
|
signing_key:
|
329
388
|
specification_version: 4
|
330
389
|
summary: ETL tool for personal data
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.7.1
|