dyna 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 468dd97763a9c340c77d46800af940b47bc6ac22
4
+ data.tar.gz: 6f446670b9ece4b36d7056dd0c2e3d61f380ebdd
5
+ SHA512:
6
+ metadata.gz: 011fd23c5040bb08d12e090669231e1db2de7ec899993456ad603e26c44080f345bad620ef86ad38a63569d9575b0301eacd5ac955a759760cb2787a5fd850ed
7
+ data.tar.gz: e336f511705841d336aabd8c3599a447bc3323aba06f300caa78f41d853eeaa93e83849d0d80968cc43f61ef30ba4f3e6a8e7dd39b2e4596047e4352af48e6f4
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dyna.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 shinya-watanabe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Dyna
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dyna`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'dyna'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dyna
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dyna.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
6
+
data/bin/dyna ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("#{File.dirname __FILE__}/../lib")
3
+ require 'rubygems'
4
+ require 'dyna'
5
+ require 'optparse'
6
+ require 'pry-byebug'
7
+
8
+ Version = Dyna::VERSION
9
+
10
+ mode = nil
11
+ file = 'Dynafile'
12
+ output_file = '-'
13
+ split = false
14
+ MAGIC_COMMENT = <<-EOS
15
+ # -*- mode: ruby -*-
16
+ # vi: set ft=ruby :
17
+ EOS
18
+
19
+ options = {
20
+ :dry_run => false,
21
+ :format => :ruby,
22
+ :color => true,
23
+ :debug => false,
24
+ }
25
+
26
+ ARGV.options do |opt|
27
+ begin
28
+ access_key = nil
29
+ secret_key = nil
30
+ region = nil
31
+ profile_name = nil
32
+ credentials_path = nil
33
+ format_passed = false
34
+ role_arn = nil
35
+ serial_number = nil
36
+ token_code = nil
37
+ role_session_name = nil
38
+
39
+ opt.on('-p', '--profile PROFILE_NAME') {|v| profile_name = v }
40
+ opt.on('', '--role_arn ROLE_ARN') {|v| role_arn = v }
41
+ opt.on('', '--serial_number SERIAL_NUMBER') {|v| serial_number = v }
42
+ opt.on('', '--token_code TOKEN_CODE') {|v| token_code = v }
43
+ opt.on('', '--role_session_name ROLE_SESSION_NAME') {|v| role_session_name = v }
44
+ opt.on('' , '--credentials-path PATH') {|v| credentials_path = v }
45
+ opt.on('-k', '--access-key ACCESS_KEY') {|v| access_key = v }
46
+ opt.on('-s', '--secret-key SECRET_KEY') {|v| secret_key = v }
47
+ opt.on('-r', '--region REGION') {|v| region = v }
48
+ opt.on('-a', '--apply') {|v| mode = :apply }
49
+ opt.on('-f', '--file FILE') {|v| file = v }
50
+ opt.on('', '--table_names TABLE_NAMES', Array) {|v| options[:table_names] = v }
51
+ opt.on('', '--exclude_table_names TABLE_NAMES', Array) {|v| options[:exclude_table_names] = v }
52
+ opt.on('', '--dry-run') {|v| options[:dry_run] = true }
53
+ opt.on('-e', '--export') {|v| mode = :export}
54
+ opt.on('-o', '--output FILE') {|v| output_file = v }
55
+ opt.on('', '--split') {|v| split = true }
56
+ opt.on('', '--split-more') {|v| split = :more }
57
+ opt.on('' , '--no-color') { options[:color] = false }
58
+ opt.on('' , '--debug') { options[:debug] = true }
59
+ opt.parse!
60
+
61
+ aws_opts = {}
62
+ if access_key and secret_key
63
+ aws_opts = {
64
+ :access_key_id => access_key,
65
+ :secret_access_key => secret_key,
66
+ }
67
+ elsif profile_name or credentials_path
68
+ credentials_opts = {}
69
+ credentials_opts[:profile_name] = profile_name if profile_name
70
+ credentials_opts[:path] = credentials_path if credentials_path
71
+ provider = AWS::Core::CredentialProviders::SharedCredentialFileProvider.new(credentials_opts)
72
+ aws_opts[:credential_provider] = provider
73
+ elsif role_arn and serial_number
74
+ credentials = Aws::AssumeRoleCredentials.new(
75
+ client: Aws::STS::Client.new,
76
+ role_arn: role_arn,
77
+ role_session_name: role_session_name,
78
+ serial_number: serial_number,
79
+ token_code: token_code,
80
+ )
81
+ aws_opts[:credentials] = credentials
82
+ elsif (access_key and !secret_key) or (!access_key and secret_key) or mode.nil?
83
+ puts opt.help
84
+ exit 1
85
+ end
86
+
87
+ aws_opts[:region] = region if region
88
+ Aws.config.update(aws_opts)
89
+
90
+ # Remap groups to exclude to regular expressions (if they're surrounded by '/')
91
+ if options[:exclude_table_names]
92
+ options[:exclude_table_names].map! do |name|
93
+ name =~ /\A\/(.*)\/\z/ ? Regexp.new($1) : Regexp.new("\A#{Regexp.escape(name)}\z")
94
+ end
95
+ end
96
+ rescue => e
97
+ $stderr.puts("[ERROR] #{e.message}")
98
+ exit 1
99
+ end
100
+ end
101
+
102
+ String.colorize = options[:color]
103
+
104
+ if options[:debug]
105
+ Aws.config.update({
106
+ :http_wire_trace => true,
107
+ :logger => Dyna::Logger.instance,
108
+ })
109
+ end
110
+
111
+ begin
112
+ logger = Dyna::Logger.instance
113
+ logger.set_debug(options[:debug])
114
+ client = Dyna::Client.new(options)
115
+
116
+ case mode
117
+ when :export
118
+ if split
119
+ logger.info('Export Table')
120
+
121
+ output_file = 'Dynafile' if output_file == '-'
122
+ requires = []
123
+ base_dir = File.dirname(output_file)
124
+
125
+ client.export(options) do |exported, converter|
126
+ write_table_file = proc do |table_file, tables|
127
+ requires << table_file
128
+
129
+ logger.info(" write `#{table_file}`")
130
+ FileUtils.mkdir_p(File.dirname(table_file))
131
+
132
+ open(table_file, 'wb') do |f|
133
+ f.puts MAGIC_COMMENT
134
+ f.puts converter.call(tables)
135
+ end
136
+ end
137
+
138
+ exported.each do |table_name, tables|
139
+ table_file = File.join(base_dir, "#{Aws::DynamoDB::Client.new.config.region}/#{table_name}.dyna")
140
+ write_table_file.call(table_file, table_name => tables)
141
+ end
142
+ end
143
+
144
+ logger.info(" write `#{output_file}`")
145
+ open(output_file, 'wb') do |f|
146
+ f.puts MAGIC_COMMENT
147
+
148
+ requires.each do |table_file|
149
+ table_file.sub!(%r|\A#{Regexp.escape base_dir}/?|, '')
150
+ f.puts "require '#{table_file}'"
151
+ end
152
+ end
153
+ else
154
+ exported = client.export(options)
155
+
156
+ if output_file == '-'
157
+ logger.info('# Export Table') if options[:format] == :ruby
158
+ puts exported
159
+ else
160
+ logger.info("Export Table to `#{output_file}`")
161
+
162
+ open(output_file, 'wb') do |f|
163
+ f.puts MAGIC_COMMENT if options[:format] == :ruby
164
+ f.puts exported
165
+ end
166
+ end
167
+ end
168
+ when :apply
169
+ unless File.exist?(file)
170
+ raise "No Dynafile found (looking for: #{file})"
171
+ end
172
+
173
+ msg = "Apply `#{file}` to DynamoDB"
174
+ msg << ' (dry-run)' if options[:dry_run]
175
+ logger.info(msg)
176
+
177
+ updated = client.apply(file)
178
+
179
+ logger.info('No change'.intense_blue) unless updated
180
+ else
181
+ raise 'must not happen'
182
+ end
183
+ rescue => e
184
+ if options[:debug]
185
+ raise e
186
+ else
187
+ $stderr.puts("[ERROR] #{e.message}".red)
188
+ $stderr.puts("#{e.backtrace.join("\n")}".red)
189
+ exit 1
190
+ end
191
+ end
data/dyna.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dyna/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dyna"
8
+ spec.version = Dyna::VERSION
9
+ spec.authors = ["wata"]
10
+ spec.email = ["wata.gm@gmail.com"]
11
+
12
+ spec.summary = %q{Codenize DynamoDB table}
13
+ spec.description = %q{Manager DynamoDB table by DSL}
14
+ spec.homepage = 'https://github.com/wata-gh/dyna'
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "bin"
21
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+ spec.add_dependency 'aws-sdk', '~> 2'
24
+ spec.add_dependency 'term-ansicolor', '~> 1.4'
25
+ spec.add_dependency 'diffy', '~> 3.1'
26
+ spec.add_dependency 'hashie', '~> 3.4'
27
+
28
+ spec.add_development_dependency 'bundler', '~> 1.13'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.0'
31
+ spec.add_development_dependency 'pry-byebug', '~> 3.4'
32
+ end
@@ -0,0 +1,89 @@
1
+ module Dyna
2
+ class Client
3
+ include Logger::ClientHelper
4
+ include Filterable
5
+
6
+ def initialize(options = {})
7
+ @options = OpenStruct.new(options)
8
+ @options_hash = options
9
+ @options.ddb = Aws::DynamoDB::Client.new
10
+ end
11
+
12
+ def apply(file)
13
+ walk(file)
14
+ end
15
+
16
+ def export(options = {})
17
+ exported = Exporter.export(@options.ddb, @options)
18
+
19
+ converter = proc do |src|
20
+ DSL.convert(@options.ddb.config.region, src)
21
+ end
22
+
23
+ if block_given?
24
+ yield(exported, converter)
25
+ else
26
+ converter.call(exported)
27
+ end
28
+ end
29
+
30
+ private
31
+ def load_file(file)
32
+ if file.kind_of?(String)
33
+ open(file) do |f|
34
+ parse(f.read, file)
35
+ end
36
+ elsif file.respond_to?(:read)
37
+ parse(file.read, file.path)
38
+ else
39
+ raise TypeError, "can't load #{file}"
40
+ end
41
+ end
42
+
43
+ def parse(src, path)
44
+ DSL.define(src, path).result
45
+ end
46
+
47
+ def walk(file)
48
+ dsl = load_file(file)
49
+ dsl_ddbs = dsl.ddbs
50
+ ddb_wrapper = DynamoDBWrapper.new(@options.ddb, @options)
51
+
52
+ dsl_ddbs.each do |region, ddb_dsl|
53
+ walk_ddb(ddb_dsl, ddb_wrapper) if @options.ddb.config.region == region
54
+ end
55
+
56
+ ddb_wrapper.updated?
57
+ end
58
+
59
+ def walk_ddb(ddb_dsl, ddb_wrapper)
60
+ table_list_dsl = ddb_dsl.tables.group_by(&:table_name).each_with_object({}) do |(k, v), h|
61
+ h[k] = v.first unless should_skip(k)
62
+ end
63
+ table_list_aws = ddb_wrapper.tables.group_by(&:table_name).each_with_object({}) do |(k, v), h|
64
+ h[k] = v.first unless should_skip(k)
65
+ end
66
+
67
+ table_list_dsl.each do |name, table_dsl|
68
+ unless table_list_aws[name]
69
+ result = ddb_wrapper.create(table_dsl)
70
+ if result
71
+ table_list_aws[name] = DynamoDBWrapper::Table.new(
72
+ @options.ddb,
73
+ result.table_description,
74
+ @options,
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ table_list_dsl.each do |name, table_dsl|
81
+ table_aws = table_list_aws.delete(name)
82
+ next unless table_aws # only dry-run and should be created
83
+ table_aws.update(table_dsl) unless table_aws.eql?(table_dsl)
84
+ end
85
+
86
+ table_list_aws.values.each(&:delete)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,87 @@
1
+ module Dyna
2
+ class DSL
3
+ class Converter
4
+ class << self
5
+ def convert(region, exported)
6
+ self.new(region, exported).convert
7
+ end
8
+ end
9
+
10
+ def initialize(region, exported)
11
+ @region = region
12
+ @exported = exported
13
+ end
14
+
15
+ def convert
16
+ output_dynamo_db
17
+ end
18
+
19
+ private
20
+ def output_dynamo_db
21
+ tables = @exported.map {|name, table|
22
+ output_table(name, table)
23
+ }.join("\n").strip
24
+
25
+ <<-EOS
26
+ dynamo_db "#{@region}" do
27
+ #{tables}
28
+ end
29
+ EOS
30
+ end
31
+
32
+ def output_table(name, table)
33
+ local_secondary_indexes = ''
34
+ global_secondary_indexes = ''
35
+ if table[:local_secondary_indexes]
36
+ local_secondary_indexes_tmpl = <<-EOS.chomp
37
+ <% table[:local_secondary_indexes].each do |index| %>
38
+ local_secondary_index <%= index[:index_name].inspect %> do
39
+ key_schema hash: <%= index[:key_schema][0][:attribute_name].inspect %>, range: <%= index[:key_schema][1][:attribute_name].inspect %><% if index[:projection] %>
40
+ projection projection_type: <%= index[:projection][:projection_type].inspect %><% end %>
41
+ end
42
+ <% end %>
43
+ EOS
44
+ local_secondary_indexes = ERB.new(local_secondary_indexes_tmpl).result(binding)
45
+ end
46
+
47
+ if table[:global_secondary_indexes]
48
+ p table[:global_secondary_indexes]
49
+ global_secondary_indexes_tmpl = <<-EOS.chomp
50
+ <% table[:global_secondary_indexes].each do |index| %>
51
+ global_secondary_index <%= index[:index_name].inspect %> do
52
+ key_schema hash: <%= index[:key_schema][0][:attribute_name].inspect %><% if index[:key_schema].size == 2 %>, range: <%= index[:key_schema][1][:attribute_name].inspect %><% end %><% if index[:projection] %>
53
+ projection projection_type: <%= index[:projection][:projection_type].inspect %><% end %>
54
+ provisioned_throughput read_capacity_units: <%= index[:provisioned_throughput][:read_capacity_units] %>, write_capacity_units: <%= index[:provisioned_throughput][:read_capacity_units] %>
55
+ end
56
+ <% end %>
57
+ EOS
58
+ global_secondary_indexes = ERB.new(global_secondary_indexes_tmpl).result(binding)
59
+ end
60
+
61
+ attribute_definitions_tmpl = <<-EOS.chomp
62
+ <% table[:attribute_definitions].each do |attr| %>
63
+ attribute_definition(
64
+ attribute_name: <%= attr[:attribute_name].inspect %>,
65
+ attribute_type: <%= attr[:attribute_type].inspect %>,
66
+ )
67
+ <% end %>
68
+ EOS
69
+ attribute_definitions = ERB.new(attribute_definitions_tmpl).result(binding)
70
+ <<-EOS
71
+ table "#{name}" do
72
+ key_schema(
73
+ hash: #{table[:key_schema][0][:attribute_name].inspect},
74
+ range: #{table[:key_schema][1][:attribute_name].inspect},
75
+ )
76
+ #{attribute_definitions}
77
+ provisioned_throughput(
78
+ read_capacity_units: #{table[:provisioned_throughput][:read_capacity_units]},
79
+ write_capacity_units: #{table[:provisioned_throughput][:write_capacity_units]},
80
+ )
81
+ #{local_secondary_indexes}#{global_secondary_indexes}
82
+ end
83
+ EOS
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,31 @@
1
+ module Dyna
2
+ class DSL
3
+ class DynamoDB
4
+ include Dyna::TemplateHelper
5
+
6
+ attr_reader :result
7
+
8
+ def initialize(context, tables, &block)
9
+ @context = context
10
+ @result = OpenStruct.new({
11
+ :tables => tables,
12
+ })
13
+
14
+ instance_eval(&block)
15
+ end
16
+
17
+ private
18
+ def table(name, &block)
19
+ if table_names.include?(name)
20
+ raise "Table `#{name}` is already defined"
21
+ end
22
+
23
+ @result.tables << Table.new(@context, name, &block).result
24
+ end
25
+
26
+ def table_names
27
+ @result.tables.map(&:table_name)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,112 @@
1
+ module Dyna
2
+ class DSL
3
+ class DynamoDB
4
+ class Table
5
+ include Dyna::TemplateHelper
6
+ attr_reader :result
7
+
8
+ def initialize(context, table_name, &block)
9
+ @table_name = table_name
10
+ @context = context
11
+
12
+ @result = Hashie::Mash.new({
13
+ :table_name => table_name,
14
+ })
15
+ instance_eval(&block)
16
+ end
17
+
18
+ def key_schema(hash:, range: nil)
19
+ @result.key_schema = [{
20
+ attribute_name: hash,
21
+ key_type: 'HASH',
22
+ }]
23
+
24
+ if range
25
+ @result.key_schema << {
26
+ attribute_name: range,
27
+ key_type: 'RANGE',
28
+ }
29
+ end
30
+ end
31
+
32
+ def attribute_definition(attribute_name:, attribute_type:)
33
+ @result.attribute_definitions ||= []
34
+ @result.attribute_definitions << {
35
+ attribute_name: attribute_name,
36
+ attribute_type: attribute_type,
37
+ }
38
+ end
39
+
40
+ def provisioned_throughput(read_capacity_units:, write_capacity_units:)
41
+ @result.provisioned_throughput = {
42
+ read_capacity_units: read_capacity_units,
43
+ write_capacity_units: write_capacity_units,
44
+ }
45
+ end
46
+
47
+ def stream_specification(stream_enabled:, stream_view_type: nil)
48
+ @result.stream_specification = {
49
+ stream_enabled: stream_enabled,
50
+ stream_view_type: stream_view_type,
51
+ }
52
+ end
53
+
54
+ def local_secondary_index(index_name, &block)
55
+ @result.local_secondary_indexes ||= []
56
+ index = LocalSecondaryIndex.new
57
+ index.instance_eval(&block)
58
+ @result.local_secondary_indexes << {
59
+ index_name: index_name,
60
+ }.merge(index.result.symbolize_keys)
61
+ end
62
+
63
+ def global_secondary_index(index_name, &block)
64
+ @result.global_secondary_indexes ||= []
65
+ index = GlobalSecondaryIndex.new
66
+ index.instance_eval(&block)
67
+ @result.global_secondary_indexes << {
68
+ index_name: index_name,
69
+ }.merge(index.result.symbolize_keys)
70
+ end
71
+
72
+ class LocalSecondaryIndex
73
+ attr_accessor :result
74
+
75
+ def initialize
76
+ @result = Hashie::Mash.new
77
+ end
78
+
79
+ def key_schema(hash:, range: nil)
80
+ @result.key_schema = [{
81
+ attribute_name: hash,
82
+ key_type: 'HASH',
83
+ }]
84
+
85
+ if range
86
+ @result.key_schema << {
87
+ attribute_name: range,
88
+ key_type: 'RANGE',
89
+ }
90
+ end
91
+ end
92
+
93
+ def projection(projection_type:, non_key_attributes: nil)
94
+ @result.projection = {
95
+ projection_type: projection_type,
96
+ non_key_attributes: non_key_attributes,
97
+ }
98
+ end
99
+ end
100
+
101
+ class GlobalSecondaryIndex < LocalSecondaryIndex
102
+ def provisioned_throughput(read_capacity_units:, write_capacity_units:)
103
+ @result.provisioned_throughput = {
104
+ read_capacity_units: read_capacity_units,
105
+ write_capacity_units: write_capacity_units,
106
+ }
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end