amazon_sdb 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +11 -0
- data/Manifest.txt +15 -0
- data/README.txt +84 -0
- data/Rakefile +23 -0
- data/lib/amazon_sdb.rb +39 -0
- data/lib/amazon_sdb/base.rb +156 -0
- data/lib/amazon_sdb/domain.rb +145 -0
- data/lib/amazon_sdb/exceptions.rb +18 -0
- data/lib/amazon_sdb/item.rb +51 -0
- data/lib/amazon_sdb/multimap.rb +267 -0
- data/lib/amazon_sdb/resultset.rb +45 -0
- data/test/test_amazon_base.rb +135 -0
- data/test/test_amazon_domain.rb +208 -0
- data/test/test_multimap.rb +177 -0
- data/test/test_sdb_harness.rb +124 -0
- metadata +90 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
lib/amazon_sdb.rb
|
6
|
+
lib/amazon_sdb/base.rb
|
7
|
+
lib/amazon_sdb/domain.rb
|
8
|
+
lib/amazon_sdb/item.rb
|
9
|
+
lib/amazon_sdb/multimap.rb
|
10
|
+
lib/amazon_sdb/resultset.rb
|
11
|
+
lib/amazon_sdb/exceptions.rb
|
12
|
+
test/test_amazon_base.rb
|
13
|
+
test/test_amazon_domain.rb
|
14
|
+
test/test_sdb_harness.rb
|
15
|
+
test/test_multimap.rb
|
data/README.txt
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
amazon_sdb
|
2
|
+
by Jacob Harris
|
3
|
+
jharris@nytimes.com
|
4
|
+
http://open.nytimes.com/
|
5
|
+
|
6
|
+
== SOURCE CODE:
|
7
|
+
|
8
|
+
http://code.nytimes.com/svn/ruby/gems/amazon_sdb
|
9
|
+
|
10
|
+
== DESCRIPTION:
|
11
|
+
|
12
|
+
Amazon sdb is a Ruby wrapper to Amazon's new Simple Database Service. Amazon sdb is a different type of database:
|
13
|
+
|
14
|
+
* Accessed over the network via RESTful calls
|
15
|
+
* No schemas or types
|
16
|
+
* Each sdb account can have up to 100 domains for data.
|
17
|
+
* Domains can hold objects referenced by unique keys
|
18
|
+
* Each object can hold up to 256 name/value attributes.
|
19
|
+
* Only name/value pairs must be unique in an objects attributes, there can be multiple name/value attributes with the same name.
|
20
|
+
* In addition to key-driven accessors, objects can also be searched with a basic query language.
|
21
|
+
* All value are stored as strings and comparisons use lexical order. Thus, it is necessary to pad integers and floats with 0s and save dates in ISO 8601 format for query comparisons to work
|
22
|
+
|
23
|
+
== FEATURES:
|
24
|
+
|
25
|
+
* A basic interface to Amazon sdb
|
26
|
+
* Includes a class for representing attribute sets in sdb
|
27
|
+
* Automatic conversion to/from sdb representations for integers and dates (for floats, it's suggested you use the Multimap#numeric function)
|
28
|
+
* The beginnings of mock-based tests for methods derived from Amazon's docs
|
29
|
+
|
30
|
+
== CAVEATS
|
31
|
+
|
32
|
+
* Not all features are tested yet (sorry!)
|
33
|
+
* Amazon has not actually opened up access to the 2007-11-07 API on which this gem is based (and my tests are for). Some things may work differently in real life.
|
34
|
+
* Errors still need to be figured out / tested.
|
35
|
+
* I don't process the data usage/costs info from Amazon yet.
|
36
|
+
|
37
|
+
== FUTURE WORK:
|
38
|
+
|
39
|
+
* Some sort of fake SQL-esque query language
|
40
|
+
* Some sort of AR/Datamapper/Ambition connection layer fun (with schema overlays)
|
41
|
+
|
42
|
+
== SYNOPSIS:
|
43
|
+
|
44
|
+
b = Amazon::sdb::Base.new(aws_public_key, aws_secret_key)
|
45
|
+
b.domains #=> list of domain
|
46
|
+
domain = b.create_domain 'my domain'
|
47
|
+
|
48
|
+
m = Multimap.new {:first_name => "Jacob", :last_name => "Harris"}
|
49
|
+
domain.put_attributes "Jacob Harris", m
|
50
|
+
resultset = domain.query "['first_name' begins-with 'Harris']"
|
51
|
+
|
52
|
+
== REQUIREMENTS:
|
53
|
+
|
54
|
+
* An Amazon Web Services account
|
55
|
+
* hpricot
|
56
|
+
|
57
|
+
== INSTALL:
|
58
|
+
|
59
|
+
* sudo gem install amazon_sdb
|
60
|
+
|
61
|
+
== LICENSE:
|
62
|
+
|
63
|
+
(The MIT License)
|
64
|
+
|
65
|
+
Copyright (c) 2007 FIX
|
66
|
+
|
67
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
68
|
+
a copy of this software and associated documentation files (the
|
69
|
+
'Software'), to deal in the Software without restriction, including
|
70
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
71
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
72
|
+
permit persons to whom the Software is furnished to do so, subject to
|
73
|
+
the following conditions:
|
74
|
+
|
75
|
+
The above copyright notice and this permission notice shall be
|
76
|
+
included in all copies or substantial portions of the Software.
|
77
|
+
|
78
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
79
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
80
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
81
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
82
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
83
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
84
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
require './lib/amazon_sdb.rb'
|
6
|
+
require 'rcov/rcovtask'
|
7
|
+
Hoe.new('amazon_sdb', Amazon::SDB::VERSION) do |p|
|
8
|
+
p.rubyforge_name = 'nytimes'
|
9
|
+
p.author = 'Jacob Harris'
|
10
|
+
p.email = 'harrisj@nytimes.com'
|
11
|
+
p.summary = 'A ruby wrapper to Amazon\'s sdb service'
|
12
|
+
p.description = 'A ruby wrapper to Amazon\'s sdb service'
|
13
|
+
p.url = "http://nytimes.rubyforge.org/amazon_sdb"
|
14
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
15
|
+
p.extra_deps << ['hpricot', '>= 0.6']
|
16
|
+
end
|
17
|
+
|
18
|
+
Rcov::RcovTask.new do |t|
|
19
|
+
t.test_files = FileList['test/test*.rb']
|
20
|
+
t.rcov_opts << "-Ilib:test"
|
21
|
+
t.verbose = true # uncomment to see the executed command
|
22
|
+
end
|
23
|
+
# vim: syntax=Ruby
|
data/lib/amazon_sdb.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "net/http"
|
3
|
+
require "uri"
|
4
|
+
require "cgi"
|
5
|
+
require "digest/md5"
|
6
|
+
require "digest/sha1"
|
7
|
+
|
8
|
+
# HMAC Digest doesn't require TLS/SSL, but we do need the OpenSSL::HMAC class
|
9
|
+
require "openssl"
|
10
|
+
require 'base64'
|
11
|
+
require 'open-uri'
|
12
|
+
require 'hpricot'
|
13
|
+
|
14
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
15
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
16
|
+
|
17
|
+
unless defined?(Amazon::SDS)
|
18
|
+
begin
|
19
|
+
$:.unshift(File.dirname(__FILE__) + "/../../amazon_sdb/lib")
|
20
|
+
require 'amazon_sdb'
|
21
|
+
rescue LoadError
|
22
|
+
require 'rubygems'
|
23
|
+
gem 'amazon_sdb'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'amazon_sdb/multimap'
|
28
|
+
require 'amazon_sdb/base'
|
29
|
+
require 'amazon_sdb/domain'
|
30
|
+
require 'amazon_sdb/item'
|
31
|
+
require 'amazon_sdb/resultset'
|
32
|
+
require 'amazon_sdb/exceptions'
|
33
|
+
|
34
|
+
module Amazon
|
35
|
+
module SDB
|
36
|
+
VERSION = '0.5.5'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Amazon
|
2
|
+
module SDB
|
3
|
+
SIGNATURE_VERSION = '1'
|
4
|
+
API_VERSION = '2007-11-07'
|
5
|
+
|
6
|
+
##
|
7
|
+
# The Amazon::SDS::Base class is the top-level interface class for your SDS interactions. It allows you to set global
|
8
|
+
# configuration settings, to manage Domain objects. If you are working within a particular domain you can also just use
|
9
|
+
# the domain initializer directly for that domain.
|
10
|
+
class Base
|
11
|
+
##
|
12
|
+
# The base is initialized with 2 parameters from your Amazon Web Services account:
|
13
|
+
# * +aws_access_key+ - Your AWS Access Key
|
14
|
+
# * +aws_secret_key+ - Your AWS Secret Key
|
15
|
+
def initialize(aws_access_key, aws_secret_key)
|
16
|
+
@access_key = aws_access_key
|
17
|
+
@secret_key = aws_secret_key
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# Since all SDS supports only lexical comparisons, it's necessary to pad numbers with extra digits when saving them to SDS.
|
22
|
+
# Under lexical matching, 23 > 123. But if we pad it sufficiently 000000023 < 000000123. By default, this is set to the
|
23
|
+
# ungodly large value of 32 digits, but you can adjust it lower if this is too much. On reading from SDS, such numbers are
|
24
|
+
# auto-coerced back, so it's probably not necessary to change.
|
25
|
+
def self.number_padding
|
26
|
+
return @@number_padding
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Change the number padding
|
31
|
+
def self.number_padding=(num)
|
32
|
+
@@number_padding = num
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.float_precision
|
36
|
+
return @@float_precision
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.float_precision=(num)
|
40
|
+
return @@float_precision
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Retrieves a list of domains in your SDS database. Each entry is a Domain object.
|
45
|
+
def domains
|
46
|
+
domains = []
|
47
|
+
nextToken = nil
|
48
|
+
base_options = {:Action => 'ListDomains'}
|
49
|
+
continue = true
|
50
|
+
|
51
|
+
while continue
|
52
|
+
options = base_options.dup
|
53
|
+
options[:NextToken] = nextToken unless nextToken.nil?
|
54
|
+
|
55
|
+
sdb_query(options) do |h|
|
56
|
+
h.search('//DomainName').each {|e| domains << Domain.new(@access_key, @secret_key, e.innerText)}
|
57
|
+
mt = h.at('//NextToken')
|
58
|
+
if mt
|
59
|
+
nextToken = mt.innerText
|
60
|
+
else
|
61
|
+
continue = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
domains
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Returns a domain object for SDS. Assumes the domain already exists, so errors might occur if you didn't create it.
|
71
|
+
def domain(name)
|
72
|
+
Domain.new(@access_key, @secret_key, name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def raise_errors(hpricot)
|
76
|
+
errnode = hpricot.at('//Errors/Error')
|
77
|
+
return unless errnode
|
78
|
+
|
79
|
+
msg = errnode.at('Message').innerText
|
80
|
+
case errnode.at('Code').innerText
|
81
|
+
when 'InvalidParameterValue'
|
82
|
+
raise InvalidParameterError, msg
|
83
|
+
when 'NumberDomainsExceeded'
|
84
|
+
raise DomainLimitError, msg
|
85
|
+
else
|
86
|
+
raise UnknownError, msg
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def create_domain(name)
|
91
|
+
sdb_query({:Action => 'CreateDomain', 'DomainName' => name}) do |h|
|
92
|
+
domain(name)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Deletes a domain. This operation is currently not supported by SDS.
|
98
|
+
def delete_domain!(name)
|
99
|
+
sdb_query({:Action => 'DeleteDomain', 'DomainName' => name})
|
100
|
+
end
|
101
|
+
|
102
|
+
def timestamp
|
103
|
+
Time.now.iso8601
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.hmac(key, msg)
|
107
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), key, msg))
|
108
|
+
end
|
109
|
+
|
110
|
+
def cgi_encode(options)
|
111
|
+
options.map do |k, v|
|
112
|
+
case v
|
113
|
+
when Array
|
114
|
+
v.map{|i| Base.uriencode(k)+'='+Base.uriencode(i)}.join('&')
|
115
|
+
else
|
116
|
+
Base.uriencode(k)+'='+Base.uriencode(v)
|
117
|
+
end
|
118
|
+
end.join('&')
|
119
|
+
end
|
120
|
+
|
121
|
+
def sdb_query(options = {})
|
122
|
+
options.merge!({'AWSAccessKeyId' => @access_key,
|
123
|
+
'SignatureVersion' => SIGNATURE_VERSION,
|
124
|
+
'Timestamp' => timestamp,
|
125
|
+
'Version' => API_VERSION })
|
126
|
+
options['Signature'] = Base.sign(@secret_key, options)
|
127
|
+
|
128
|
+
# send to S3
|
129
|
+
url = BASE_PATH + '?' + cgi_encode(options)
|
130
|
+
|
131
|
+
# puts "Requesting #{url}" #if $DEBUG
|
132
|
+
open(url) do |f|
|
133
|
+
h = Hpricot.XML(f)
|
134
|
+
|
135
|
+
raise_errors h
|
136
|
+
yield h if block_given?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.uriencode(str)
|
141
|
+
CGI.escape str.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.sign(key, query_options)
|
145
|
+
option_array = query_options.to_a.map {|pair| [pair[0].to_s, pair[1].to_s]}.sort {|a, b| a[0].downcase <=> b[0].downcase }
|
146
|
+
return hmac(key, option_array.map {|pair| pair[0]+pair[1]}.join('')).chop
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
@@number_padding = 32
|
151
|
+
@@float_precision = 8
|
152
|
+
|
153
|
+
BASE_PATH = 'http://sdb.amazonaws.com/'
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Amazon
|
2
|
+
module SDB
|
3
|
+
|
4
|
+
##
|
5
|
+
# Each sdb account can have up to 100 domains. This class represents a single domain and may be instantiated either indirectly
|
6
|
+
# from the Amazon::sdb::Base class or via the Domain#initialize method. This class is what you can use to directly set attributes
|
7
|
+
# on domains. Be aware that the following limits apply:
|
8
|
+
# - 100 attributes per each call
|
9
|
+
# - 256 total attribute name-value pairs per item
|
10
|
+
# - 250 million attributes per domain
|
11
|
+
# - 10 GB of total user data storage per domain
|
12
|
+
class Domain < Base
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
##
|
16
|
+
# Creates a new Domain object.
|
17
|
+
def initialize(aws_access_key, aws_secret_key, name)
|
18
|
+
super(aws_access_key, aws_secret_key)
|
19
|
+
@name = name
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Sets attributes for a given key in the domain. If there are no attributes supplied, it creates an empty set.
|
24
|
+
# Takes the following arguments:
|
25
|
+
# - key - a string key for the attribute set
|
26
|
+
# - multimap - an collection of attributes for the set in a Multimap object. If nothing, creates an empty set.
|
27
|
+
# - options - for put options
|
28
|
+
def put_attributes(key, multimap=nil, options = {})
|
29
|
+
req_options = {'Action' => 'PutAttributes', 'DomainName' => name, 'ItemName' => key}
|
30
|
+
|
31
|
+
unless multimap.nil?
|
32
|
+
req_options.merge! case multimap
|
33
|
+
when Hash, Array
|
34
|
+
Multimap.new(multimap).to_sdb(options)
|
35
|
+
when Multimap
|
36
|
+
multimap.to_sdb(options)
|
37
|
+
else
|
38
|
+
raise ArgumentError, "The second argument must be a multimap, hash, or array"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
sdb_query(req_options) do |h|
|
43
|
+
# check for success?
|
44
|
+
if h.search('//Success').any?
|
45
|
+
return Item.new(self, key, multimap)
|
46
|
+
else
|
47
|
+
# error?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Gets the attribute list for a key. Arguments:
|
54
|
+
# - <tt>key</tt> - the key for the attribute set
|
55
|
+
def get_attributes(key, *attr_list)
|
56
|
+
options = {'Action' => 'GetAttributes', 'DomainName' => name, 'ItemName' => key}
|
57
|
+
|
58
|
+
unless attr_list.nil? or attr_list.empty?
|
59
|
+
options["AttributeName"] = attr_list.map {|x| x.to_s }
|
60
|
+
end
|
61
|
+
|
62
|
+
sdb_query(options) do |h|
|
63
|
+
attr_nodes = h.search('//GetAttributesResult/Attribute')
|
64
|
+
attr_array = []
|
65
|
+
attr_nodes.each do |a|
|
66
|
+
attr_array << [a.at('Name').innerText, a.at('Value').innerText]
|
67
|
+
end
|
68
|
+
|
69
|
+
if attr_array.any?
|
70
|
+
return Item.new(self, key, Multimap.new(attr_array))
|
71
|
+
else
|
72
|
+
raise RecordNotFoundError, "No record was found for key=#{key}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Not implemented yet.
|
79
|
+
def delete_attributes(key, multimap=nil)
|
80
|
+
options = {'Action' => 'DeleteAttributes', 'DomainName' => name, 'ItemName' => key}
|
81
|
+
|
82
|
+
unless multimap.nil?
|
83
|
+
case multimap
|
84
|
+
when String, Symbol
|
85
|
+
options.merge! "Attribute.0.Name" => multimap.to_s
|
86
|
+
when Array
|
87
|
+
multimap.each_with_index do |k, i|
|
88
|
+
options["Attribute.#{i}.Name"] = k
|
89
|
+
end
|
90
|
+
when Hash
|
91
|
+
options.merge! Multimap.new(multimap).to_sdb
|
92
|
+
when Multimap
|
93
|
+
options.merge! multimap.to_sdb
|
94
|
+
else
|
95
|
+
raise ArgumentError, "Bad input paramter for attributes"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
sdb_query(options) do |h|
|
100
|
+
if h.search('//Success').any?
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Returns a list of matching items that match a filter
|
108
|
+
# Options include:
|
109
|
+
# - <tt>max_results</tt> = the max items to return for a listing (top/default is 100)
|
110
|
+
# - <tt>:more_token</tt> = to retrieve a second or more page of results, the more token should be provided
|
111
|
+
# - <tt>:load_attrs</tt> = this query normally returns just a list of names, the attributes have to be retrieved separately. To load the attributes for matching results automatically, set to true (normally false)
|
112
|
+
def query(query_options = {})
|
113
|
+
req_options = {'Action' => 'Query', 'DomainName' => name}
|
114
|
+
|
115
|
+
unless query_options[:expr].nil?
|
116
|
+
req_options['QueryExpression'] = query_options[:expr]
|
117
|
+
end
|
118
|
+
|
119
|
+
if query_options[:next_token]
|
120
|
+
req_options['NextToken'] = query_options[:next_token]
|
121
|
+
end
|
122
|
+
|
123
|
+
if query_options[:max_results]
|
124
|
+
req_options['MaxNumberOfItems'] = query_options[:max_results]
|
125
|
+
end
|
126
|
+
|
127
|
+
sdb_query(req_options) do |h|
|
128
|
+
more_token = nil
|
129
|
+
results = h.search('//QueryResponse/QueryResult/ItemName')
|
130
|
+
|
131
|
+
items = results.map {|n| Item.new(self, n.innerText) }
|
132
|
+
|
133
|
+
if query_options[:load_attrs]
|
134
|
+
items.each {|i| i.reload! }
|
135
|
+
end
|
136
|
+
|
137
|
+
mt = h.search('//NextToken')
|
138
|
+
more_token = mt.inner_text unless mt.nil?
|
139
|
+
|
140
|
+
return ResultSet.new(self, items, more_token)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|