ralexa 0.0.1
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/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:
|