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