simplydb 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 JT Archie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,37 @@
1
+ = simplydb
2
+
3
+ A minimal interface to Amazon SimpleDB that has separation of interfaces. From the low level HTTP request access to high level Ruby abstraction ORM.
4
+
5
+ require 'rubygems'
6
+ require 'simplydb'
7
+
8
+ interface = SimplyDB::Interface.new({
9
+ :access_key => ENV['AWS_ACCESS_KEY'],
10
+ :secret_key => ENV['AWS_SECRET_KEY']
11
+ })
12
+
13
+ if interface.create_domain("MyDomain")
14
+ interface.put_attributes('MyDomain', 'Item123', {'color'=>['red','brick','garnet']})
15
+
16
+ attributes = interface.get_attributes('MyDomain', 'Item123')
17
+ puts "Item123 = #{attributes.inspect}"
18
+
19
+ items = interface.select("select color from MyDomain where color = 'brick'")
20
+ puts "Items = #{items.inspect}"
21
+
22
+ interface.delete_domain("MyDomain")
23
+ end
24
+
25
+ == Note on Patches/Pull Requests
26
+
27
+ * Fork the project.
28
+ * Make your feature addition or bug fix.
29
+ * Add tests for it. This is important so I don't break it in a
30
+ future version unintentionally.
31
+ * Commit, do not mess with rakefile, version, or history.
32
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
33
+ * Send me a pull request. Bonus points for topic branches.
34
+
35
+ == Copyright
36
+
37
+ Copyright (c) 2010 JT Archie. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "simplydb"
8
+ gem.summary = %Q{A minimal interface to Amazon SimpleDB.}
9
+ gem.description = %Q{A minimal interface to Amazon SimpleDB that has separation of interfaces. From the low level HTTP request access to high level Ruby abstraction ORM.}
10
+ gem.email = "jtarchie@gmail.com"
11
+ gem.homepage = "http://github.com/jtarchie/simplydb"
12
+ gem.authors = ["JT Archie"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_dependency "typhoeus", ">= 0.1.27"
15
+ gem.add_dependency "nokogiri", ">= 1.4.2"
16
+ gem.add_dependency "uuidtools", ">= 2.1.1"
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.rcov = true
34
+ end
35
+
36
+ task :spec => :check_dependencies
37
+
38
+ task :default => :spec
39
+
40
+ desc "irb loaded with SimplyDB (rubygems not required)"
41
+ task :irb do
42
+ exec("irb -I lib/ -r 'simplydb' -r 'simplydb/record/base'")
43
+ end
44
+ task :console => :irb
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "simplydb #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,19 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'simplydb'
3
+
4
+ interface = SimplyDB::Interface.new({
5
+ :access_key => ENV['AWS_ACCESS_KEY'],
6
+ :secret_key => ENV['AWS_SECRET_KEY']
7
+ })
8
+
9
+ if interface.create_domain("MyDomain")
10
+ interface.put_attributes('MyDomain', 'Item123', {'color'=>['red','brick','garnet']})
11
+
12
+ attributes = interface.get_attributes('MyDomain', 'Item123')
13
+ puts "Item123 = #{attributes.inspect}"
14
+
15
+ items = interface.select("select color from MyDomain where color = 'brick'")
16
+ puts "Items = #{items.inspect}"
17
+
18
+ interface.delete_domain("MyDomain")
19
+ end
@@ -0,0 +1,38 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'simplydb'
3
+ require 'simplydb/record/base'
4
+
5
+ SimplyDB::Record::Base.establish_connection({
6
+ 'access_key' => ENV['AWS_ACCESS_KEY'],
7
+ 'secret_key' => ENV['AWS_SECRET_KEY']
8
+ })
9
+
10
+ class MyDomain < SimplyDB::Record::Base; end
11
+
12
+ MyDomain.create_domain
13
+
14
+ puts "domain name: #{MyDomain.domain_name}"
15
+ puts "domain exists?: #{MyDomain.domain_exists?}"
16
+
17
+ model = MyDomain.new(
18
+ :name => 'JT Archie',
19
+ :age => 27
20
+ )
21
+ puts "attribues: #{model.attributes.inspect}"
22
+ model[:age] = 29
23
+ puts "attribues: #{model.attributes.inspect}"
24
+
25
+ puts "model.new_record?: #{model.new_record?}"
26
+
27
+ model.item_name = "Testing"
28
+ puts "save: #{model.save}"
29
+
30
+ sleep 2 #let SimpleDB propogate data (not supporting consistant read, yet)
31
+
32
+ got_model = MyDomain.find('Testing')
33
+ puts "got_model: #{got_model.attributes.inspect}"
34
+
35
+ models = MyDomain.find_by_select('SELECT * FROM my_domain')
36
+ puts "models: #{models.inspect}"
37
+
38
+ #MyDomain.delete_domain
@@ -0,0 +1,66 @@
1
+ require 'openssl'
2
+ require 'digest/sha1'
3
+ require 'base64'
4
+ require 'uri'
5
+ require 'simplydb/clients/typhoeus'
6
+
7
+ module SimplyDB
8
+ class Client
9
+ include SimplyDB::Extensions
10
+
11
+ attr_accessor :options, :http_client
12
+
13
+ def initialize(options = {})
14
+ @options = {
15
+ :async => false,
16
+ :protocol => 'https://',
17
+ :host => 'sdb.amazonaws.com',
18
+ :port => 443,
19
+ :path => "/",
20
+ :signature_version => '2',
21
+ :version => '2009-04-15',
22
+ :signature_method => 'HmacSHA256',
23
+ }.merge(options)
24
+ @http_client = (@options[:client] || SimplyDB::Clients::Typhoeus).new(options)
25
+ end
26
+
27
+ def async?
28
+ return options[:async] == true
29
+ end
30
+
31
+ def base_url
32
+ "#{@options[:protocol]}#{@options[:host]}:#{@options[:port]}#{@options[:path]}"
33
+ end
34
+
35
+ def string_to_sign(method, params)
36
+ return "#{method.to_s.upcase}\n#{options[:host]}\n/\n" + escape_hash(params)
37
+ end
38
+
39
+ def generate_signature(method, params)
40
+ Base64.encode64(
41
+ OpenSSL::HMAC.digest(
42
+ OpenSSL::Digest::Digest.new('sha256'),
43
+ @options[:secret_key],
44
+ string_to_sign(method, params)
45
+ )
46
+ ).chomp
47
+ end
48
+
49
+ def params_with_signature(method, params)
50
+ params.merge!({
51
+ 'AWSAccessKeyId' => @options[:access_key],
52
+ 'SignatureVersion' => @options[:signature_version],
53
+ 'Timestamp' => Time.now.iso8601,
54
+ 'Version' => @options[:version],
55
+ 'SignatureMethod' => @options[:signature_method]
56
+ })
57
+ params['Signature'] = generate_signature(method, params)
58
+ return params
59
+ end
60
+
61
+ def call(method, params ={}, &on_complete)
62
+ options = {:method => method, :url => base_url, :params => params_with_signature(method, params)}
63
+ @http_client.request(options, !async?) {|request| on_complete.call(request.body)}
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,35 @@
1
+ require 'typhoeus'
2
+
3
+ module SimplyDB
4
+ module Clients
5
+ class Typhoeus
6
+
7
+ attr_accessor :hydra, :options
8
+
9
+ def initialize(options = {})
10
+ @options = options
11
+ @hydra = ::Typhoeus::Hydra.new
12
+ end
13
+
14
+ def request(options={}, force = true, &block)
15
+ request = ::Typhoeus::Request.new(options[:url],
16
+ :method => options[:method],
17
+ :params => options[:params]
18
+ )
19
+
20
+ @hydra.queue(request)
21
+
22
+ unless force
23
+ request.on_complete {|response| block.call(response)}
24
+ else
25
+ @hydra.run
26
+ block.call(request.response)
27
+ end
28
+ end
29
+
30
+ def run!
31
+ @hydra.run
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ module SimplyDB
2
+ module Error
3
+ # ref http://docs.amazonwebservices.com/AmazonSimpleDB/latest/DeveloperGuide/APIError.html
4
+ class AccessFailure < RuntimeError; end
5
+ class AttributeDoesNotExist < RuntimeError; end
6
+ class AuthFailure < RuntimeError; end
7
+ class AuthMissingFailure < RuntimeError; end
8
+ class ConditionalCheckFailed < RuntimeError; end
9
+ class ExistsAndExpectedValue < RuntimeError; end
10
+ class FeatureDeprecated < RuntimeError; end
11
+ class IncompleteExpectedExpression < RuntimeError; end
12
+ class InternalError < RuntimeError; end
13
+ class InvalidAction < RuntimeError; end
14
+ class InvalidHTTPAuthHeader < RuntimeError; end
15
+ class InvalidHttpRequest < RuntimeError; end
16
+ class InvalidLiteral < RuntimeError; end
17
+ class InvalidNextToken < RuntimeError; end
18
+ class InvalidNumberPredicates < RuntimeError; end
19
+ class InvalidNumberValueTests < RuntimeError; end
20
+ class InvalidParameterCombination < RuntimeError; end
21
+ class InvalidParameterValue < RuntimeError; end
22
+ class InvalidQueryExpression < RuntimeError; end
23
+ class InvalidResponseGroups < RuntimeError; end
24
+ class InvalidService < RuntimeError; end
25
+ class InvalidSOAPRequest < RuntimeError; end
26
+ class InvalidSortExpression < RuntimeError; end
27
+ class InvalidURI < RuntimeError; end
28
+ class InvalidWSAddressingProperty < RuntimeError; end
29
+ class InvalidWSDLVersion < RuntimeError; end
30
+ class MalformedSOAPSignature < RuntimeError; end
31
+ class MissingAction < RuntimeError; end
32
+ class MissingParameter < RuntimeError; end
33
+ class MissingWSAddressingProperty < RuntimeError; end
34
+ class MultipleExistsConditions < RuntimeError; end
35
+ class MultipleExpectedNames < RuntimeError; end
36
+ class MultipleExpectedValues < RuntimeError; end
37
+ class MultiValuedAttribute < RuntimeError; end
38
+ class NoSuchDomain < RuntimeError; end
39
+ class NoSuchVersion < RuntimeError; end
40
+ class NotYetImplemented < RuntimeError; end
41
+ class NumberDomainsExceeded < RuntimeError; end
42
+ class NumberDomainAttributesExceeded < RuntimeError; end
43
+ class NumberDomainBytesExceeded < RuntimeError; end
44
+ class NumberItemAttributesExceeded < RuntimeError; end
45
+ class NumberSubmittedAttributesExceeded < RuntimeError; end
46
+ class NumberSubmittedItemsExceeded < RuntimeError; end
47
+ class RequestExpired < RuntimeError; end
48
+ class RequestTimeout < RuntimeError; end
49
+ class ServiceUnavailable < RuntimeError; end
50
+ class TooManyRequestedAttributes < RuntimeError; end
51
+ class UnsupportedHttpVerb < RuntimeError; end
52
+ class UnsupportedNextToken < RuntimeError; end
53
+ class URITooLong < RuntimeError; end
54
+
55
+ #Standard AWS errors
56
+ class SignatureDoesNotMatch < RuntimeError; end
57
+ class InvalidClientTokenId < RuntimeError; end
58
+ class InvalidRequest < RuntimeError; end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ module SimplyDB
2
+ module Extensions
3
+ def underscore(camel_cased_word)
4
+ camel_cased_word.to_s.gsub(/::/, '/').
5
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
6
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
7
+ tr("-", "_").
8
+ downcase
9
+ end
10
+
11
+ def escape_value(string)
12
+ string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
13
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
14
+ end.gsub(' ', '%20')
15
+ end
16
+
17
+ def escape_hash(params = {})
18
+ return params.collect{|k,v| [k.to_s, v.to_s]}.sort.collect { |key, value| [escape_value(key), escape_value(value)].join('=') }.join('&')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,207 @@
1
+ require 'nokogiri'
2
+
3
+ module SimplyDB
4
+ class Interface
5
+ attr_accessor :options, :client, :request_id, :box_usage, :next_token, :on_error
6
+ def initialize(options = {})
7
+ @options = {}.merge(options)
8
+ @client = SimplyDB::Client.new(options)
9
+ end
10
+
11
+ def create_domain(name, &block)
12
+ call({'Action' => 'CreateDomain', 'DomainName' => name}) do |doc|
13
+ ret_val = doc.css("CreateDomainResponse").length > 0
14
+ block.call(ret_val) if block_given?
15
+ ret_val
16
+ end
17
+ end
18
+
19
+ def delete_domain(name, &block)
20
+ call({'Action' => 'DeleteDomain', 'DomainName' => name}) do |doc|
21
+ ret_val = doc.css("DeleteDomainResponse").length > 0
22
+ block.call(ret_val) if block_given?
23
+ ret_val
24
+ end
25
+ end
26
+
27
+ def list_domains(&block)
28
+ call({'Action' => 'ListDomains'}) do |doc|
29
+ domains = doc.css('DomainName').collect{|d| d.text}
30
+ block.call(domains) if block_given?
31
+ domains
32
+ end
33
+ end
34
+
35
+ def domain_metadata(name, &block)
36
+ call({'Action' => 'DomainMetadata','DomainName' => name}) do |doc|
37
+ attributes = doc.css("DomainMetadataResult").first.children.inject({}) do |memo, child|
38
+ memo[child.name] = child.text
39
+ memo
40
+ end
41
+ block.call(attributes) if block_given?
42
+ attributes
43
+ end
44
+ end
45
+
46
+ def put_attributes(name, id, attributes = {}, expected = {}, replace = false, &block)
47
+ params = define_attributes(attributes, expected, replace)
48
+ params.merge!({
49
+ 'DomainName' => name,
50
+ 'ItemName' => id,
51
+ 'Action' => 'PutAttributes'
52
+ })
53
+ call(params) do |doc|
54
+ ret_val = doc.css("PutAttributesResponse").length > 0
55
+ block.call(ret_val) if block_given?
56
+ ret_val
57
+ end
58
+ end
59
+
60
+ def batch_put_attributes(name, items = {}, &block)
61
+ params = {'DomainName' => name, 'ActionName' => 'BatchPutAttributes'}
62
+ items.keys.each_with_index do |key, i|
63
+ params["Item.#{i}.ItemName"] = key
64
+ items[key].inject(0) do |j, (name,value)|
65
+ value = [value] unless value.is_a?(Array)
66
+ value.each do |v|
67
+ params["Item.#{i}.Attribute.#{j}.Name"] = name
68
+ params["Item.#{i}.Attribute.#{j}.Value"] = value
69
+ j+=1
70
+ end
71
+ j
72
+ end
73
+ end
74
+ call(params) do |doc|
75
+ ret_val = doc.css("BatchPutAttributesResponse").length > 0
76
+ block.call(ret_val) if block_given?
77
+ ret_val
78
+ end
79
+ end
80
+
81
+ def delete_attributes(name, id, attributes = {}, expected = {}, &block)
82
+ params = define_attributes(attributes, expected)
83
+ params.merge!({
84
+ 'DomainName' => name,
85
+ 'ItemName' => id,
86
+ 'Action' => 'DeleteAttributes'
87
+ })
88
+ call(params) do |doc|
89
+ ret_val = doc.css("DeleteAttributesResponse").length > 0
90
+ block.call(ret_val) if block_given?
91
+ ret_val
92
+ end
93
+ end
94
+
95
+ def get_attributes(name, id, wanted_attributes = [], consistent_read = false, &block)
96
+ params = {
97
+ 'Action' => 'GetAttributes',
98
+ 'DomainName' => name,
99
+ 'ItemName' => id,
100
+ 'ConsistentRead' => consistent_read.to_s,
101
+ }
102
+ wanted_attributes.each_with_index {|name, index| params["AttributeName.#{index}"] = name}
103
+
104
+ call(params) do |doc|
105
+ attributes = doc.css("Attribute").inject({}) do |memo, attribute|
106
+ name = attribute.css("Name").first.text
107
+ value = attribute.css("Value").first.text
108
+ if memo.has_key?(name)
109
+ memo[name] = [memo[name]] unless memo[name].is_a?(Array)
110
+ memo[name] << value
111
+ else
112
+ memo[name] = value
113
+ end
114
+ memo
115
+ end
116
+ block.call(attributes) if block_given?
117
+ attributes
118
+ end
119
+ end
120
+
121
+ def select(expression, consistent_read = false, next_token = nil, &block)
122
+ params = {
123
+ 'Action' => 'Select',
124
+ 'SelectExpression' => expression,
125
+ 'ConsistentRead' => consistent_read.to_s,
126
+ }
127
+ params['NextToken'] = next_token unless next_token.nil?
128
+ call(params) do |doc|
129
+ ret_val = doc.css("SelectResponse SelectResult Item").inject({}) do |items, item|
130
+ item_name = item.css("Name").first.text
131
+ items[item_name] = item.css("Attribute").inject({}) do |attributes, attribute|
132
+ attribute_name = attribute.css("Name").first.text
133
+ attribute_value = attribute.css("Value").first.text
134
+ if attributes.has_key?(attribute_name)
135
+ attributes[attribute_name] = [attributes[attribute_name]] unless attributes[attribute_name].is_a?(Array)
136
+ attributes[attribute_name] << attribute_value
137
+ else
138
+ attributes[attribute_name] = attribute_value
139
+ end
140
+ attributes
141
+ end
142
+ items
143
+ end
144
+ block.call(ret_val) if block_given?
145
+ ret_val
146
+ end
147
+ end
148
+
149
+ private
150
+ def define_attributes(attributes = {}, expected = {}, replace = false)
151
+ params = {}
152
+ attributes.sort.inject(0) do |index, (k,v)|
153
+ v = [v] unless v.is_a?(Array)
154
+ v.each do |value|
155
+ params["Attribute.#{index}.Name"] = k
156
+ params["Attribute.#{index}.Value"] = value
157
+ params["Attribute.#{index}.Replace"] = "1" if replace
158
+ index += 1
159
+ end
160
+ index
161
+ end
162
+ expected.sort.inject(0) {|index, (k,v)|
163
+ case v
164
+ when Array
165
+ v.each do |value|
166
+ params["Expected.#{index}.Name"] = k
167
+ params["Expected.#{index}.Value"] = value
168
+ index += 1
169
+ end
170
+ when :exists
171
+ params["Expected.#{index}.Name"] = k
172
+ params["Expected.#{index}.Exists"] = v
173
+ index += 1
174
+ else
175
+ params["Expected.#{index}.Name"] = k
176
+ params["Expected.#{index}.Value"] = v
177
+ index += 1
178
+ end
179
+ index
180
+ }
181
+ return params
182
+ end
183
+
184
+ def call(params = {}, attempts = 3, &block)
185
+ @client.call(:post, params) do |body|
186
+ begin
187
+ doc = Nokogiri::XML(body)
188
+ if error = doc.css("Response Errors Error").first
189
+ raise SimplyDB::Error.const_get(error.css("Code").first.content), error.css("Message").first.content
190
+ else
191
+ #gather some stats from the request
192
+ @request_id = doc.css("RequestId").first.text
193
+ @box_usage = doc.css("BoxUsage").first.text.to_f
194
+ @next_token = doc.css("NextToken").first.text unless doc.css("NextToken").empty?
195
+ block.call(doc)
196
+ end
197
+ rescue SimplyDB::Error::ServiceUnavailable => e
198
+ if attempts > 0
199
+ call(params, attempts - 1, &block)
200
+ else
201
+ raise
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end