chronicle-etl 0.5.2 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +72 -46
- data/chronicle-etl.gemspec +7 -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 -0
- data/lib/chronicle/etl/cli/connectors.rb +2 -2
- data/lib/chronicle/etl/cli/jobs.rb +16 -3
- data/lib/chronicle/etl/cli/main.rb +32 -20
- data/lib/chronicle/etl/cli/plugins.rb +19 -15
- data/lib/chronicle/etl/cli/secrets.rb +1 -1
- data/lib/chronicle/etl/cli.rb +1 -0
- data/lib/chronicle/etl/config.rb +3 -0
- data/lib/chronicle/etl/configurable.rb +4 -0
- data/lib/chronicle/etl/exceptions.rb +5 -0
- data/lib/chronicle/etl/job_definition.rb +4 -4
- data/lib/chronicle/etl/logger.rb +6 -6
- data/lib/chronicle/etl/models/base.rb +1 -1
- data/lib/chronicle/etl/models/entity.rb +3 -1
- data/lib/chronicle/etl/oauth_authorizer.rb +140 -0
- data/lib/chronicle/etl/registry/connectors.rb +60 -0
- data/lib/chronicle/etl/registry/plugin_registration.rb +19 -0
- data/lib/chronicle/etl/registry/plugins.rb +163 -0
- data/lib/chronicle/etl/registry/registry.rb +3 -52
- data/lib/chronicle/etl/registry/self_registering.rb +1 -1
- data/lib/chronicle/etl/runner.rb +53 -26
- data/lib/chronicle/etl/secrets.rb +15 -1
- data/lib/chronicle/etl/transformers/transformer.rb +4 -0
- data/lib/chronicle/etl/version.rb +1 -1
- data/lib/chronicle/etl.rb +1 -0
- metadata +101 -11
- data/lib/chronicle/etl/registry/plugin_registry.rb +0 -84
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a2de46efc3c5fbdc7ac120137bef56e13a138c8a95c8dd7d0a3542a65be65959
|
4
|
+
data.tar.gz: e8e3e9ae236e270b2926037419d5349170f85b8597c640c0b7b899552257fdb9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58b293d45d1a7f4589aee080d0fb1348e45d76753a8d48bb33e694d6a2b6ea123d1495a12a42e98ed6d3e65926ad48bd7de1fb98dac9bc7bac9225ef00d32fb3
|
7
|
+
data.tar.gz: e73c73b67b4e3790347da3df22ed7a11357fdf43e91b734925503f8d0c189aabd07e26f51ed25cf1409fd68ba74dd72e1e7e01c113f773832c509172f0b4ee84
|
data/README.md
CHANGED
@@ -6,21 +6,28 @@
|
|
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** (see [list](#available-plugins-and-connectors)). 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
|
+
|
18
|
+
## Chronicle-ETL in action
|
19
|
+
|
20
|
+
![demo](https://user-images.githubusercontent.com/6291/161410839-b5ce931a-2353-4585-b530-929f46e3f960.svg)
|
21
|
+
|
22
|
+
### Longer screencast
|
23
|
+
|
24
|
+
[![asciicast](https://asciinema.org/a/483455.svg)](https://asciinema.org/a/483455)
|
17
25
|
|
18
26
|
## Installation
|
19
27
|
|
20
28
|
Using homebrew:
|
21
29
|
```sh
|
22
30
|
$ brew install chronicle-app/etl/chronicle-etl
|
23
|
-
|
24
31
|
```
|
25
32
|
Using rubygems:
|
26
33
|
```sh
|
@@ -38,11 +45,15 @@ $ chronicle-etl --version
|
|
38
45
|
# Display help
|
39
46
|
$ chronicle-etl help
|
40
47
|
|
41
|
-
#
|
48
|
+
# Run a basic job
|
42
49
|
$ chronicle-etl --extractor NAME --transformer NAME --loader NAME
|
43
50
|
|
44
51
|
# Read test.csv and display it to stdout as a table
|
45
|
-
$ chronicle-etl --extractor csv --input
|
52
|
+
$ chronicle-etl --extractor csv --input data.csv --loader table
|
53
|
+
|
54
|
+
# Show available plugins and install one
|
55
|
+
$ chronicle-etl plugins:list
|
56
|
+
$ chronicle-etl plugins:install shell
|
46
57
|
|
47
58
|
# Retrieve shell commands run in the last 5 hours
|
48
59
|
$ chronicle-etl -e shell --since 5h
|
@@ -79,37 +90,36 @@ Options:
|
|
79
90
|
[--silent], [--no-silent] # Silence all output
|
80
91
|
```
|
81
92
|
|
82
|
-
### Saving
|
83
|
-
|
84
|
-
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 of setting the CLI flags for each run.
|
93
|
+
### Saving a job
|
94
|
+
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.
|
85
95
|
|
86
96
|
```sh
|
87
97
|
# Save a job named 'sample' to ~/.config/chronicle/etl/jobs/sample.yml
|
88
98
|
$ chronicle-etl jobs:save sample --extractor pinboard --since 10d
|
89
99
|
|
90
|
-
# Show details about the job
|
91
|
-
$ chronicle-etl jobs:show sample
|
92
|
-
|
93
100
|
# Run the job
|
94
101
|
$ chronicle-etl jobs:run sample
|
95
|
-
|
96
|
-
|
102
|
+
|
103
|
+
# Show details about the job
|
104
|
+
$ chronicle-etl jobs:show sample
|
97
105
|
|
98
106
|
# Show all saved jobs
|
99
107
|
$ chronicle-etl jobs:list
|
100
108
|
```
|
101
109
|
|
102
|
-
## Connectors
|
103
|
-
|
110
|
+
## Connectors and plugins
|
111
|
+
|
112
|
+
Connectors let you work with different data formats or third-party providers.
|
113
|
+
|
114
|
+
### Built-in Connectors
|
115
|
+
|
116
|
+
`chronicle-etl` comes with several built-in connectors for common formats and sources.
|
104
117
|
|
105
118
|
```sh
|
106
119
|
# List all available connectors
|
107
120
|
$ chronicle-etl connectors:list
|
108
121
|
```
|
109
122
|
|
110
|
-
### Built-in Connectors
|
111
|
-
`chronicle-etl` comes with several built-in connectors for common formats and sources.
|
112
|
-
|
113
123
|
#### Extractors
|
114
124
|
- [`csv`](https://github.com/chronicle-app/chronicle-etl/blob/main/lib/chronicle/etl/extractors/csv_extractor.rb) - Load records from CSV files or stdin
|
115
125
|
- [`json`](https://github.com/chronicle-app/chronicle-etl/blob/main/lib/chronicle/etl/extractors/json_extractor.rb) - Load JSON (either [line-separated objects](https://en.wikipedia.org/wiki/JSON_streaming#Line-delimited_JSON) or one object)
|
@@ -124,18 +134,19 @@ $ chronicle-etl connectors:list
|
|
124
134
|
- [`json`](https://github.com/chronicle-app/chronicle-etl/blob/main/lib/chronicle/etl/loaders/json_loader.rb) - Load records serialized as JSON
|
125
135
|
- [`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
|
126
136
|
|
127
|
-
|
128
|
-
|
137
|
+
### Chronicle Plugins for third-party services
|
138
|
+
|
139
|
+
Plugins provide access to data from third-party platforms, services, or formats. Plugins are packaged as separate gems and can be installed through the CLI (under the hood, it's a `gem install chronicle-PLUGINNAME`)
|
129
140
|
|
130
|
-
|
141
|
+
#### Plugin usage
|
131
142
|
|
132
143
|
```bash
|
144
|
+
# List available plugins
|
145
|
+
$ chronicle-etl plugins:list
|
146
|
+
|
133
147
|
# Install a plugin
|
134
148
|
$ chronicle-etl plugins:install NAME
|
135
149
|
|
136
|
-
# List installed plugins
|
137
|
-
$ chronicle-etl plugins:list
|
138
|
-
|
139
150
|
# Use a plugin
|
140
151
|
$ chronicle-etl plugins:install shell
|
141
152
|
$ chronicle-etl --extractor shell:history --limit 10
|
@@ -143,31 +154,45 @@ $ chronicle-etl --extractor shell:history --limit 10
|
|
143
154
|
# Uninstall a plugin
|
144
155
|
$ chronicle-etl plugins:uninstall NAME
|
145
156
|
```
|
146
|
-
|
147
|
-
|
157
|
+
#### Available plugins and connectors
|
158
|
+
|
159
|
+
The following are the officially-supported list of plugins and their available connectors:
|
160
|
+
|
161
|
+
| Plugin | Type | Identifier | Description | Description |
|
162
|
+
|---------------------------------------------------------------------|-------------|------------------|-------------------------------------------|-------------------------------------------|
|
163
|
+
| [email](https://github.com/chronicle-app/chronicle-email) | extractor | imap | emails over an IMAP connection | emails over an IMAP connection |
|
164
|
+
| [email](https://github.com/chronicle-app/chronicle-email) | extractor | mbox | emails from an .mbox file | emails from an .mbox file |
|
165
|
+
| [email](https://github.com/chronicle-app/chronicle-email) | transformer | email | email to Chronicle Schema | email to Chronicle Schema |
|
166
|
+
| [foursquare](https://github.com/chronicle-app/chronicle-foursquare) | extractor | checkins | Foursqure visits | Foursqure visits |
|
167
|
+
| [foursquare](https://github.com/chronicle-app/chronicle-foursquare) | transformer | checkin | checkin to Chronicle Schema | checkin to Chronicle Schema |
|
168
|
+
| [github](https://github.com/chronicle-app/chronicle-github) | extractor | activity | user activity stream | user activity stream |
|
169
|
+
| [imessage](https://github.com/chronicle-app/chronicle-imessage) | extractor | messages | imessages from local macOS | imessages from local macOS |
|
170
|
+
| [imessage](https://github.com/chronicle-app/chronicle-imessage) | transformer | message | imessage to Chronicle Schema | imessage to Chronicle Schema |
|
171
|
+
| [pinboard](https://github.com/chronicle-app/chronicle-pinboard) | extractor | bookmarks | Pinboard.in bookmarks | Pinboard.in bookmarks |
|
172
|
+
| [pinboard](https://github.com/chronicle-app/chronicle-pinboard) | transformer | bookmark | bookmark to Chronicle Schema | bookmark to Chronicle Schema |
|
173
|
+
| [safari](https://github.com/chronicle-app/chronicle-safari) | extractor | browser-history | browser history | browser history |
|
174
|
+
| [safari ](https://github.com/chronicle-app/chronicle-safari ) | transformer | browser-history | browser history to Chronicle Schema | browser history to Chronicle Schema |
|
175
|
+
| [shell](https://github.com/chronicle-app/chronicle-shell) | extractor | history | shell command history (bash / zsh) | shell command history (bash / zsh) |
|
176
|
+
| [shell](https://github.com/chronicle-app/chronicle-shell) | transformer | command | command to Chronicle Schema | command to Chronicle Schema |
|
177
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | extractor | liked-tracks | liked tracks | liked tracks |
|
178
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | extractor | saved-albums | saved albums | saved albums |
|
179
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | extractor | listens | recently listened tracks (last 50 tracks) | recently listened tracks (last 50 tracks) |
|
180
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | transformer | like | like to Chronicle Schema | like to Chronicle Schema |
|
181
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | transformer | listen | listen to Chronicle Schema | listen to Chronicle Schema |
|
182
|
+
| [spotify](https://github.com/chronicle-app/chronicle-spotify) | authorizer | | OAuth authorizer | OAuth authorizer |
|
183
|
+
| [zulip](https://github.com/chronicle-app/chronicle-zulip) | extractor | private-messages | private messages | private messages |
|
184
|
+
| [zulip](https://github.com/chronicle-app/chronicle-zulip) | transformer | message | message to Chronicle Schema | message to Chronicle Schema |
|
185
|
+
|
186
|
+
|
187
|
+
### Coming soon
|
148
188
|
|
149
189
|
A few dozen importers exist [in my Memex project](https://hyfen.net/memex/) and I'm porting them over to the Chronicle system. The [Chronicle Plugin Tracker](https://github.com/orgs/chronicle-app/projects/1/views/1) lets you keep track what's available and what's coming soon.
|
150
190
|
|
151
191
|
If you don't see a plugin for a third-party provider or data source that you're interested in using with `chronicle-etl`, [please open an issue](https://github.com/chronicle-app/chronicle-etl/issues/new). If you want to work together on a plugin, please [get in touch](#get-in-touch)!
|
152
192
|
|
153
|
-
#### Currently available
|
154
|
-
|
155
|
-
| Name | Description | Availability |
|
156
|
-
|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------------------------------|
|
157
|
-
| [imessage](https://github.com/chronicle-app/chronicle-imessage) | iMessage messages and attachments | Available |
|
158
|
-
| [shell](https://github.com/chronicle-app/chronicle-shell) | Shell command history | Available (still needs zsh support) |
|
159
|
-
| [email](https://github.com/chronicle-app/chronicle-email) | Emails and attachments from IMAP or .mbox files | Available (still needs IMAP support) |
|
160
|
-
| [pinboard](https://github.com/chronicle-app/chronicle-email) | Bookmarks and tags | Available |
|
161
|
-
| [safari](https://github.com/chronicle-app/chronicle-safari) | Browser history from local sqlite db | Available |
|
162
|
-
| [github](https://github.com/chronicle-app/chronicle-github) | Github activity stream | Available |
|
163
|
-
|
164
|
-
#### Coming soon
|
165
|
-
|
166
193
|
In summary, the following **are coming soon**:
|
167
194
|
anki, arc, bear, chrome, facebook, firefox, fitbit, foursquare, git, github, goodreads, google-calendar, images, instagram, lastfm, shazam, slack, strava, things, twitter, whatsapp, youtube.
|
168
195
|
|
169
|
-
Please check the [Chronicle Plugin Tracker](https://github.com/orgs/chronicle-app/projects/1/views/1) for details.
|
170
|
-
|
171
196
|
### Writing your own plugin
|
172
197
|
|
173
198
|
Additional connectors are packaged as separate ruby gems. You can view the [iMessage plugin](https://github.com/chronicle-app/chronicle-imessage) for an example.
|
@@ -202,6 +227,7 @@ module Chronicle
|
|
202
227
|
end
|
203
228
|
```
|
204
229
|
|
230
|
+
|
205
231
|
## Secrets Management
|
206
232
|
|
207
233
|
If your job needs secrets such as access tokens or passwords, `chronicle-etl` has a built-in secret management system.
|
@@ -236,8 +262,8 @@ $ chronicle-etl secrets:unset pinboard access_token
|
|
236
262
|
## Roadmap
|
237
263
|
|
238
264
|
- Keep tackling **new plugins**. See: [Chronicle Plugin Tracker](https://github.com/orgs/chronicle-app/projects/1)
|
239
|
-
- Add support for **incremental extractions** #37
|
240
|
-
- **Improve stdin extractor and shell command transformer**
|
265
|
+
- Add support for **incremental extractions** ([#37](https://github.com/chronicle-app/chronicle-etl/issues/37))
|
266
|
+
- **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))
|
241
267
|
- **Add documentation for Chronicle Schema**. It's found throughout this project but never explained.
|
242
268
|
|
243
269
|
## Development
|
data/chronicle-etl.gemspec
CHANGED
@@ -40,10 +40,14 @@ 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 "gems", ">= 1"
|
44
|
+
spec.add_dependency "launchy"
|
43
45
|
spec.add_dependency "marcel", "~> 1.0.2"
|
44
46
|
spec.add_dependency "mini_exiftool", "~> 2.10"
|
45
47
|
spec.add_dependency "nokogiri", "~> 1.13"
|
48
|
+
spec.add_dependency "omniauth", "~> 2"
|
46
49
|
spec.add_dependency "sequel", "~> 5.35"
|
50
|
+
spec.add_dependency "sinatra", "~> 2"
|
47
51
|
spec.add_dependency "sqlite3", "~> 1.4"
|
48
52
|
spec.add_dependency "thor", "~> 1.2"
|
49
53
|
spec.add_dependency "thor-hollaback", "~> 0.2"
|
@@ -54,12 +58,14 @@ Gem::Specification.new do |spec|
|
|
54
58
|
spec.add_dependency "xdg", ">= 4.0"
|
55
59
|
|
56
60
|
spec.add_development_dependency "bundler", "~> 2.1"
|
61
|
+
spec.add_development_dependency "fakefs", "~> 1.4"
|
57
62
|
spec.add_development_dependency "guard-rspec", "~> 4.7.3"
|
58
|
-
spec.add_development_dependency "fakefs"
|
59
63
|
spec.add_development_dependency "pry-byebug", "~> 3.9"
|
60
64
|
spec.add_development_dependency "rake", "~> 13.0"
|
61
65
|
spec.add_development_dependency "rspec", "~> 3.9"
|
62
66
|
spec.add_development_dependency "rubocop", "~> 1.25.1"
|
63
67
|
spec.add_development_dependency "simplecov", "~> 0.21"
|
68
|
+
spec.add_development_dependency "vcr", "~> 6.1"
|
69
|
+
spec.add_development_dependency "webmock", "~> 3"
|
64
70
|
spec.add_development_dependency "yard", "~> 0.9.7"
|
65
71
|
end
|
@@ -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::Plugins.installed?(provider)
|
41
|
+
cli_fail(message: "Plugin for #{provider} is not installed.")
|
42
|
+
end
|
43
|
+
|
44
|
+
begin
|
45
|
+
Chronicle::ETL::Registry::Plugins.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,6 +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
|
+
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
|
+
|
9
13
|
cli_exit(status: :failure, message: message, exception: exception)
|
10
14
|
end
|
11
15
|
|
@@ -13,7 +13,7 @@ module Chronicle
|
|
13
13
|
desc "list", "Lists available connectors"
|
14
14
|
# Display all available connectors that chronicle-etl has access to
|
15
15
|
def list
|
16
|
-
connector_info = Chronicle::ETL::Registry.connectors.map do |connector_registration|
|
16
|
+
connector_info = Chronicle::ETL::Registry::Connectors.connectors.map do |connector_registration|
|
17
17
|
{
|
18
18
|
identifier: connector_registration.identifier,
|
19
19
|
phase: connector_registration.phase,
|
@@ -43,7 +43,7 @@ module Chronicle
|
|
43
43
|
end
|
44
44
|
|
45
45
|
begin
|
46
|
-
connector = Chronicle::ETL::Registry.find_by_phase_and_identifier(phase.to_sym, identifier)
|
46
|
+
connector = Chronicle::ETL::Registry::Connectors.find_by_phase_and_identifier(phase.to_sym, identifier)
|
47
47
|
rescue Chronicle::ETL::ConnectorNotAvailableError, Chronicle::ETL::PluginError => e
|
48
48
|
cli_fail(message: "Could not find #{phase} #{identifier}", exception: e)
|
49
49
|
end
|
@@ -43,6 +43,17 @@ module Chronicle
|
|
43
43
|
LONG_DESC
|
44
44
|
# Run an ETL job
|
45
45
|
def start(name = nil)
|
46
|
+
# If someone runs `$ chronicle-etl` with no arguments, show help menu.
|
47
|
+
# TODO: decide if we should check that there's nothing in stdin pipe
|
48
|
+
# in case user wants to actually run this sort of job stdin->null->stdout
|
49
|
+
if name.nil? && options[:extractor].nil?
|
50
|
+
m = Chronicle::ETL::CLI::Main.new
|
51
|
+
m.help
|
52
|
+
cli_exit
|
53
|
+
end
|
54
|
+
|
55
|
+
cli_fail(message: "Job '#{name}' does not exist") if name && !Chronicle::ETL::Config.exists?("jobs", name)
|
56
|
+
|
46
57
|
job_definition = build_job_definition(name, options)
|
47
58
|
|
48
59
|
if job_definition.plugins_missing?
|
@@ -82,11 +93,10 @@ LONG_DESC
|
|
82
93
|
|
83
94
|
if write_config
|
84
95
|
Chronicle::ETL::Config.write("jobs", name, job_definition.definition)
|
85
|
-
cli_exit(message: "Job saved. Run it with `$chronicle-etl jobs:run #{name}`")
|
96
|
+
cli_exit(message: "Job saved. Run it with `$ chronicle-etl jobs:run #{name}`")
|
86
97
|
else
|
87
98
|
cli_fail(message: "\nJob not saved")
|
88
99
|
end
|
89
|
-
|
90
100
|
rescue Chronicle::ETL::JobDefinitionError => e
|
91
101
|
cli_fail(message: "Job definition error", exception: e)
|
92
102
|
end
|
@@ -94,6 +104,8 @@ LONG_DESC
|
|
94
104
|
desc "show", "Show details about a job"
|
95
105
|
# Show an ETL job
|
96
106
|
def show(name = nil)
|
107
|
+
cli_fail(message: "Job '#{name}' does not exist") if name && !Chronicle::ETL::Config.exists?("jobs", name)
|
108
|
+
|
97
109
|
job_definition = build_job_definition(name, options)
|
98
110
|
job_definition.validate!
|
99
111
|
puts Chronicle::ETL::Job.new(job_definition)
|
@@ -132,10 +144,11 @@ LONG_DESC
|
|
132
144
|
job_definition.validate!
|
133
145
|
# FIXME: clumsy to make CLI responsible for setting secrets here. Think about a better way to do this
|
134
146
|
job_definition.apply_default_secrets
|
135
|
-
|
136
147
|
job = Chronicle::ETL::Job.new(job_definition)
|
137
148
|
runner = Chronicle::ETL::Runner.new(job)
|
138
149
|
runner.run!
|
150
|
+
rescue RunnerError => e
|
151
|
+
cli_fail(message: "#{e.message}", exception: e)
|
139
152
|
end
|
140
153
|
|
141
154
|
# TODO: probably could merge this with something in cli/plugin
|
@@ -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
|
@@ -54,24 +57,40 @@ module Chronicle
|
|
54
57
|
klass, task = ::Thor::Util.find_class_and_task_by_namespace("#{meth}:#{meth}")
|
55
58
|
klass.start(['-h', task].compact, shell: shell)
|
56
59
|
else
|
57
|
-
shell.say "ABOUT".bold
|
58
|
-
shell.say " #{'chronicle-etl'.italic} is a
|
60
|
+
shell.say "ABOUT:".bold
|
61
|
+
shell.say " #{'chronicle-etl'.italic} is a toolkit for extracting and working with your digital"
|
62
|
+
shell.say " history. 📜"
|
63
|
+
shell.say
|
64
|
+
shell.say " A job #{'extracts'.underline} personal data from a source, #{'transforms'.underline} it (Chronicle"
|
65
|
+
shell.say " Schema or preserves raw data), and then #{'loads'.underline} it to a destination. Use"
|
66
|
+
shell.say " built-in extractors (json, csv, stdin) and loaders (csv, json, table,"
|
67
|
+
shell.say " rest) or use plugins to connect to third-party services."
|
68
|
+
shell.say
|
69
|
+
shell.say " Plugins: https://github.com/chronicle-app/chronicle-etl#currently-available"
|
59
70
|
shell.say
|
60
|
-
shell.say "USAGE".bold
|
61
|
-
shell.say "
|
71
|
+
shell.say "USAGE:".bold
|
72
|
+
shell.say " # Basic job usage:".italic.light_black
|
73
|
+
shell.say " $ chronicle-etl --extractor NAME --transformer NAME --loader NAME"
|
62
74
|
shell.say
|
63
|
-
shell.say "
|
64
|
-
shell.say "
|
65
|
-
shell.say " $ chronicle-etl connectors:list"
|
75
|
+
shell.say " # Read test.csv and display it to stdout as a table:".italic.light_black
|
76
|
+
shell.say " $ chronicle-etl --extractor csv --input data.csv --loader table"
|
66
77
|
shell.say
|
67
|
-
shell.say "
|
68
|
-
shell.say " $ chronicle-etl
|
78
|
+
shell.say " # Show available plugins:".italic.light_black
|
79
|
+
shell.say " $ chronicle-etl plugins:list"
|
69
80
|
shell.say
|
70
|
-
shell.say "
|
81
|
+
shell.say " # Save an access token as a secret and use it in a job:".italic.light_black
|
82
|
+
shell.say " $ chronicle-etl secrets:set pinboard access_token username:foo123"
|
83
|
+
shell.say " $ chronicle-etl secrets:list"
|
84
|
+
shell.say " $ chronicle-etl -e pinboard --since 1mo"
|
85
|
+
shell.say
|
86
|
+
shell.say " # Show full job options:".italic.light_black
|
71
87
|
shell.say " $ chronicle-etl jobs help run"
|
88
|
+
shell.say
|
89
|
+
shell.say "FULL DOCUMENTATION:".bold
|
90
|
+
shell.say " https://github.com/chronicle-app/chronicle-etl".blue
|
91
|
+
shell.say
|
72
92
|
|
73
93
|
list = []
|
74
|
-
|
75
94
|
::Thor::Util.thor_classes_in(Chronicle::ETL::CLI).each do |thor_class|
|
76
95
|
list += thor_class.printable_tasks(false)
|
77
96
|
end
|
@@ -79,25 +98,18 @@ module Chronicle
|
|
79
98
|
list.unshift ["help", "# This help menu"]
|
80
99
|
|
81
100
|
shell.say
|
82
|
-
shell.say 'ALL COMMANDS'.bold
|
101
|
+
shell.say 'ALL COMMANDS:'.bold
|
83
102
|
shell.print_table(list, indent: 2, truncate: true)
|
84
103
|
shell.say
|
85
|
-
shell.say "VERSION".bold
|
104
|
+
shell.say "VERSION:".bold
|
86
105
|
shell.say " #{Chronicle::ETL::VERSION}"
|
87
106
|
shell.say
|
88
107
|
shell.say " Display current version:".italic.light_black
|
89
108
|
shell.say " $ chronicle-etl --version"
|
90
|
-
shell.say
|
91
|
-
shell.say "FULL DOCUMENTATION".bold
|
92
|
-
shell.say " https://github.com/chronicle-app/chronicle-etl".blue
|
93
|
-
shell.say
|
94
109
|
end
|
95
110
|
end
|
96
111
|
|
97
112
|
no_commands do
|
98
|
-
def testb
|
99
|
-
puts "hi"
|
100
|
-
end
|
101
113
|
def set_color_output
|
102
114
|
String.disable_colorization true if options[:'no-color'] || ENV['NO_COLOR']
|
103
115
|
end
|
@@ -16,7 +16,7 @@ module Chronicle
|
|
16
16
|
cli_fail(message: "Please specify a plugin to install") unless plugins.any?
|
17
17
|
|
18
18
|
installed, not_installed = plugins.partition do |plugin|
|
19
|
-
Chronicle::ETL::Registry::
|
19
|
+
Chronicle::ETL::Registry::Plugins.installed?(plugin)
|
20
20
|
end
|
21
21
|
|
22
22
|
puts "Already installed: #{installed.join(", ")}" if installed.any?
|
@@ -27,7 +27,7 @@ module Chronicle
|
|
27
27
|
|
28
28
|
not_installed.each do |plugin|
|
29
29
|
spinner.update(title: "Installing #{plugin}")
|
30
|
-
Chronicle::ETL::Registry::
|
30
|
+
Chronicle::ETL::Registry::Plugins.install(plugin)
|
31
31
|
|
32
32
|
rescue Chronicle::ETL::PluginError => e
|
33
33
|
spinner.error("Error".red)
|
@@ -41,7 +41,7 @@ module Chronicle
|
|
41
41
|
def uninstall(name)
|
42
42
|
spinner = TTY::Spinner.new("[:spinner] Uninstalling plugin #{name}...", format: :dots_2)
|
43
43
|
spinner.auto_spin
|
44
|
-
Chronicle::ETL::Registry::
|
44
|
+
Chronicle::ETL::Registry::Plugins.uninstall(name)
|
45
45
|
spinner.success("(#{'successful'.green})")
|
46
46
|
rescue Chronicle::ETL::PluginError => e
|
47
47
|
spinner.error("Error".red)
|
@@ -51,20 +51,24 @@ module Chronicle
|
|
51
51
|
desc "list", "Lists available plugins"
|
52
52
|
# Display all available plugins that chronicle-etl has access to
|
53
53
|
def list
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
54
|
+
values = Chronicle::ETL::Registry::Plugins.all
|
55
|
+
.map do |plugin|
|
56
|
+
[
|
57
|
+
plugin.name,
|
58
|
+
plugin.description,
|
59
|
+
plugin.installed ? '✓' : '',
|
60
|
+
plugin.version
|
61
|
+
]
|
62
62
|
end
|
63
63
|
|
64
|
-
headers = ['name', 'description', '
|
65
|
-
table = TTY::Table.new(headers,
|
66
|
-
puts "
|
67
|
-
puts table.render(
|
64
|
+
headers = ['name', 'description', 'installed', 'version'].map{ |h| h.to_s.upcase.bold }
|
65
|
+
table = TTY::Table.new(headers, values)
|
66
|
+
puts "Available plugins:"
|
67
|
+
puts table.render(
|
68
|
+
indent: 2,
|
69
|
+
padding: [0, 0],
|
70
|
+
alignments: [:left, :left, :center, :left]
|
71
|
+
)
|
68
72
|
end
|
69
73
|
end
|
70
74
|
end
|