chronicle-etl 0.5.3 → 0.5.4
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.
- checksums.yaml +4 -4
- data/README.md +13 -12
- data/chronicle-etl.gemspec +4 -1
- data/lib/chronicle/etl/authorization_server.rb +48 -0
- data/lib/chronicle/etl/authorizer.rb +37 -0
- data/lib/chronicle/etl/cli/authorizations.rb +63 -0
- data/lib/chronicle/etl/cli/cli_base.rb +4 -1
- data/lib/chronicle/etl/cli/jobs.rb +4 -1
- data/lib/chronicle/etl/cli/main.rb +3 -0
- data/lib/chronicle/etl/cli/secrets.rb +1 -1
- data/lib/chronicle/etl/cli.rb +1 -0
- data/lib/chronicle/etl/exceptions.rb +2 -0
- data/lib/chronicle/etl/logger.rb +6 -6
- data/lib/chronicle/etl/models/entity.rb +2 -0
- data/lib/chronicle/etl/oauth_authorizer.rb +142 -0
- data/lib/chronicle/etl/registry/plugin_registry.rb +13 -2
- data/lib/chronicle/etl/runner.rb +2 -2
- data/lib/chronicle/etl/secrets.rb +15 -1
- data/lib/chronicle/etl/version.rb +1 -1
- data/lib/chronicle/etl.rb +1 -0
- metadata +58 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f6b4272cd7f2cfcc12e6327324b5d2f11e76036dcf2442de1fe9ad08e041bb2
|
4
|
+
data.tar.gz: 4558bf48b7de7c64b691e8ef6403304c864e818360bc7c741a290f7650a7eb8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f7d7f4fd89d284a3a7ad5bffc05cf50ba6ea4e909457585dccfe9071ee91fb36bea505b3fa559a9b6ddab8f37845cf45651c1d1abc4045282c95c99ca9a5944
|
7
|
+
data.tar.gz: e2cf8a277c463d3b8ddef811e398fbae2e2649fb8227d14874443969b659ee68e74603f438ef4294b5caa59289f26dd5674dd28f9befe537b86b72ecf2ce7b40
|
data/README.md
CHANGED
@@ -6,14 +6,14 @@
|
|
6
6
|
|
7
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.
|
8
8
|
|
9
|
-
If you don’t want to spend all your time writing scrapers, reverse-engineering APIs, or parsing takeout data, this
|
9
|
+
If you don’t want to spend all your time writing scrapers, reverse-engineering APIs, or parsing takeout data, this tool is for you! (*If you do enjoy these things, please see the [open issues](https://github.com/chronicle-app/chronicle-etl/issues).*)
|
10
10
|
|
11
|
-
**`chronicle-etl` is a CLI tool that gives you a unified interface
|
11
|
+
**`chronicle-etl` is a CLI tool that gives you a unified interface to your personal data.** It uses the ETL pattern to *extract* data 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 destination (e.g. a CSV file, JSON, external API).
|
12
12
|
|
13
13
|
## What does `chronicle-etl` give you?
|
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.
|
15
|
-
* **Plugins for many third-party providers**.
|
16
|
-
* **A common, opinionated schema**: You can normalize different datasets into a single schema so that, for example, all your iMessages and emails are
|
14
|
+
* **A CLI tool for working with personal data**. You can monitor progress of exports, manipulate the output, set up recurring jobs, manage credentials, and more.
|
15
|
+
* **Plugins for many third-party providers**. This plugin system allows you to access data from dozens of third-party services, all accessible through a common CLI interface.
|
16
|
+
* **A common, opinionated schema**: You can normalize different datasets into a single schema so that, for example, all your iMessages and emails are represented in a common schema. (Don’t want to use this schema? `chronicle-etl` always allows you to fall back on working with the raw extraction data.)
|
17
17
|
|
18
18
|
## Chronicle-ETL in action
|
19
19
|
|
@@ -45,7 +45,7 @@ $ chronicle-etl --version
|
|
45
45
|
# Display help
|
46
46
|
$ chronicle-etl help
|
47
47
|
|
48
|
-
#
|
48
|
+
# Run a basic job
|
49
49
|
$ chronicle-etl --extractor NAME --transformer NAME --loader NAME
|
50
50
|
|
51
51
|
# Read test.csv and display it to stdout as a table
|
@@ -88,7 +88,7 @@ Options:
|
|
88
88
|
|
89
89
|
### Saving jobs
|
90
90
|
|
91
|
-
You can save details about a job to a local config file (saved by default in `~/.config/chronicle/etl/jobs/
|
91
|
+
You can save details about a job to a local config file (saved by default in `~/.config/chronicle/etl/jobs/JOB_NAME.yml`) to save yourself the trouble specifying options each time.
|
92
92
|
|
93
93
|
```sh
|
94
94
|
# Save a job named 'sample' to ~/.config/chronicle/etl/jobs/sample.yml
|
@@ -99,8 +99,6 @@ $ chronicle-etl jobs:show sample
|
|
99
99
|
|
100
100
|
# Run the job
|
101
101
|
$ chronicle-etl jobs:run sample
|
102
|
-
# Or more simply:
|
103
|
-
$ chronicle-etl sample
|
104
102
|
|
105
103
|
# Show all saved jobs
|
106
104
|
$ chronicle-etl jobs:list
|
@@ -132,7 +130,7 @@ $ chronicle-etl connectors:list
|
|
132
130
|
- [`rest`](https://github.com/chronicle-app/chronicle-etl/blob/main/lib/chronicle/etl/loaders/rest_loader.rb) - Serialize records with [JSONAPI](https://jsonapi.org/) and send to a REST API
|
133
131
|
|
134
132
|
## Chronicle Plugins
|
135
|
-
Plugins provide access to data from third-party platforms, services, or formats. Plugins are packaged as separate rubygems and can be installed through the CLI (
|
133
|
+
Plugins provide access to data from third-party platforms, services, or formats. Plugins are packaged as separate rubygems and can be installed through the CLI (under the hood, it's a `gem install chronicle-PLUGINNAME`)
|
136
134
|
|
137
135
|
### Plugin usage
|
138
136
|
|
@@ -167,6 +165,8 @@ If you don't see a plugin for a third-party provider or data source that you're
|
|
167
165
|
| [pinboard](https://github.com/chronicle-app/chronicle-email) | Bookmarks and tags | Available |
|
168
166
|
| [safari](https://github.com/chronicle-app/chronicle-safari) | Browser history from local sqlite db | Available |
|
169
167
|
| [shell](https://github.com/chronicle-app/chronicle-shell) | Shell command history | Available (still needs zsh support) |
|
168
|
+
| [zulip](https://github.com/chronicle-app/chronicle-zulip) | Zulip message history | Available (for private messages) |
|
169
|
+
|
170
170
|
|
171
171
|
#### Coming soon
|
172
172
|
|
@@ -243,8 +243,9 @@ $ chronicle-etl secrets:unset pinboard access_token
|
|
243
243
|
## Roadmap
|
244
244
|
|
245
245
|
- Keep tackling **new plugins**. See: [Chronicle Plugin Tracker](https://github.com/orgs/chronicle-app/projects/1)
|
246
|
-
- Add
|
247
|
-
-
|
246
|
+
- Add an **OAuth2 authorizer** for services that require this type of authorization ([#48](https://github.com/chronicle-app/chronicle-etl/issues/48))
|
247
|
+
- Add support for **incremental extractions** ([#37](https://github.com/chronicle-app/chronicle-etl/issues/37))
|
248
|
+
- **Improve stdin extractor and shell command transformer** so that users can easily integrate their own scripts/languages/tools into jobs ([#5](https://github.com/chronicle-app/chronicle-etl/issues/48))
|
248
249
|
- **Add documentation for Chronicle Schema**. It's found throughout this project but never explained.
|
249
250
|
|
250
251
|
## Development
|
data/chronicle-etl.gemspec
CHANGED
@@ -40,10 +40,13 @@ Gem::Specification.new do |spec|
|
|
40
40
|
spec.add_dependency "activesupport", "~> 7.0"
|
41
41
|
spec.add_dependency "chronic_duration", "~> 0.10.6"
|
42
42
|
spec.add_dependency "colorize", "~> 0.8.1"
|
43
|
+
spec.add_dependency 'launchy'
|
43
44
|
spec.add_dependency "marcel", "~> 1.0.2"
|
44
45
|
spec.add_dependency "mini_exiftool", "~> 2.10"
|
45
46
|
spec.add_dependency "nokogiri", "~> 1.13"
|
47
|
+
spec.add_dependency 'omniauth', "~> 2"
|
46
48
|
spec.add_dependency "sequel", "~> 5.35"
|
49
|
+
spec.add_dependency 'sinatra', "~> 2"
|
47
50
|
spec.add_dependency "sqlite3", "~> 1.4"
|
48
51
|
spec.add_dependency "thor", "~> 1.2"
|
49
52
|
spec.add_dependency "thor-hollaback", "~> 0.2"
|
@@ -54,8 +57,8 @@ Gem::Specification.new do |spec|
|
|
54
57
|
spec.add_dependency "xdg", ">= 4.0"
|
55
58
|
|
56
59
|
spec.add_development_dependency "bundler", "~> 2.1"
|
57
|
-
spec.add_development_dependency "guard-rspec", "~> 4.7.3"
|
58
60
|
spec.add_development_dependency "fakefs"
|
61
|
+
spec.add_development_dependency "guard-rspec", "~> 4.7.3"
|
59
62
|
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
60
63
|
spec.add_development_dependency "rake", "~> 13.0"
|
61
64
|
spec.add_development_dependency "rspec", "~> 3.9"
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'omniauth'
|
3
|
+
|
4
|
+
module Chronicle
|
5
|
+
module ETL
|
6
|
+
class AuthorizationServer < Sinatra::Base
|
7
|
+
class << self
|
8
|
+
attr_accessor :latest_authorization
|
9
|
+
end
|
10
|
+
|
11
|
+
configure do
|
12
|
+
set :inline_templates, true
|
13
|
+
set :dump_errors, false
|
14
|
+
set :raise_errors, true
|
15
|
+
disable :logging
|
16
|
+
set :sessions, true
|
17
|
+
set :quiet, true
|
18
|
+
set :threaded, true
|
19
|
+
set :environment, ENV['APP_ENV'] == 'test' ? :test : :production
|
20
|
+
end
|
21
|
+
|
22
|
+
use OmniAuth::Builder do
|
23
|
+
Chronicle::ETL::OauthAuthorizer.all.each do |klass|
|
24
|
+
args = [klass.client_id, klass.client_secret, klass.options].compact
|
25
|
+
provider(
|
26
|
+
klass.strategy,
|
27
|
+
*args
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
OmniAuth.config.logger = Chronicle::ETL::Logger
|
33
|
+
OmniAuth.config.silence_get_warning = true
|
34
|
+
OmniAuth.config.allowed_request_methods = %i[get]
|
35
|
+
|
36
|
+
get '/auth/:provider/callback' do
|
37
|
+
authorization = request.env['omniauth.auth'].to_h.deep_transform_keys(&:to_sym)
|
38
|
+
self.class.latest_authorization = authorization
|
39
|
+
erb "<h1>Settings saved for #{params[:provider]}</h1><p>You can now close this tab and return to your terminal!</p>"
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/auth/failure' do
|
43
|
+
# TODO: handle this
|
44
|
+
erb "<h1>Authentication Failed:</h1><h3>message:<h3> <pre>#{params}</pre>"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module ETL
|
3
|
+
# An authorization strategy for a third-party data source
|
4
|
+
class Authorizer
|
5
|
+
class << self
|
6
|
+
attr_reader :provider_name
|
7
|
+
|
8
|
+
# Macro for setting provider on an Authorizer
|
9
|
+
def provider(provider_name)
|
10
|
+
@provider_name = provider_name
|
11
|
+
end
|
12
|
+
|
13
|
+
# From all loaded Authorizers, return the first one that matches
|
14
|
+
# a given provider
|
15
|
+
#
|
16
|
+
# @todo Have a proper identifier system for authorizers
|
17
|
+
# (to have more than one per plugin)
|
18
|
+
def find_by_provider(provider)
|
19
|
+
ObjectSpace.each_object(::Class).select {|klass| klass < self }.find do |authorizer|
|
20
|
+
authorizer.provider_name == provider
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Construct a new authorizer
|
26
|
+
def initialize(args)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Main entry-point for authorization flows. Implemented by subclass
|
30
|
+
def authorize!
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require_relative 'oauth_authorizer'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'launchy'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
module Chronicle
|
8
|
+
module ETL
|
9
|
+
module CLI
|
10
|
+
# CLI commands for authorizing chronicle-etl with third-party services
|
11
|
+
class Authorizations < SubcommandBase
|
12
|
+
default_task 'new'
|
13
|
+
namespace :authorizations
|
14
|
+
|
15
|
+
desc "authorize", "Authorize with a third-party provider"
|
16
|
+
option :port, desc: 'Port to run authorization server on', type: :numeric, default: 4567
|
17
|
+
option :credentials, desc: 'Secrets namespace for where to read credentials from (default: PROVIDER)', type: :string, banner: 'NAMESPACE'
|
18
|
+
option :secrets, desc: 'Secrets namespace for where authorization should be saved to (default: PROVIDER)', type: :string, banner: 'NAMESPACE'
|
19
|
+
option :print, desc: 'Show authorization results (instead of just saving secrets)', type: :boolean, default: false
|
20
|
+
def new(provider)
|
21
|
+
authorizer_klass = find_authorizer_klass(provider)
|
22
|
+
credentials = load_credentials(provider: provider, credentials_source: options[:credentials])
|
23
|
+
authorizer = authorizer_klass.new(port: options[:port], credentials: credentials)
|
24
|
+
|
25
|
+
secrets = authorizer.authorize!
|
26
|
+
secrets_namespace = options[:secrets] || provider
|
27
|
+
Chronicle::ETL::Secrets.set_all(secrets_namespace, secrets)
|
28
|
+
|
29
|
+
pp secrets if options[:print]
|
30
|
+
|
31
|
+
cli_exit(message: "Authorization saved to '#{secrets_namespace}' secrets")
|
32
|
+
rescue StandardError => e
|
33
|
+
cli_fail(message: "Authorization not successful.\n" + e.message, exception: e)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_authorizer_klass(provider)
|
39
|
+
# TODO: this assumes provider:plugin one-to-one
|
40
|
+
unless Chronicle::ETL::Registry::PluginRegistry.installed?(provider)
|
41
|
+
cli_fail(message: "Plugin for #{provider} is not installed.")
|
42
|
+
end
|
43
|
+
|
44
|
+
begin
|
45
|
+
Chronicle::ETL::Registry::PluginRegistry.activate(provider)
|
46
|
+
rescue PluginError => e
|
47
|
+
cli_fail(message: "Could not load plugin '#{provider}'.\n" + e.message, exception: e)
|
48
|
+
end
|
49
|
+
|
50
|
+
Authorizer.find_by_provider(provider.to_sym) || cli_fail(message: "No authorizer available for '#{provider}'")
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_credentials(provider:, credentials_source: nil)
|
54
|
+
if credentials_source && !Chronicle::ETL::Secrets.exists?(credentials_source)
|
55
|
+
cli_fail(message: "OAuth credentials specified as '#{credentials_source}' but a secrets namespace with that name does not exist.")
|
56
|
+
end
|
57
|
+
|
58
|
+
Chronicle::ETL::Secrets.read(credentials_source || provider)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -6,7 +6,10 @@ module Chronicle
|
|
6
6
|
no_commands do
|
7
7
|
# Shorthand for cli_exit(status: :failure)
|
8
8
|
def cli_fail(message: nil, exception: nil)
|
9
|
-
|
9
|
+
if exception && Chronicle::ETL::Logger.log_level > Chronicle::ETL::Logger::DEBUG
|
10
|
+
message += "\nRe-run the command with --verbose to see details."
|
11
|
+
end
|
12
|
+
|
10
13
|
cli_exit(status: :failure, message: message, exception: exception)
|
11
14
|
end
|
12
15
|
|
@@ -52,6 +52,8 @@ LONG_DESC
|
|
52
52
|
cli_exit
|
53
53
|
end
|
54
54
|
|
55
|
+
cli_fail(message: "Job '#{name}' does not exist") if name && !Chronicle::ETL::Config.exists?("jobs", name)
|
56
|
+
|
55
57
|
job_definition = build_job_definition(name, options)
|
56
58
|
|
57
59
|
if job_definition.plugins_missing?
|
@@ -102,6 +104,8 @@ LONG_DESC
|
|
102
104
|
desc "show", "Show details about a job"
|
103
105
|
# Show an ETL job
|
104
106
|
def show(name = nil)
|
107
|
+
cli_fail(message: "Job '#{name}' does not exist") if name && !Chronicle::ETL::Config.exists?("jobs", name)
|
108
|
+
|
105
109
|
job_definition = build_job_definition(name, options)
|
106
110
|
job_definition.validate!
|
107
111
|
puts Chronicle::ETL::Job.new(job_definition)
|
@@ -140,7 +144,6 @@ LONG_DESC
|
|
140
144
|
job_definition.validate!
|
141
145
|
# FIXME: clumsy to make CLI responsible for setting secrets here. Think about a better way to do this
|
142
146
|
job_definition.apply_default_secrets
|
143
|
-
|
144
147
|
job = Chronicle::ETL::Job.new(job_definition)
|
145
148
|
runner = Chronicle::ETL::Runner.new(job)
|
146
149
|
runner.run!
|
@@ -27,6 +27,9 @@ module Chronicle
|
|
27
27
|
desc 'secrets:COMMAND', 'Manage secrets', hide: true
|
28
28
|
subcommand 'secrets', Secrets
|
29
29
|
|
30
|
+
desc 'authorizations', 'Authorize', hide: true
|
31
|
+
subcommand 'authorizations', Authorizations
|
32
|
+
|
30
33
|
# Entrypoint for the CLI
|
31
34
|
def self.start(given_args = ARGV, config = {})
|
32
35
|
# take a subcommand:command and splits them so Thor knows how to hand off to the subcommand class
|
@@ -46,7 +46,7 @@ module Chronicle
|
|
46
46
|
all_secrets.each do |namespace, secrets|
|
47
47
|
rows += secrets.map do |key, value|
|
48
48
|
# hidden_value = (value[0..5] + ("*" * [0, [value.length - 5, 30].min].max)).truncate(30)
|
49
|
-
truncated_value = value
|
49
|
+
truncated_value = value&.truncate(30)
|
50
50
|
[namespace, key, truncated_value]
|
51
51
|
end
|
52
52
|
end
|
data/lib/chronicle/etl/cli.rb
CHANGED
@@ -4,6 +4,7 @@ require 'chronicle/etl'
|
|
4
4
|
|
5
5
|
require 'chronicle/etl/cli/cli_base'
|
6
6
|
require 'chronicle/etl/cli/subcommand_base'
|
7
|
+
require 'chronicle/etl/cli/authorizations'
|
7
8
|
require 'chronicle/etl/cli/connectors'
|
8
9
|
require 'chronicle/etl/cli/jobs'
|
9
10
|
require 'chronicle/etl/cli/plugins'
|
data/lib/chronicle/etl/logger.rb
CHANGED
@@ -17,8 +17,8 @@ module Chronicle
|
|
17
17
|
def output message, level
|
18
18
|
return unless level >= @log_level
|
19
19
|
|
20
|
-
if @
|
21
|
-
@
|
20
|
+
if @ui_element
|
21
|
+
@ui_element.log(message)
|
22
22
|
else
|
23
23
|
$stderr.puts(message)
|
24
24
|
end
|
@@ -40,12 +40,12 @@ module Chronicle
|
|
40
40
|
output(message, DEBUG)
|
41
41
|
end
|
42
42
|
|
43
|
-
def
|
44
|
-
@
|
43
|
+
def attach_to_ui(ui_element)
|
44
|
+
@ui_elemenet = ui_element
|
45
45
|
end
|
46
46
|
|
47
|
-
def
|
48
|
-
@
|
47
|
+
def detach_from_ui
|
48
|
+
@ui_element = nil
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'omniauth'
|
2
|
+
require 'tty-spinner'
|
3
|
+
|
4
|
+
module Chronicle
|
5
|
+
module ETL
|
6
|
+
# An authorization strategy that uses oauth2 (and omniauth under the hood)
|
7
|
+
class OauthAuthorizer < Authorizer
|
8
|
+
class << self
|
9
|
+
attr_reader :strategy, :provider_name, :authorization_to_secret_map
|
10
|
+
attr_accessor :client_id, :client_secret
|
11
|
+
|
12
|
+
# Macro for specifying which omniauth strategy to use
|
13
|
+
def omniauth_strategy(strategy)
|
14
|
+
@strategy = strategy
|
15
|
+
end
|
16
|
+
|
17
|
+
# Macro for specifying which omniauth scopes to request
|
18
|
+
def scope(value)
|
19
|
+
options[:scope] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
# Macro for specifying hash of returned authorization to secrets hash
|
23
|
+
def pluck_secrets(map)
|
24
|
+
@authorization_to_secret_map = map
|
25
|
+
end
|
26
|
+
|
27
|
+
# # Macro for specifying options to pass to omniauth
|
28
|
+
def options
|
29
|
+
@options ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns all subclasses of OauthAuthorizer
|
33
|
+
# (Used by AuthorizationServer to build omniauth providers)
|
34
|
+
def all
|
35
|
+
ObjectSpace.each_object(::Class).select { |klass| klass < self }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :authorization
|
40
|
+
|
41
|
+
# Create a new instance of OauthAuthorizer
|
42
|
+
def initialize(port:, credentials: {})
|
43
|
+
@port = port
|
44
|
+
@credentials = credentials
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
# Start up an authorization server and handle the oauth flow
|
49
|
+
def authorize!
|
50
|
+
associate_oauth_credentials
|
51
|
+
@server = load_server
|
52
|
+
spinner = TTY::Spinner.new(":spinner :title", format: :dots_2)
|
53
|
+
Chronicle::ETL::Logger.attach_to_ui(spinner)
|
54
|
+
spinner.auto_spin
|
55
|
+
spinner.update(title: "Starting temporary authorization server on port #{@port}""")
|
56
|
+
|
57
|
+
server_thread = start_authorization_server(port: @port)
|
58
|
+
start_oauth_flow
|
59
|
+
|
60
|
+
spinner.update(title: "Waiting for authorization to complete in your browser")
|
61
|
+
sleep 0.1 while authorization_pending?(server_thread)
|
62
|
+
|
63
|
+
@server.quit!
|
64
|
+
server_thread.join
|
65
|
+
spinner.success("(#{'successful'.green})")
|
66
|
+
Chronicle::ETL::Logger.detach_from_ui
|
67
|
+
|
68
|
+
# TODO: properly handle failed authorizations
|
69
|
+
raise Chronicle::ETL::AuthorizationError unless @server.latest_authorization
|
70
|
+
|
71
|
+
@authorization = @server.latest_authorization
|
72
|
+
|
73
|
+
extract_secrets(authorization: @authorization, pluck_values: self.class.authorization_to_secret_map)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def authorization_pending?(server_thread)
|
79
|
+
server_thread.status && !@server.latest_authorization
|
80
|
+
end
|
81
|
+
|
82
|
+
def associate_oauth_credentials
|
83
|
+
self.class.client_id = @credentials[:client_id]
|
84
|
+
self.class.client_secret = @credentials[:client_secret]
|
85
|
+
end
|
86
|
+
|
87
|
+
def load_server
|
88
|
+
# Load at runtime so that we can set omniauth strategies based on
|
89
|
+
# which chronicle plugin has been loaded.
|
90
|
+
require_relative './authorization_server'
|
91
|
+
Chronicle::ETL::AuthorizationServer
|
92
|
+
end
|
93
|
+
|
94
|
+
def start_authorization_server(port:)
|
95
|
+
@server.settings.port = port
|
96
|
+
suppress_webrick_logging(@server)
|
97
|
+
Thread.abort_on_exception = true
|
98
|
+
Thread.report_on_exception = false
|
99
|
+
|
100
|
+
Thread.new do
|
101
|
+
@server.run!({ port: @port }) do |s|
|
102
|
+
s.silent = true if s.class.to_s == "Thin::Server"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def start_oauth_flow
|
108
|
+
url = "http://localhost:#{@port}/auth/#{omniauth_strategy}"
|
109
|
+
Launchy.open(url)
|
110
|
+
rescue Launchy::CommandNotFoundError
|
111
|
+
Chronicle::ETL::Logger.info("Please open #{url} in a browser to continue")
|
112
|
+
end
|
113
|
+
|
114
|
+
def suppress_webrick_logging(server)
|
115
|
+
require 'webrick'
|
116
|
+
server.set(
|
117
|
+
:server_settings,
|
118
|
+
{
|
119
|
+
AccessLog: [],
|
120
|
+
# TODO: make this windows friendly
|
121
|
+
# https://github.com/winton/stasis/commit/77da36f43285fda129300e382f18dfaff48571b0
|
122
|
+
Logger: WEBrick::Log::new("/dev/null")
|
123
|
+
}
|
124
|
+
)
|
125
|
+
rescue LoadError
|
126
|
+
# no worries if we're not using WEBrick
|
127
|
+
end
|
128
|
+
|
129
|
+
def extract_secrets(authorization:, pluck_values:)
|
130
|
+
return authorization unless pluck_values&.any?
|
131
|
+
|
132
|
+
pluck_values.each_with_object({}) do |(key, identifiers), secrets|
|
133
|
+
secrets[key] = authorization.dig(*identifiers)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def omniauth_strategy
|
138
|
+
self.class.strategy
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -11,6 +11,18 @@ module Chronicle
|
|
11
11
|
# @todo Better validation for whether a gem is actually a plugin
|
12
12
|
# @todo Add ways to load a plugin that don't require a gem on rubygems.org
|
13
13
|
module PluginRegistry
|
14
|
+
class << self
|
15
|
+
# Start of a system for having non-gem plugins. Right now, we just
|
16
|
+
# make registry aware of existenc of name of non-gem plugin
|
17
|
+
def register_standalone(name)
|
18
|
+
standalones << name
|
19
|
+
end
|
20
|
+
|
21
|
+
def standalones
|
22
|
+
@standalones ||= []
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
14
26
|
# Does this plugin exist?
|
15
27
|
def self.exists?(name)
|
16
28
|
# TODO: implement this. Could query rubygems.org or use a hardcoded
|
@@ -33,8 +45,7 @@ module Chronicle
|
|
33
45
|
|
34
46
|
# Check whether a given plugin is installed
|
35
47
|
def self.installed?(name)
|
36
|
-
|
37
|
-
all_installed.map(&:name).include?(gem_name)
|
48
|
+
(standalones + all_installed.map { |gem| gem.name.gsub("chronicle-", "") }).include?(name)
|
38
49
|
end
|
39
50
|
|
40
51
|
# Activate a plugin with given name by `require`ing it
|
data/lib/chronicle/etl/runner.rb
CHANGED
@@ -63,7 +63,7 @@ class Chronicle::ETL::Runner
|
|
63
63
|
def prepare_ui
|
64
64
|
total = @extractor.results_count
|
65
65
|
@progress_bar = Chronicle::ETL::Utils::ProgressBar.new(title: 'Running job', total: total)
|
66
|
-
Chronicle::ETL::Logger.
|
66
|
+
Chronicle::ETL::Logger.attach_to_ui(@progress_bar)
|
67
67
|
end
|
68
68
|
|
69
69
|
def run_extraction
|
@@ -99,7 +99,7 @@ class Chronicle::ETL::Runner
|
|
99
99
|
def finish_job
|
100
100
|
@job_logger.save
|
101
101
|
@progress_bar&.finish
|
102
|
-
Chronicle::ETL::Logger.
|
102
|
+
Chronicle::ETL::Logger.detach_from_ui
|
103
103
|
Chronicle::ETL::Logger.info(tty_log_completion)
|
104
104
|
end
|
105
105
|
|
@@ -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
|
-
}.
|
59
|
+
}.deep_stringify_keys
|
46
60
|
Chronicle::ETL::Config.write("secrets", namespace, data)
|
47
61
|
end
|
48
62
|
|
data/lib/chronicle/etl.rb
CHANGED
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.
|
4
|
+
version: 0.5.4
|
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-04-
|
11
|
+
date: 2022-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.8.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: launchy
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: marcel
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +108,20 @@ dependencies:
|
|
94
108
|
- - "~>"
|
95
109
|
- !ruby/object:Gem::Version
|
96
110
|
version: '1.13'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: omniauth
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2'
|
97
125
|
- !ruby/object:Gem::Dependency
|
98
126
|
name: sequel
|
99
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,6 +136,20 @@ dependencies:
|
|
108
136
|
- - "~>"
|
109
137
|
- !ruby/object:Gem::Version
|
110
138
|
version: '5.35'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sinatra
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '2'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '2'
|
111
153
|
- !ruby/object:Gem::Dependency
|
112
154
|
name: sqlite3
|
113
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -235,33 +277,33 @@ dependencies:
|
|
235
277
|
- !ruby/object:Gem::Version
|
236
278
|
version: '2.1'
|
237
279
|
- !ruby/object:Gem::Dependency
|
238
|
-
name:
|
280
|
+
name: fakefs
|
239
281
|
requirement: !ruby/object:Gem::Requirement
|
240
282
|
requirements:
|
241
|
-
- - "
|
283
|
+
- - ">="
|
242
284
|
- !ruby/object:Gem::Version
|
243
|
-
version:
|
285
|
+
version: '0'
|
244
286
|
type: :development
|
245
287
|
prerelease: false
|
246
288
|
version_requirements: !ruby/object:Gem::Requirement
|
247
289
|
requirements:
|
248
|
-
- - "
|
290
|
+
- - ">="
|
249
291
|
- !ruby/object:Gem::Version
|
250
|
-
version:
|
292
|
+
version: '0'
|
251
293
|
- !ruby/object:Gem::Dependency
|
252
|
-
name:
|
294
|
+
name: guard-rspec
|
253
295
|
requirement: !ruby/object:Gem::Requirement
|
254
296
|
requirements:
|
255
|
-
- - "
|
297
|
+
- - "~>"
|
256
298
|
- !ruby/object:Gem::Version
|
257
|
-
version:
|
299
|
+
version: 4.7.3
|
258
300
|
type: :development
|
259
301
|
prerelease: false
|
260
302
|
version_requirements: !ruby/object:Gem::Requirement
|
261
303
|
requirements:
|
262
|
-
- - "
|
304
|
+
- - "~>"
|
263
305
|
- !ruby/object:Gem::Version
|
264
|
-
version:
|
306
|
+
version: 4.7.3
|
265
307
|
- !ruby/object:Gem::Dependency
|
266
308
|
name: pry-byebug
|
267
309
|
requirement: !ruby/object:Gem::Requirement
|
@@ -372,7 +414,10 @@ files:
|
|
372
414
|
- chronicle-etl.gemspec
|
373
415
|
- exe/chronicle-etl
|
374
416
|
- lib/chronicle/etl.rb
|
417
|
+
- lib/chronicle/etl/authorization_server.rb
|
418
|
+
- lib/chronicle/etl/authorizer.rb
|
375
419
|
- lib/chronicle/etl/cli.rb
|
420
|
+
- lib/chronicle/etl/cli/authorizations.rb
|
376
421
|
- lib/chronicle/etl/cli/cli_base.rb
|
377
422
|
- lib/chronicle/etl/cli/connectors.rb
|
378
423
|
- lib/chronicle/etl/cli/jobs.rb
|
@@ -407,6 +452,7 @@ files:
|
|
407
452
|
- lib/chronicle/etl/models/base.rb
|
408
453
|
- lib/chronicle/etl/models/entity.rb
|
409
454
|
- lib/chronicle/etl/models/raw.rb
|
455
|
+
- lib/chronicle/etl/oauth_authorizer.rb
|
410
456
|
- lib/chronicle/etl/registry/connector_registration.rb
|
411
457
|
- lib/chronicle/etl/registry/plugin_registry.rb
|
412
458
|
- lib/chronicle/etl/registry/registry.rb
|