chimps 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +17 -0
  2. data/LICENSE +674 -0
  3. data/README.rdoc +48 -0
  4. data/VERSION +1 -0
  5. data/bin/chimps +4 -0
  6. data/examples/batch.yaml +69 -0
  7. data/lib/chimps/cli.rb +102 -0
  8. data/lib/chimps/commands/base.rb +107 -0
  9. data/lib/chimps/commands/batch.rb +68 -0
  10. data/lib/chimps/commands/create.rb +33 -0
  11. data/lib/chimps/commands/destroy.rb +28 -0
  12. data/lib/chimps/commands/download.rb +76 -0
  13. data/lib/chimps/commands/help.rb +89 -0
  14. data/lib/chimps/commands/list.rb +54 -0
  15. data/lib/chimps/commands/query.rb +59 -0
  16. data/lib/chimps/commands/search.rb +59 -0
  17. data/lib/chimps/commands/show.rb +32 -0
  18. data/lib/chimps/commands/test.rb +40 -0
  19. data/lib/chimps/commands/update.rb +33 -0
  20. data/lib/chimps/commands/upload.rb +63 -0
  21. data/lib/chimps/commands.rb +46 -0
  22. data/lib/chimps/config.rb +57 -0
  23. data/lib/chimps/request.rb +302 -0
  24. data/lib/chimps/response.rb +146 -0
  25. data/lib/chimps/typewriter.rb +326 -0
  26. data/lib/chimps/utils/error.rb +40 -0
  27. data/lib/chimps/utils/extensions.rb +109 -0
  28. data/lib/chimps/utils/uses_curl.rb +26 -0
  29. data/lib/chimps/utils/uses_model.rb +51 -0
  30. data/lib/chimps/utils/uses_yaml_data.rb +94 -0
  31. data/lib/chimps/utils.rb +11 -0
  32. data/lib/chimps/workflows/batch.rb +127 -0
  33. data/lib/chimps/workflows/downloader.rb +102 -0
  34. data/lib/chimps/workflows/uploader.rb +238 -0
  35. data/lib/chimps/workflows.rb +11 -0
  36. data/lib/chimps.rb +22 -0
  37. data/spec/chimps/cli_spec.rb +22 -0
  38. data/spec/chimps/commands/base_spec.rb +25 -0
  39. data/spec/chimps/commands/list_spec.rb +25 -0
  40. data/spec/chimps/response_spec.rb +8 -0
  41. data/spec/chimps/typewriter_spec.rb +114 -0
  42. data/spec/spec_helper.rb +17 -0
  43. data/spec/support/custom_matchers.rb +6 -0
  44. metadata +133 -0
@@ -0,0 +1,59 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ # A command to issue a GET request against the Infochimps paid
5
+ # query API.
6
+ class Query < Chimps::Command
7
+
8
+ BANNER = "usage: chimps query [OPTIONS] DATASET [PROP=VALUE] ..."
9
+ HELP = <<EOF
10
+
11
+ Make a query of the given DATASET on the Infochimps paid query API
12
+ (not the main Infochimps site).
13
+
14
+ Properties and values can be supplied directly on the command line,
15
+ from an input YAML file, or multiple YAML documents streamed in via
16
+ STDIN, in order of decreasing precedence.
17
+
18
+ You can learn more about the Infochimps query API, discover datasets
19
+ to query, and look up the available parameters at
20
+
21
+ http://api.infochimps.com
22
+
23
+ You can learn about the main Infochimps site API at
24
+
25
+ http://infochimps.org/api
26
+ EOF
27
+
28
+ include Chimps::Utils::UsesYamlData
29
+ IGNORE_YAML_FILES_ON_COMMAND_LINE = true # must come after include
30
+
31
+ # The dataset to query.
32
+ #
33
+ # @return [String]
34
+ def dataset
35
+ raise CLIError.new("Must provide a dataset to query.") if argv.first.blank?
36
+ argv.first
37
+ end
38
+
39
+ # The path on the Infochimps query API to query.
40
+ #
41
+ # @return [String]
42
+ def path
43
+ dataset + ".json"
44
+ end
45
+
46
+ # Issue the GET request.
47
+ def execute!
48
+ response = QueryRequest.new(path, :query_params => data, :authenticate => true).get
49
+ if response.error?
50
+ response.print
51
+ else
52
+ puts response.inspect
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,59 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ # A command to issue a GET request to create a search at
5
+ # Infochimps.
6
+ class Search < Chimps::Command
7
+
8
+ # Default number of search results returned.
9
+ # DEFAULT_LIMIT = 20
10
+
11
+ BANNER = "usage: chimps search [OPTIONS] QUERY"
12
+ HELP = <<EOF
13
+
14
+ Perform a search on Infochimps. By default the search will be of
15
+ datasets and will return all matches for the given QUERY.
16
+ EOF
17
+
18
+ # Path to search resource
19
+ PATH = 'search.json'
20
+
21
+ # Models this command applies to (default first)
22
+ MODELS = %w[dataset collection source license]
23
+ include Chimps::Utils::UsesModel
24
+
25
+ # FIXME have to implement this on the server side.
26
+ # def limit
27
+ # @limit ||= DEFAULT_LIMIT
28
+ # end
29
+
30
+ def define_options
31
+ # on_tail("-n", "--num-results NUM", "Return the given number of results instead of the default #{DEFAULT_LIMIT}") do |n|
32
+ # @limit = n.to_i
33
+ # end
34
+
35
+ on_tail("-s", "--[no-]skip-column-names", "don't print column names in output.") do |s|
36
+ @skip_column_names = s
37
+ end
38
+
39
+ end
40
+
41
+ def query
42
+ raise CLIError.new("Must provide a query to search for") if argv.blank?
43
+ argv.join(' ')
44
+ end
45
+
46
+ def params
47
+ {
48
+ :query => query,
49
+ :model => model
50
+ }
51
+ end
52
+
53
+ def execute!
54
+ Chimps::Request.new(PATH, :params => params).get.print(:skip_column_names => @skip_column_names)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -0,0 +1,32 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ class Show < Chimps::Command
5
+
6
+ BANNER = "usage: chimps show [OPTIONS] ID_OR_HANDLE"
7
+ HELP = <<EOF
8
+
9
+ Return a description of the resource (defaults to dataset) with the
10
+ given ID or HANDLE
11
+ EOF
12
+
13
+ # Models this command applies to (default first)
14
+ MODELS = %w[dataset collection source license tag category]
15
+ include Chimps::Utils::UsesModel
16
+
17
+ # The path of the URL to send a Request to.
18
+ #
19
+ # This is different from Chimps::Commands::UsesModel in that it
20
+ # submits to the YAML path.
21
+ def model_path
22
+ "#{plural_model}/#{model_identifier}.yaml"
23
+ end
24
+
25
+ def execute!
26
+ puts Chimps::Request.new(model_path).get.body
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,40 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ # A command to test whether API authentication with Infochimps is
5
+ # working.
6
+ class Test < Chimps::Command
7
+
8
+ BANNER = "usage: chimps test"
9
+ HELP = <<EOF
10
+
11
+ Print diagnostic information on the API credentials being used by chimps
12
+ and send a test request to Infochimps to make sure the API credentials
13
+ work.
14
+
15
+ EOF
16
+
17
+ # Path to submit test requests to.
18
+ def path
19
+ "api_accounts/#{Chimps::CONFIG[:site][:key]}"
20
+ end
21
+
22
+ # Issue the request.
23
+ def execute!
24
+ puts "Reading identity file at #{CONFIG[:identity_file]}" if Chimps.verbose?
25
+ response = Chimps::Request.new(path, :sign => true).get
26
+ if response.error?
27
+ case
28
+ when response.code == 404 then puts "ERROR Unrecognized API key" # record not found
29
+ when response.code == 401 then puts "ERROR Signature does not match API key and query. Is your secret key correct?" # unauthorized
30
+ else
31
+ nil # response gets printed anyway
32
+ end
33
+ end
34
+ response.print
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+
@@ -0,0 +1,33 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ # A command to issue a PUT request to update a resource at
5
+ # Infochimps.
6
+ class Update < Chimps::Command
7
+
8
+ BANNER = "usage: chimps update [OPTIONS] ID_OR_HANDLE [PROP=VALUE] ..."
9
+ HELP = <<EOF
10
+
11
+ Updates a single resource of a given type (defaults to dataset)
12
+ identified by ID_OR_HANDLE using the properties and values supplied.
13
+
14
+ Properties and values can be supplied directly on the command line,
15
+ from an input YAML file, or multiple YAML documents streamed in via
16
+ STDIN, in order of decreasing precedence.
17
+ EOF
18
+
19
+ # Models this command applies to (default first)
20
+ MODELS = %w[dataset source license]
21
+ include Chimps::Utils::UsesModel
22
+ include Chimps::Utils::UsesYamlData
23
+
24
+ # Issue the PUT request.
25
+ def execute!
26
+ ensure_data_is_present!
27
+ Request.new(model_path, :data => {model.to_sym => data } , :authenticate => true).put.print
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,63 @@
1
+ module Chimps
2
+ module Commands
3
+
4
+ # A command for uploading data to Infochimps.
5
+ class Upload < Chimps::Command
6
+
7
+ BANNER = "usage: chimps upload [OPTIONS] ID_OR_HANDLE PATH [PATH] ..."
8
+ HELP = <<EOF
9
+
10
+ Upload data from your local machine for an existing dataset identified
11
+ by ID_OR_HANDLE on Infochimps.
12
+
13
+ chimps will package all paths supplied into a local archive and then
14
+ upload this archive to Infochimps. The local archive defaults to a
15
+ sensible name in the current directory but can also be customized.
16
+
17
+ If the only file to be packaged is already a package (.zip, .tar,
18
+ .tar.gz, &.c) then it will not be packaged again.
19
+ EOF
20
+
21
+ # The path to the archive
22
+ attr_reader :archive
23
+
24
+ # The data format to annotate the upload with.
25
+ #
26
+ # Chimps will try to guess if this isn't given.
27
+ attr_reader :fmt
28
+
29
+ # The ID or handle of the dataset to upload data for.
30
+ #
31
+ # @return [String]
32
+ def dataset
33
+ raise CLIError.new("Must provide an ID or URL-escaped handle as the first argument") if argv.first.blank?
34
+ argv.first
35
+ end
36
+
37
+ # A list of local paths to upload.
38
+ #
39
+ # @return [Array<String>]
40
+ def local_paths
41
+ raise CLIError.new("Must provide some paths to upload") if argv.length < 2
42
+ argv[1..-1]
43
+ end
44
+
45
+ def define_upload_options
46
+ on_tail("-a", "--archive-path", "Path to the archive to be created. Defaults to a timestamped ZIP file named after the dataset in the current directory.") do |path|
47
+ @archive = path
48
+ end
49
+
50
+ on_tail("-f", "--format FORMAT", "Data format to annotate upload with. Tries to guess if not given.") do |f|
51
+ @fmt = f
52
+ end
53
+
54
+ end
55
+
56
+ # Upload the data.
57
+ def execute!
58
+ Chimps::Workflows::Uploader.new(:dataset => dataset, :archive => archive, :local_paths => local_paths, :fmt => fmt).execute!
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,46 @@
1
+ module Chimps
2
+
3
+ # A namespace to hold the various commands Chimps defines.
4
+ module Commands
5
+
6
+ # Construct a new command from the given +command_name+ and +argv.
7
+ # The resulting command will be initialized but will not have been
8
+ # executed.
9
+ #
10
+ # @param [String] command_name
11
+ # @param [Array<String>] argv
12
+ # @return [Chimps::Command]
13
+ def self.construct command_name, argv
14
+ self.constants.each do |constant_name|
15
+ return "Chimps::Commands::#{constant_name}".constantize.new(argv) if constant_name.downcase == command_name
16
+ end
17
+ raise CLIError.new("Invalid command: #{command_name}. Try running `chimps help'")
18
+ end
19
+
20
+ # Construct a new command from the given +command_name+ and
21
+ # +argv+.
22
+ #
23
+ # Delegates to Chimps::Commands.construct, so see its
24
+ # documentation for more information.
25
+ def construct command_name, argv
26
+ Chimps::Commands.construct command_name, argv
27
+ end
28
+
29
+ # A list of all the commmand names defined by Chimps. Each name
30
+ # maps to a corresponding subclass of Chimps::Command living in
31
+ # the Chimps::Commands module.
32
+ NAMES = %w[search help test create show update destroy upload list download batch query]
33
+
34
+ NAMES.each do |name|
35
+ autoload name.capitalize.to_sym, "chimps/commands/#{name}"
36
+ end
37
+
38
+ # Is +name+ a Chimps command name?
39
+ #
40
+ # @param [String] name
41
+ # @return [true, false]
42
+ def command_name? name
43
+ NAMES.include?(name)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ module Chimps
2
+
3
+ # Default configuration for Chimps. User-specific configuration
4
+ # usually lives in a YAML file <tt>~/.chimps</tt>.
5
+ CONFIG = {
6
+ :query => {
7
+ :host => 'http://api.infochimps.com'
8
+ },
9
+ :site => {
10
+ :host => 'http://infochimps.org'
11
+ },
12
+ :identity_file => File.expand_path(ENV["CHIMPS_RC"] || "~/.chimps"),
13
+ :verbose => nil,
14
+ :timestamp_format => "%Y-%m-%d_%H-%M-%S"
15
+ }
16
+
17
+ # Is Chimps in verbose mode?
18
+ #
19
+ # @return [true, false]
20
+ def self.verbose?
21
+ CONFIG[:verbose]
22
+ end
23
+
24
+ # The username Chimps will pass to Infochimps.
25
+ #
26
+ # @return [String]
27
+ def self.username
28
+ CONFIG[:site][:username] or raise AuthenticationError.new("No site username set in #{Chimps::CONFIG[:identity_file]}")
29
+ end
30
+
31
+ # Defines methods to load the Chimps configuration.
32
+ module Config
33
+
34
+ # The root of the Chimps source base.
35
+ #
36
+ # @return [String]
37
+ def self.chimps_root
38
+ File.expand_path File.join(File.dirname(__FILE__), '../..')
39
+ end
40
+
41
+ # Load the configuration settings from the configuration/identity
42
+ # file.
43
+ def self.load
44
+ # FIXME this is a terrible hack...and it only goes to 2 deep!
45
+ if File.exist?(CONFIG[:identity_file])
46
+ require 'yaml'
47
+ YAML.load_file(CONFIG[:identity_file]).each_pair do |key, value|
48
+ if value.is_a?(Hash) && CONFIG.include?(key)
49
+ CONFIG[key].merge!(value)
50
+ else
51
+ CONFIG[key] = value
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,302 @@
1
+ require 'restclient'
2
+
3
+ module Chimps
4
+
5
+ # A class to encapsulate requests made of Infochimps.
6
+ #
7
+ # Essentialy a wrapper for RestClient::Resource with added
8
+ # funcionality for automatically signing requests and parsing
9
+ # Infochimps API responses.
10
+ class Request < RestClient::Resource
11
+
12
+ # Default headers to pass with every request.
13
+ DEFAULT_HEADERS = { :content_type => 'application/json', :accept => 'application/json' }
14
+
15
+ # Path of the URL to submit to. Must be a String.
16
+ attr_accessor :path
17
+
18
+ # Parameters to include in the query string of the URL to submit
19
+ # to. Must be a Hash.
20
+ attr_accessor :query_params
21
+
22
+ # Data to include in the body of the request. Must be a Hash.
23
+ attr_accessor :data
24
+
25
+ # Initialize a Request to the given +path+.
26
+ #
27
+ # Query parameters and data can be passed in as hashes named
28
+ # <tt>:params</tt> and <tt>:data</tt>, respectively.
29
+ #
30
+ # If <tt>:sign</tt> is passed in the +options+ then the URL of
31
+ # this request will be signed with the Chimps user's Infochimps
32
+ # API key and secret. Failure to properly sign will raise an
33
+ # error.
34
+ #
35
+ # If <tt>:sign_if_possible</tt> is passed in the +options+ then an
36
+ # attemp to sign the URL will be made though an error will _not_
37
+ # raise an error.
38
+ #
39
+ # @param [String] path
40
+ # @param [Hash] options
41
+ # @option options [Hash] params Query parameters to include in the URL
42
+ # @option options [Hash] data Data to include in the request body
43
+ # @option options [true, false] sign Sign this request, raising an error on failure
44
+ # @option options [true, false] sign_if_possible Sign this request, no error on failure
45
+ # @return [Chimps::Request]
46
+ def initialize path, options={}
47
+ @path = path
48
+ @query_params = options[:query_params] || options[:params] || {}
49
+ @data = options[:data] || {}
50
+ @authentication_required = [:authenticate, :authenticated, :authenticate_if_possible, :sign, :signed, :sign_if_possible].any? { |key| options.include?(key) }
51
+ @forgive_authentication_error = options[:sign_if_possible] || options[:authenticate_if_possible]
52
+ authenticate_if_necessary!
53
+ super url_with_query_string
54
+ end
55
+
56
+ # Should the request be authenticated?
57
+ #
58
+ # @return [true, false]
59
+ def authenticate?
60
+ @authentication_required
61
+ end
62
+ alias_method :sign?, :authenticate?
63
+
64
+ # Is this request authentiable (has the Chimps user specified an
65
+ # API key and secret in their configuration file)?
66
+ #
67
+ # @return [true, false]
68
+ def authenticable?
69
+ !Chimps::CONFIG[:site][:key].blank? && !Chimps::CONFIG[:site][:secret].blank?
70
+ end
71
+ alias_method :signable?, :authenticable?
72
+
73
+ # The host to send requests to.
74
+ #
75
+ # @return [String]
76
+ def host
77
+ @host ||= ENV["CHIMPS_HOST"] || Chimps::CONFIG[:site][:host]
78
+ end
79
+
80
+ # Return the URL for this request with the (signed, if necessary)
81
+ # query string appended.
82
+ #
83
+ # @return [String]
84
+ def url_with_query_string
85
+ base_url = File.join(host, path)
86
+ base_url += "?#{query_string}" unless query_string.blank?
87
+ base_url
88
+ end
89
+
90
+ # Return the query string for this request, signed if necessary.
91
+ #
92
+ # @return [String]
93
+ def query_string
94
+ (authenticate? && authenticable?) ? signed_query_string : unsigned_query_string
95
+ end
96
+
97
+ # Perform a GET request to this URL, returning a parsed response.
98
+ #
99
+ # Any headers in +options+ will passed to
100
+ # RestClient::Resource.get.
101
+ #
102
+ # @param [Hash] options
103
+ # @return [Chimps::Response]
104
+ def get options={}
105
+ handle_exceptions do
106
+ puts "GET #{url}" if Chimps.verbose?
107
+ Response.new(super(DEFAULT_HEADERS.merge(options)))
108
+ end
109
+ end
110
+
111
+ # Perform a POST request to this URL, returning a parsed response.
112
+ #
113
+ # Any headers in +options+ will passed to
114
+ # RestClient::Resource.post.
115
+ #
116
+ # @param [Hash] options
117
+ # @return [Chimps::Response]
118
+ def post options={}
119
+ handle_exceptions do
120
+ puts "POST #{url}" if Chimps.verbose?
121
+ Response.new(super(data_text, DEFAULT_HEADERS.merge(options)))
122
+ end
123
+ end
124
+
125
+ # Perform a PUT request to this URL, returning a parsed response.
126
+ #
127
+ # Any headers in +options+ will passed to
128
+ # RestClient::Resource.put.
129
+ #
130
+ # @param [Hash] options
131
+ # @return [Chimps::Response]
132
+ def put options={}
133
+ handle_exceptions do
134
+ puts "PUT #{url}" if Chimps.verbose?
135
+ Response.new(super(data_text, DEFAULT_HEADERS.merge(options)))
136
+ end
137
+ end
138
+
139
+ # Perform a DELETE request to this URL, returning a parsed
140
+ # response.
141
+ #
142
+ # Any headers in +options+ will passed to
143
+ # RestClient::Resource.delete.
144
+ #
145
+ # @param [Hash] options
146
+ # @return [Chimps::Response]
147
+ def delete options={}
148
+ handle_exceptions do
149
+ puts "DELETE #{url}" if Chimps.verbose?
150
+ Response.new(super(DEFAULT_HEADERS.merge(options)))
151
+ end
152
+ end
153
+
154
+ protected
155
+ # Yield to +block+ but rescue any RestClient errors by wrapping
156
+ # them in a Chimps::Response.
157
+ def handle_exceptions &block
158
+ begin
159
+ yield
160
+ rescue RestClient::Exception => e
161
+ return Response.new(e.response, :error => e.message)
162
+ end
163
+ end
164
+
165
+ # Authenticate this request by stuffing the <tt>:requested_at</tt>
166
+ # and <tt>:api_key</tt> properties into its <tt>:query_params</tt>
167
+ # hash.
168
+ #
169
+ # Will do nothing at all if Chimps::Request#authenticate? returns
170
+ # false.
171
+ def authenticate_if_necessary!
172
+ return unless authenticate?
173
+ raise Chimps::AuthenticationError.new("API key or secret missing from #{CONFIG[:identity_file]}") unless (authenticable? || @forgive_authentication_error)
174
+ query_params[:requested_at] = Time.now.to_i.to_s
175
+ query_params[:api_key] = Chimps::CONFIG[:site][:key]
176
+ end
177
+
178
+ # Return the sorted keys of the query params.
179
+ #
180
+ # @return [Array]
181
+ def alphabetical_params
182
+ query_params.keys.map(&:to_s).sort
183
+ end
184
+
185
+ # Return an unsigned query string for this request.
186
+ #
187
+ # Query parameters will be used in alphabetical order.
188
+ #
189
+ # @return [String]
190
+ def unsigned_query_string
191
+ # alphabetical_params.map { |key| "#{CGI::escape(key.to_s)}=#{CGI::escape(query_params[key.to_sym].to_s)}" }.join("&") # doesn't flatten nested hashes properly
192
+ RestClient::Payload.generate(query_params)
193
+ end
194
+
195
+ # Return an unsigned query string for this request without the
196
+ # <tt>&</tt> and <tt>=</tt> characters.
197
+ #
198
+ # This is the text that will be signed for GET and DELETE
199
+ # requests.
200
+ #
201
+ # @return [String]
202
+ def unsigned_query_string_stripped
203
+ require 'cgi'
204
+ @query_params_text ||= alphabetical_params.map { |key| CGI::escape(key.to_s) + CGI::escape(query_params[key.to_sym].to_s) }.join('')
205
+ end
206
+
207
+ # Return the data of this request as a string.
208
+ #
209
+ # This is the text that will be signed for POST and PUT requests.
210
+ #
211
+ # @return [String]
212
+ def data_text
213
+ require 'json'
214
+ @data_text ||= data.to_json
215
+ end
216
+
217
+ # Sign +string+ by concatenting it with the secret and computing
218
+ # the MD5 digest of the whole thing.
219
+ #
220
+ # @param [String]
221
+ # @return [String]
222
+ def sign string
223
+ raise Chimps::AuthenticationError.new("No API secret stored in #{CONFIG[:identity_file]}.") unless (authenticable? || @forgive_authentication_error)
224
+ require 'digest/md5'
225
+ Digest::MD5.hexdigest(string + CONFIG[:site][:secret])
226
+ end
227
+
228
+ # Append the signature to the unsigned query string.
229
+ #
230
+ # The signature made from the Chimps user's API secret and either
231
+ # the query string text (stripped of <tt>&</tt> and <tt>=</tt>)
232
+ # for GET and DELETE requests or the request body for POST and PUT
233
+ # requests.
234
+ #
235
+ # @return [String]
236
+ def signed_query_string
237
+ signature = sign(data.blank? ? unsigned_query_string_stripped : data_text)
238
+ "#{unsigned_query_string}&signature=#{signature}"
239
+ end
240
+
241
+ end
242
+
243
+ # A class to encapsulate requests made against the Infochimps paid
244
+ # query API.
245
+ class QueryRequest < Request
246
+
247
+ # Is this request authentiable (has the Chimps user specified an
248
+ # API key and secret in their configuration file)?
249
+ #
250
+ # @return [true, false]
251
+ def authenticable?
252
+ !Chimps::CONFIG[:query][:key].blank? && !Chimps::CONFIG[:query][:secret].blank?
253
+ end
254
+
255
+ # The host to send requests to.
256
+ #
257
+ # @return [String]
258
+ def host
259
+ @host ||= ENV["CHIMPS_QUERY_HOST"] || Chimps::CONFIG[:query][:host]
260
+ end
261
+
262
+ # Authenticate this request by stuffing the <tt>:requested_at</tt>
263
+ # and <tt>:api_key</tt> properties into its <tt>:query_params</tt>
264
+ # hash.
265
+ #
266
+ # Will do nothing at all if Chimps::Request#authenticate? returns
267
+ # false.
268
+ def authenticate_if_necessary!
269
+ return unless authenticate?
270
+ raise Chimps::AuthenticationError.new("API key or secret missing from #{CONFIG[:identity_file]}") unless (authenticable? || @forgive_authentication_error)
271
+ query_params[:requested_at] = Time.now.to_i.to_s
272
+ query_params[:apikey] = Chimps::CONFIG[:query][:key]
273
+ end
274
+
275
+ # Sign +string+ by concatenting it with the secret and computing
276
+ # the MD5 digest of the whole thing.
277
+ #
278
+ # @param [String]
279
+ # @return [String]
280
+ def sign string
281
+ raise Chimps::AuthenticationError.new("No API secret stored in #{CONFIG[:identity_file]}.") unless (authenticable? || @forgive_authentication_error)
282
+ require 'digest/md5'
283
+ Digest::MD5.hexdigest(string + CONFIG[:key][:secret])
284
+ end
285
+
286
+ # Append the signature to the unsigned query string.
287
+ #
288
+ # The signature made from the Chimps user's API secret and either
289
+ # the query string text (stripped of <tt>&</tt> and <tt>=</tt>)
290
+ # for GET and DELETE requests or the request body for POST and PUT
291
+ # requests.
292
+ #
293
+ # @return [String]
294
+ def signed_query_string
295
+ unsigned_query_string
296
+ end
297
+
298
+
299
+
300
+ end
301
+
302
+ end