chronicle-etl 0.5.2 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  require 'colorize'
2
2
  require 'chronic_duration'
3
+ require "tty-spinner"
3
4
 
4
5
  class Chronicle::ETL::Runner
5
6
  def initialize(job)
@@ -8,76 +9,101 @@ class Chronicle::ETL::Runner
8
9
  end
9
10
 
10
11
  def run!
12
+ begin_job
11
13
  validate_job
12
14
  instantiate_connectors
13
15
  prepare_job
14
16
  prepare_ui
15
17
  run_extraction
18
+ rescue Chronicle::ETL::ExtractionError => e
19
+ @job_logger&.error
20
+ raise(Chronicle::ETL::RunnerError, "Extraction failed. #{e.message}")
21
+ rescue Interrupt
22
+ @job_logger&.error
23
+ raise(Chronicle::ETL::RunInterruptedError, "Job interrupted.")
24
+ rescue StandardError => e
25
+ # Just throwing this in here until we have better exception handling in
26
+ # loaders, etc
27
+ @job_logger&.error
28
+ raise(Chronicle::ETL::RunnerError, "Error running job. #{e.message}")
29
+ ensure
16
30
  finish_job
17
31
  end
18
32
 
19
33
  private
20
34
 
35
+ def begin_job
36
+ Chronicle::ETL::Logger.info(tty_log_job_initialize)
37
+ @initialization_spinner = TTY::Spinner.new(":spinner :title", format: :dots_2)
38
+ end
39
+
21
40
  def validate_job
41
+ @initialization_spinner.update(title: "Validating job")
22
42
  @job.job_definition.validate!
23
43
  end
24
44
 
25
45
  def instantiate_connectors
46
+ @initialization_spinner.update(title: "Initializing connectors")
26
47
  @extractor = @job.instantiate_extractor
27
48
  @loader = @job.instantiate_loader
28
49
  end
29
50
 
30
51
  def prepare_job
31
- Chronicle::ETL::Logger.info(tty_log_job_start)
52
+ @initialization_spinner.update(title: "Preparing job")
32
53
  @job_logger.start
33
54
  @loader.start
55
+
56
+ @initialization_spinner.update(title: "Preparing extraction")
57
+ @initialization_spinner.auto_spin
34
58
  @extractor.prepare
59
+ @initialization_spinner.success("(#{'successful'.green})")
60
+ Chronicle::ETL::Logger.info("\n")
35
61
  end
36
62
 
37
63
  def prepare_ui
38
64
  total = @extractor.results_count
39
65
  @progress_bar = Chronicle::ETL::Utils::ProgressBar.new(title: 'Running job', total: total)
40
- Chronicle::ETL::Logger.attach_to_progress_bar(@progress_bar)
66
+ Chronicle::ETL::Logger.attach_to_ui(@progress_bar)
41
67
  end
42
68
 
43
- # TODO: refactor this further
44
69
  def run_extraction
45
70
  @extractor.extract do |extraction|
46
- unless extraction.is_a?(Chronicle::ETL::Extraction)
47
- raise Chronicle::ETL::RunnerTypeError, "Extracted should be a Chronicle::ETL::Extraction"
48
- end
49
-
50
- transformer = @job.instantiate_transformer(extraction)
51
- record = transformer.transform
52
-
53
- Chronicle::ETL::Logger.debug(tty_log_transformation(transformer))
54
- @job_logger.log_transformation(transformer)
55
-
56
- @loader.load(record) unless @job.dry_run?
57
- rescue Chronicle::ETL::TransformationError => e
58
- Chronicle::ETL::Logger.error(tty_log_transformation_failure(e, transformer))
59
- ensure
71
+ process_extraction(extraction)
60
72
  @progress_bar.increment
61
73
  end
62
74
 
63
75
  @progress_bar.finish
76
+
77
+ # This is typically a slow method (writing to stdout, writing a big file, etc)
78
+ # TODO: consider adding a spinner?
64
79
  @loader.finish
65
80
  @job_logger.finish
66
- rescue Interrupt
67
- Chronicle::ETL::Logger.error("\n#{'Job interrupted'.red}")
68
- @job_logger.error
69
- rescue StandardError => e
70
- raise e
81
+ end
82
+
83
+ def process_extraction(extraction)
84
+ # For each extraction from our extractor, we create a new tarnsformer
85
+ transformer = @job.instantiate_transformer(extraction)
86
+
87
+ # And then transform that record, logging it if we're in debug log level
88
+ record = transformer.transform
89
+ Chronicle::ETL::Logger.debug(tty_log_transformation(transformer))
90
+ @job_logger.log_transformation(transformer)
91
+
92
+ # Then send the results to the loader
93
+ @loader.load(record) unless @job.dry_run?
94
+ rescue Chronicle::ETL::TransformationError => e
95
+ # TODO: have an option to cancel job if we encounter an error
96
+ Chronicle::ETL::Logger.error(tty_log_transformation_failure(e, transformer))
71
97
  end
72
98
 
73
99
  def finish_job
74
100
  @job_logger.save
75
101
  @progress_bar&.finish
76
- Chronicle::ETL::Logger.detach_from_progress_bar
102
+ Chronicle::ETL::Logger.detach_from_ui
77
103
  Chronicle::ETL::Logger.info(tty_log_completion)
78
104
  end
79
105
 
80
- def tty_log_job_start
106
+ def tty_log_job_initialize
81
107
  output = "Beginning job "
82
108
  output += "'#{@job.name}'".bold if @job.name
83
109
  output
@@ -95,8 +121,9 @@ class Chronicle::ETL::Runner
95
121
 
96
122
  def tty_log_completion
97
123
  status = @job_logger.success ? 'Success' : 'Failed'
98
- output = "\nCompleted job "
99
- output += "'#{@job.name}'".bold if @job.name
124
+ job_completion = @job_logger.success ? 'Completed' : 'Partially completed'
125
+ output = "\n#{job_completion} job"
126
+ output += " '#{@job.name}'".bold if @job.name
100
127
  output += " in #{ChronicDuration.output(@job_logger.duration)}" if @job_logger.duration
101
128
  output += "\n Status:\t".light_black + status
102
129
  output += "\n Completed:\t".light_black + "#{@job_logger.job_log.num_records_processed}"
@@ -1,9 +1,16 @@
1
+ require "active_support/core_ext/hash/keys"
2
+
1
3
  module Chronicle
2
4
  module ETL
3
5
  # Secret management module
4
6
  module Secrets
5
7
  module_function
6
8
 
9
+ # Whether a given namespace exists
10
+ def exists?(namespace)
11
+ Chronicle::ETL::Config.exists?("secrets", namespace)
12
+ end
13
+
7
14
  # Save a setting to a namespaced config file
8
15
  def set(namespace, key, value)
9
16
  config = read(namespace)
@@ -11,6 +18,13 @@ module Chronicle
11
18
  write(namespace, config)
12
19
  end
13
20
 
21
+ # Save a hash to a secrets namespace
22
+ def set_all(namespace, secrets)
23
+ config = read(namespace)
24
+ config = config.merge(secrets.deep_stringify_keys)
25
+ write(namespace, config)
26
+ end
27
+
14
28
  # Remove a setting from a namespaced config file
15
29
  def unset(namespace, key)
16
30
  config = read(namespace)
@@ -42,7 +56,7 @@ module Chronicle
42
56
  data = {
43
57
  secrets: (secrets || {}).transform_keys(&:to_s),
44
58
  chronicle_etl_version: Chronicle::ETL::VERSION
45
- }.transform_keys(&:to_s) # Should I implement deeply_transform_keys...?
59
+ }
46
60
  Chronicle::ETL::Config.write("secrets", namespace, data)
47
61
  end
48
62
 
@@ -10,6 +10,10 @@ module Chronicle
10
10
  # options::
11
11
  # Options for configuring this Transformer
12
12
  def initialize(extraction, options = {})
13
+ unless extraction.is_a?(Chronicle::ETL::Extraction)
14
+ raise Chronicle::ETL::RunnerTypeError, "Extracted should be a Chronicle::ETL::Extraction"
15
+ end
16
+
13
17
  @extraction = extraction
14
18
  apply_options(options)
15
19
  end
@@ -1,5 +1,5 @@
1
1
  module Chronicle
2
2
  module ETL
3
- VERSION = "0.5.2"
3
+ VERSION = "0.5.5"
4
4
  end
5
5
  end
data/lib/chronicle/etl.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require_relative 'etl/registry/registry'
2
+ require_relative 'etl/authorizer'
2
3
  require_relative 'etl/config'
3
4
  require_relative 'etl/configurable'
4
5
  require_relative 'etl/exceptions'
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.5.2
4
+ version: 0.5.5
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-30 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.8.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: gems
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: launchy
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: marcel
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,20 @@ dependencies:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
124
  version: '1.13'
125
+ - !ruby/object:Gem::Dependency
126
+ name: omniauth
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2'
97
139
  - !ruby/object:Gem::Dependency
98
140
  name: sequel
99
141
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +150,20 @@ dependencies:
108
150
  - - "~>"
109
151
  - !ruby/object:Gem::Version
110
152
  version: '5.35'
153
+ - !ruby/object:Gem::Dependency
154
+ name: sinatra
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2'
111
167
  - !ruby/object:Gem::Dependency
112
168
  name: sqlite3
113
169
  requirement: !ruby/object:Gem::Requirement
@@ -235,33 +291,33 @@ dependencies:
235
291
  - !ruby/object:Gem::Version
236
292
  version: '2.1'
237
293
  - !ruby/object:Gem::Dependency
238
- name: guard-rspec
294
+ name: fakefs
239
295
  requirement: !ruby/object:Gem::Requirement
240
296
  requirements:
241
297
  - - "~>"
242
298
  - !ruby/object:Gem::Version
243
- version: 4.7.3
299
+ version: '1.4'
244
300
  type: :development
245
301
  prerelease: false
246
302
  version_requirements: !ruby/object:Gem::Requirement
247
303
  requirements:
248
304
  - - "~>"
249
305
  - !ruby/object:Gem::Version
250
- version: 4.7.3
306
+ version: '1.4'
251
307
  - !ruby/object:Gem::Dependency
252
- name: fakefs
308
+ name: guard-rspec
253
309
  requirement: !ruby/object:Gem::Requirement
254
310
  requirements:
255
- - - ">="
311
+ - - "~>"
256
312
  - !ruby/object:Gem::Version
257
- version: '0'
313
+ version: 4.7.3
258
314
  type: :development
259
315
  prerelease: false
260
316
  version_requirements: !ruby/object:Gem::Requirement
261
317
  requirements:
262
- - - ">="
318
+ - - "~>"
263
319
  - !ruby/object:Gem::Version
264
- version: '0'
320
+ version: 4.7.3
265
321
  - !ruby/object:Gem::Dependency
266
322
  name: pry-byebug
267
323
  requirement: !ruby/object:Gem::Requirement
@@ -332,6 +388,34 @@ dependencies:
332
388
  - - "~>"
333
389
  - !ruby/object:Gem::Version
334
390
  version: '0.21'
391
+ - !ruby/object:Gem::Dependency
392
+ name: vcr
393
+ requirement: !ruby/object:Gem::Requirement
394
+ requirements:
395
+ - - "~>"
396
+ - !ruby/object:Gem::Version
397
+ version: '6.1'
398
+ type: :development
399
+ prerelease: false
400
+ version_requirements: !ruby/object:Gem::Requirement
401
+ requirements:
402
+ - - "~>"
403
+ - !ruby/object:Gem::Version
404
+ version: '6.1'
405
+ - !ruby/object:Gem::Dependency
406
+ name: webmock
407
+ requirement: !ruby/object:Gem::Requirement
408
+ requirements:
409
+ - - "~>"
410
+ - !ruby/object:Gem::Version
411
+ version: '3'
412
+ type: :development
413
+ prerelease: false
414
+ version_requirements: !ruby/object:Gem::Requirement
415
+ requirements:
416
+ - - "~>"
417
+ - !ruby/object:Gem::Version
418
+ version: '3'
335
419
  - !ruby/object:Gem::Dependency
336
420
  name: yard
337
421
  requirement: !ruby/object:Gem::Requirement
@@ -372,7 +456,10 @@ files:
372
456
  - chronicle-etl.gemspec
373
457
  - exe/chronicle-etl
374
458
  - lib/chronicle/etl.rb
459
+ - lib/chronicle/etl/authorization_server.rb
460
+ - lib/chronicle/etl/authorizer.rb
375
461
  - lib/chronicle/etl/cli.rb
462
+ - lib/chronicle/etl/cli/authorizations.rb
376
463
  - lib/chronicle/etl/cli/cli_base.rb
377
464
  - lib/chronicle/etl/cli/connectors.rb
378
465
  - lib/chronicle/etl/cli/jobs.rb
@@ -407,8 +494,11 @@ files:
407
494
  - lib/chronicle/etl/models/base.rb
408
495
  - lib/chronicle/etl/models/entity.rb
409
496
  - lib/chronicle/etl/models/raw.rb
497
+ - lib/chronicle/etl/oauth_authorizer.rb
410
498
  - lib/chronicle/etl/registry/connector_registration.rb
411
- - lib/chronicle/etl/registry/plugin_registry.rb
499
+ - lib/chronicle/etl/registry/connectors.rb
500
+ - lib/chronicle/etl/registry/plugin_registration.rb
501
+ - lib/chronicle/etl/registry/plugins.rb
412
502
  - lib/chronicle/etl/registry/registry.rb
413
503
  - lib/chronicle/etl/registry/self_registering.rb
414
504
  - lib/chronicle/etl/runner.rb
@@ -1,84 +0,0 @@
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 use a hardcoded
17
- # list somewhere
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
- # Check whether a given plugin is installed
35
- def self.installed?(name)
36
- gem_name = "chronicle-#{name}"
37
- all_installed.map(&:name).include?(gem_name)
38
- end
39
-
40
- # Activate a plugin with given name by `require`ing it
41
- def self.activate(name)
42
- # By default, activates the latest available version of a gem
43
- # so don't have to run Kernel#gem separately
44
- require "chronicle/#{name}"
45
- rescue Gem::ConflictError => e
46
- # TODO: figure out if there's more we can do here
47
- raise Chronicle::ETL::PluginConflictError.new(name), "Plugin '#{name}' couldn't be loaded. #{e.message}"
48
- rescue StandardError, LoadError => e
49
- # StandardError to catch random non-loading problems that might occur
50
- # when requiring the plugin (eg class macro invoked the wrong way)
51
- # TODO: decide if this should be separated
52
- raise Chronicle::ETL::PluginLoadError.new(name), "Plugin '#{name}' couldn't be loaded"
53
- end
54
-
55
- # Install a plugin to local gems
56
- def self.install(name)
57
- return if installed?(name)
58
-
59
- gem_name = "chronicle-#{name}"
60
- raise(Chronicle::ETL::PluginNotAvailableError.new(gem_name), "Plugin #{name} doesn't exist") unless exists?(gem_name)
61
-
62
- Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
63
- Gem.install(gem_name)
64
-
65
- activate(name)
66
- rescue Gem::UnsatisfiableDependencyError
67
- # TODO: we need to catch a lot more than this here
68
- raise Chronicle::ETL::PluginNotAvailableError.new(name), "Plugin #{name} could not be installed."
69
- end
70
-
71
- # Uninstall a plugin
72
- def self.uninstall(name)
73
- gem_name = "chronicle-#{name}"
74
- Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
75
- uninstaller = Gem::Uninstaller.new(gem_name)
76
- uninstaller.uninstall
77
- rescue Gem::InstallError
78
- # TODO: strengthen this exception handling
79
- raise(Chronicle::ETL::PluginError.new(name), "Plugin #{name} wasn't uninstalled")
80
- end
81
- end
82
- end
83
- end
84
- end