datapimp 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +6 -0
  4. data/Guardfile +5 -0
  5. data/LICENSE.txt +4 -18
  6. data/README.md +199 -17
  7. data/Rakefile +24 -0
  8. data/bin/datapimp +18 -0
  9. data/datapimp.gemspec +27 -7
  10. data/lib/datapimp/cli/01_extensions.rb +25 -0
  11. data/lib/datapimp/cli/config.rb +24 -0
  12. data/lib/datapimp/cli/setup.rb +35 -0
  13. data/lib/datapimp/cli/sync.rb +29 -0
  14. data/lib/datapimp/cli.rb +49 -0
  15. data/lib/datapimp/clients/amazon.rb +172 -0
  16. data/lib/datapimp/clients/dropbox.rb +178 -0
  17. data/lib/datapimp/clients/github.rb +59 -0
  18. data/lib/datapimp/clients/google.rb +145 -0
  19. data/lib/datapimp/configuration.rb +210 -0
  20. data/lib/datapimp/core_ext.rb +5 -0
  21. data/lib/datapimp/data_sources/dropbox.rb +8 -0
  22. data/lib/datapimp/data_sources/excel.rb +5 -0
  23. data/lib/datapimp/data_sources/github.rb +5 -0
  24. data/lib/datapimp/data_sources/google.rb +5 -0
  25. data/lib/datapimp/data_sources/json.rb +5 -0
  26. data/lib/datapimp/data_sources/nokogiri.rb +5 -0
  27. data/lib/datapimp/data_sources.rb +10 -0
  28. data/lib/datapimp/sync/dropbox_delta.rb +67 -0
  29. data/lib/datapimp/sync/dropbox_folder.rb +10 -0
  30. data/lib/datapimp/sync/google_drive_folder.rb +5 -0
  31. data/lib/datapimp/sync.rb +30 -0
  32. data/lib/datapimp/version.rb +1 -1
  33. data/lib/datapimp.rb +30 -2
  34. data/packaging/wrapper.sh +32 -0
  35. data/spec/spec_helper.rb +29 -0
  36. data/spec/support/test_helpers.rb +7 -0
  37. data/tasks/distribution/configuration.rb +15 -0
  38. data/tasks/distribution/executable.rb +28 -0
  39. data/tasks/distribution/package.rb +85 -0
  40. data/tasks/distribution/package_helpers.rb +12 -0
  41. data/tasks/distribution/release.rb +49 -0
  42. data/tasks/distribution/release_notes.erb +14 -0
  43. data/tasks/distribution/release_notes.rb +62 -0
  44. data/tasks/distribution/tarball.rb +47 -0
  45. data/tasks/distribution/travelling_ruby.rb +87 -0
  46. data/tasks/package.rake +41 -0
  47. data/tasks/upload.rake +40 -0
  48. metadata +300 -26
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 25225d44e31870eb3e377a8fa095cc4453e49648
4
+ data.tar.gz: 2d1998a952fd6d5da7f16c67bc5ceb8441637547
5
+ SHA512:
6
+ metadata.gz: 50f0280170efb19fa6adaaf3b625e35ffb86cba675a5be09e8deee7c8f594ed988bd7649e142cb5c316905bf01d5afaa6f6de25d3e8450d758cead505bd48c47
7
+ data.tar.gz: e0f1f525c2b4a91902aef779128b67069be36d182685f0a2186446e471db9569370128a0003dc65c12a4e9f0cd7a8516b82c97286c09968594a9ac60e1fd1a95
data/.gitignore CHANGED
@@ -15,3 +15,5 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *.log
19
+ dev-branch/
data/Gemfile CHANGED
@@ -2,3 +2,9 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in datapimp.gemspec
4
4
  gemspec
5
+
6
+ group :development do
7
+ gem 'pry'
8
+ gem 'pry-nav'
9
+ gem 'active_model_serializers'
10
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'rspec', :all_on_start => true do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec/" }
5
+ end
data/LICENSE.txt CHANGED
@@ -1,22 +1,8 @@
1
1
  Copyright (c) 2013 Jonathan Soeder
2
2
 
3
- MIT License
3
+ Permission to use this gem is granted on a project by project basis.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
5
+ For open source projects, the license is identical to the MIT license.
12
6
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
7
+ For commercial ventures, the license is subject to the completion of a
8
+ contractual agreement between the company and Jonathan Soeder.
data/README.md CHANGED
@@ -1,29 +1,211 @@
1
- # Datapimp
1
+ ### Data driven API programming
2
2
 
3
- TODO: Write a gem description
3
+ This gem promotes a declarative style of developing JSON APIs that automates a lot of common patterns
4
+ found in the development of systems which have multiple users, with multiple roles and permissions.
4
5
 
5
- ## Installation
6
+ An API is a collection of resources, and resources can be queried, or commands can be run against resources, each by certain users in certain ways depending on the user's authorization level.
6
7
 
7
- Add this line to your application's Gemfile:
8
+ The DSL provided by this gem allows you to accomplish the tasks of writing your API documentation, your integration tests, as well as your actual implementation code at the same time, and in such a way that exposes metadata.
8
9
 
9
- gem 'datapimp'
10
+ Because the API exposes metadata about the resources it contains, their schemas, and the various policies and details about them, it is possible for API client libraries to configure themselves accordingly.
10
11
 
11
- And then execute:
12
+ ### Example
12
13
 
13
- $ bundle
14
+ ```ruby
15
+ require 'datapimp/dsl'
14
16
 
15
- Or install it yourself as:
17
+ api :my_app => "My Application" do
18
+ version :v1
16
19
 
17
- $ gem install datapimp
20
+ desc "Public users include anyone with access to the URL"
21
+ policy :public_users do
22
+ allow :books, :commands => false, :queries => true
23
+ end
18
24
 
19
- ## Usage
25
+ desc "Authenticated users register and are given an auth token"
26
+ policy :logged_in_users do
27
+ authenticate_with :header => 'X-AUTH-TOKEN', :param => :auth_token
28
+ allow :books, :commands => true, :queries => true
29
+ end
20
30
 
21
- TODO: Write usage instructions here
31
+ desc "Admin users have the admin flag set to true"
32
+ policy :admin_users do
33
+ extends :logged_in_users
34
+ test :admin?
35
+ end
36
+ end
37
+ ```
22
38
 
23
- ## Contributing
39
+ An API can be inspected:
24
40
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
41
+ ```ruby
42
+ api("My Application").authentication_header #=> "X-AUTH-TOKEN"
43
+ api("My Application").policies #=> [:public_users, :logged_in_users, :admin_users]
44
+ api("My Application").policy(:admin_users).resource(:books).allowed_commands #=> [:create, :update, :delete]
45
+ ```
46
+
47
+ An API is made up of resources:
48
+
49
+ ```ruby
50
+ resource "Books" do
51
+ serializer do
52
+ desc "A unique id for the book", :type => :integer
53
+ attribute :id
54
+
55
+ desc "The title of the book", :type => :string
56
+ attribute :title
57
+
58
+ desc "The year the book was published", :type => :integer
59
+ attribute :year
60
+
61
+ desc "A reference to the author", :type => "Author"
62
+ has_one :author
63
+ end
64
+
65
+ command :update, "Update a book's attributes" do
66
+ # Will ensure the command is run with
67
+ # Book.accessible_to(current_user).find(id).
68
+ scope :accessible_to
69
+
70
+ params do
71
+ duck :id, :method => :to_s
72
+
73
+ optional do
74
+ string :title
75
+ end
76
+ end
77
+ end
78
+
79
+ query do
80
+ start_from :scope => :accessible_to
81
+
82
+ params do
83
+ desc "The year the book was published (example: YYYY)"
84
+ integer :year_published
85
+ end
86
+
87
+ role :admin do
88
+ start_from :scope => :all
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ A resource can also be inspected:
95
+
96
+ ```ruby
97
+ meta_data = api("My Application").resource("Books").meta_data
98
+
99
+ meta_data.attributes # {:id => "The id of the book", :year_published => "The year it was published"}
100
+ meta_data.commands => [:update]
101
+ meta_data.command(:update).arguments #=> [:id, :year, :title]
102
+ meta_data.command(:update).optional_arguments #=> [:year, :title]
103
+ ```
104
+
105
+ This inspection goes a long way to some advanced features, such as
106
+ automated documentation and integration test generation, or in writing
107
+ tools for generating client libraries and the like.
108
+
109
+ ### Customizing the elements
110
+
111
+ How are each of these behaviors is stored in code? In a way that will be
112
+ very familiar to Rails developers, following common naming conventions
113
+ and file organization patterns.
114
+
115
+ ```
116
+ - app
117
+ - commands
118
+ - application_command.rb
119
+ - create_book.rb
120
+ - update_book.rb
121
+ - contexts
122
+ - application_context.rb
123
+ - book_context.rb
124
+ - serializers
125
+ - book_serializer.rb
126
+ ```
127
+
128
+ ### Request Context: Current User, Resource, and REST
129
+
130
+ From the programmer's perspective, a typical resource is made up of several request patterns:
131
+
132
+ - Filter Context (index, show)
133
+ - Commands (aka mutations. create, update, destroy)
134
+ - Serializers (aka presenters, views)
135
+
136
+ Each of these objects can be configured to behave in certain ways that may be dependent on the user or role making the request to interact with them.
137
+
138
+ Most API requests can be thought of in the following ways:
139
+
140
+ ```ruby
141
+ # A Typical read request ( query / filter or detail view )
142
+
143
+ response = present( this_resource ) # resource -> filter context
144
+ .to(this_user) # filter context: relevant for this user
145
+ .in(this_presentation) # serializer: different slices / renderings
146
+
147
+ response.cache_key # russian doll style / max-updated-at friendly
148
+ response.etag # http client conditional get
149
+ ```
150
+
151
+ The filter context and serializer classes make this easy. They also
152
+ make writing -- or rather, generating -- documentation and tests very
153
+ easy as well.
154
+
155
+ ```ruby
156
+ # Typical mutation request ( create, update, delete )
157
+
158
+ outcome = run(this_command)
159
+ .as(this_user)
160
+ .against(this_set_of_one_or_more_records)
161
+ .with(these_arguments)
162
+
163
+ outcome.success?
164
+
165
+ outcome.error_messages
166
+
167
+ outcome.result
168
+ ```
169
+
170
+ The command class determines the specifics of the above style of
171
+ request.
172
+
173
+ ### The Filter Context
174
+
175
+ The filter context system is used to standardize the way we write
176
+ typical index and show actions in a typical Rails app. A user is
177
+ requesting to view a set of records, or an individual records.
178
+
179
+ Given a user making a request to view a specific resource, we arrive at
180
+ the 'filter context'. The filter context is responsible for 'scoping' a
181
+ resource to the set of records that user is permitted to view.
182
+
183
+ Based on the combination of parameters used to build that filter, we
184
+ compute a cache key that simplifies the process of server caching and
185
+ http client caching at the same time.
186
+
187
+ The filter context itself and the available parameters and their allowed
188
+ values are specified by the DSL, which simplifies the process of writing
189
+ complex queries and also provides configuration meta-data that aids in
190
+ the process of developing client user interfaces, API documentation, and
191
+ test code.
192
+
193
+ ### Commands
194
+
195
+ The command class allows you to declare the available parameters, the
196
+ required values, their data types, etc. It also allows you to declare
197
+ which users can run the command, and further restrict the parameters
198
+ allowed and the values they accept.
199
+
200
+ ### Serializers
201
+
202
+ - ActiveModel Serializers
203
+ - Documentation DSL
204
+ - Metadata for inspection + documentation generation
205
+
206
+ ## API Documentation & Integration Tests
207
+
208
+ - rspec_api_documentation gem
209
+ - plan: take advantage of metadata defined above to auto-generate
210
+ documentation with the ability to pass expectation blocks as pass /
211
+ fail indicators
data/Rakefile CHANGED
@@ -1 +1,25 @@
1
+ Dir[File.join(Dir.pwd, 'tasks', '**', '*.rb')].each { |f| require f }
2
+ Dir[File.join(Dir.pwd, 'tasks', '*.rake')].each { |f| load f }
3
+
1
4
  require "bundler/gem_tasks"
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ Distribution.configure do |config|
10
+ config.package_name = 'datapimp'
11
+ config.version = Datapimp::VERSION
12
+ config.rb_version = '20150210-2.1.5'
13
+ config.packaging_dir = File.expand_path 'packaging'
14
+ config.native_extensions = [
15
+ #'github-markdown-0.6.8',
16
+ #'escape_utils-1.0.1',
17
+ #'charlock_holmes-0.7.3',
18
+ #'posix-spawn-0.3.9',
19
+ #'nokogumbo-1.3.0',
20
+ #'rugged-0.21.4',
21
+ #'nokogiri-1.6.5',
22
+ ]
23
+ end
24
+
25
+ task :default => :spec
data/bin/datapimp ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "pathname"
5
+
6
+ $:.unshift Pathname(File.dirname(__FILE__)).join("..","lib")
7
+
8
+ require "pry"
9
+ require 'colored'
10
+ require "commander/import"
11
+ require 'datapimp'
12
+ require 'datapimp/cli'
13
+
14
+ program :name, 'Datapimp'
15
+ program :version, Datapimp::VERSION
16
+ program :description, 'Datapimp style command line utilities'
17
+
18
+ Datapimp::Cli.load_commands
data/datapimp.gemspec CHANGED
@@ -8,16 +8,36 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Datapimp::VERSION
9
9
  spec.authors = ["Jonathan Soeder"]
10
10
  spec.email = ["jonathan.soeder@gmail.com"]
11
- spec.description = %q{Write a gem description}
12
- spec.summary = %q{Write a gem summary}
11
+ spec.description = %q{Your rails app in a custom tailored suit.}
12
+ spec.summary = %q{A collection of API development patterns that I have accumulated in my career as a boss.}
13
13
  spec.homepage = ""
14
14
  spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = `git ls-files`.split("\n")
17
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+
20
+ spec.add_dependency 'pry'
21
+ spec.add_dependency 'hashie', '>= 0.3.0'
22
+ spec.add_dependency 'colored'
23
+ spec.add_dependency 'commander'
24
+ spec.add_dependency 'fog-aws'
25
+ spec.add_dependency 'dropbox-api'
26
+ spec.add_dependency 'google_drive'
27
+ spec.add_dependency 'rack-contrib'
28
+ spec.add_dependency 'uri_template'
29
+ spec.add_dependency 'dnsimple-ruby'
30
+ spec.add_dependency 'rack-proxy'
31
+ spec.add_dependency 'axlsx'
32
+ spec.add_dependency 'launchy'
33
+ spec.add_dependency 'oauth', '~> 0.4.7'
34
+ spec.add_dependency 'octokit', '>= 3.0.0'
35
+
36
+ spec.add_development_dependency "rake", '~> 0'
37
+ spec.add_development_dependency "rack-test", '~> 0'
38
+ spec.add_development_dependency 'rspec', '~> 3.0'
39
+ spec.add_development_dependency "pry-nav", '~> 0'
40
+
19
41
  spec.require_paths = ["lib"]
20
42
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
43
  end
@@ -0,0 +1,25 @@
1
+ class Commander::Command
2
+ def action(*args, &block)
3
+
4
+ wrapper = lambda do |a, options|
5
+ if options.config
6
+ read = Pathname(options.config).read
7
+ json = JSON.parse(read)
8
+
9
+ Datapimp.config.apply_all(json)
10
+ end
11
+
12
+ Datapimp.config.apply_all(options.to_hash)
13
+
14
+ block.call(a, options)
15
+ end
16
+
17
+ send(:when_called, *args, &wrapper)
18
+ end
19
+
20
+ class Options
21
+ def to_hash
22
+ __hash__
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ command 'config set' do |c|
2
+ c.syntax = 'datapimp config set KEY=VALUE KEY=VALUE [options]'
3
+ c.description = 'manipulate configuration settings'
4
+
5
+ c.option '--global', 'Set the configuration globally'
6
+ c.option '--local', 'Set the configuration globally'
7
+
8
+ c.example "set a bunch of config parameters", "datapimp config set DROPBOX_APP_KEY=xxx DROPBOX_APP_SECRET=yyy GITHUB_APP_SECRET=zzz"
9
+
10
+ c.action do |args, _options|
11
+ Datapimp::Configuration.initialize!
12
+
13
+ args.select { |pair| pair.match(/=/) }
14
+ .map { |pair| pair.split('=') }
15
+ .each do |group|
16
+ key, value = group
17
+ Datapimp.config.set(key, value, false, global: !!(_options.global))
18
+ end
19
+
20
+ Datapimp.config.save!
21
+
22
+ Datapimp.config.show
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ command "setup google" do |c|
2
+ c.syntax = "datapimp set up google"
3
+ c.description = "setup integration with google drive"
4
+
5
+ c.action do |args, options|
6
+ Datapimp::Sync.google.setup()
7
+ end
8
+ end
9
+
10
+ command "setup amazon" do |c|
11
+ c.syntax = "datapimp setup amazon"
12
+ c.description = "setup integration with amazon"
13
+
14
+ c.action do |args, options|
15
+ Datapimp::Sync.amazon.interactive_setup()
16
+ end
17
+ end
18
+
19
+ command "setup dropbox" do |c|
20
+ c.syntax = "datapimp set up dropbox"
21
+ c.description = "setup integration with dropbox"
22
+
23
+ c.action do |args, options|
24
+ Datapimp::Sync.dropbox.setup()
25
+ end
26
+ end
27
+
28
+ command "setup github" do |c|
29
+ c.syntax = "datapimp set up github"
30
+ c.description = "setup integration with github"
31
+
32
+ c.action do |args, options|
33
+ Datapimp::Sync.github.setup()
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ command "sync folder" do |c|
2
+ c.description = "Synchronize the contents of a local folder with a file sharing service"
3
+ c.syntax = "datapimp sync folder LOCAL_PATH REMOTE_PATH [OPTIONS]"
4
+
5
+ c.option '--type TYPE', String, "Which service is hosting the folder"
6
+
7
+ Datapimp::Cli.accepts_keys_for(c, :amazon, :google, :github, :dropbox)
8
+
9
+ c.action do |args, options|
10
+
11
+ end
12
+ end
13
+
14
+ command "sync data" do |c|
15
+ c.description = "Synchronize the contents of a local data store with its remote source"
16
+ c.syntax = "datapimp sync data [OPTIONS]"
17
+
18
+ c.option '--type TYPE', String, "What type of source data is this? #{ Datapimp::Sync.data_source_types.join(", ") }"
19
+ c.option '--output FILE', String, "Write the output to a file"
20
+ c.option '--columns NAMES', Array, "Extract only these columns"
21
+
22
+ c.example "Syncing an excel file from dropbox ", "datapimp sync data --type dropbox --columns name,description --dropbox-app-key ABC --dropbox-app-secret DEF --dropbox-client-token HIJ --dropbox-client-secret JKL spreadsheets/test.xslx"
23
+
24
+ Datapimp::Cli.accepts_keys_for(c, :google, :github, :dropbox)
25
+
26
+ c.action do |args, options|
27
+
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ require 'launchy'
2
+
3
+ module Datapimp
4
+ module Cli
5
+ def self.load_commands(from_path=nil)
6
+ from_path ||= Datapimp.lib.join("datapimp","cli")
7
+ Dir[from_path.join("**/*.rb")].each {|f| require(f) }
8
+ end
9
+
10
+ def self.accepts_keys_for(c, *services)
11
+ services.map!(&:to_sym)
12
+
13
+ if services.include?(:dropbox)
14
+ c.option "--dropbox-app-key KEY", String, "The dropbox app key"
15
+ c.option "--dropbox-app-secret SECRET", String, "The dropbox app secret"
16
+ c.option "--dropbox-app-sandbox", "The dropbox app is a sandbox app"
17
+ c.option "--dropbox-client-token TOKEN", String, "The dropbox client token"
18
+ c.option "--dropbox-client-secret SECRET", String, "The dropbox client secret"
19
+ c.option "--dropbox-path-prefix PATH", String, "The path prefix for this folder (for sandboxed apps)"
20
+ end
21
+
22
+ if services.include?(:google)
23
+ c.option "--google-client-id KEY", String, "The google client id"
24
+ c.option "--google-client-secret KEY", String, "The google client secret"
25
+ c.option "--google-refresh-token", String, "The google refresh token"
26
+ c.option "--google-access-token KEY", String, "The google access token"
27
+ end
28
+
29
+ if services.include?(:dnsimple)
30
+ c.option "--dnsimple-api-token", String, "The DNSimple API Token"
31
+ c.option "--dnsimple-username", String, "The DNSimple Username"
32
+ end
33
+
34
+ if services.include?(:github)
35
+ c.option "--github-username", String, "The Github Username"
36
+ c.option "--github-organization", String, "The Github Organization"
37
+ c.option "--github-access-token", String, "The Github Personal Access Token"
38
+ c.option "--github-app-key", String, "The Github App Key"
39
+ c.option "--github-app-secret", String, "The Github App Secret"
40
+ end
41
+
42
+ if services.include?(:amazon) || services.include?(:aws)
43
+ c.option '--aws-secret-access-key', String, 'AWS Secret Access Key'
44
+ c.option '--aws-access-key-id', String, 'AWS Access Key ID'
45
+ end
46
+
47
+ end
48
+ end
49
+ end