doi_extractor 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|