chronicle-etl 0.4.4 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f035ef95ebae675973ce505c71345c0c2da640b20a3e88050f4c88c76caf656
4
- data.tar.gz: '0486e4ce5bfdb85ad6ccb5a792ac7aa5a897afecf839c759bb78a2f33136d34e'
3
+ metadata.gz: b8faa084cfe4a9f080ee5494c69b268b78bfa8f3502354e740264e6941f13daf
4
+ data.tar.gz: 1bf4f2751c71cadedc78a2fe3ed5b09bf86cd601a909e2fa2db0a0de8cc2c21d
5
5
  SHA512:
6
- metadata.gz: f9a1ba3cb4a9abd3bc8a499012b3456b1a2b4cf1f55bed1213f0b1baa6ea96d0ad6e54a470425fa5aa4961061630095218a31f64ef4a39bea15c547219f9a7a8
7
- data.tar.gz: d82ff59fd2875d55b079b7814b6a028f98f80f17d3ae2bb3291e5ae6cfb7e1b06f571e16fc73c83dea41d7682f24eb9b7ee3fa6ae7cc709ede57e12011e6a0be
6
+ metadata.gz: ff10779b663a3321b779fb03e07249856174d96fb96e405ae906a47441c288d6a245c852525801ba250cce1125cf05c523ef4ec75fdfb4335cef9003091437ed
7
+ data.tar.gz: 509f6f92e95341d212c54b6b000bc54e8ba03898497191a3e5d3b14db7bff3ed625d0fee403888fbb6103c1edc14de66b215d9aa84ddb68cefcf51c0e6c74138
data/.rubocop.yml CHANGED
@@ -11,6 +11,9 @@ Style/StringLiterals:
11
11
  Layout/MultilineAssignmentLayout:
12
12
  Enabled: false
13
13
 
14
+ Layout/MultilineMethodCallIndentation:
15
+ EnforcedStyle: indented
16
+
14
17
  Layout/RedundantLineBreak:
15
18
  Enabled: false
16
19
 
data/README.md CHANGED
@@ -16,12 +16,21 @@ If you don’t want to spend all your time writing scrapers, reverse-engineering
16
16
  * **A common, opinionated schema**: You can normalize different datasets into a single schema so that, for example, all your iMessages and emails are stored in a common schema. Don’t want to use the schema? `chronicle-etl` always allows you to fall back on working with the raw extraction data.
17
17
 
18
18
  ## Installation
19
+
20
+ Using homebrew:
21
+ ```sh
22
+ $ brew install chronicle-app/etl/chronicle-etl
23
+
24
+ ```
25
+ Using rubygems:
19
26
  ```sh
20
- # Install chronicle-etl
21
- gem install chronicle-etl
27
+ $ gem install chronicle-etl
22
28
  ```
23
29
 
24
- After installation, the `chronicle-etl` command will be available in your shell. Homebrew support [is coming soon](https://github.com/chronicle-app/chronicle-etl/issues/13).
30
+ Confirm it installed successfully:
31
+ ```sh
32
+ $ chronicle-etl --version
33
+ ```
25
34
 
26
35
  ## Basic usage and running jobs
27
36
 
@@ -34,28 +43,60 @@ $ chronicle-etl --extractor NAME --transformer NAME --loader NAME
34
43
 
35
44
  # Read test.csv and display it to stdout as a table
36
45
  $ chronicle-etl --extractor csv --input ./data.csv --loader table
46
+
47
+ # Retrieve shell commands run in the last 5 hours
48
+ $ chronicle-etl -e shell --since 5h
49
+
50
+ # Get email senders from an .mbox email archive file
51
+ $ chronicle-etl --extractor email:mbox -i sample-email-archive.mbox -t email --fields actor.slug
52
+
53
+ # Save an access token as a secret and use it in a job
54
+ $ chronicle-etl secrets:set pinboard access_token username:foo123
55
+ $ chronicle-etl secrets:list # Verify that's it's available
56
+ $ chronicle-etl -e pinboard --since 1mo # Used automatically based on plugin name
37
57
  ```
38
58
 
39
59
  ### Common options
40
60
  ```sh
41
61
  Options:
42
- -j, [--name=NAME] # Job configuration name
43
- -e, [--extractor=EXTRACTOR-NAME] # Extractor class. Default: stdin
44
- [--extractor-opts=key:value] # Extractor options
45
- -t, [--transformer=TRANFORMER-NAME] # Transformer class. Default: null
46
- [--transformer-opts=key:value] # Transformer options
47
- -l, [--loader=LOADER-NAME] # Loader class. Default: stdout
48
- [--loader-opts=key:value] # Loader options
49
- -i, [--input=FILENAME] # Input filename or directory
50
- [--since=DATE] # Load records SINCE this date. Overrides job's `load_since` configuration option in extractor's options
51
- [--until=DATE] # Load records UNTIL this date
52
- [--limit=N] # Only extract the first LIMIT records
53
- -o, [--output=OUTPUT] # Output filename
54
- [--fields=field1 field2 ...] # Output only these fields
55
- [--log-level=LOG_LEVEL] # Log level (debug, info, warn, error, fatal)
56
- # Default: info
57
- -v, [--verbose], [--no-verbose] # Set log level to verbose
58
- [--silent], [--no-silent] # Silence all output
62
+ -e, [--extractor=NAME] # Extractor class. Default: stdin
63
+ [--extractor-opts=key:value] # Extractor options
64
+ -t, [--transformer=NAME] # Transformer class. Default: null
65
+ [--transformer-opts=key:value] # Transformer options
66
+ -l, [--loader=NAME] # Loader class. Default: table
67
+ [--loader-opts=key:value] # Loader options
68
+ -i, [--input=FILENAME] # Input filename or directory
69
+ [--since=DATE] # Load records SINCE this date (or fuzzy time duration)
70
+ [--until=DATE] # Load records UNTIL this date (or fuzzy time duration)
71
+ [--limit=N] # Only extract the first LIMIT records
72
+ -o, [--output=OUTPUT] # Output filename
73
+ [--fields=field1 field2 ...] # Output only these fields
74
+ [--header-row], [--no-header-row] # Output the header row of tabular output
75
+
76
+ [--log-level=LOG_LEVEL] # Log level (debug, info, warn, error, fatal)
77
+ # Default: info
78
+ -v, [--verbose], [--no-verbose] # Set log level to verbose
79
+ [--silent], [--no-silent] # Silence all output
80
+ ```
81
+
82
+ ### Saving jobs
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.
85
+
86
+ ```sh
87
+ # Save a job named 'sample' to ~/.config/chronicle/etl/jobs/sample.yml
88
+ $ chronicle-etl jobs:save sample --extractor pinboard --since 10d
89
+
90
+ # Show details about the job
91
+ $ chronicle-etl jobs:show sample
92
+
93
+ # Run the job
94
+ $ chronicle-etl jobs:run sample
95
+ # Or more simply:
96
+ $ chronicle-etl sample
97
+
98
+ # Show all saved jobs
99
+ $ chronicle-etl jobs:list
59
100
  ```
60
101
 
61
102
  ## Connectors
@@ -83,58 +124,51 @@ $ chronicle-etl connectors:list
83
124
  - [`json`](https://github.com/chronicle-app/chronicle-etl/blob/main/lib/chronicle/etl/loaders/json_loader.rb) - Load records serialized as JSON
84
125
  - [`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
85
126
 
86
- ### Plugins
87
- Plugins provide access to data from third-party platforms, services, or formats.
127
+ ## Chronicle Plugins
128
+ 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 (which installs the Gems under the hood).
129
+
130
+ ### Plugin usage
88
131
 
89
132
  ```bash
90
133
  # Install a plugin
91
134
  $ chronicle-etl plugins:install NAME
92
135
 
93
- # Install the imessage plugin
94
- $ chronicle-etl plugins:install imessage
95
-
96
136
  # List installed plugins
97
137
  $ chronicle-etl plugins:list
98
138
 
139
+ # Use a plugin
140
+ $ chronicle-etl plugins:install shell
141
+ $ chronicle-etl --extractor shell:history --limit 10
142
+
99
143
  # Uninstall a plugin
100
144
  $ chronicle-etl plugins:uninstall NAME
101
145
  ```
102
146
 
103
- A few dozen importers exist [in my Memex project](https://hyfen.net/memex/) and they’re being ported over to the Chronicle system. This table shows what’s available now and what’s coming. Rows are sorted in very rough order of priority.
147
+ ### Status
148
+
149
+ 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.
104
150
 
105
- If you want to work together on a connector, please [get in touch](#get-in-touch)!
151
+ 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
+
153
+ #### Currently available
106
154
 
107
155
  | Name | Description | Availability |
108
156
  |-----------------------------------------------------------------|---------------------------------------------------------------------------------------------|----------------------------------|
109
157
  | [imessage](https://github.com/chronicle-app/chronicle-imessage) | iMessage messages and attachments | Available |
110
- | [shell](https://github.com/chronicle-app/chronicle-shell) | Shell command history | Available (zsh support pending) |
111
- | [email](https://github.com/chronicle-app/chronicle-email) | Emails and attachments from IMAP or .mbox files | Available (imap support pending) |
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) |
112
160
  | [pinboard](https://github.com/chronicle-app/chronicle-email) | Bookmarks and tags | Available |
113
161
  | [safari](https://github.com/chronicle-app/chronicle-safari) | Browser history from local sqlite db | Available |
114
- | github | Github user and repo activity | In progress |
115
- | chrome | Browser history from local sqlite db | Needs porting |
116
- | whatsapp | Messaging history (via individual chat exports) or reverse-engineered local desktop install | Unstarted |
117
- | anki | Studying and card creation history | Needs porting |
118
- | facebook | Messaging and history posting via data export files | Needs porting |
119
- | twitter | History via API or export data files | Needs porting |
120
- | foursquare | Location history via API | Needs porting |
121
- | goodreads | Reading history via export csv (RIP goodreads API) | Needs porting |
122
- | lastfm | Listening history via API | Needs porting |
123
- | images | Process image files | Needs porting |
124
- | arc | Location history from synced icloud backup files | Needs porting |
125
- | firefox | Browser history from local sqlite db | Needs porting |
126
- | fitbit | Personal analytics via API | Needs porting |
127
- | git | Commit history on a repo | Needs porting |
128
- | google-calendar | Calendar events via API | Needs porting |
129
- | instagram | Posting and messaging history via export data | Needs porting |
130
- | shazam | Song tags via reverse-engineered API | Needs porting |
131
- | slack | Messaging history via API | Need rethinking |
132
- | strava | Activity history via API | Needs porting |
133
- | things | Task activity via local sqlite db | Needs porting |
134
- | bear | Note taking activity via local sqlite db | Needs porting |
135
- | youtube | Video activity via takeout data and API | Needs porting |
136
-
137
- ### Writing your own connector
162
+ | [github](https://github.com/chronicle-app/chronicle-github) | Github activity stream | Available |
163
+
164
+ #### Coming soon
165
+
166
+ In summary, the following **are coming soon**:
167
+ anki, arc, bear, chrome, facebook, firefox, fitbit, foursquare, git, github, goodreads, google-calendar, images, instagram, lastfm, shazam, slack, strava, things, twitter, whatsapp, youtube.
168
+
169
+ Please check the [Chronicle Plugin Tracker](https://github.com/orgs/chronicle-app/projects/1/views/1) for details.
170
+
171
+ ### Writing your own plugin
138
172
 
139
173
  Additional connectors are packaged as separate ruby gems. You can view the [iMessage plugin](https://github.com/chronicle-app/chronicle-imessage) for an example.
140
174
 
@@ -149,7 +183,7 @@ module Chronicle
149
183
  class FooExtractor < Chronicle::ETL::Extractor
150
184
  register_connector do |r|
151
185
  r.identifier = 'foo'
152
- r.description = 'From foo.com'
186
+ r.description = 'from foo.com'
153
187
  end
154
188
 
155
189
  setting :access_token, required: true
@@ -168,6 +202,44 @@ module Chronicle
168
202
  end
169
203
  ```
170
204
 
205
+ ## Secrets Management
206
+
207
+ If your job needs secrets such as access tokens or passwords, `chronicle-etl` has a built-in secret management system.
208
+
209
+ Secrets are organized in namespaces. Typically, you use one namespace per plugin (`pinboard` secrets for the `pinboard` plugin). When you run a job that uses the `pinboard` plugin extractor, for example, the secrets from that namespace will automatically be included in the extractor's options. To override which secrets get included, you can use do it in the connector options with `secrets: ALT-NAMESPACE`.
210
+
211
+ Under the hood, secrets are stored in `~/.config/chronicle/etl/secrets/NAMESPACE.yml` with 0600 permissions on each file.
212
+
213
+ ### Using the secret manager
214
+
215
+ ```sh
216
+ # Save a secret under the 'pinboard' namespace
217
+ $ chronicle-etl secrets:set pinboard access_token username:foo123
218
+
219
+ # Set a secret using stdin
220
+ $ echo -n "username:foo123" | chronicle-etl secrets:set pinboard access_token
221
+
222
+ # List available secretes
223
+ $ chronicle-etl secrets:list
224
+
225
+ # Use 'pinboard' secrets in the pinboard extractor's options (happens automatically)
226
+ $ chronicle-etl -e pinboard --since 1mo
227
+
228
+ # Use a custom secrets namespace
229
+ $ chronicle-etl secrets:set pinboard-alt access_token different-username:foo123
230
+ $ chronicle-etl -e pinboard --extractor-opts secrets:pinboard-alt --since 1mo
231
+
232
+ # Remove a secret
233
+ $ chronicle-etl secrets:unset pinboard access_token
234
+ ```
235
+
236
+ ## Roadmap
237
+
238
+ - 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** (#5) so that users can easily integrate their own scripts/tools into jobs
241
+ - **Add documentation for Chronicle Schema**. It's found throughout this project but never explained.
242
+
171
243
  ## Development
172
244
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
173
245
 
@@ -43,22 +43,23 @@ Gem::Specification.new do |spec|
43
43
  spec.add_dependency "marcel", "~> 1.0.2"
44
44
  spec.add_dependency "mini_exiftool", "~> 2.10"
45
45
  spec.add_dependency "nokogiri", "~> 1.13"
46
- spec.add_dependency "runcom", ">= 6.0"
47
46
  spec.add_dependency "sequel", "~> 5.35"
48
47
  spec.add_dependency "sqlite3", "~> 1.4"
49
48
  spec.add_dependency "thor", "~> 1.2"
50
49
  spec.add_dependency "thor-hollaback", "~> 0.2"
51
50
  spec.add_dependency "tty-progressbar", "~> 0.17"
51
+ spec.add_dependency "tty-prompt", "~> 0.23"
52
52
  spec.add_dependency "tty-spinner"
53
53
  spec.add_dependency "tty-table", "~> 0.11"
54
- spec.add_dependency "tty-prompt", "~> 0.23"
54
+ spec.add_dependency "xdg", ">= 4.0"
55
55
 
56
56
  spec.add_development_dependency "bundler", "~> 2.1"
57
+ spec.add_development_dependency "guard-rspec", "~> 4.7.3"
58
+ spec.add_development_dependency "fakefs"
57
59
  spec.add_development_dependency "pry-byebug", "~> 3.9"
58
60
  spec.add_development_dependency "rake", "~> 13.0"
59
61
  spec.add_development_dependency "rspec", "~> 3.9"
62
+ spec.add_development_dependency "rubocop", "~> 1.25.1"
60
63
  spec.add_development_dependency "simplecov", "~> 0.21"
61
- spec.add_development_dependency "guard-rspec", "~> 4.7.3"
62
64
  spec.add_development_dependency "yard", "~> 0.9.7"
63
- spec.add_development_dependency "rubocop", "~> 1.25.1"
64
65
  end
@@ -4,6 +4,8 @@ module Chronicle
4
4
  module ETL
5
5
  module CLI
6
6
  # CLI commands for working with ETL connectors
7
+ #
8
+ # @todo make this work with new plugin system (i.e. no loading of all plugins)
7
9
  class Connectors < SubcommandBase
8
10
  default_task 'list'
9
11
  namespace :connectors
@@ -11,8 +13,6 @@ module Chronicle
11
13
  desc "list", "Lists available connectors"
12
14
  # Display all available connectors that chronicle-etl has access to
13
15
  def list
14
- Chronicle::ETL::Registry.load_all!
15
-
16
16
  connector_info = Chronicle::ETL::Registry.connectors.map do |connector_registration|
17
17
  {
18
18
  identifier: connector_registration.identifier,
@@ -9,8 +9,6 @@ module Chronicle
9
9
  default_task "start"
10
10
  namespace :jobs
11
11
 
12
- class_option :name, aliases: '-j', desc: 'Job configuration name'
13
-
14
12
  class_option :extractor, aliases: '-e', desc: "Extractor class. Default: stdin", banner: 'NAME'
15
13
  class_option :'extractor-opts', desc: 'Extractor options', type: :hash, default: {}
16
14
  class_option :transformer, aliases: '-t', desc: 'Transformer class. Default: null', banner: 'NAME'
@@ -20,8 +18,8 @@ module Chronicle
20
18
 
21
19
  # This is an array to deal with shell globbing
22
20
  class_option :input, aliases: '-i', desc: 'Input filename or directory', default: [], type: 'array', banner: 'FILENAME'
23
- class_option :since, desc: "Load records SINCE this date", banner: 'DATE'
24
- class_option :until, desc: "Load records UNTIL this date", banner: 'DATE'
21
+ class_option :since, desc: "Load records SINCE this date (or fuzzy time duration)", banner: 'DATE'
22
+ class_option :until, desc: "Load records UNTIL this date (or fuzzy time duration)", banner: 'DATE'
25
23
  class_option :limit, desc: "Only extract the first LIMIT records", banner: 'N'
26
24
 
27
25
  class_option :output, aliases: '-o', desc: 'Output filename', type: 'string'
@@ -44,12 +42,12 @@ module Chronicle
44
42
  If you do not want to use the command line flags, you can also configure a job with a .yml config file. You can either specify the path to this file or use the filename and place the file in ~/.config/chronicle/etl/jobs/NAME.yml and call it with `--job NAME`
45
43
  LONG_DESC
46
44
  # Run an ETL job
47
- def start
48
- job_definition = build_job_definition(options)
45
+ def start(name = nil)
46
+ job_definition = build_job_definition(name, options)
49
47
 
50
48
  if job_definition.plugins_missing?
51
49
  missing_plugins = job_definition.errors[:plugins]
52
- .select { |error| error.is_a?(Chronicle::ETL::PluginLoadError) }
50
+ .select { |error| error.is_a?(Chronicle::ETL::PluginNotInstalledError) }
53
51
  .map(&:name)
54
52
  .uniq
55
53
  install_missing_plugins(missing_plugins)
@@ -57,25 +55,46 @@ LONG_DESC
57
55
 
58
56
  run_job(job_definition)
59
57
  rescue Chronicle::ETL::JobDefinitionError => e
60
- cli_fail(message: "Error running job.\n#{job_definition.errors}", exception: e)
58
+ message = ""
59
+ job_definition.errors.each_pair do |category, errors|
60
+ message << "Problem with #{category}:\n - #{errors.map(&:to_s).join("\n - ")}"
61
+ end
62
+ cli_fail(message: "Error running job.\n#{message}", exception: e)
61
63
  end
62
64
 
63
- desc "create", "Create a job"
65
+ option :'skip-confirmation', aliases: '-y', type: :boolean
66
+ desc "save", "Save a job"
64
67
  # Create an ETL job
65
- def create
66
- job_definition = build_job_definition(options)
68
+ def save(name)
69
+ write_config = true
70
+ job_definition = build_job_definition(name, options)
67
71
  job_definition.validate!
68
72
 
69
- path = File.join('chronicle', 'etl', 'jobs', options[:name])
70
- Chronicle::ETL::Config.write(path, job_definition.definition)
73
+ if Chronicle::ETL::Config.exists?("jobs", name) && !options[:'skip-confirmation']
74
+ prompt = TTY::Prompt.new
75
+ write_config = false
76
+ message = "Job '#{name}' exists already. Ovewrite it?"
77
+ begin
78
+ write_config = prompt.yes?(message)
79
+ rescue TTY::Reader::InputInterrupt
80
+ end
81
+ end
82
+
83
+ if write_config
84
+ Chronicle::ETL::Config.write("jobs", name, job_definition.definition)
85
+ cli_exit(message: "Job saved. Run it with `$chronicle-etl jobs:run #{name}`")
86
+ else
87
+ cli_fail(message: "\nJob not saved")
88
+ end
89
+
71
90
  rescue Chronicle::ETL::JobDefinitionError => e
72
91
  cli_fail(message: "Job definition error", exception: e)
73
92
  end
74
93
 
75
94
  desc "show", "Show details about a job"
76
95
  # Show an ETL job
77
- def show
78
- job_definition = build_job_definition(options)
96
+ def show(name = nil)
97
+ job_definition = build_job_definition(name, options)
79
98
  job_definition.validate!
80
99
  puts Chronicle::ETL::Job.new(job_definition)
81
100
  rescue Chronicle::ETL::JobDefinitionError => e
@@ -88,7 +107,7 @@ LONG_DESC
88
107
  jobs = Chronicle::ETL::Config.available_jobs
89
108
 
90
109
  job_details = jobs.map do |job|
91
- r = Chronicle::ETL::Config.load("chronicle/etl/jobs/#{job}.yml")
110
+ r = Chronicle::ETL::Config.load("jobs", job)
92
111
 
93
112
  extractor = r[:extractor][:name] if r[:extractor]
94
113
  transformer = r[:transformer][:name] if r[:transformer]
@@ -109,6 +128,11 @@ LONG_DESC
109
128
  private
110
129
 
111
130
  def run_job(job_definition)
131
+ # FIXME: have to validate here so next method can work. This is clumsy
132
+ job_definition.validate!
133
+ # FIXME: clumsy to make CLI responsible for setting secrets here. Think about a better way to do this
134
+ job_definition.apply_default_secrets
135
+
112
136
  job = Chronicle::ETL::Job.new(job_definition)
113
137
  runner = Chronicle::ETL::Runner.new(job)
114
138
  runner.run!
@@ -128,29 +152,30 @@ LONG_DESC
128
152
  end
129
153
 
130
154
  # Create job definition by reading config file and then overwriting with flag options
131
- def build_job_definition(options)
155
+ def build_job_definition(name, options)
132
156
  definition = Chronicle::ETL::JobDefinition.new
133
- definition.add_config(load_job_config(options[:name]))
157
+ definition.add_config(load_job_config(name))
134
158
  definition.add_config(process_flag_options(options).transform_keys(&:to_sym))
135
159
  definition
136
160
  end
137
161
 
138
162
  def load_job_config name
139
- Chronicle::ETL::Config.load_job_from_config(name)
163
+ Chronicle::ETL::Config.read_job(name)
140
164
  end
141
165
 
142
166
  # Takes flag options and turns them into a runner config
167
+ # TODO: this needs a lot of refactoring
143
168
  def process_flag_options options
144
- extractor_options = options[:'extractor-opts'].merge({
169
+ extractor_options = options[:'extractor-opts'].transform_keys(&:to_sym).merge({
145
170
  input: (options[:input] if options[:input].any?),
146
171
  since: options[:since],
147
172
  until: options[:until],
148
- limit: options[:limit],
173
+ limit: options[:limit]
149
174
  }.compact)
150
175
 
151
- transformer_options = options[:'transformer-opts']
176
+ transformer_options = options[:'transformer-opts'].transform_keys(&:to_sym)
152
177
 
153
- loader_options = options[:'loader-opts'].merge({
178
+ loader_options = options[:'loader-opts'].transform_keys(&:to_sym).merge({
154
179
  output: options[:output],
155
180
  header_row: options[:header_row],
156
181
  fields: options[:fields]
@@ -24,6 +24,9 @@ module Chronicle
24
24
  desc 'plugins:COMMAND', 'Configure plugins', hide: true
25
25
  subcommand 'plugins', Plugins
26
26
 
27
+ desc 'secrets:COMMAND', 'Manage secrets', hide: true
28
+ subcommand 'secrets', Secrets
29
+
27
30
  # Entrypoint for the CLI
28
31
  def self.start(given_args = ARGV, config = {})
29
32
  # take a subcommand:command and splits them so Thor knows how to hand off to the subcommand class
@@ -15,15 +15,25 @@ module Chronicle
15
15
  def install(*plugins)
16
16
  cli_fail(message: "Please specify a plugin to install") unless plugins.any?
17
17
 
18
- spinner = TTY::Spinner.new("[:spinner] Installing #{plugins.join(", ")}...", format: :dots_2)
18
+ installed, not_installed = plugins.partition do |plugin|
19
+ Chronicle::ETL::Registry::PluginRegistry.installed?(plugin)
20
+ end
21
+
22
+ puts "Already installed: #{installed.join(", ")}" if installed.any?
23
+ cli_exit unless not_installed.any?
24
+
25
+ spinner = TTY::Spinner.new("[:spinner] Installing #{not_installed.join(", ")}...", format: :dots_2)
19
26
  spinner.auto_spin
20
- plugins.each do |plugin|
27
+
28
+ not_installed.each do |plugin|
21
29
  spinner.update(title: "Installing #{plugin}")
22
30
  Chronicle::ETL::Registry::PluginRegistry.install(plugin)
31
+
23
32
  rescue Chronicle::ETL::PluginError => e
24
33
  spinner.error("Error".red)
25
34
  cli_fail(message: "Plugin '#{plugin}' could not be installed", exception: e)
26
35
  end
36
+
27
37
  spinner.success("(#{'successful'.green})")
28
38
  end
29
39
 
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+
5
+ module Chronicle
6
+ module ETL
7
+ module CLI
8
+ # CLI commands for working with ETL plugins
9
+ class Secrets < SubcommandBase
10
+ default_task 'list'
11
+ namespace :secrets
12
+
13
+ desc "set NAMESPACE KEY [VALUE]", "Add a secret. VALUE can be set as argument or from stdin"
14
+ def set(namespace, key, value=nil)
15
+ validate_namespace(namespace)
16
+
17
+ if value
18
+ # came as argument
19
+ elsif $stdin.respond_to?(:stat) && $stdin.stat.pipe?
20
+ value = $stdin.read
21
+ else
22
+ prompt = TTY::Prompt.new
23
+ value = prompt.mask("Please enter #{key} for #{namespace}:")
24
+ end
25
+
26
+ Chronicle::ETL::Secrets.set(namespace, key, value.strip)
27
+ cli_exit(message: "Secret set")
28
+ rescue TTY::Reader::InputInterrupt
29
+ cli_fail(message: "\nSecret not set")
30
+ end
31
+
32
+ desc "unset NAMESPACE KEY", "Remove a secret"
33
+ def unset(namespace, key)
34
+ validate_namespace(namespace)
35
+
36
+ Chronicle::ETL::Secrets.unset(namespace, key)
37
+ cli_exit(message: "Secret unset")
38
+ end
39
+
40
+ desc "list", "List available secrets"
41
+ def list(namespace=nil)
42
+ all_secrets = Chronicle::ETL::Secrets.all(namespace)
43
+ cli_exit(message: "No secrets are stored") unless all_secrets.any?
44
+
45
+ rows = []
46
+ all_secrets.each do |namespace, secrets|
47
+ rows += secrets.map do |key, value|
48
+ # hidden_value = (value[0..5] + ("*" * [0, [value.length - 5, 30].min].max)).truncate(30)
49
+ truncated_value = value.truncate(30)
50
+ [namespace, key, truncated_value]
51
+ end
52
+ end
53
+
54
+ headers = ['namespace', 'key', 'value'].map { |h| h.upcase.bold }
55
+
56
+ puts "Available secrets:"
57
+ table = TTY::Table.new(headers, rows)
58
+ puts table.render(indent: 0, padding: [0, 2])
59
+ end
60
+
61
+ private
62
+
63
+ def validate_namespace(namespace)
64
+ cli_fail(message: "'#{namespace}' is not a valid namespace") unless Chronicle::ETL::Secrets.valid_namespace_name?(namespace)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -7,4 +7,5 @@ require 'chronicle/etl/cli/subcommand_base'
7
7
  require 'chronicle/etl/cli/connectors'
8
8
  require 'chronicle/etl/cli/jobs'
9
9
  require 'chronicle/etl/cli/plugins'
10
+ require 'chronicle/etl/cli/secrets'
10
11
  require 'chronicle/etl/cli/main'