-
Notifications
You must be signed in to change notification settings - Fork 166
Data Pull Script Script (LG-9705) #8345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
323ae60
Add first pass at extendable data-pull script
zachmargolis c93ddae
lints
zachmargolis 62bb465
Add some tests, JSON output
zachmargolis 54279b2
Update email-lookup to have one row per email, confirmed_at column
zachmargolis 819efd7
Merge remote-tracking branch 'origin/main' into margolis-data-pull-sc…
zachmargolis a8b3ac0
Add changelog
zachmargolis 4d81671
- Remove profile-lookup subtask
zachmargolis 6a44eb7
Specs for email-lookup
zachmargolis c96a9b0
Remove --reason, will be handled by devops script
zachmargolis 16cc9c3
Set default LOGIN_TASK_LOG_LEVEL to minimize extra output
zachmargolis f5cf067
remove redundant rubocop disable
zachmargolis 90c2598
Round timestamps for consistency in CI
zachmargolis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| #!/usr/bin/env ruby | ||
|
|
||
| require_relative '../config/environment.rb' | ||
| require 'data_pull' | ||
| DataPull.new(argv: ARGV.dup, stdout: STDOUT, stderr: STDERR).run |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| require 'optparse' | ||
|
|
||
| class DataPull | ||
| attr_reader :argv, :stdout, :stderr | ||
|
|
||
| def initialize(argv:, stdout:, stderr:) | ||
| @argv = argv | ||
| @stdout = stdout | ||
| @stderr = stderr | ||
| end | ||
|
|
||
| Result = Struct.new( | ||
| :table, # tabular output, rendered as an ASCII table or as CSV | ||
| :log_message, # summary message used for audit logging, DO NOT PUT PII HERE | ||
| keyword_init: true, | ||
| ) | ||
|
|
||
| Config = Struct.new( | ||
| :include_missing, | ||
| :format, | ||
| :show_help, | ||
| keyword_init: true, | ||
| ) do | ||
| alias_method :include_missing?, :include_missing | ||
| alias_method :show_help?, :show_help | ||
| end | ||
|
|
||
| def config | ||
| @config ||= Config.new( | ||
| include_missing: true, | ||
| format: :table, | ||
| show_help: false, | ||
| ) | ||
| end | ||
|
|
||
| def run | ||
| option_parser.parse!(argv) | ||
| subtask_class = subtask(argv.shift) | ||
|
|
||
| if config.show_help? || !subtask_class | ||
| stdout.puts option_parser | ||
| return | ||
| end | ||
|
|
||
| result = subtask_class.new.run(args: argv, include_missing: config.include_missing?) | ||
|
|
||
| stderr.puts result.log_message | ||
|
|
||
| render_output(result.table) | ||
| end | ||
|
|
||
| # @param [Array<Array<String>>] rows | ||
| def render_output(rows) | ||
| return if rows.blank? | ||
|
|
||
| case config.format | ||
| when :table | ||
| require 'terminal-table' | ||
| table = Terminal::Table.new | ||
| header, *body = rows | ||
| table << header | ||
| table << :separator | ||
| body.each do |row| | ||
| table << row | ||
| end | ||
| stdout.puts table | ||
| when :csv | ||
| require 'csv' | ||
| CSV.instance(stdout) do |csv| | ||
| rows.each do |row| | ||
| csv << row | ||
| end | ||
| end | ||
| when :json | ||
| headers, *body = rows | ||
|
|
||
| objects = body.map do |values| | ||
| headers.zip(values).to_h | ||
| end | ||
|
|
||
| stdout.puts JSON.pretty_generate(objects) | ||
| else | ||
| raise "Unknown format=#{config.format}" | ||
| end | ||
| end | ||
|
|
||
| # @api private | ||
| # A subtask is a class that has a run method, the type signature should look like: | ||
| # +#run(args: Array<String>, include_missing: Boolean) -> Result+ | ||
| # @return [Class,nil] | ||
| def subtask(name) | ||
| { | ||
| 'uuid-lookup' => UuidLookup, | ||
| 'uuid-convert' => UuidConvert, | ||
| 'email-lookup' => EmailLookup, | ||
| 'profile-status' => ProfileStatus, | ||
| }[name] | ||
| end | ||
|
Comment on lines
+87
to
+97
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like having these all in the same file is fine for now? Open to feedback if others feel differently |
||
|
|
||
| def option_parser | ||
| @option_parser ||= OptionParser.new do |opts| | ||
| opts.banner = <<~EOS | ||
| #{$PROGRAM_NAME} [subcommand] [arguments] [options] | ||
|
|
||
| Example usage: | ||
|
|
||
| * #{$PROGRAM_NAME} uuid-lookup email1@example.com email2@example.com | ||
|
|
||
| * #{$PROGRAM_NAME} uuid-convert partner-uuid1 partner-uuid2 | ||
|
|
||
| * #{$PROGRAM_NAME} email-lookup uuid1 uuid2... | ||
|
|
||
| * #{$PROGRAM_NAME} profile-status uuid1 uuid2... | ||
|
|
||
| Options: | ||
| EOS | ||
|
|
||
| opts.on('--help') do | ||
| config.show_help = true | ||
| end | ||
|
|
||
| opts.on('--csv') do | ||
| config.format = :csv | ||
| end | ||
|
|
||
| opts.on('--table', 'Output format as an ASCII table (default)') do\ | ||
| config.format = :table | ||
| end | ||
|
|
||
| opts.on('--json') do | ||
| config.format = :json | ||
| end | ||
|
|
||
| opts.on('--[no-]include-missing', <<~STR) do |include_missing| | ||
| Whether or not to add rows in the output for missing inputs, defaults to off | ||
| STR | ||
| config.include_missing = include_missing | ||
| end | ||
| end | ||
| end | ||
|
|
||
| class UuidLookup | ||
| def run(args:, include_missing:) | ||
| emails = args | ||
|
|
||
| table = [] | ||
| table << %w[email uuid] | ||
|
|
||
| uuids = [] | ||
|
|
||
| emails.each do |email| | ||
| user = User.find_with_email(email) | ||
| if user | ||
| table << [email, user.uuid] | ||
| uuids << user.uuid | ||
| elsif include_missing | ||
| table << [email, '[NOT FOUND]'] | ||
| end | ||
| end | ||
|
|
||
| Result.new( | ||
| log_message: "uuid-lookup, uuids: #{uuids.join(', ')}", | ||
| table:, | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| class UuidConvert | ||
| def run(args:, include_missing:) | ||
| partner_uuids = args | ||
|
|
||
| table = [] | ||
| table << %w[partner_uuid source internal_uuid] | ||
| identities = AgencyIdentity.includes(:user, :agency).where(uuid: partner_uuids) | ||
|
|
||
| identities.each do |identity| | ||
| table << [identity.uuid, identity.agency.name, identity.user.uuid] | ||
| end | ||
|
|
||
| if include_missing | ||
| (partner_uuids - identities.map(&:uuid)).each do |missing_uuid| | ||
| table << [missing_uuid, '[NOT FOUND]', '[NOT FOUND]'] | ||
| end | ||
| end | ||
|
|
||
| Result.new( | ||
| log_message: "uuid-convert, uuids: #{identities.map { |u| u.user.uuid }.join(', ')}", | ||
| table:, | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| class EmailLookup | ||
| def run(args:, include_missing:) | ||
| uuids = args | ||
|
|
||
| users = User.includes(:email_addresses).where(uuid: uuids) | ||
|
|
||
| table = [] | ||
| table << %w[uuid email] | ||
|
|
||
| users.each do |user| | ||
| table << [user.uuid, *user.email_addresses.map(&:email)] | ||
|
zachmargolis marked this conversation as resolved.
Outdated
|
||
| end | ||
|
|
||
| if include_missing | ||
| (uuids - users.map(&:uuid)).each do |missing_uuid| | ||
| table << [missing_uuid, '[NOT FOUND]'] | ||
| end | ||
| end | ||
|
|
||
| Result.new( | ||
| log_message: "email-lookup, uuids: #{users.map(&:uuid).join(', ')}", | ||
| table:, | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| class ProfileStatus | ||
| def run(*) | ||
| Result.new | ||
| end | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| require 'rails_helper' | ||
| require 'tableparser' | ||
| require 'data_pull' | ||
|
|
||
| RSpec.describe DataPull do | ||
| let(:stdout) { StringIO.new } | ||
| let(:stderr) { StringIO.new } | ||
| let(:argv) { [] } | ||
|
|
||
| subject(:data_pull) { DataPull.new(argv:, stdout:, stderr:) } | ||
|
|
||
| describe 'command line flags' do | ||
| let(:argv) { ['uuid-lookup', user.email_addresses.first.email] } | ||
| let(:user) { create(:user) } | ||
|
|
||
| describe '--help' do | ||
| before { argv << '--help' } | ||
| it 'prints a help message' do | ||
| data_pull.run | ||
|
|
||
| expect(stdout.string).to include('Options:') | ||
| end | ||
| end | ||
|
|
||
| describe '--csv' do | ||
| before { argv << '--csv' } | ||
| it 'formats output as CSV' do | ||
| data_pull.run | ||
|
|
||
| expect(CSV.parse(stdout.string)).to eq( | ||
| [ | ||
| ['email', 'uuid'], | ||
| [user.email_addresses.first.email, user.uuid], | ||
| ], | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| describe '--table' do | ||
| before { argv << '--table' } | ||
| it 'formats output as an ASCII table' do | ||
| data_pull.run | ||
|
|
||
| expect(Tableparser.parse(stdout.string)).to eq( | ||
| [ | ||
| ['email', 'uuid'], | ||
| [user.email_addresses.first.email, user.uuid], | ||
| ], | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| describe '--json' do | ||
| before { argv << '--json' } | ||
| it 'formats output as JSON' do | ||
| data_pull.run | ||
|
|
||
| expect(JSON.parse(stdout.string)).to eq( | ||
| [ | ||
| { | ||
| 'email' => user.email_addresses.first.email, | ||
| 'uuid' => user.uuid, | ||
| }, | ||
| ], | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| describe '--include-missing' do | ||
| let(:argv) { ['uuid-lookup', 'does_not_exist@example.com', '--include-missing', '--json'] } | ||
| it 'adds rows for missing values' do | ||
| data_pull.run | ||
|
|
||
| expect(JSON.parse(stdout.string)).to eq( | ||
| [ | ||
| { | ||
| 'email' => 'does_not_exist@example.com', | ||
| 'uuid' => '[NOT FOUND]', | ||
| }, | ||
| ], | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| describe '--no-include-missing' do | ||
| let(:argv) { ['uuid-lookup', 'does_not_exist@example.com', '--no-include-missing', '--json'] } | ||
| it 'does not add rows for missing values' do | ||
| data_pull.run | ||
|
|
||
| expect(JSON.parse(stdout.string)).to be_empty | ||
| end | ||
| end | ||
| end | ||
|
|
||
| describe DataPull::UuidLookup do | ||
| subject(:subtask) { DataPull::UuidLookup.new } | ||
|
|
||
| describe '#run' do | ||
| let(:include_missing) { true } | ||
| end | ||
| end | ||
|
|
||
| describe DataPull::UuidConvert do | ||
| subject(:subtask) { DataPull::UuidConvert.new } | ||
|
|
||
| describe '#run' do | ||
| let(:include_missing) { true } | ||
| end | ||
| end | ||
|
|
||
| describe DataPull::EmailLookup do | ||
| subject(:subtask) { DataPull::EmailLookup.new } | ||
|
|
||
| describe '#run' do | ||
| let(:include_missing) { true } | ||
| end | ||
| end | ||
|
|
||
| describe DataPull::ProfileStatus do | ||
| subject(:subtask) { DataPull::ProfileStatus.new } | ||
|
|
||
| describe '#run' do | ||
| end | ||
| end | ||
| end |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the specs, I wanted to be able to parse the
Terminal::Tableoutput, and as far as I could tell, that gem doesn't have any parsing.I happened to have my own smol gem (https://github.com/zachmargolis/tableparser) that does this, so I opted to use it here. Open to removing if anybody feels strongly!
It's a pretty limited gem, I YARD doc'ed it as best I could
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of using something one of us "owns" rather than introducing a 3rd-party dependency... no objection here.