ralexa 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +131 -0
- data/Rakefile +13 -0
- data/lib/ralexa/abstract_xml_service.rb +27 -0
- data/lib/ralexa/canonicalized_query_string.rb +23 -0
- data/lib/ralexa/client.rb +49 -0
- data/lib/ralexa/session.rb +18 -0
- data/lib/ralexa/top_sites.rb +59 -0
- data/lib/ralexa/uri_signer.rb +89 -0
- data/lib/ralexa/version.rb +3 -0
- data/lib/ralexa.rb +22 -0
- data/ralexa.gemspec +23 -0
- data/spec/canonicalized_query_string_spec.rb +35 -0
- data/spec/client_spec.rb +32 -0
- data/spec/fixtures/global.xml +45 -0
- data/spec/fixtures/list_countries.xml +22 -0
- data/spec/ralexa_spec.rb +13 -0
- data/spec/session_spec.rb +15 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/top_sites_spec.rb +87 -0
- data/spec/uri_signer_spec.rb +62 -0
- metadata +122 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Flippa
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
Ralexa
|
2
|
+
======
|
3
|
+
|
4
|
+
Ruby client for Amazon Alexa APIs.
|
5
|
+
|
6
|
+
These include [Alexa Top Sites][1] and [Alexa Web Information Service][2].
|
7
|
+
|
8
|
+
HTTP transport thanks to `Net::HTTP`,
|
9
|
+
with a little help from [addressable][3].
|
10
|
+
|
11
|
+
XML parsing courtesy of [Nokogiri][4].
|
12
|
+
|
13
|
+
Thorough test coverage provided by `MiniTest::Spec`.
|
14
|
+
|
15
|
+
[1]: http://aws.amazon.com/awis/
|
16
|
+
[2]: http://aws.amazon.com/alexatopsites/
|
17
|
+
[3]: https://github.com/sporkmonger/addressable
|
18
|
+
[4]: http://nokogiri.org/
|
19
|
+
|
20
|
+
|
21
|
+
Installation
|
22
|
+
------------
|
23
|
+
|
24
|
+
```sh
|
25
|
+
# with bundler
|
26
|
+
echo 'gem "ralexa"' >> Gemfile
|
27
|
+
bundle
|
28
|
+
|
29
|
+
# or just the gem
|
30
|
+
gem install ralexa
|
31
|
+
```
|
32
|
+
|
33
|
+
|
34
|
+
Usage Examples
|
35
|
+
--------------
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
|
39
|
+
# grab a Ralexa::Session instance to hold your credentials.
|
40
|
+
session = Ralexa.session("aws_access_key_id", "aws_secret_access_key")
|
41
|
+
|
42
|
+
# all countries
|
43
|
+
countries = session.top_sites.list_countries
|
44
|
+
|
45
|
+
# global top sites
|
46
|
+
global = session.top_sites.global
|
47
|
+
|
48
|
+
# per-country top sites
|
49
|
+
first_by_country = {}
|
50
|
+
countries.each do |c|
|
51
|
+
first_by_country[c.name] = session.top_sites.country(c.code).first.url
|
52
|
+
end
|
53
|
+
|
54
|
+
# individual country lookup
|
55
|
+
puts "Top Australian Sites"
|
56
|
+
session.top_sites.country("AU").each do |s|
|
57
|
+
puts "#{s.url} (#{s.page_views} pageviews)"
|
58
|
+
end
|
59
|
+
|
60
|
+
puts "bam!"
|
61
|
+
```
|
62
|
+
|
63
|
+
|
64
|
+
Status
|
65
|
+
------
|
66
|
+
|
67
|
+
<table>
|
68
|
+
<tr>
|
69
|
+
<th>Service</th>
|
70
|
+
<th>Action</th>
|
71
|
+
<th>ResponseGroup</th>
|
72
|
+
<th>Supported</th>
|
73
|
+
</tr>
|
74
|
+
<tr>
|
75
|
+
<td rowspan="3">Alexa Top Sites</th>
|
76
|
+
<td>TopSites</th>
|
77
|
+
<td>Global</th>
|
78
|
+
<td>Yes</th>
|
79
|
+
</tr>
|
80
|
+
<tr>
|
81
|
+
<td></th>
|
82
|
+
<td>Country</th>
|
83
|
+
<td>Yes</th>
|
84
|
+
</tr>
|
85
|
+
<tr>
|
86
|
+
<td></th>
|
87
|
+
<td>ListCountries</th>
|
88
|
+
<td>Yes</th>
|
89
|
+
</tr>
|
90
|
+
<tr>
|
91
|
+
<td rowspan="5">Alexa Web Information Services</th>
|
92
|
+
<td>UrlInfo</th>
|
93
|
+
<td>*</th>
|
94
|
+
<td>Send a pull request!</th>
|
95
|
+
</tr>
|
96
|
+
<tr>
|
97
|
+
<td>TrafficHistory</th>
|
98
|
+
<td>*</th>
|
99
|
+
<td>Send a pull request!</th>
|
100
|
+
</tr>
|
101
|
+
<tr>
|
102
|
+
<td>CategoryBrowse</th>
|
103
|
+
<td>*</th>
|
104
|
+
<td>Send a pull request!</th>
|
105
|
+
</tr>
|
106
|
+
<tr>
|
107
|
+
<td>CategoryListings</th>
|
108
|
+
<td>*</th>
|
109
|
+
<td>Send a pull request!</th>
|
110
|
+
</tr>
|
111
|
+
<tr>
|
112
|
+
<td>SitesLinkingIn</th>
|
113
|
+
<td>*</th>
|
114
|
+
<td>Send a pull request!</th>
|
115
|
+
</tr>
|
116
|
+
</table>
|
117
|
+
|
118
|
+
|
119
|
+
Contributing
|
120
|
+
------------
|
121
|
+
|
122
|
+
1. Fork it
|
123
|
+
2. Create your feature branch (`git checkout -b some-new-feature`)
|
124
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
125
|
+
4. Push to the branch (`git push origin some-new-feature`)
|
126
|
+
5. Create new Pull Request
|
127
|
+
|
128
|
+
License
|
129
|
+
-------
|
130
|
+
|
131
|
+
(c) 2012 Flippa, The MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
3
|
+
module Ralexa
|
4
|
+
class AbstractXmlService
|
5
|
+
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# Returns the response as an XML document stripped of namespaces.
|
13
|
+
def dispatch(*params)
|
14
|
+
Nokogiri::XML.parse(
|
15
|
+
@client.get(host, path, merged_params(*params))
|
16
|
+
).remove_namespaces!
|
17
|
+
end
|
18
|
+
|
19
|
+
# A hash of the provided params hashes merged into the default_params.
|
20
|
+
def merged_params(*params)
|
21
|
+
params.reduce(default_params) do |merged_params, params|
|
22
|
+
merged_params.merge(params)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Ralexa
|
4
|
+
class CanonicalizedQueryString
|
5
|
+
|
6
|
+
def initialize(parameters)
|
7
|
+
@parameters = parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
@parameters.sort.map do |k, v|
|
12
|
+
"%s=%s" % [ escape_rfc3986(k), escape_rfc3986(v) ]
|
13
|
+
end.join("&")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def escape_rfc3986(string)
|
19
|
+
URI.escape(string.to_s, /[^A-Za-z0-9\-_.~]/)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "addressable/uri"
|
2
|
+
require "net/http"
|
3
|
+
|
4
|
+
require "ralexa/uri_signer"
|
5
|
+
|
6
|
+
module Ralexa
|
7
|
+
class Client
|
8
|
+
|
9
|
+
def initialize(access_key_id, secret_access_key)
|
10
|
+
@access_key_id = access_key_id
|
11
|
+
@secret_access_key = secret_access_key
|
12
|
+
end
|
13
|
+
|
14
|
+
# Dependency injectors.
|
15
|
+
attr_writer :net_http
|
16
|
+
|
17
|
+
def get(host, path, query_values)
|
18
|
+
uri = signed_uri(host, path, query_values)
|
19
|
+
response = net_http.get_response(uri)
|
20
|
+
response.error! unless response.is_a?(Net::HTTPSuccess)
|
21
|
+
response.body
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def signed_uri(host, path, query_values)
|
27
|
+
uri_signer.sign_uri(
|
28
|
+
Addressable::URI.new(
|
29
|
+
scheme: "http",
|
30
|
+
host: host,
|
31
|
+
path: path,
|
32
|
+
query_values: query_values
|
33
|
+
)
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def uri_signer
|
38
|
+
UriSigner.new(@access_key_id, @secret_access_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Injectable dependencies.
|
43
|
+
|
44
|
+
def net_http
|
45
|
+
@net_http || Net::HTTP
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "ralexa/client"
|
2
|
+
require "ralexa/top_sites"
|
3
|
+
|
4
|
+
module Ralexa
|
5
|
+
class Session
|
6
|
+
|
7
|
+
def initialize(access_key_id, secret_access_key)
|
8
|
+
@client = Client.new(access_key_id, secret_access_key)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Provides a TopSites instance with an authenticated client.
|
12
|
+
# See: http://docs.amazonwebservices.com/AlexaTopSites/latest/index.html
|
13
|
+
def top_sites
|
14
|
+
TopSites.new(@client)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "ralexa/abstract_xml_service"
|
2
|
+
|
3
|
+
module Ralexa
|
4
|
+
class TopSites < AbstractXmlService
|
5
|
+
|
6
|
+
# A global list of top sites.
|
7
|
+
def global(params = {})
|
8
|
+
top_sites_from_document(dispatch(
|
9
|
+
{"ResponseGroup" => "Country"},
|
10
|
+
params
|
11
|
+
))
|
12
|
+
end
|
13
|
+
|
14
|
+
# Top sites for the specified two letter country code.
|
15
|
+
def country(code, params = {})
|
16
|
+
top_sites_from_document(dispatch(
|
17
|
+
{"ResponseGroup" => "Country", "CountryCode" => code.to_s.upcase},
|
18
|
+
params
|
19
|
+
))
|
20
|
+
end
|
21
|
+
|
22
|
+
# All countries that have Alexa top sites.
|
23
|
+
def list_countries(params = {})
|
24
|
+
dispatch(
|
25
|
+
{"ResponseGroup" => "ListCountries"},
|
26
|
+
params
|
27
|
+
).at("//TopSitesResult/Alexa/TopSites/Countries").elements.map do |node|
|
28
|
+
Country.new(
|
29
|
+
node.at("Name").text,
|
30
|
+
node.at("Code").text,
|
31
|
+
node.at("TotalSites").text.to_i,
|
32
|
+
node.at("PageViews").text.to_f * 1_000_000,
|
33
|
+
node.at("Users").text.to_f * 1_000_000,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def host; "ats.amazonaws.com" end
|
41
|
+
def path; "/" end
|
42
|
+
def default_params; {"Action" => "TopSites"} end
|
43
|
+
|
44
|
+
def top_sites_from_document(document)
|
45
|
+
document.at("//TopSites/Country/Sites").elements.map do |node|
|
46
|
+
Site.new(
|
47
|
+
node.at("DataUrl").text,
|
48
|
+
node.at("Country/Rank").text.to_i,
|
49
|
+
node.at("Country/Reach/PerMillion").text.to_i * 1_000_000,
|
50
|
+
(node.at("Country/PageViews/PerMillion").text.to_f * 1_000_000).to_i,
|
51
|
+
node.at("Country/PageViews/PerUser").text.to_f
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Country = Struct.new(:name, :code, :total_sites, :page_views, :users)
|
57
|
+
Site = Struct.new(:url, :rank, :reach, :page_views, :page_views_per_user)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest/sha2"
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
require "ralexa/canonicalized_query_string"
|
6
|
+
|
7
|
+
module Ralexa
|
8
|
+
class UriSigner
|
9
|
+
|
10
|
+
SIGNATURE_VERSION = 2
|
11
|
+
|
12
|
+
def initialize(access_key_id, secret_access_key)
|
13
|
+
@access_key_id = access_key_id
|
14
|
+
@secret_access_key = secret_access_key
|
15
|
+
end
|
16
|
+
|
17
|
+
# dependency injectors
|
18
|
+
attr_writer :digest
|
19
|
+
attr_writer :hmac_signer
|
20
|
+
attr_writer :base64_encoder
|
21
|
+
attr_writer :time_utc
|
22
|
+
|
23
|
+
def sign_uri(original_uri)
|
24
|
+
original_uri.dup.tap do |uri|
|
25
|
+
uri.query_values = uri.query_values.merge(
|
26
|
+
"AWSAccessKeyId" => @access_key_id,
|
27
|
+
"Timestamp" => timestamp,
|
28
|
+
"SignatureVersion" => SIGNATURE_VERSION.to_s,
|
29
|
+
"SignatureMethod" => signature_method,
|
30
|
+
)
|
31
|
+
uri.query_values = uri.query_values.merge(
|
32
|
+
"Signature" => signature(uri)
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def string_to_sign(uri)
|
40
|
+
[
|
41
|
+
"GET",
|
42
|
+
uri.host.downcase,
|
43
|
+
uri.path,
|
44
|
+
CanonicalizedQueryString.new(uri.query_values)
|
45
|
+
].join("\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
def timestamp
|
49
|
+
time_utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
|
50
|
+
end
|
51
|
+
|
52
|
+
def signature_method
|
53
|
+
"Hmac#{digest.name}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def signature(uri)
|
57
|
+
encode(hmac(string_to_sign(uri)))
|
58
|
+
end
|
59
|
+
|
60
|
+
def hmac(string_to_sign)
|
61
|
+
hmac_signer.digest(digest, @secret_access_key, string_to_sign)
|
62
|
+
end
|
63
|
+
|
64
|
+
def encode(input)
|
65
|
+
base64_encoder.encode64(input).chop
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Injectable dependencies.
|
70
|
+
|
71
|
+
def digest
|
72
|
+
@digest ||= OpenSSL::Digest::SHA256.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def hmac_signer
|
76
|
+
@hmac_signer || OpenSSL::HMAC
|
77
|
+
end
|
78
|
+
|
79
|
+
def base64_encoder
|
80
|
+
@base64_encoder || Base64
|
81
|
+
end
|
82
|
+
|
83
|
+
def time_utc
|
84
|
+
# don't store in ivar.
|
85
|
+
@time_utc || Time.now.utc
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/lib/ralexa.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
%w{
|
2
|
+
abstract_xml_service
|
3
|
+
awis
|
4
|
+
canonicalized_query_string
|
5
|
+
client
|
6
|
+
session
|
7
|
+
top_sites
|
8
|
+
uri_signer
|
9
|
+
version
|
10
|
+
}.each do |file|
|
11
|
+
require "ralexa/#{file}"
|
12
|
+
end
|
13
|
+
|
14
|
+
module Ralexa
|
15
|
+
|
16
|
+
# An authenticated Session instance.
|
17
|
+
# Holds credentials, provides access to service instances.
|
18
|
+
def self.session(access_key_id, secret_access_key)
|
19
|
+
Session.new(access_key_id, secret_access_key)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/ralexa.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/ralexa/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Paul Annesley"]
|
6
|
+
gem.email = ["paul@annesley.cc"]
|
7
|
+
gem.description = %q{Ruby client for Amazon Alexa APIs.}
|
8
|
+
gem.summary = %q{Ruby client for Amazon Alexa APIs such as TopSites.}
|
9
|
+
gem.homepage = "https://github.com/flippa/ralexa"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "ralexa"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Ralexa::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency "addressable"
|
19
|
+
gem.add_runtime_dependency "nokogiri"
|
20
|
+
|
21
|
+
gem.add_development_dependency "minitest"
|
22
|
+
gem.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative "spec_helper"
|
4
|
+
|
5
|
+
require "ralexa/canonicalized_query_string"
|
6
|
+
|
7
|
+
module Ralexa
|
8
|
+
describe CanonicalizedQueryString do
|
9
|
+
|
10
|
+
def q(parameters)
|
11
|
+
CanonicalizedQueryString.new(parameters).to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
it "sorts parameters by key into a query string" do
|
15
|
+
q("a" => "3", "c" => "2", "b" => "1").must_equal "a=3&b=1&c=2"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "does not URL-encode RFC 3986 unreserved characters" do
|
19
|
+
q("AZaz09-_.~" => "AZaz09-_.~").must_equal "AZaz09-_.~=AZaz09-_.~"
|
20
|
+
end
|
21
|
+
|
22
|
+
it "percent-encodes other characters" do
|
23
|
+
q("=&/" => "✈").must_equal "%3D%26%2F=%E2%9C%88"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "encodes space as '%20' not '+'" do
|
27
|
+
q("a b" => "c d").must_equal "a%20b=c%20d"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "handles empty parameters" do
|
31
|
+
q("a" => "", "b" => nil).must_equal "a=&b="
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
require "ralexa/client"
|
4
|
+
|
5
|
+
module Ralexa
|
6
|
+
describe Client do
|
7
|
+
|
8
|
+
let(:client) do
|
9
|
+
Client.new("id", "secret").tap do |c|
|
10
|
+
c.net_http = net_http
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:net_http) { MiniTest::Mock.new }
|
15
|
+
let(:response) { MiniTest::Mock.new }
|
16
|
+
|
17
|
+
it "returns the response body of HTTP GET to a signed URI" do
|
18
|
+
net_http.expect :get_response, response, [ %r{^http://example.org/test\?.*Signature=} ]
|
19
|
+
response.expect :is_a?, true, [ Net::HTTPSuccess ]
|
20
|
+
response.expect :body, "<xml/>"
|
21
|
+
client.get("example.org", "/test", "a" => "b").must_equal "<xml/>"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises an error if the HTTP request failed" do
|
25
|
+
net_http.expect :get_response, response, [ %r{^http://example.org/test\?.*Signature=} ]
|
26
|
+
response.expect :is_a?, false, [ Net::HTTPSuccess ]
|
27
|
+
def response.error!; raise Net::HTTPError.new("", self); end
|
28
|
+
->{ client.get("example.org", "/test", "a" => "b") }.must_raise Net::HTTPError
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<aws:TopSitesResponse xmlns:aws="http://alexa.amazonaws.com/doc/2005-10-05/"><aws:Response xmlns:aws="http://ats.amazonaws.com/doc/2005-11-21"><aws:OperationRequest><aws:RequestId>8a6c3a3f-a3c8-48d8-4775-3914d8f36e88</aws:RequestId></aws:OperationRequest><aws:TopSitesResult><aws:Alexa>
|
3
|
+
|
4
|
+
<aws:TopSites>
|
5
|
+
<aws:Country>
|
6
|
+
<aws:CountryName>GLOBAL</aws:CountryName>
|
7
|
+
<aws:CountryCode>*</aws:CountryCode>
|
8
|
+
<aws:TotalSites>366720</aws:TotalSites>
|
9
|
+
<aws:Sites>
|
10
|
+
<aws:Site>
|
11
|
+
<aws:DataUrl>wikipedia.org</aws:DataUrl>
|
12
|
+
<aws:Country>
|
13
|
+
<aws:Rank>6</aws:Rank>
|
14
|
+
<aws:Reach>
|
15
|
+
<aws:PerMillion>143000</aws:PerMillion>
|
16
|
+
</aws:Reach>
|
17
|
+
<aws:PageViews>
|
18
|
+
<aws:PerMillion>5219</aws:PerMillion>
|
19
|
+
<aws:PerUser>3.88</aws:PerUser>
|
20
|
+
</aws:PageViews>
|
21
|
+
</aws:Country>
|
22
|
+
<aws:Global>
|
23
|
+
<aws:Rank>6</aws:Rank>
|
24
|
+
</aws:Global>
|
25
|
+
</aws:Site>
|
26
|
+
<aws:Site>
|
27
|
+
<aws:DataUrl>bbc.co.uk</aws:DataUrl>
|
28
|
+
<aws:Country>
|
29
|
+
<aws:Rank>52</aws:Rank>
|
30
|
+
<aws:Reach>
|
31
|
+
<aws:PerMillion>20540</aws:PerMillion>
|
32
|
+
</aws:Reach>
|
33
|
+
<aws:PageViews>
|
34
|
+
<aws:PerMillion>898.5</aws:PerMillion>
|
35
|
+
<aws:PerUser>4.65</aws:PerUser>
|
36
|
+
</aws:PageViews>
|
37
|
+
</aws:Country>
|
38
|
+
<aws:Global>
|
39
|
+
<aws:Rank>52</aws:Rank>
|
40
|
+
</aws:Global>
|
41
|
+
</aws:Site>
|
42
|
+
</aws:Sites>
|
43
|
+
</aws:Country>
|
44
|
+
</aws:TopSites>
|
45
|
+
</aws:Alexa></aws:TopSitesResult><aws:ResponseStatus xmlns:aws="http://alexa.amazonaws.com/doc/2005-10-05/"><aws:StatusCode>Success</aws:StatusCode></aws:ResponseStatus></aws:Response></aws:TopSitesResponse>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<aws:TopSitesResponse xmlns:aws="http://alexa.amazonaws.com/doc/2005-10-05/"><aws:Response xmlns:aws="http://ats.amazonaws.com/doc/2005-11-21"><aws:OperationRequest><aws:RequestId>a914be78-f2cf-71be-97dd-6e51501050b2</aws:RequestId></aws:OperationRequest><aws:TopSitesResult><aws:Alexa>
|
3
|
+
|
4
|
+
<aws:TopSites>
|
5
|
+
<aws:Countries>
|
6
|
+
<aws:Country>
|
7
|
+
<aws:Name>Australia</aws:Name>
|
8
|
+
<aws:Code>AU</aws:Code>
|
9
|
+
<aws:TotalSites>42920</aws:TotalSites>
|
10
|
+
<aws:PageViews>1.074</aws:PageViews>
|
11
|
+
<aws:Users>1.148</aws:Users>
|
12
|
+
</aws:Country>
|
13
|
+
<aws:Country>
|
14
|
+
<aws:Name>United States</aws:Name>
|
15
|
+
<aws:Code>US</aws:Code>
|
16
|
+
<aws:TotalSites>484842</aws:TotalSites>
|
17
|
+
<aws:PageViews>18.023</aws:PageViews>
|
18
|
+
<aws:Users>18.998</aws:Users>
|
19
|
+
</aws:Country>
|
20
|
+
</aws:Countries>
|
21
|
+
</aws:TopSites>
|
22
|
+
</aws:Alexa></aws:TopSitesResult><aws:ResponseStatus xmlns:aws="http://alexa.amazonaws.com/doc/2005-10-05/"><aws:StatusCode>Success</aws:StatusCode></aws:ResponseStatus></aws:Response></aws:TopSitesResponse>
|
data/spec/ralexa_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
require "ralexa/session"
|
4
|
+
|
5
|
+
module Ralexa
|
6
|
+
describe Session do
|
7
|
+
|
8
|
+
describe ".top_sites" do
|
9
|
+
it "returns a Ralexa::TopSites instance" do
|
10
|
+
Session.new("id", "secret").top_sites.must_be_kind_of ::Ralexa::TopSites
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
require "ralexa/top_sites"
|
4
|
+
|
5
|
+
module Ralexa
|
6
|
+
describe TopSites do
|
7
|
+
|
8
|
+
let(:top_sites) { TopSites.new(client) }
|
9
|
+
let(:client) { MiniTest::Mock.new }
|
10
|
+
|
11
|
+
describe "#global" do
|
12
|
+
before do
|
13
|
+
expect_parameters = {
|
14
|
+
"Action" => "TopSites",
|
15
|
+
"ResponseGroup" => "Country",
|
16
|
+
}
|
17
|
+
client.expect :get, fixture("global.xml"),
|
18
|
+
["ats.amazonaws.com", "/", expect_parameters]
|
19
|
+
end
|
20
|
+
it "fetches, parses and returns top sites" do
|
21
|
+
top_sites_match_fixture(top_sites.global)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#country" do
|
26
|
+
before do
|
27
|
+
expect_parameters = {
|
28
|
+
"Action" => "TopSites",
|
29
|
+
"ResponseGroup" => "Country",
|
30
|
+
"CountryCode" => "AU",
|
31
|
+
}
|
32
|
+
client.expect :get, fixture("global.xml"),
|
33
|
+
["ats.amazonaws.com", "/", expect_parameters]
|
34
|
+
end
|
35
|
+
it "fetches, parses and returns top sites for specified country" do
|
36
|
+
top_sites_match_fixture(top_sites.country("AU"))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#list_countries" do
|
41
|
+
before do
|
42
|
+
expect_parameters = {
|
43
|
+
"Action" => "TopSites",
|
44
|
+
"ResponseGroup" => "ListCountries",
|
45
|
+
}
|
46
|
+
client.expect :get, fixture("list_countries.xml"),
|
47
|
+
["ats.amazonaws.com", "/", expect_parameters]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "fetches, parses and returns countries" do
|
51
|
+
countries = top_sites.list_countries
|
52
|
+
countries.size.must_equal 2
|
53
|
+
a, u = countries
|
54
|
+
|
55
|
+
u.name.must_equal "United States"
|
56
|
+
u.code.must_equal "US"
|
57
|
+
u.total_sites.must_equal 484842
|
58
|
+
u.page_views.must_equal 18_023_000
|
59
|
+
u.users.must_equal 18_998_000
|
60
|
+
|
61
|
+
a.name.must_equal "Australia"
|
62
|
+
a.code.must_equal "AU"
|
63
|
+
a.total_sites.must_equal 42920
|
64
|
+
a.page_views.must_equal 1_074_000
|
65
|
+
a.users.must_equal 1_148_000
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def top_sites_match_fixture(sites)
|
70
|
+
sites.size.must_equal 2
|
71
|
+
w, b = sites
|
72
|
+
|
73
|
+
w.url.must_equal "wikipedia.org"
|
74
|
+
w.rank.must_equal 6
|
75
|
+
w.reach.must_equal 143_000_000_000
|
76
|
+
w.page_views.must_equal 5_219_000_000
|
77
|
+
w.page_views_per_user.must_equal 3.88
|
78
|
+
|
79
|
+
b.url.must_equal "bbc.co.uk"
|
80
|
+
b.rank.must_equal 52
|
81
|
+
b.reach.must_equal 20_540_000_000
|
82
|
+
b.page_views.must_equal 898_500_000
|
83
|
+
b.page_views_per_user.must_equal 4.65
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
require "addressable/uri"
|
4
|
+
require "ralexa/uri_signer"
|
5
|
+
|
6
|
+
module Ralexa
|
7
|
+
describe UriSigner do
|
8
|
+
|
9
|
+
let(:time_utc) { Time.now.utc }
|
10
|
+
let(:digest) { MiniTest::Mock.new }
|
11
|
+
let(:hmac_signer) { MiniTest::Mock.new }
|
12
|
+
let(:base64_encoder) { MiniTest::Mock.new }
|
13
|
+
|
14
|
+
def signer
|
15
|
+
UriSigner.new("id", "secret").tap do |rs|
|
16
|
+
rs.digest = digest
|
17
|
+
rs.hmac_signer = hmac_signer
|
18
|
+
rs.base64_encoder = base64_encoder
|
19
|
+
rs.time_utc = time_utc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "for http://example.org/path?b=c&a=b" do
|
24
|
+
|
25
|
+
let(:uri) { Addressable::URI.parse("http://example.org/path?b=c&a=b") }
|
26
|
+
let(:signed_uri) { signer.sign_uri(uri) }
|
27
|
+
let(:timestamp) { time_utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") }
|
28
|
+
|
29
|
+
before do
|
30
|
+
ts = URI.escape(timestamp, /:/)
|
31
|
+
combined = [
|
32
|
+
"GET",
|
33
|
+
"example.org",
|
34
|
+
"/path",
|
35
|
+
"AWSAccessKeyId=id&SignatureMethod=HmacFAKE&SignatureVersion=2&Timestamp=#{ts}&a=b&b=c"
|
36
|
+
].join("\n")
|
37
|
+
hmac_signer.expect :digest, "the_hmac", [ digest, "secret", combined ]
|
38
|
+
base64_encoder.expect :encode64, "base64_encoded\n", [ "the_hmac" ]
|
39
|
+
digest.expect :name, "FAKE"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns a signed URL" do
|
43
|
+
signed_uri.host.must_equal "example.org"
|
44
|
+
signed_uri.path.must_equal "/path"
|
45
|
+
signed_uri.query_values.must_equal(
|
46
|
+
"b" => "c",
|
47
|
+
"a" => "b",
|
48
|
+
"AWSAccessKeyId" => "id",
|
49
|
+
"Signature" => "base64_encoded",
|
50
|
+
"SignatureMethod" => "HmacFAKE",
|
51
|
+
"SignatureVersion" => "2",
|
52
|
+
"Timestamp" => timestamp,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "does not modify the passed uri" do
|
57
|
+
signed_uri.wont_be_same_as uri
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ralexa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Annesley
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: addressable
|
16
|
+
requirement: &70290636275640 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70290636275640
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: nokogiri
|
27
|
+
requirement: &70290636275220 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70290636275220
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: minitest
|
38
|
+
requirement: &70290636274800 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70290636274800
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70290636274380 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70290636274380
|
58
|
+
description: Ruby client for Amazon Alexa APIs.
|
59
|
+
email:
|
60
|
+
- paul@annesley.cc
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- Gemfile
|
67
|
+
- LICENSE
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- lib/ralexa.rb
|
71
|
+
- lib/ralexa/abstract_xml_service.rb
|
72
|
+
- lib/ralexa/canonicalized_query_string.rb
|
73
|
+
- lib/ralexa/client.rb
|
74
|
+
- lib/ralexa/session.rb
|
75
|
+
- lib/ralexa/top_sites.rb
|
76
|
+
- lib/ralexa/uri_signer.rb
|
77
|
+
- lib/ralexa/version.rb
|
78
|
+
- ralexa.gemspec
|
79
|
+
- spec/canonicalized_query_string_spec.rb
|
80
|
+
- spec/client_spec.rb
|
81
|
+
- spec/fixtures/global.xml
|
82
|
+
- spec/fixtures/list_countries.xml
|
83
|
+
- spec/ralexa_spec.rb
|
84
|
+
- spec/session_spec.rb
|
85
|
+
- spec/spec_helper.rb
|
86
|
+
- spec/top_sites_spec.rb
|
87
|
+
- spec/uri_signer_spec.rb
|
88
|
+
homepage: https://github.com/flippa/ralexa
|
89
|
+
licenses: []
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ! '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.8.11
|
109
|
+
signing_key:
|
110
|
+
specification_version: 3
|
111
|
+
summary: Ruby client for Amazon Alexa APIs such as TopSites.
|
112
|
+
test_files:
|
113
|
+
- spec/canonicalized_query_string_spec.rb
|
114
|
+
- spec/client_spec.rb
|
115
|
+
- spec/fixtures/global.xml
|
116
|
+
- spec/fixtures/list_countries.xml
|
117
|
+
- spec/ralexa_spec.rb
|
118
|
+
- spec/session_spec.rb
|
119
|
+
- spec/spec_helper.rb
|
120
|
+
- spec/top_sites_spec.rb
|
121
|
+
- spec/uri_signer_spec.rb
|
122
|
+
has_rdoc:
|