chimps 0.1.0

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.
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