simplydb 0.0.0

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/.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