sdbtools 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+