doi_extractor 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +54 -0
- data/Capfile +34 -0
- data/Gemfile +6 -0
- data/README.md +50 -0
- data/Rakefile +43 -0
- data/bin/doi_extractor +18 -0
- data/config/deploy.rb +34 -0
- data/config/deploy/production.rb +65 -0
- data/doi_extractor.gemspec +29 -0
- data/lib/doi_extractor.rb +25 -0
- data/lib/doi_extractor/cancel_command.rb +24 -0
- data/lib/doi_extractor/command.rb +81 -0
- data/lib/doi_extractor/command_line_parser.rb +154 -0
- data/lib/doi_extractor/create_command.rb +32 -0
- data/lib/doi_extractor/download_command.rb +109 -0
- data/lib/doi_extractor/download_location.rb +86 -0
- data/lib/doi_extractor/errors.rb +34 -0
- data/lib/doi_extractor/ipums_client.rb +159 -0
- data/lib/doi_extractor/ipums_uri_builder.rb +51 -0
- data/lib/doi_extractor/old_ruby_patch.rb +25 -0
- data/lib/doi_extractor/options.rb +132 -0
- data/lib/doi_extractor/secrets.rb +18 -0
- data/lib/doi_extractor/status_command.rb +62 -0
- data/lib/doi_extractor/version.rb +3 -0
- data/spec/fixtures/api_creds.yml +2 -0
- data/spec/reports/SPEC-DoiExtractor-CancelCommand-when-successful.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-CancelCommand.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-create-command-with-email.xml +14 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-create-command.xml +9 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-download-command.xml +9 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-a-valid-status-command.xml +9 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-an-invalid-command.xml +9 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser-with-an-unknown-option.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-CommandLineParser.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-CreateCommand-when-an-existing-extract-is-processing.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-CreateCommand-when-successful.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-CreateCommand.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-DownloadCommand-user-cancels-download.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-DownloadCommand-when-an-extract-is-available-when-force-is-not-set.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-DownloadCommand-when-an-extract-is-available.xml +13 -0
- data/spec/reports/SPEC-DoiExtractor-DownloadCommand.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-DownloadLocation.xml +11 -0
- data/spec/reports/SPEC-DoiExtractor-IpumsClient.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-internal-environment.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-live-environment.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder-local-environment.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-IpumsUriBuilder.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-create-command-with-invalid-doi-version.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-create-command.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-download-command.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-Options-for-command-with-status-command.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-Options-for-command.xml +3 -0
- data/spec/reports/SPEC-DoiExtractor-Options-when-setting-path-values.xml +9 -0
- data/spec/reports/SPEC-DoiExtractor-Options.xml +5 -0
- data/spec/reports/SPEC-DoiExtractor-Secrets.xml +7 -0
- data/spec/reports/SPEC-DoiExtractor-StatusCommand.xml +5 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/test_input.rb +36 -0
- data/spec/unit/cancel_command_spec.rb +28 -0
- data/spec/unit/command_line_parser_spec.rb +68 -0
- data/spec/unit/create_command_spec.rb +44 -0
- data/spec/unit/download_command_spec.rb +139 -0
- data/spec/unit/download_location_spec.rb +71 -0
- data/spec/unit/ipums_client_spec.rb +23 -0
- data/spec/unit/ipums_uri_builder_spec.rb +26 -0
- data/spec/unit/options_spec.rb +86 -0
- data/spec/unit/secrets_spec.rb +14 -0
- data/spec/unit/status_command_spec.rb +46 -0
- metadata +282 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
class CommandLineParser
|
3
|
+
|
4
|
+
attr_reader :top_level_parser, :command_parsers, :command_parser, :options, :error
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@command_parsers = {
|
8
|
+
'status' => OptionParser.new do |opts|
|
9
|
+
opts.banner = "Usage: #{File.basename($0)} status [options]"
|
10
|
+
opts.version = VERSION
|
11
|
+
default_opts(opts)
|
12
|
+
api_opts(opts)
|
13
|
+
path_opts(opts)
|
14
|
+
extract_group_opts(opts)
|
15
|
+
end,
|
16
|
+
|
17
|
+
'create' => OptionParser.new do |opts|
|
18
|
+
opts.banner = "Usage: #{File.basename($0)} create [options]"
|
19
|
+
opts.version = VERSION
|
20
|
+
default_opts(opts)
|
21
|
+
api_opts(opts)
|
22
|
+
|
23
|
+
opts.on(:REQUIRED, '-v', '--doi-version DOI_VERSION', 'DOI version') do |v|
|
24
|
+
self.options.doi_version = v
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on('-y', '--year YEAR', 'Specify year (defaults to current year)') do |v|
|
28
|
+
self.options.year = v
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-E', '--email EMAIL', 'Specify email address to notify when all extracts are complete') do |v|
|
32
|
+
self.options.email = v
|
33
|
+
end
|
34
|
+
end,
|
35
|
+
|
36
|
+
'download' => OptionParser.new do |opts|
|
37
|
+
opts.banner = "Usage: #{File.basename($0)} download [options]"
|
38
|
+
opts.version = VERSION
|
39
|
+
default_opts(opts)
|
40
|
+
api_opts(opts)
|
41
|
+
path_opts(opts)
|
42
|
+
extract_group_opts(opts)
|
43
|
+
force_opts(opts, 'Downloads')
|
44
|
+
end,
|
45
|
+
|
46
|
+
'cancel' => OptionParser.new do |opts|
|
47
|
+
opts.banner = "Usage: #{File.basename($0)} cancel [options]"
|
48
|
+
opts.version = VERSION
|
49
|
+
default_opts(opts)
|
50
|
+
api_opts(opts)
|
51
|
+
extract_group_opts(opts)
|
52
|
+
force_opts(opts, 'Cancels')
|
53
|
+
end
|
54
|
+
}
|
55
|
+
|
56
|
+
@top_level_parser = OptionParser.new do |opts|
|
57
|
+
opts.version = VERSION
|
58
|
+
opts.banner = "Usage: #{File.basename($0)} <COMMAND> [options]\nAvailable Commands: #{@command_parsers.keys.join(', ')}\n\nSee '#{File.basename($0)} <COMMAND> --help' for more information on a specific command"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse(args)
|
63
|
+
begin
|
64
|
+
@top_level_parser.order!(args)
|
65
|
+
|
66
|
+
command = args.shift.to_s.downcase
|
67
|
+
@command_parser = @command_parsers[command]
|
68
|
+
|
69
|
+
if @command_parser
|
70
|
+
@options = Options.for_command(command)
|
71
|
+
@command_parser.order!(args)
|
72
|
+
if @options.valid?
|
73
|
+
@valid = true
|
74
|
+
else
|
75
|
+
@valid = false
|
76
|
+
@error = @options.errors.join("\n")
|
77
|
+
end
|
78
|
+
else
|
79
|
+
@valid = false
|
80
|
+
@error = 'Invalid Command'
|
81
|
+
end
|
82
|
+
rescue OptionParser::ParseError => err
|
83
|
+
@error = err
|
84
|
+
@valid = false
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def valid?
|
89
|
+
@valid
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
[error, @command_parser || @top_level_parser].compact.join("\n\n")
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def default_opts(opts)
|
99
|
+
opts.on('-e', '--environment ENVIRONMENT', Options::ENVIRONMENTS, "Select environment (defaults to live; values: #{Options::ENVIRONMENTS.join(', ')})") do |v|
|
100
|
+
self.options.environment = v
|
101
|
+
end
|
102
|
+
|
103
|
+
opts.on('-p', '--project PROJECT', Options::PROJECTS, "Select project (required; values: #{Options::PROJECTS.join(', ')})") do |v|
|
104
|
+
self.options.project = v
|
105
|
+
end
|
106
|
+
|
107
|
+
opts.on('-V', '--[no-]verbose', 'Run verbosely') do |v|
|
108
|
+
self.options.verbose = v
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def api_opts(opts)
|
113
|
+
opts.on('-a', '--api-path URI', URI, 'API path (calculated from env and project by default)') do |v|
|
114
|
+
self.options.api_uri = v
|
115
|
+
end
|
116
|
+
|
117
|
+
opts.on('--api-user USER', 'API username (Reads config from /pkg by default)') do |v|
|
118
|
+
self.options.api_username = v
|
119
|
+
end
|
120
|
+
|
121
|
+
opts.on('--api-password PASSWORD', 'API password (Reads config from /pkg by default)') do |v|
|
122
|
+
self.options.api_password = v
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def path_opts(opts)
|
127
|
+
opts.on('-b', '--download-base-path PATH', 'Override download path') do |v|
|
128
|
+
self.options.download_base_path = v
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def extract_group_opts(opts)
|
133
|
+
opts.on('-i', '--id ID', Integer, 'DOI Extract ID') do |v|
|
134
|
+
self.options.extract_group_id = v
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def force_opts(opts, verb)
|
139
|
+
opts.on('-f', '--force', "#{verb} without prompting") do |v|
|
140
|
+
self.options.force = v
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class << self
|
145
|
+
private :new
|
146
|
+
|
147
|
+
def parse(args)
|
148
|
+
opts = new
|
149
|
+
opts.parse(args.dup)
|
150
|
+
opts
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
class CreateCommand < Command
|
3
|
+
|
4
|
+
def _execute
|
5
|
+
|
6
|
+
say('checking existing DOI extracts', true)
|
7
|
+
if any_current_doi_extracts?
|
8
|
+
fail('A DOI Extract is already in progress. Nothing created.')
|
9
|
+
end
|
10
|
+
|
11
|
+
say('Creating DOI extract group...')
|
12
|
+
ex = ipums_client.create_doi_extract(options.doi_version, options.year, options.email)
|
13
|
+
|
14
|
+
fail('Server did not return any data; extract status is unknown') unless ex
|
15
|
+
fail("Server created DOI extract with ID #{ex.id} and invalid status #{ex.status}") unless ex.status == 'empty'
|
16
|
+
|
17
|
+
say("DOI Extract with ID #{ex.id} submitted. To check status run the following command:\n doi_extractor status -p #{options.project} -e #{options.environment} -i #{ex.id}\n")
|
18
|
+
|
19
|
+
if options.email
|
20
|
+
say("An email will be sent to #{options.email} after the last extract has finished. At that time you will be able to download and archive the extracts.")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def any_current_doi_extracts?
|
26
|
+
in_progress_status = ['empty', 'waiting_to_populate', 'populating', 'unsubmitted', 'waiting_to_submit', 'submitting', 'submitted']
|
27
|
+
recent = ipums_client.recent_doi_extracts
|
28
|
+
recent.any? { |e| in_progress_status.include?(e.status) }
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
class DownloadCommand < Command
|
3
|
+
|
4
|
+
def _execute
|
5
|
+
say('retrieving DOI extract', true)
|
6
|
+
doi_extract = ipums_client.get_doi_extract(options.extract_group_id)
|
7
|
+
|
8
|
+
unless doi_extract
|
9
|
+
fail("no DOI extract with ID #{id} found")
|
10
|
+
end
|
11
|
+
|
12
|
+
unless doi_extract.all_data_available
|
13
|
+
fail('The specified DOI extract group is not ready to download')
|
14
|
+
end
|
15
|
+
|
16
|
+
location = DownloadLocation.new(options.download_base_path, doi_extract)
|
17
|
+
|
18
|
+
if options.force
|
19
|
+
say('skipping user confirmation', true)
|
20
|
+
else
|
21
|
+
i = ask("download DOI extract files to #{location.path}?")
|
22
|
+
if i.to_s.downcase[0...1] != 'y'
|
23
|
+
fail('download cancelled')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
location.ensure!
|
28
|
+
|
29
|
+
doi_extract.extract_requests.each_with_index do |er, idx|
|
30
|
+
|
31
|
+
if location.complete_extract_request?(er)
|
32
|
+
say("ER #{er.id} already downloaded")
|
33
|
+
else
|
34
|
+
say_nb("ER #{er.id} downloading (#{idx + 1} of #{doi_extract.extract_requests.count})...")
|
35
|
+
location.clean_tmp!(er)
|
36
|
+
er.files.each do |f|
|
37
|
+
save_path = File.join(location.extract_request_tmp_path(er), f.filename)
|
38
|
+
ipums_client.download_extract_file(save_path, er.id, f.ext)
|
39
|
+
|
40
|
+
# Special handling of ddi files
|
41
|
+
if f.ext.downcase == 'xml'
|
42
|
+
FileUtils.cp(save_path, location.ddi_path)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
say(location.package_tmp_to_extract(er), true)
|
47
|
+
say("Complete")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
location.clean_tmp!
|
52
|
+
|
53
|
+
codebook_er = doi_extract.codebook_extract
|
54
|
+
full_ddi = codebook_er ? codebook_er.files.detect { |f| f.ext == 'xml' } : nil
|
55
|
+
|
56
|
+
if full_ddi.nil?
|
57
|
+
fail('Missing full DDI codebook')
|
58
|
+
end
|
59
|
+
|
60
|
+
ipums_client.download_extract_file(location.complete_ddi_path, codebook_er.id, 'xml')
|
61
|
+
|
62
|
+
|
63
|
+
# Write summary file to output location
|
64
|
+
File.open(location.summary_file_path, 'w') do |f|
|
65
|
+
f.puts "File generated on #{Time.now.strftime('%c')}"
|
66
|
+
f.puts
|
67
|
+
f.puts "Extract Request Group ID #{doi_extract.id}"
|
68
|
+
f.puts "DOI Version v#{doi_extract.doi_version}"
|
69
|
+
f.puts
|
70
|
+
f.puts "Citation:"
|
71
|
+
f.puts doi_extract.citation
|
72
|
+
f.puts
|
73
|
+
f.puts 'Included Extracts:'
|
74
|
+
|
75
|
+
write_extract_summary(f, location, doi_extract)
|
76
|
+
end
|
77
|
+
|
78
|
+
location.remove_tmp
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def write_extract_summary(file, location, doi_extract)
|
83
|
+
|
84
|
+
headers = ['ID', 'Sample', 'Archive Filename', 'Data Filename']
|
85
|
+
spacer = 4
|
86
|
+
|
87
|
+
extract_data = doi_extract.extract_requests.map do |er|
|
88
|
+
[er.id.to_s, location.extract_request_name(er), location.extract_request_filename(er), er.extract_file_name]
|
89
|
+
end
|
90
|
+
|
91
|
+
max_lengths = (extract_data + [headers]).reduce([0, 0, 0, 0]) do |lengths, data|
|
92
|
+
lengths.zip(data).map { |l, d| [l, d.length].max }
|
93
|
+
end
|
94
|
+
|
95
|
+
fmt = "%-#{max_lengths.first + spacer}s"
|
96
|
+
max_lengths[1..-1].each do |l|
|
97
|
+
fmt << "%#{l + spacer}s"
|
98
|
+
end
|
99
|
+
|
100
|
+
file.puts sprintf(fmt, *headers)
|
101
|
+
|
102
|
+
extract_data.each do |d|
|
103
|
+
file.puts sprintf(fmt, *d)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
class DownloadLocation
|
3
|
+
|
4
|
+
attr_reader :path
|
5
|
+
|
6
|
+
def initialize(base_path, extract_group)
|
7
|
+
@extract_group = extract_group
|
8
|
+
@base_path = base_path
|
9
|
+
if extract_group.nil? || extract_group.year.nil? || extract_group.doi_version.nil?
|
10
|
+
raise 'Cannot calculate path: missing doi extract data'
|
11
|
+
end
|
12
|
+
@path = File.join(base_path, "#{extract_group.year}_v#{extract_group.doi_version}")
|
13
|
+
end
|
14
|
+
|
15
|
+
def exist?
|
16
|
+
Dir.exist? path
|
17
|
+
end
|
18
|
+
|
19
|
+
def ensure!
|
20
|
+
FileUtils.mkdir_p path
|
21
|
+
FileUtils.mkdir_p ddi_path
|
22
|
+
end
|
23
|
+
|
24
|
+
def tmp_path
|
25
|
+
File.join(path, 'tmp')
|
26
|
+
end
|
27
|
+
|
28
|
+
def remove_tmp
|
29
|
+
if Dir.exist?(tmp_path)
|
30
|
+
FileUtils.rm_rf tmp_path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def clean_tmp!(er = nil)
|
35
|
+
remove_tmp
|
36
|
+
|
37
|
+
FileUtils.mkdir_p tmp_path
|
38
|
+
if er
|
39
|
+
FileUtils.mkdir_p extract_request_tmp_path(er)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def summary_file_path
|
44
|
+
File.join(path, 'summary.txt')
|
45
|
+
end
|
46
|
+
|
47
|
+
def complete_ddi_path
|
48
|
+
File.join(ddi_path, 'complete.xml')
|
49
|
+
end
|
50
|
+
|
51
|
+
def ddi_path
|
52
|
+
File.join(path, 'ddi')
|
53
|
+
end
|
54
|
+
|
55
|
+
def package_tmp_to_extract(er)
|
56
|
+
cmd = %[tar -czvf "#{extract_request_path(er)}" -C "#{tmp_path}" "#{extract_request_name(er)}"]
|
57
|
+
output = `#{cmd} 2>&1`
|
58
|
+
raise "error running command: #{cmd}\n#{output}" unless $?.success?
|
59
|
+
output
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_request_tmp_path(er)
|
63
|
+
File.join(tmp_path, extract_request_name(er))
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def complete_extract_request?(er)
|
68
|
+
file = extract_request_path(er)
|
69
|
+
File.exist?(file)
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_request_path(er)
|
73
|
+
File.join(path, extract_request_filename(er))
|
74
|
+
end
|
75
|
+
|
76
|
+
def extract_request_filename(er)
|
77
|
+
"#{extract_request_name(er)}.tar.gz"
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_request_name(er)
|
81
|
+
s = er.samples.first
|
82
|
+
[s.name, s.long_description].compact.join(' - ')
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
|
3
|
+
class CommandFailError < StandardError
|
4
|
+
|
5
|
+
attr_reader :inner_exception, :exit_code
|
6
|
+
|
7
|
+
def initialize(message, inner_exception = nil, exit_code = 1)
|
8
|
+
super(message)
|
9
|
+
@inner_exception = inner_exception
|
10
|
+
@exit_code = exit_code
|
11
|
+
end
|
12
|
+
|
13
|
+
def error_report
|
14
|
+
report = []
|
15
|
+
|
16
|
+
report << "Failed: #{self.message}"
|
17
|
+
|
18
|
+
if inner_exception
|
19
|
+
report << "caused by: #{inner_exception.class}: #{inner_exception.message}"
|
20
|
+
inner_exception.backtrace.each do |l|
|
21
|
+
report << " #{l}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
report << ' '
|
26
|
+
|
27
|
+
report.join("\n")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ApiError < StandardError
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module DoiExtractor
|
2
|
+
class IpumsClient
|
3
|
+
|
4
|
+
DEFAULT_READ_TIMEOUT = 120
|
5
|
+
|
6
|
+
attr_reader :uri, :username, :password
|
7
|
+
|
8
|
+
def initialize(uri, username, password)
|
9
|
+
case uri
|
10
|
+
when String
|
11
|
+
@uri = URI(uri)
|
12
|
+
when URI
|
13
|
+
@uri = uri
|
14
|
+
else
|
15
|
+
raise 'Invalid uri'
|
16
|
+
end
|
17
|
+
|
18
|
+
@username = username
|
19
|
+
@password = password
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_doi_extract(doi_version, year, email)
|
23
|
+
res = post('api/extracts/v1/extract_request_groups', {type: 'doi', year: year, doi_version: doi_version, email: email}, 600)
|
24
|
+
parse_body(res)
|
25
|
+
end
|
26
|
+
|
27
|
+
def submit_doi_extract(id)
|
28
|
+
res = post("api/extracts/v1/extract_request_groups/#{id}/submit")
|
29
|
+
parse_body(res)
|
30
|
+
end
|
31
|
+
|
32
|
+
def recent_doi_extracts
|
33
|
+
res = get('api/extracts/v1/extract_request_groups/recent', {type: 'doi'})
|
34
|
+
parse_body(res)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_doi_extract(id)
|
38
|
+
res = get("api/extracts/v1/extract_request_groups/#{id}")
|
39
|
+
parse_body(res)
|
40
|
+
end
|
41
|
+
|
42
|
+
def cancel_doi_extract(id)
|
43
|
+
res = post("api/extracts/v1/extract_request_groups/#{id}/cancel")
|
44
|
+
parse_body(res)
|
45
|
+
end
|
46
|
+
|
47
|
+
def submit_extract(id)
|
48
|
+
res = post("api/extracts/v1/extract_requests/#{id}/submit")
|
49
|
+
parse_body(res)
|
50
|
+
end
|
51
|
+
|
52
|
+
def download_extract_file(file_path, extract_request_id, extension)
|
53
|
+
path = "api/extracts/v1/extract_requests/#{extract_request_id}/file/#{extension}"
|
54
|
+
File.open(file_path, 'w') do |f|
|
55
|
+
perform_request('get', path, nil, f, 600)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def get(path, params = nil)
|
60
|
+
io = StringIO.new
|
61
|
+
perform_request('get', path, params, io)
|
62
|
+
io.string
|
63
|
+
end
|
64
|
+
|
65
|
+
def post(path, params = nil, read_timeout = DEFAULT_READ_TIMEOUT)
|
66
|
+
io = StringIO.new
|
67
|
+
perform_request('post', path, params, io, read_timeout)
|
68
|
+
io.string
|
69
|
+
end
|
70
|
+
|
71
|
+
def perform_request(method, path, params, response_io, read_timeout = DEFAULT_READ_TIMEOUT, redirect_limit = 5)
|
72
|
+
raise ApiError, 'too many HTTP redirects' if redirect_limit == 0
|
73
|
+
|
74
|
+
request_uri = self.uri.dup
|
75
|
+
request_uri.path = '/' + combine_uri_paths(request_uri.path, path)
|
76
|
+
|
77
|
+
Net::HTTP.start(uri.host, uri.port, {use_ssl: uri.scheme == 'https'}) do |http|
|
78
|
+
# in seconds
|
79
|
+
http.read_timeout = read_timeout
|
80
|
+
request = build_request(method, request_uri, params)
|
81
|
+
|
82
|
+
request.basic_auth(username, password)
|
83
|
+
request.add_field('Accept', 'application/json')
|
84
|
+
|
85
|
+
redirect_location = nil
|
86
|
+
|
87
|
+
http.request(request) do |response|
|
88
|
+
case response
|
89
|
+
when Net::HTTPSuccess
|
90
|
+
response.read_body do |chunk|
|
91
|
+
response_io.write chunk
|
92
|
+
end
|
93
|
+
when Net::HTTPRedirection
|
94
|
+
redirect_location = response['location']
|
95
|
+
when Net::HTTPUnprocessableEntity
|
96
|
+
body = response.body
|
97
|
+
body = body && body != '' ? JSON.parse(body) : nil
|
98
|
+
msg = body.map { |k, v| "#{k}: #{v}" }.join(', ')
|
99
|
+
raise ApiError, "API Server returned an error: #{msg}"
|
100
|
+
else
|
101
|
+
raise ApiError, "Invalid API Server Response from [#{method.to_s.upcase} #{path}]: #{response.code} - #{response.message}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
if redirect_location
|
106
|
+
perform_request('get', redirect_location, nil, response_io, read_timeout, redirect_limit - 1)
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def combine_uri_paths(*parts)
|
113
|
+
parts.compact.map(&:to_s).map(&:strip).map { |p| p.sub(/^\/+/, '').sub(/\/+$/, '') }.join('/')
|
114
|
+
end
|
115
|
+
|
116
|
+
def build_request(method, request_uri, params)
|
117
|
+
case method.to_s.downcase
|
118
|
+
when 'get'
|
119
|
+
case params
|
120
|
+
when Hash
|
121
|
+
request_uri.query = URI.encode_www_form(params)
|
122
|
+
when String
|
123
|
+
request_uri.query = params
|
124
|
+
when nil
|
125
|
+
# Do nothing
|
126
|
+
else
|
127
|
+
raise ApiError, "Invalid GET params: #{params}"
|
128
|
+
end
|
129
|
+
Net::HTTP::Get.new(request_uri.request_uri)
|
130
|
+
when 'post'
|
131
|
+
r = Net::HTTP::Post.new(request_uri.request_uri)
|
132
|
+
case params
|
133
|
+
when Hash
|
134
|
+
r.set_content_type('application/json')
|
135
|
+
r.body = JSON.generate(params)
|
136
|
+
when String
|
137
|
+
r.set_content_type('application/json')
|
138
|
+
r.body = params
|
139
|
+
when nil
|
140
|
+
# Do nothing
|
141
|
+
else
|
142
|
+
raise ApiError, "Invalid POST params: #{params}"
|
143
|
+
end
|
144
|
+
r
|
145
|
+
else
|
146
|
+
raise ApiError, "HTTP method #{method} not implemented"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_body(body)
|
151
|
+
if body && body != ''
|
152
|
+
JSON.parse(body, object_class: OpenStruct)
|
153
|
+
else
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|