chimps 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/LICENSE +674 -0
- data/README.rdoc +48 -0
- data/VERSION +1 -0
- data/bin/chimps +4 -0
- data/examples/batch.yaml +69 -0
- data/lib/chimps/cli.rb +102 -0
- data/lib/chimps/commands/base.rb +107 -0
- data/lib/chimps/commands/batch.rb +68 -0
- data/lib/chimps/commands/create.rb +33 -0
- data/lib/chimps/commands/destroy.rb +28 -0
- data/lib/chimps/commands/download.rb +76 -0
- data/lib/chimps/commands/help.rb +89 -0
- data/lib/chimps/commands/list.rb +54 -0
- data/lib/chimps/commands/query.rb +59 -0
- data/lib/chimps/commands/search.rb +59 -0
- data/lib/chimps/commands/show.rb +32 -0
- data/lib/chimps/commands/test.rb +40 -0
- data/lib/chimps/commands/update.rb +33 -0
- data/lib/chimps/commands/upload.rb +63 -0
- data/lib/chimps/commands.rb +46 -0
- data/lib/chimps/config.rb +57 -0
- data/lib/chimps/request.rb +302 -0
- data/lib/chimps/response.rb +146 -0
- data/lib/chimps/typewriter.rb +326 -0
- data/lib/chimps/utils/error.rb +40 -0
- data/lib/chimps/utils/extensions.rb +109 -0
- data/lib/chimps/utils/uses_curl.rb +26 -0
- data/lib/chimps/utils/uses_model.rb +51 -0
- data/lib/chimps/utils/uses_yaml_data.rb +94 -0
- data/lib/chimps/utils.rb +11 -0
- data/lib/chimps/workflows/batch.rb +127 -0
- data/lib/chimps/workflows/downloader.rb +102 -0
- data/lib/chimps/workflows/uploader.rb +238 -0
- data/lib/chimps/workflows.rb +11 -0
- data/lib/chimps.rb +22 -0
- data/spec/chimps/cli_spec.rb +22 -0
- data/spec/chimps/commands/base_spec.rb +25 -0
- data/spec/chimps/commands/list_spec.rb +25 -0
- data/spec/chimps/response_spec.rb +8 -0
- data/spec/chimps/typewriter_spec.rb +114 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/custom_matchers.rb +6 -0
- 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
|