This repository has been archived by the owner on Jan 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathGOVDataSDK.rb
252 lines (205 loc) · 9.01 KB
/
GOVDataSDK.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
require 'rubygems'
require 'net/http'
require 'uri'
require 'thread'
#require 'hmac-sha1'
require 'json'
require 'open-uri'
require 'net/https'
require 'openssl'
#require 'always_verify_ssl_certificates'
require 'cgi'
require 'nokogiri'
module GOV
URL_API_V2 = 'https://data.dol.gov'
URL_API_V1 = 'http://api.dol.gov'
API_VALID_ARGUMENTS = %w[top skip select orderby filter]
# This class handles storing the host, API key, and SharedSecret for your
# DataRequest objects. A DataContext is valid if it has values for host, key, and secret.
class DataContext
attr_accessor :host, :key, :data, :uri
def initialize host, key, data, uri
@host, @key, @data, @uri = host, key, data, uri
end
end
# This class handles requesting data using the API.
# All DataRequest objects must be initialized with a DataContext
# that provides the DatRequest with a host, and API key.
# After generating a request, call #call_api to submit it.
class DataRequest
attr_accessor :context
def initialize context
@context = context
@mutex = Mutex.new
@active_requests = []
end
# This method consturcts and submits the data request.
# It calls the passed block when it completes, returning both a result and an error.
# If error is not nil, there was an error during processing.
# The request is submitted in another thread, so call #wait_until_finished to ensure
# that all requests have processed after submitting a request.
# You can make multiple requests with #call_api from a single DataRequest object,
# and #wait_until_finished wll correctly wait for all of them.
def call_api method, arguments = {}, &block
# Ensures only a valid DataContext is used
unless @context.is_a? DataContext
block.call nil, 'A context object was not provided.'
return
end
# Ensures only valid arguments are used
query = []
arguments.each_pair do |key, value|
if API_VALID_ARGUMENTS.member? key.to_s
query << "$#{key}=#{URI.escape value.to_s}"
else
if context.host == "http://go.usa.gov"
### See comment in go.usa.gov section below
query << "#{key}=#{CGI::escape(value.to_s)}"
elsif context.host == GOV::URL_API_V2
query << "#{key}/#{URI.escape value.to_s}"
else
query << "#{key}=#{URI.escape value.to_s}"
end
end
end
# Generates timestamp and url
timestamp = GOV.timestamp
# By default, these calls do not require SSL
sdkUseSSL = FALSE
# Creates a new thread, creates an authenticaed request, and requests data from the host
@mutex.synchronize do
@active_requests << Thread.new do
#TODO: Finish the conditional formatting below
if context.host == GOV::URL_API_V1
# For DOL V1
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}?KEY=#{@context.key}", query.join('&')].join '&'
elsif context.host == GOV::URL_API_V2
# For DOL V2
url = URI.parse ["#{context.host}/#{@context.uri}/#{@context.data}", query.join('/')].join '/'
url.port = 443
elsif context.host == "http://go.usa.gov"
# For go.USA.gov
### THIS SHOULD USE SSL. Have not been able to make it work with SSL. Strangely, for now, works w/o it.
sdkUseSSL = FALSE
login = query.at(0)
longURL = query.at(1)
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}?#{login}&apiKey=#{@context.key}&#{longURL}"].join '&'
#url.port = 443
elsif context.host == "http://www.ncdc.noaa.gov"
# NOAA National Climatic Data Center
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}?token=#{@context.key}", query.join('&')].join '&'
elsif ((context.host == "http://api.eia.gov") || (context.host == "http://developer.nrel.gov") || (context.host == "http://api.stlouisfed.org") || (context.host == "http://healthfinder.gov"))
# Energy EIA API (beta)
# Energy NREL
# St. Louis Fed
# NIH Healthfinder
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}?api_key=#{@context.key}", query.join('&')].join '&'
elsif ((context.host == "http://api.census.gov") || (context.host == "http://pillbox.nlm.nih.gov"))
# Census.gov
# NIH Pillbox
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}?key=#{@context.key}", query.join('&')].join '&'
else
####### RETEST #######
url = URI.parse ["#{@context.host}/#{@context.uri}/#{method}", query.join('&')].join '?'
end
#print url
if context.host == GOV::URL_API_V2
#OpenSSL::SSL.const_set(:VERIFY_PEER, OpenSSL::SSL::VERIFY_NONE)
request = Net::HTTP::Get.new url
request.add_field 'X-API-KEY', @context.key
request.add_field 'Accept', 'application/json'
result = Net::HTTP.start url.host, url.port, :use_ssl => url.scheme == 'https' do |http|
http.request request
end
elsif context.host == GOV::URL_API_V1
request = Net::HTTP::Get.new [url.path, url.query].join '?'
result = Net::HTTP.start url.host, url.port do |http|
http.request request
end
else
request = Net::HTTP::Get.new [url.path, url.query].join '?'
request.add_field 'Accept', 'application/json'
result = Net::HTTP.start url.host, url.port do |http|
http.request request
end
end
# commented code below should no longer be needed since DOL simplified its API call syntax
#if context.host == "http://api.dol.gov"
# request.add_field 'Authorization', "Timestamp=#{timestamp}&ApiKey=#{@context.key}&Signature=#{signature timestamp, url}"
#end
if context.host == GOV::URL_API_V1
rawresult = result.body
result = Nokogiri::XML(rawresult)
block.call result, nil
else
if result.is_a? Net::HTTPSuccess
rawresult = result.body
#print rawresult
if context.host == GOV::URL_API_V2
begin
result = JSON.parse(rawresult)
rescue
print "parse attempt failed"
end
else
#Cleanup jsonresult.
result = result.body.gsub(/\\+"/, '"')
result = result.gsub(/\\+n/, "")
result = result.gsub(/\"\"\{/, "{")
result = result.gsub(/}\"\"/, "}")
print result
begin
result = JSON.parse(result)['d']
rescue
print "parse attempt failed"
end
end
# If the JSON is not parsed successfully, we need to avoid an error regarding result.include?. In this case, we return the raw API output. Either the parser above needs work or the JSON results need love.
# This may be an area that needs future improvements
if !result.nil?
if (result.include?'results')
result = result['results'] if result.is_a? Hash
end
block.call result, nil
else
# print rawresult
block.call rawresult, nil
end
else
block.call nil, "Error Will Robinson: #{result.message}"
end
end
@mutex.synchronize do
@active_requests.delete Thread.current
end
end
end
end
# Halts program until all ongoing requests sent by this DataRequest finish
def wait_until_finished
@active_requests.dup.each do |n|
n.join
end
end
# commented code below should no longer be needed since DOL simplified its API call syntax
#private
# Generates a signature using your SharedSecret and the request path
#def signature timestamp, url
# HMAC::SHA1.hexdigest @context.secret, [url.path, url.query + "&Timestamp=#{timestamp}&ApiKey=#{@context.key}"].join('?')
#end
end
module_function
def timestamp
Time.now.utc.strftime "%Y-%m-%dT%H:%M:%SZ"
end
end
class String
# converts date strings provided by the API (of the format /Date(milliseconds-since-Epoch)/) into Ruby Time objects
def to_api_date
if match(/\A\/Date\((\d+)\)\/\Z/)
Time.at $1.to_i / 1000
else
raise TypeError, "Not a valid date format"
end
end
end