cartodb-rb-client 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +5 -0
  2. data/.rvmrc +2 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE +28 -0
  5. data/README.markdown +324 -0
  6. data/Rakefile +10 -0
  7. data/cartodb-rb-client.gemspec +34 -0
  8. data/lib/cartodb-rb-client/cartodb/client/authorization.rb +58 -0
  9. data/lib/cartodb-rb-client/cartodb/client/cache.rb +14 -0
  10. data/lib/cartodb-rb-client/cartodb/client/connection/base.rb +44 -0
  11. data/lib/cartodb-rb-client/cartodb/client/connection/cartodb.rb +231 -0
  12. data/lib/cartodb-rb-client/cartodb/client/connection/postgres.rb +255 -0
  13. data/lib/cartodb-rb-client/cartodb/client/connection.rb +4 -0
  14. data/lib/cartodb-rb-client/cartodb/client/error.rb +68 -0
  15. data/lib/cartodb-rb-client/cartodb/client/utils.rb +20 -0
  16. data/lib/cartodb-rb-client/cartodb/client.rb +4 -0
  17. data/lib/cartodb-rb-client/cartodb/helpers/sql_helper.rb +30 -0
  18. data/lib/cartodb-rb-client/cartodb/helpers.rb +1 -0
  19. data/lib/cartodb-rb-client/cartodb/init.rb +30 -0
  20. data/lib/cartodb-rb-client/cartodb/libs/object.rb +15 -0
  21. data/lib/cartodb-rb-client/cartodb/libs/string.rb +116 -0
  22. data/lib/cartodb-rb-client/cartodb/libs.rb +2 -0
  23. data/lib/cartodb-rb-client/cartodb/model/base.rb +20 -0
  24. data/lib/cartodb-rb-client/cartodb/model/constants.rb +30 -0
  25. data/lib/cartodb-rb-client/cartodb/model/defaults.rb +15 -0
  26. data/lib/cartodb-rb-client/cartodb/model/geo.rb +73 -0
  27. data/lib/cartodb-rb-client/cartodb/model/getters.rb +79 -0
  28. data/lib/cartodb-rb-client/cartodb/model/persistence.rb +69 -0
  29. data/lib/cartodb-rb-client/cartodb/model/query.rb +66 -0
  30. data/lib/cartodb-rb-client/cartodb/model/schema.rb +111 -0
  31. data/lib/cartodb-rb-client/cartodb/model/scope.rb +163 -0
  32. data/lib/cartodb-rb-client/cartodb/model/setters.rb +42 -0
  33. data/lib/cartodb-rb-client/cartodb/model.rb +11 -0
  34. data/lib/cartodb-rb-client/cartodb/types/metadata.rb +89 -0
  35. data/lib/cartodb-rb-client/cartodb/types/pg_result.rb +17 -0
  36. data/lib/cartodb-rb-client/cartodb/types.rb +2 -0
  37. data/lib/cartodb-rb-client/cartodb.rb +6 -0
  38. data/lib/cartodb-rb-client/install_utils.rb +19 -0
  39. data/lib/cartodb-rb-client/version.rb +7 -0
  40. data/lib/cartodb-rb-client.rb +17 -0
  41. data/run_tests.sh +6 -0
  42. data/spec/client_spec.rb +278 -0
  43. data/spec/model/data_spec.rb +130 -0
  44. data/spec/model/metadata_spec.rb +116 -0
  45. data/spec/model/scopes_spec.rb +171 -0
  46. data/spec/spec_helper.rb +28 -0
  47. data/spec/support/cartodb_config.yml +15 -0
  48. data/spec/support/cartodb_factories.rb +33 -0
  49. data/spec/support/cartodb_helpers.rb +14 -0
  50. data/spec/support/cartodb_models.rb +22 -0
  51. data/spec/support/database.yml +5 -0
  52. data/spec/support/shp/cereal.dbf +0 -0
  53. data/spec/support/shp/cereal.shp +0 -0
  54. data/spec/support/shp/cereal.shx +0 -0
  55. data/spec/support/shp/cereal.zip +0 -0
  56. data/spec/support/shp/parcelas.dbf +0 -0
  57. data/spec/support/shp/parcelas.shp +0 -0
  58. data/spec/support/shp/parcelas.shx +0 -0
  59. data/spec/support/shp/parcelas.zip +0 -0
  60. data/spec/support/shp/zonas.dbf +0 -0
  61. data/spec/support/shp/zonas.shp +0 -0
  62. data/spec/support/shp/zonas.shx +0 -0
  63. data/spec/support/shp/zonas.zip +0 -0
  64. data/spec/support/whs_features.csv +33425 -0
  65. metadata +311 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ bin/
data/.rvmrc ADDED
@@ -0,0 +1,2 @@
1
+ rvm use --create 1.9.2@cartodb-rb-client > /dev/null
2
+ rvm wrapper current textmate
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cartodb-rb-client.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem "rspec", "~> 2.3.0"
8
+ gem "bundler", "~> 1.0.0"
9
+ gem "rcov", ">= 0"
10
+ gem 'ruby-debug', :platforms => :mri_18
11
+ gem 'ruby-debug19', :require => 'ruby-debug', :platforms => :mri_19
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2011, Vizzuality
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ 1. Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ 2. Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ 3. All advertising materials mentioning features or use of this software
12
+ must display the following acknowledgement:
13
+ This product includes software developed by Vizzuality.
14
+ 4. Neither the name of Vizzuality nor the
15
+ names of its contributors may be used to endorse or promote products
16
+ derived from this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY
19
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
22
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
data/README.markdown ADDED
@@ -0,0 +1,324 @@
1
+ cartoDB Ruby Client
2
+ ===================
3
+
4
+ cartoDB ruby client that allows an easy and simple interaction with the cartoDB API.
5
+
6
+ Requirements
7
+ -------------
8
+
9
+ The only requirement is an Internet connection and a working version of the Ruby language interpreter. Current ruby versions supported are 1.8.7 and 1.9.2
10
+
11
+ Setup
12
+ ------
13
+
14
+ 1. Install the client gem:
15
+
16
+ gem install cartodb-rb-client
17
+
18
+ or if you are using bundler, put this line in your Gemfile:
19
+
20
+ gem 'cartodb-rb-client'
21
+
22
+ 2. Log into http://cartodb.com, get your OAUTH credentials and put them in a YAML file:
23
+
24
+
25
+ *cartodb\_config.yml:*
26
+
27
+ host: 'YOUR_CARTODB_DOMAIN'
28
+ oauth_key: 'YOUR_OAUTH_KEY'
29
+ oauth_secret: 'YOUR_OAUTH_SECRET'
30
+ oauth_access_token: 'YOUR_OAUTH_ACCES_TOKEN'
31
+ oauth_access_token_secret: 'YOUR_OAUTH_ACCES_TOKEN_SECRET'
32
+
33
+ We also support xAuth protocol. In order to use it, provide your username and password instead of your access token:
34
+
35
+ host: 'YOUR_CARTODB_DOMAIN'
36
+ oauth_key: 'YOUR_OAUTH_KEY'
37
+ oauth_secret: 'YOUR_OAUTH_SECRET'
38
+ username: 'YOUR_CARTODB_USERNAME'
39
+ password: 'YOUR_CARTODB_PASSWORD'
40
+
41
+ 3. Setup your cartoDB connection object. To do so, load the YAML file and assign it to a CartoDB::Config object:
42
+
43
+ CartoDB::Settings = YAML.load_file(Rails.root.join('config/cartodb_config.yml'))
44
+ CartoDB::Connection = CartoDB::Client::Connection.new
45
+
46
+ And that's it. Now you should be able to run querys against the cartoDB servers using the CartoDB::Connection object.
47
+
48
+ Rails apps
49
+ ----------
50
+
51
+ If you're developing a Rails app, you just need to add the cartodb\_config.yml file to your rails app config directory. And that's it. You can access cartoDB anywhere in your app's code using the CartoDB::Connection class.
52
+
53
+ Using the cartoDB API
54
+ -----------
55
+
56
+ List of supported methods to interact with cartoDB:
57
+
58
+ ####1. Create table.
59
+
60
+ Creates a new table in cartoDB. The table's name will be normalized, for example, 'table #1' will become 'table\_1'.
61
+
62
+ Arguments:
63
+
64
+ - **table\_name**: table's name.
65
+
66
+ - **schema**: list of fields the table will contain.
67
+
68
+ - **the\_geom\_type**: Type of geometry the\_geom field will have. Currently we only support 'POINT', but we'll support more types soon.
69
+
70
+ Example:
71
+
72
+ CartoDB::Connection.create_table 'table #1', [{:name => 'field1', :type => 'text'}], 'POINT'
73
+
74
+ Results:
75
+
76
+ {:id => 1,
77
+ :name => "table_1",
78
+ :schema =>
79
+ [["cartodb_id", "number"],
80
+ ["field1", "string"],
81
+ ["updated_at", "date"],
82
+ ["created_at", "date"]]}
83
+
84
+ ####2. Add column.
85
+
86
+ Adds a new column to an existing table.
87
+
88
+ Arguments:
89
+
90
+ - **table_name**: table's name.
91
+ - **column_name**: new column's name.
92
+ - **column_type**: new column's data type. Supported types: string, numeric, date, boolean and geometry.
93
+
94
+ Example:
95
+
96
+ CartoDB::Connection.add\_column 'table_1', 'my_column', 'numeric'
97
+
98
+ Results:
99
+
100
+ []
101
+
102
+ ####3. Drop column.
103
+
104
+ Removes an existing column in the specified table.
105
+
106
+ Arguments:
107
+
108
+ - **table_name**: table's name which column will be dropped.
109
+ - **column_name**: name of the column to be dropped.
110
+
111
+ Example:
112
+
113
+ CartoDB::Connection.drop\_column 'table_1', 'my_column'
114
+
115
+ Results:
116
+
117
+ []
118
+
119
+ ####4. Change column.
120
+
121
+ Changes name and data type of an existing column.
122
+
123
+ Arguments:
124
+
125
+ - **table_name**: table's name which column will be changed.
126
+ - **old_column_name**: current name of the column to be changed.
127
+ - **new_column_name**: new name for the column.
128
+ - **column_type**: new data type of the column.
129
+
130
+ Example:
131
+
132
+ CartoDB::Connection.change\_column 'table_1', 'field1', 'myfield', 'boolean'
133
+
134
+ Results:
135
+
136
+ []
137
+
138
+ ####5. List tables.
139
+
140
+ List all tables in your cartoDB account.
141
+
142
+ Example:
143
+
144
+ CartoDB::Connection.tables
145
+
146
+ Results:
147
+
148
+ {:total_entries => 1,
149
+ :tables =>
150
+ [{:id => 1,
151
+ :name => "table_1",
152
+ :privacy => "PUBLIC",
153
+ :tags => "",
154
+ :schema =>
155
+ [["cartodb_id", "number"],
156
+ ["the_geom", "geometry", "geometry", "point"],
157
+ ["field1", "string"],
158
+ ["created_at", "string"],
159
+ ["updated_at", "string"]],
160
+ :updated_at => Mon, 12 Sep 2011 00:00:00 +0000,
161
+ :rows_counted => 1}]}
162
+
163
+ ####6. Table's detail
164
+
165
+ Shows information about the specified table.
166
+
167
+ Arguments:
168
+
169
+ - **table_name**: Name of the table you want to get info.
170
+
171
+ Example:
172
+
173
+ CartoDB::Connection.table 'table_1'
174
+
175
+ Results:
176
+
177
+ {:id => 1,
178
+ :name => "table_1",
179
+ :privacy => "PRIVATE",
180
+ :tags => "",
181
+ :schema =>
182
+ [["cartodb_id", "number"],
183
+ ["myfield", "boolean"],
184
+ ["updated_at", "date"],
185
+ ["created_at", "date"]]}
186
+
187
+ ####7. Drop table.
188
+
189
+ Deletes the specified table.
190
+
191
+ Arguments:
192
+
193
+ - **table_name**: Name of the table to delete.
194
+
195
+ Example:
196
+
197
+ CartoDB::Connection.drop_table 'table_1'
198
+
199
+ Results:
200
+
201
+ []
202
+
203
+ ####8. Get single row.
204
+
205
+ You can get a single row with this method by specifying its cartodb_id.
206
+
207
+ Arguments:
208
+
209
+ - **table_name**: Name of the table.
210
+ - **row_id**: Id of the row we want.
211
+
212
+ Example:
213
+
214
+ CartoDB::Connection.row 'table_1', 1
215
+
216
+ Result:
217
+
218
+ {:id => 1,
219
+ :updated_at => Tue, 13 Sep 2011 00:00:00 +0000,
220
+ :created_at => Tue, 13 Sep 2011 00:00:00 +0000,
221
+ :cartodb_id => 1,
222
+ :field1 => "cartoDB is awesome!"}
223
+
224
+
225
+ ####9. Insert row.
226
+
227
+ Inserts a new row in the specified table.
228
+
229
+ Arguments:
230
+
231
+ - **table_name**: Name of the table.
232
+ - **row**: A ruby hash with the name of the columns we want to insert
233
+ data in, and its values.
234
+
235
+ Example:
236
+
237
+ CartoDB::Connection.insert_row 'table_1', :field1 => 'cartoDB is
238
+ awesome!'
239
+
240
+ Results:
241
+
242
+ {:id => 1,
243
+ :updated_at => Tue, 13 Sep 2011 00:00:00 +0000,
244
+ :created_at => Tue, 13 Sep 2011 00:00:00 +0000,
245
+ :cartodb_id => 1,
246
+ :field1 => "cartoDB is awesome!"}
247
+
248
+ ####10. Update row.
249
+
250
+ Updates a single row in the specified table.
251
+
252
+ Arguments:
253
+
254
+ - **table_name**: Name of the table.
255
+ - **row_id**: Id of the row we want to update.
256
+ - **row**: A ruby hash containing the column names and values for the
257
+ update.
258
+
259
+ Example:
260
+
261
+ CartoDB::Connection.update_row 'table_1', 1, :field1 => 'cartoDB is
262
+ *really* awesome!'
263
+
264
+ Result:
265
+
266
+ {:id => 1,
267
+ :updated_at => Tue, 13 Sep 2011 00:00:00 +0000,
268
+ :created_at => Tue, 13 Sep 2011 00:00:00 +0000,
269
+ :cartodb_id => 1,
270
+ :field1 => "cartoDB is *really* awesome!"}
271
+
272
+ ####11. Delete row.
273
+
274
+ Deletes a row in the specified table.
275
+
276
+ Arguments:
277
+
278
+ - **table_name**: Name of the table.
279
+ - **row_id**: Id of the row we want to delete.
280
+
281
+ Example:
282
+
283
+ CartoDB::Connection.delete_row 'table_1', 1
284
+
285
+ Result:
286
+
287
+ {:time => 0.008, :total_rows => 0, :rows => []}
288
+
289
+ ####12. Execute a sql query.
290
+
291
+ Executes an sql query against your database in cartoDB.
292
+
293
+ Arguments:
294
+
295
+ - **sql**: String containing the query we want to execute.
296
+ - **options**: A ruby hash containing optional params to run the query.
297
+ Currently we support pagination using the :page and :rows_per_page
298
+ parameters in the options argument.
299
+
300
+ Example:
301
+
302
+ # At first, lets introduce some dummy data for the test
303
+ 10.times{ CartoDB::Connection.insert_row 'table_1', :field1 => 'cartoDB is awesome!'}
304
+
305
+ # And now, the query itself
306
+ CartoDB::Connection.query 'SELECT * FROM table_1', :page => 1,
307
+ :rows_per_page => 5
308
+
309
+ Results:
310
+
311
+ {:time=>0.017,
312
+ :total_rows=>10,
313
+ :rows=>[{:updated_at=>Tue, 13 Sep 2011 00:00:00 +0000,
314
+ :created_at=>Tue, 13 Sep 2011 00:00:00 +0000,
315
+ :cartodb_id=>2,
316
+ :field1=>"cartoDB is awesome!"},
317
+ ...
318
+ ]}
319
+
320
+ More info
321
+ ---------
322
+
323
+ You can also check the oficial [cartoDB Documentation](http://developers.cartodb.com/) if you want more info about the cartoDB API.
324
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ if defined? RSpec
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
10
+ end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cartodb-rb-client/version"
4
+ require "cartodb-rb-client/install_utils"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "cartodb-rb-client"
8
+ s.version = Cartodb::Rb::Client::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Fernando Espinosa"]
11
+ s.email = ["ferdev@vizzuality.com"]
12
+ s.homepage = %q{http://github.com/vizzuality/cartodb-rb-client}
13
+ s.licenses = ["BSD"]
14
+ s.summary = %q{Ruby client for the cartoDB API}
15
+ s.description = %q{Allows quick and easy connection to the cartodb API.}
16
+
17
+ s.required_rubygems_version = ">= 1.3.6"
18
+
19
+ s.rubyforge_project = "cartodb-rb-client"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ s.add_dependency 'typhoeus', '0.2.4'
26
+ s.add_dependency 'oauth', '0.4.5'
27
+ s.add_dependency 'mime-types', '1.16'
28
+ s.add_dependency 'activesupport', '>= 3.0.0', '<= 3.1.0'
29
+ s.add_dependency 'i18n', '>= 0.5.0', '<= 0.6.0'
30
+ s.add_dependency 'rgeo', '0.3.2'
31
+ s.add_dependency 'rgeo-geojson', '0.2.1'
32
+ s.add_dependency 'pg', '0.11.0' if postgresql_installed?
33
+ s.add_dependency 'json', '1.5.3'
34
+ end
@@ -0,0 +1,58 @@
1
+ require 'oauth/request_proxy/typhoeus_request'
2
+
3
+ module CartoDB
4
+ module Client
5
+ module Authorization
6
+
7
+ def signed_request(request_uri, arguments)
8
+ arguments[:disable_ssl_peer_verification] = true
9
+
10
+ request = Typhoeus::Request.new(request_uri, arguments)
11
+
12
+ request.headers.merge!({"Authorization" => oauth_helper(request, request_uri).header})
13
+
14
+ request
15
+ end
16
+ private :signed_request
17
+
18
+ def access_token
19
+ return @access_token if @access_token
20
+
21
+ @access_token ||= if CartoDB::Settings['oauth_access_token'] && CartoDB::Settings['oauth_access_token_secret']
22
+ OAuth::AccessToken.new(oauth_consumer, CartoDB::Settings['oauth_access_token'], CartoDB::Settings['oauth_access_token_secret'])
23
+ elsif CartoDB::Settings['username'] && CartoDB::Settings['password']
24
+
25
+ x_auth_params = {
26
+ :x_auth_mode => :client_auth,
27
+ :x_auth_username => CartoDB::Settings['username'],
28
+ :x_auth_password => CartoDB::Settings['password']
29
+ }
30
+ response = oauth_consumer.request(:post, oauth_consumer.access_token_url, nil, {}, x_auth_params)
31
+
32
+ values = response.body.split('&').inject({}) { |h,v| h[v.split("=")[0]] = v.split("=")[1]; h }
33
+
34
+ OAuth::AccessToken.new(oauth_consumer, values["oauth_token"], values["oauth_token_secret"])
35
+ else
36
+ nil
37
+ end
38
+ end
39
+ private :access_token
40
+
41
+ def oauth_params
42
+ {:consumer => oauth_consumer, :token => access_token}
43
+ end
44
+ private :oauth_params
45
+
46
+ def oauth_consumer
47
+ @oauth_consumer ||= OAuth::Consumer.new(CartoDB::Settings['oauth_key'], CartoDB::Settings['oauth_secret'], :site => CartoDB::Settings['host'])
48
+ end
49
+ private :oauth_consumer
50
+
51
+ def oauth_helper(request, request_uri)
52
+ OAuth::Client::Helper.new(request, oauth_params.merge(:request_uri => request_uri))
53
+ end
54
+ private :oauth_helper
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ module CartoDB
2
+ module Client
3
+ class Cache < Hash
4
+
5
+ def get(key)
6
+ self[key]
7
+ end
8
+
9
+ def set(key, object, timeout = 0)
10
+ self[key] = object
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ module CartoDB
2
+ module Client
3
+ module Connection
4
+ class Base
5
+
6
+ attr_reader :active_connection
7
+
8
+ def initialize
9
+ raise Exception.new 'CartoDB settings not found' if CartoDB::Settings.nil?
10
+
11
+ @active_connection = if cartodb_settings?
12
+ CartoDB::Client::Connection::CartoDBConnection.new(settings)
13
+ elsif postgresql_settings?
14
+ CartoDB::Client::Connection::PostgreSQL.new(settings)
15
+ end
16
+ end
17
+
18
+ def method_missing(method, *args, &block)
19
+ if @active_connection.respond_to?(method)
20
+ @active_connection.send(method, *args, &block)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ def settings
27
+ CartoDB::Settings || {}
28
+ end
29
+ private :settings
30
+
31
+ def cartodb_settings?
32
+ settings.has_key?('oauth_key') && settings.has_key?('oauth_secret')
33
+ end
34
+ private :cartodb_settings?
35
+
36
+ def postgresql_settings?
37
+ settings.has_key?('host') && settings.has_key?('user') && settings.has_key?('password') && settings.has_key?('database')
38
+ end
39
+ private :postgresql_settings?
40
+
41
+ end
42
+ end
43
+ end
44
+ end