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 ADDED
@@ -0,0 +1,11 @@
1
+ == 0.5.5 / 2007-12-15
2
+
3
+ * 2 major enhancements
4
+ * Updated for new Amazon API release
5
+ * Thorough tests
6
+
7
+ == 0.5.0 / 2007-11-05
8
+
9
+ * 1 major enhancement
10
+ * Initial release
11
+
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