sdbtools 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.
Files changed (5) hide show
  1. data/Rakefile +48 -0
  2. data/VERSION +1 -0
  3. data/bin/sdbtool +353 -0
  4. data/lib/sdbtools.rb +348 -0
  5. metadata +80 -0
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sdbtools"
8
+ gem.summary = %Q{A high-level OO interface to Amazon SimpleDB}
9
+ gem.description = <<END
10
+ SDBTools layers a higher-level OO interface on top of RightAWS, as well as
11
+ providing some command-line utilities for working with SimpleDB.
12
+ END
13
+ gem.email = "devs@devver.net"
14
+ gem.homepage = "http://github.com/devver/sdbtools"
15
+ gem.authors = ["Avdi Grimm"]
16
+ gem.add_dependency "right_aws", "~> 1.10"
17
+ gem.add_development_dependency "rspec", ">= 1.2.9"
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
+ require 'rake/rdoctask'
41
+ Rake::RDocTask.new do |rdoc|
42
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
43
+
44
+ rdoc.rdoc_dir = 'rdoc'
45
+ rdoc.title = "sdbtools #{version}"
46
+ rdoc.rdoc_files.include('README*')
47
+ rdoc.rdoc_files.include('lib/**/*.rb')
48
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/sdbtool ADDED
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ gem 'main', '~> 4.2'
4
+ gem 'fattr', '~> 2.1'
5
+ gem 'highline', '~> 1.5'
6
+ gem 'right_aws', '~> 1.10'
7
+ gem 'progressbar', '~> 0.0.3'
8
+ gem 'libxml-ruby', '~> 1.1'
9
+
10
+ require 'main'
11
+ require 'highline/import'
12
+ require 'fattr'
13
+ require 'right_aws'
14
+ require 'progressbar'
15
+ require 'pstore'
16
+ require 'pathname'
17
+ require 'fileutils'
18
+ require 'yaml'
19
+
20
+ require File.expand_path('../lib/sdbtools', File.dirname(__FILE__))
21
+
22
+ at_exit do
23
+ if $!.kind_of?(Exception) && (!$!.kind_of?(SystemExit))
24
+ $stderr.puts <<"END"
25
+ This program has encountered an error and cannot continue. Please see the log
26
+ for details.
27
+ END
28
+ end
29
+ end
30
+
31
+
32
+ Main do
33
+ description 'A tool for working with Amazon SimpleDB'
34
+
35
+ class NullObject
36
+ def methods_missing(*args, &block)
37
+ self
38
+ end
39
+ end
40
+
41
+ def initialize
42
+ HighLine.track_eof = false
43
+ RightAws::RightAWSParser.xml_lib = 'libxml'
44
+ end
45
+
46
+ def timestamp
47
+ Time.now.strftime('%Y%m%d%H%M')
48
+ end
49
+
50
+ option 'access_key' do
51
+ description 'Amazon AWS access key'
52
+ argument :required
53
+ default ENV.fetch('AMAZON_ACCESS_KEY_ID') {
54
+ ENV.fetch('AWS_ACCESS_KEY_ID')
55
+ }
56
+ attr
57
+ end
58
+
59
+ option 'secret_key' do
60
+ description 'Amazon AWS secret key'
61
+ argument :required
62
+ default ENV.fetch('AMAZON_SECRET_ACCESS_KEY') {
63
+ ENV.fetch('AWS_SECRET_ACCESS_KEY')
64
+ }
65
+ attr
66
+ end
67
+
68
+ option 'server' do
69
+ description 'SimpleDB server hostname'
70
+ argument :required
71
+ default 'sdb.amazonaws.com'
72
+ end
73
+
74
+ option 'port' do
75
+ description 'SimpleDB server port number'
76
+ argument :required
77
+ cast :int
78
+ default 443
79
+ end
80
+
81
+ option 'protocol' do
82
+ description 'SimpleDB protocol'
83
+ argument :required
84
+ default 'https'
85
+ end
86
+
87
+ option 'nil-rep' do
88
+ description 'How to represent nil values'
89
+ argument :required
90
+ default '<[<[<NIL>]>]>'
91
+ end
92
+
93
+ option 'progress' do
94
+ description "Show progress bar"
95
+ cast :bool
96
+ default true
97
+ attr
98
+ end
99
+
100
+ option 'verbose' do
101
+ description "Verbose output"
102
+ cast :bool
103
+ default false
104
+ attr
105
+ end
106
+
107
+ option 'chunk-size', 'n' do
108
+ description "The number of items to process at once"
109
+ cast :int
110
+ default 100
111
+ end
112
+
113
+ option 'log' do
114
+ description 'File to log to'
115
+ default 'sdbtool.log'
116
+ attr
117
+ end
118
+
119
+ mixin 'domain' do
120
+ argument 'domain' do
121
+ description 'The SimpleDB domain to work on'
122
+ optional
123
+ end
124
+
125
+ fattr(:domain) {
126
+ params['domain'].value || choose(*db.domains) do |q|
127
+ q.prompt = "Domain to operate on?"
128
+ end
129
+ }
130
+ end
131
+
132
+
133
+ fattr(:db) {
134
+ SimpleDB::Database.new(
135
+ access_key,
136
+ secret_key,
137
+ :server => params['server'].value,
138
+ :port => params['port'].value,
139
+ :logger => logger,
140
+ :nil_representation => params['nil-rep'].value,
141
+ :protocol => params['protocol'].value)
142
+ }
143
+
144
+ def after_parse_parameters
145
+ end
146
+
147
+ def before_run
148
+ stderr 'sdbtool-errors.log'
149
+ logger ::Logger.new(log)
150
+ unless verbose
151
+ logger.level = ::Logger::WARN
152
+ end
153
+ end
154
+
155
+ mode 'dump' do
156
+ description 'Dump the contents of a domain to a file'
157
+
158
+ mixin 'domain'
159
+
160
+ argument 'file' do
161
+ description 'File to dump into'
162
+ optional
163
+ attr
164
+ end
165
+
166
+ def run
167
+ self.file ||= ask("Filename for this dump?") do |q|
168
+ q.default = "#{domain}-#{timestamp}.yaml"
169
+ end
170
+ dump_domain = db.domain(domain)
171
+ logger.info "Preparing to dump domain #{dump_domain.name}"
172
+ dump = db.make_dump(dump_domain, file)
173
+ dump.chunk_size = params['chunk-size'].value
174
+ count = dump.incomplete_count
175
+ logger.info "Dumping #{count} objects to #{file}"
176
+ pbar = progress ? ProgressBar.new("dump", count,stdout) : NullObject.new
177
+ dump.callback = lambda do |item_name|
178
+ logger.info "Dumped #{item_name}"
179
+ pbar.inc
180
+ end
181
+ dump.start!
182
+ pbar.finish
183
+ logger.info "Dump succeeded"
184
+ end
185
+ end
186
+
187
+ mode 'count' do
188
+ description 'Count the number of items in a domain'
189
+ mixin 'domain'
190
+
191
+ def run
192
+ domain = db.domain(params['domain'].value)
193
+ puts domain.count
194
+ end
195
+ end
196
+
197
+ mode 'load' do
198
+ description 'Load a domain from a file'
199
+
200
+ mixin 'domain'
201
+
202
+ argument 'file' do
203
+ description 'The file to load from'
204
+ attr
205
+ end
206
+
207
+ def run
208
+ target_domain = if db.domain_exists?(domain)
209
+ db.domain(domain)
210
+ else
211
+ logger.info "Creating domain #{domain}"
212
+ db.create_domain(domain)
213
+ end
214
+ say "About to load domain #{target_domain.name} from #{file}"
215
+ return unless agree("Are you sure you want to continue?")
216
+ load = db.make_load(target_domain, file)
217
+ load.chunk_size = params['chunk-size'].value
218
+ count = load.incomplete_count
219
+ pbar = progress ? ProgressBar.new("load", count, stdout) : NullObject.new
220
+ load.callback = lambda do |item_name|
221
+ logger.info "Loaded #{item_name}"
222
+ pbar.inc
223
+ end
224
+ load.start!
225
+ pbar.finish
226
+ logger.info "Finished loading #{file} into #{domain}"
227
+ end
228
+
229
+ end
230
+
231
+ mode 'status' do
232
+ description 'Show the status of an operation'
233
+
234
+ argument 'title' do
235
+ description 'The title of the operation'
236
+ optional
237
+ attr
238
+ end
239
+
240
+ option 'failed' do
241
+ description 'Show details about failed items'
242
+ default false
243
+ cast :bool
244
+ end
245
+
246
+ def run
247
+ self.title ||= choose(*Dir['*.simpledb_op_status'])
248
+ status_file = if title =~ /\.simpledb_op_status$/
249
+ title
250
+ else
251
+ title + '.simpledb_op_status'
252
+ end
253
+ task = SimpleDB::Task.new(status_file, logger)
254
+ if params['failed'].given?
255
+ task.failed_items.each do |item_name, info|
256
+ puts item_name + ": "
257
+ puts
258
+ puts info
259
+ puts "-" * 60
260
+ end
261
+ else
262
+ puts task.report
263
+ end
264
+ end
265
+ end
266
+
267
+ mode 'info' do
268
+ description 'Show information about a dump file'
269
+
270
+ argument 'file' do
271
+ description 'The dump file to examine'
272
+ attr
273
+ end
274
+
275
+ def run
276
+ dump_file = SimpleDB::DumpFile.new(file)
277
+ puts "File #{file} contains #{dump_file.size} records"
278
+ end
279
+ end
280
+
281
+ mode 'delete-domain' do
282
+ description "Delete a SimpleDB domain"
283
+ mixin 'domain'
284
+
285
+ def run
286
+ 2.times do
287
+ return unless agree("Are you sure you want to delete #{params['domain'].value}?") # "
288
+ end
289
+ db.delete_domain(params['domain'].value)
290
+ end
291
+ end
292
+
293
+ mode 'create-domain' do
294
+ description "Create a SimpleDB domain"
295
+
296
+ mixin 'domain'
297
+
298
+ def run
299
+ db.create_domain(params['domain'].value)
300
+ end
301
+ end
302
+
303
+ mode 'show' do
304
+ description "Show a single SimpleDB item"
305
+ mixin 'domain'
306
+
307
+ argument 'item_name' do
308
+ description "The name of the item to show"
309
+ arity -1 # inifinite
310
+ end
311
+
312
+ def run
313
+ db.domain(domain).items(params['item_name'].values).each do |item|
314
+ puts item.to_yaml
315
+ end
316
+ end
317
+ end
318
+
319
+ mode 'list' do
320
+ mixin 'domain'
321
+ description "List all objects matching SELECT query"
322
+
323
+ argument 'query' do
324
+ description "A SimpleDB SELECT where-clause"
325
+ end
326
+
327
+ def run
328
+ logger.info "Selecting items where '#{params['query'].value}'"
329
+ items = db.domain(domain).select(params['query'].value)
330
+ logger.info "Selected #{items.count} items"
331
+ items.each do |item|
332
+ puts item['itemName()']
333
+ end
334
+ end
335
+ end
336
+
337
+ mode 'remove' do
338
+ mixin 'domain'
339
+ description "Delete an item by name"
340
+
341
+ argument 'item_name' do
342
+ description "The name of the item to delete"
343
+ end
344
+
345
+ def run
346
+ db.domain(domain).delete(params['item_name'].value)
347
+ end
348
+ end
349
+
350
+ def run
351
+ help!
352
+ end
353
+ end
data/lib/sdbtools.rb ADDED
@@ -0,0 +1,348 @@
1
+ require 'fattr'
2
+ require 'right_aws'
3
+
4
+ module SimpleDB
5
+ class Operation
6
+ include Enumerable
7
+
8
+ def initialize(sdb, method, *args)
9
+ @sdb = sdb
10
+ @method = method
11
+ @args = args
12
+ end
13
+
14
+ def each
15
+ next_token = nil
16
+ begin
17
+ args = @args.dup
18
+ args << next_token
19
+ results = @sdb.send(@method, *args)
20
+ yield(results)
21
+ next_token = results[:next_token]
22
+ end while next_token
23
+ end
24
+
25
+ end
26
+
27
+ class Database
28
+ def initialize(access_key=nil, secret_key=nil, options={})
29
+ @sdb = RightAws::SdbInterface.new(access_key, secret_key, options)
30
+ @logger = options.fetch(:logger){::Logger.new($stderr)}
31
+ end
32
+
33
+ def domains
34
+ domains_op = Operation.new(@sdb, :list_domains, nil)
35
+ domains_op.inject([]) {|domains, results|
36
+ domains.concat(results[:domains])
37
+ domains
38
+ }
39
+ end
40
+
41
+ def domain(domain_name)
42
+ Domain.new(@sdb, domain_name)
43
+ end
44
+
45
+ def domain_exists?(domain_name)
46
+ domains.include?(domain_name)
47
+ end
48
+
49
+ def create_domain(domain_name)
50
+ @sdb.create_domain(domain_name)
51
+ domain(domain_name)
52
+ end
53
+
54
+ def delete_domain(domain_name)
55
+ @sdb.delete_domain(domain_name)
56
+ end
57
+
58
+ def make_dump(domain, filename)
59
+ Dump.new(domain, filename, @logger)
60
+ end
61
+
62
+ def make_load(domain, filename)
63
+ Load.new(domain, filename, @logger)
64
+ end
65
+
66
+ private
67
+ end
68
+
69
+ class Domain
70
+ attr_reader :name
71
+
72
+ def initialize(sdb, name)
73
+ @sdb = sdb
74
+ @name = name
75
+ @item_names = nil
76
+ @count = nil
77
+ end
78
+
79
+ def [](item_name)
80
+ @sdb.get_attributes(name, item_name)[:attributes]
81
+ end
82
+
83
+ def item_names
84
+ return @item_names if @item_names
85
+ query = Operation.new(@sdb, :query, @name, nil, nil)
86
+ @item_names = query.inject([]) {|names, results|
87
+ names.concat(results[:items])
88
+ names
89
+ }
90
+ end
91
+
92
+ def count
93
+ return @count if @count
94
+ op = Operation.new(@sdb, :select, "select count(*) from #{name}")
95
+ @count = op.inject(0) {|count, results|
96
+ count += results[:items].first["Domain"]["Count"].first.to_i
97
+ count
98
+ }
99
+ end
100
+
101
+ def items(item_names)
102
+ names = item_names.map{|n| "'#{n}'"}.join(', ')
103
+ query = "select * from #{name} where itemName() in (#{names})"
104
+ select = Operation.new(@sdb, :select, query)
105
+ select.inject({}) {|items, results|
106
+ results[:items].each do |item|
107
+ item_name = item.keys.first
108
+ item_value = item.values.first
109
+ items[item_name] = item_value
110
+ end
111
+ items
112
+ }
113
+ end
114
+
115
+ def put(item_name, attributes)
116
+ @sdb.put_attributes(@name, item_name, attributes)
117
+ end
118
+
119
+ def select(query)
120
+ op = Operation.new(@sdb, :select, "select * from #{name} where #{query}")
121
+ op.inject([]){|items,results|
122
+ batch_items = results[:items].map{|pair|
123
+ item = pair.values.first
124
+ item.merge!({'itemName()' => pair.keys.first})
125
+ item
126
+ }
127
+ items.concat(batch_items)
128
+ }
129
+ end
130
+
131
+ def delete(item_name)
132
+ @sdb.delete_attributes(@name, item_name)
133
+ end
134
+ end
135
+
136
+ class Task
137
+ fattr :callback => lambda{}
138
+ fattr :chunk_size => 100
139
+
140
+ def initialize(status_file, logger)
141
+ @logger = logger
142
+ @status_file = Pathname(status_file)
143
+ @status = PStore.new(@status_file.to_s)
144
+ unless @status_file.exist?
145
+ initialize_status!(yield)
146
+ end
147
+ @status.transaction(false) do
148
+ @logger.info "Initializing #{@status_file} for PID #{$$}"
149
+ @status[$$] = {}
150
+ @status[$$][:working_items] = []
151
+ end
152
+ end
153
+
154
+ def session
155
+ yield
156
+ ensure
157
+ release_working_items!
158
+ end
159
+
160
+ def incomplete_count
161
+ @status.transaction(true) {|status|
162
+ Array(status[:incomplete_items]).size
163
+ }
164
+ end
165
+
166
+ def failed_items
167
+ @status.transaction(true) do |status|
168
+ status[:failed_items]
169
+ end
170
+ end
171
+
172
+ def reserve_items!(size)
173
+ @status.transaction(false) do |status|
174
+ chunk = status[:incomplete_items].slice!(0,size)
175
+ status[$$][:working_items].concat(chunk)
176
+ @logger.info("Reserved #{chunk.size} items")
177
+ chunk
178
+ end
179
+ end
180
+
181
+ def finish_items!(items)
182
+ @status.transaction(false) do |status|
183
+ items.each do |item_name|
184
+ status[:complete_items] <<
185
+ status[$$][:working_items].delete(item_name)
186
+ @logger.info("Marked item #{item_name} complete")
187
+ end
188
+ end
189
+ end
190
+
191
+ def release_working_items!
192
+ @logger.info("Releasing working items")
193
+ @status.transaction(false) do |status|
194
+ items = status[$$][:working_items]
195
+ status[:incomplete_items].concat(items)
196
+ status[$$][:working_items].clear
197
+ end
198
+ end
199
+
200
+ def report
201
+ @status.transaction(true) do |status|
202
+ done = status[:complete_items].size
203
+ not_done = status[:incomplete_items].size
204
+ failed = status[:failed_items].size
205
+ puts "Items (not done/done/failed): #{not_done}/#{done}/#{failed}"
206
+ status.roots.select{|root| root.kind_of?(Integer)}.each do |root|
207
+ pid = root
208
+ items = status[root][:working_items].size
209
+ puts "Process #{pid} working on #{items} items"
210
+ end
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def record_failed_item!(item_name, info)
217
+ @logger.info "Problem with item #{item_name}"
218
+ @status.transaction(false) do |status|
219
+ status[:failed_items] ||= {}
220
+ status[:failed_items][item_name] = info
221
+ end
222
+ end
223
+
224
+ def initialize_status!(item_names)
225
+ @status.transaction(false) do |status|
226
+ status[:incomplete_items] = item_names
227
+ status[:failed_items] = {}
228
+ status[:complete_items] = []
229
+ end
230
+ end
231
+ end
232
+
233
+ class Dump < Task
234
+ def initialize(domain, filename, logger)
235
+ @domain = domain
236
+ @dump_filename = Pathname(filename)
237
+ super(status_filename, logger) {
238
+ @domain.item_names
239
+ }
240
+ end
241
+
242
+ def status_filename
243
+ Pathname(
244
+ @dump_filename.basename(@dump_filename.extname).to_s +
245
+ ".simpledb_op_status")
246
+ end
247
+
248
+ def start!
249
+ session do
250
+ until (chunk = reserve_items!(chunk_size)).empty?
251
+ items = @domain.items(chunk)
252
+ dump_items(items)
253
+ finish_items!(chunk)
254
+ items.each do |item| callback.call(item) end
255
+ end
256
+ end
257
+ end
258
+
259
+ private
260
+
261
+ def dump_items(items)
262
+ @logger.info "Dumping #{items.size} items to #{@dump_filename}"
263
+ FileUtils.touch(@dump_filename) unless @dump_filename.exist?
264
+ file = File.new(@dump_filename.to_s)
265
+ file.flock(File::LOCK_EX)
266
+ @dump_filename.open('a+') do |f|
267
+ items.each_pair do |item_name, attributes|
268
+ @logger.info "Dumping item #{item_name}"
269
+ YAML.dump({item_name => attributes}, f)
270
+ end
271
+ end
272
+ ensure
273
+ file.flock(File::LOCK_UN)
274
+ end
275
+
276
+ end
277
+
278
+ class Load < Task
279
+ def initialize(domain, filename, logger)
280
+ @domain = domain
281
+ @dump_filename = Pathname(filename)
282
+ @dump_file = DumpFile.new(filename)
283
+ super(status_filename, logger) {
284
+ @dump_file.item_names
285
+ }
286
+ end
287
+
288
+ def status_filename
289
+ Pathname(
290
+ @dump_filename.basename(@dump_filename.extname).to_s +
291
+ "-load-#{@domain.name}.simpledb_op_status")
292
+ end
293
+
294
+ def start!
295
+ session do
296
+ chunk = []
297
+ reserved = Set.new
298
+ @dump_file.each do |item_name, attributes|
299
+ if reserved.empty?
300
+ finish_items!(chunk)
301
+ reserved.replace(chunk = reserve_items!(chunk_size))
302
+ break if chunk.empty?
303
+ end
304
+ if reserved.include?(item_name)
305
+ @logger.info("#{item_name} is reserved, loading to #{@domain.name}")
306
+ begin
307
+ @domain.put(item_name, attributes)
308
+ rescue
309
+ record_failed_item!(
310
+ item_name,
311
+ "#{$!.class.name}: #{$!.message}")
312
+ end
313
+ reserved.delete(item_name)
314
+ callback.call(item_name)
315
+ end
316
+ end
317
+ finish_items!(chunk)
318
+ end
319
+ end
320
+ end
321
+
322
+ class DumpFile
323
+ include Enumerable
324
+
325
+ def initialize(path)
326
+ @path = Pathname(path)
327
+ end
328
+
329
+ def item_names
330
+ map{|item_name, item| item_name}
331
+ end
332
+
333
+ def size
334
+ inject(0){|size, item_name, item|
335
+ size += 1
336
+ }
337
+ end
338
+
339
+ def each
340
+ @path.open('r') {|f|
341
+ YAML.load_documents(f) {|doc|
342
+ yield doc.keys.first, doc.values.first
343
+ }
344
+ }
345
+ end
346
+ end
347
+
348
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sdbtools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Avdi Grimm
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-18 00:00:00 -05:00
13
+ default_executable: sdbtool
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: right_aws
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: "1.10"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.9
34
+ version:
35
+ description: |
36
+ SDBTools layers a higher-level OO interface on top of RightAWS, as well as
37
+ providing some command-line utilities for working with SimpleDB.
38
+
39
+ email: devs@devver.net
40
+ executables:
41
+ - sdbtool
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - Rakefile
48
+ - VERSION
49
+ - bin/sdbtool
50
+ - lib/sdbtools.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/devver/sdbtools
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --charset=UTF-8
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A high-level OO interface to Amazon SimpleDB
79
+ test_files: []
80
+