grafana 0.8.2 → 1.0.0

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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +8 -3
  3. data/lib/grafana/admin.rb +39 -65
  4. data/lib/grafana/alerts.rb +334 -14
  5. data/lib/grafana/annotations.rb +284 -9
  6. data/lib/grafana/client.rb +38 -1
  7. data/lib/grafana/dashboard.rb +182 -39
  8. data/lib/grafana/dashboard_permissions.rb +132 -0
  9. data/lib/grafana/dashboard_versions.rb +101 -5
  10. data/lib/grafana/datasource.rb +93 -49
  11. data/lib/grafana/folder.rb +198 -0
  12. data/lib/grafana/folder_and_dashboard_search.rb +57 -0
  13. data/lib/grafana/folder_permissions.rb +155 -0
  14. data/lib/grafana/login.rb +41 -35
  15. data/lib/grafana/network.rb +128 -91
  16. data/lib/grafana/organization.rb +65 -34
  17. data/lib/grafana/organizations.rb +119 -175
  18. data/lib/grafana/playlist.rb +599 -0
  19. data/lib/grafana/preferences.rb +122 -0
  20. data/lib/grafana/tags.rb +19 -8
  21. data/lib/grafana/teams.rb +364 -0
  22. data/lib/grafana/tools.rb +44 -12
  23. data/lib/grafana/user.rb +78 -39
  24. data/lib/grafana/users.rb +104 -53
  25. data/lib/grafana/validator.rb +47 -2
  26. data/lib/grafana/version.rb +3 -3
  27. metadata +13 -38
  28. data/doc/Array.html +0 -200
  29. data/doc/Boolean.html +0 -122
  30. data/doc/FalseClass.html +0 -132
  31. data/doc/Grafana.html +0 -172
  32. data/doc/Hash.html +0 -212
  33. data/doc/Logging.html +0 -326
  34. data/doc/Object.html +0 -286
  35. data/doc/Time.html +0 -200
  36. data/doc/TrueClass.html +0 -132
  37. data/doc/_index.html +0 -380
  38. data/doc/class_list.html +0 -51
  39. data/doc/file.README.html +0 -117
  40. data/doc/file_list.html +0 -56
  41. data/doc/frames.html +0 -17
  42. data/doc/index.html +0 -117
  43. data/doc/method_list.html +0 -771
  44. data/doc/top-level-namespace.html +0 -112
@@ -0,0 +1,122 @@
1
+ module Grafana
2
+
3
+ #
4
+ #
5
+ #
6
+ #
7
+ #
8
+ # original API Documentation can be found under: http://docs.grafana.org/http_api/preferences/#user-and-org-preferences-api
9
+ #
10
+ module Preferences
11
+
12
+ # Get Current User Prefs
13
+ # GET /api/user/preferences
14
+ def user_preferences()
15
+
16
+ v, mv = version.values
17
+ return { 'status' => 404, 'message' => format( 'folder has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
18
+
19
+ endpoint = '/api/user/preferences'
20
+ @logger.debug("Getting current user preferences (GET #{endpoint})") if @debug
21
+ get(endpoint)
22
+ end
23
+
24
+
25
+ # Update Current User Prefs
26
+ # PUT /api/user/preferences
27
+ #
28
+ # theme - One of: light, dark, or an empty string for the default theme
29
+ # homeDashboardId - The numerical :id of a favorited dashboard, default: 0
30
+ # timezone - One of: utc, browser, or an empty string for the default
31
+ #
32
+ def update_user_preferences(params)
33
+
34
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
35
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
36
+
37
+ endpoint = '/api/user/preferences'
38
+ @logger.debug("update current user preferences (GET #{endpoint})") if @debug
39
+
40
+ update_preferences( endpoint, params )
41
+ end
42
+
43
+
44
+ # Get Current Org Prefs
45
+ # GET /api/org/preferences
46
+ def org_preferences()
47
+
48
+ v, mv = version.values
49
+ return { 'status' => 404, 'message' => format( 'folder has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
50
+
51
+ endpoint = '/api/org/preferences'
52
+ @logger.debug("Getting current organisation preferences (GET #{endpoint})") if @debug
53
+ get(endpoint)
54
+ end
55
+
56
+
57
+
58
+ # Update Current Org Prefs
59
+ # PUT /api/org/preferences
60
+ #
61
+ # theme - One of: light, dark, or an empty string for the default theme
62
+ # homeDashboardId - The numerical :id of a favorited dashboard, default: 0
63
+ # timezone - One of: utc, browser, or an empty string for the default
64
+ #
65
+ def update_org_preferences(params)
66
+
67
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
68
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
69
+
70
+ endpoint = '/api/org/preferences'
71
+ @logger.debug("update current organisation preferences (GET #{endpoint})") if @debug
72
+
73
+ update_preferences( endpoint, params )
74
+ end
75
+
76
+
77
+ private
78
+ def update_preferences( endpoint, params )
79
+
80
+ theme = validate( params, required: false, var: 'theme' , type: String )
81
+ timezone = validate( params, required: false, var: 'timezone' , type: String )
82
+ home_dashboard = validate( params, required: false, var: 'home_dashboard')
83
+
84
+ valid_theme = %w[light dark]
85
+ valid_timezone = %w[utc browser]
86
+
87
+ v, mv = version.values
88
+ return { 'status' => 404, 'message' => format( 'folder has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
89
+
90
+ unless(theme.nil?)
91
+ v_theme = validate_hash( theme, valid_theme )
92
+ return v_theme unless( v_theme == true )
93
+ end
94
+
95
+ unless(timezone.nil?)
96
+ v_timez = validate_hash( timezone, valid_timezone )
97
+ return v_timez unless( v_timez == true )
98
+ end
99
+
100
+ unless(home_dashboard.nil?)
101
+ v_dashboard = dashboard(home_dashboard)
102
+ return { 'status' => 404, 'message' => format('dashboard \'%s\' not found',home_dashboard) }\
103
+ unless(v_dashboard.dig('status') == 200)
104
+
105
+ dashboard_id = v_dashboard.dig('dashboard','id')
106
+ end
107
+
108
+ payload = {
109
+ theme: theme,
110
+ homeDashboardId: dashboard_id,
111
+ timezone: timezone
112
+ }
113
+ payload.reject!{ |_, y| y.nil? }
114
+
115
+ # endpoint = '/api/user/preferences'
116
+ # @logger.debug("update current preferences (GET #{endpoint})") if @debug
117
+
118
+ put(endpoint, payload.to_json)
119
+ end
120
+
121
+ end
122
+ end
data/lib/grafana/tags.rb CHANGED
@@ -3,18 +3,32 @@ module Grafana
3
3
  module Tags
4
4
 
5
5
  # expand the Template Tags
6
+ # helper function to expand the Dashboard Tags with an own Array of Tags
6
7
  #
8
+ # @param params [Hash] params
9
+ # @option params [Hash] dashboard
10
+ # @option params [Array] additional_tags
7
11
  #
12
+ # @example
13
+ # params = {
14
+ # dashboard: {
15
+ # rows: [
16
+ #
17
+ # ]
18
+ # },
19
+ # additional_tag: [ 'tag1', 'tag2' ]
20
+ # }
21
+ # expand_tags( params )
22
+ #
23
+ # @return [Hash]
8
24
  #
9
25
  def expand_tags( params )
10
26
 
11
27
  raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
28
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
12
29
 
13
- dashboard = params.dig(:dashboard)
14
- additional_tags = params.dig(:additional_tags) || []
15
-
16
- raise ArgumentError.new(format('wrong type. \'dashboard\' must be an Hash, given \'%s\'', dashboard.class.to_s)) unless( dashboard.is_a?(Hash) )
17
- raise ArgumentError.new(format('wrong type. \'additional_tags\' must be an Array, given \'%s\'', additional_tags.class.to_s)) unless( additional_tags.is_a?(Array) )
30
+ dashboard = validate( params, required: true, var: 'dashboard', type: Hash )
31
+ additional_tags = validate( params, required: true, var: 'additional_tags', type: Array )
18
32
 
19
33
  # add tags
20
34
  # dashboard = JSON.parse( dashboard ) if( dashboard.is_a?( String ) )
@@ -24,12 +38,9 @@ module Grafana
24
38
  current_tags = dashboard.dig( 'dashboard', 'tags' )
25
39
 
26
40
  if( !current_tags.nil? && additional_tags.count > 0 )
27
-
28
41
  current_tags << additional_tags
29
-
30
42
  current_tags.flatten!
31
43
  current_tags.sort!
32
-
33
44
  dashboard['dashboard']['tags'] = current_tags
34
45
  end
35
46
 
@@ -0,0 +1,364 @@
1
+
2
+ module Grafana
3
+
4
+ # http://docs.grafana.org/http_api/team/
5
+ #
6
+ # This API can be used to create/update/delete Teams and to add/remove users to Teams.
7
+ # All actions require that the user has the Admin role for the organization.
8
+ #
9
+ module Teams
10
+
11
+ # http://docs.grafana.org/http_api/team/#team-search-with-paging
12
+ #
13
+ # GET /api/teams/search?perpage=50&page=1&query=myteam
14
+ # or
15
+ # GET /api/teams/search?name=myteam
16
+ #
17
+ # Default value for the perpage parameter is 1000 and for the page parameter is 1.
18
+ #
19
+ # The totalCount field in the response can be used for pagination of the teams list
20
+ # E.g. if totalCount is equal to 100 teams and the perpage parameter is set to 10 then there are 10 pages of teams.
21
+ #
22
+ # The query parameter is optional and it will return results where the query value is contained in the name field.
23
+ # Query values with spaces need to be url encoded e.g. query=my%20team.
24
+ #
25
+ # The name parameter returns a single team if the parameter matches the name field.
26
+
27
+
28
+
29
+ #
30
+ # Status Codes:
31
+ #
32
+ # 200 - Ok
33
+ # 401 - Unauthorized
34
+ # 403 - Permission denied
35
+ # 404 - Team not found (if searching by name)
36
+ #
37
+ def search_team( params )
38
+
39
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
40
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
41
+
42
+ v, mv = version.values
43
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
44
+
45
+ perpage = validate( params, required: false, var: 'perpage', type: Integer ) || 1000
46
+ page = validate( params, required: false, var: 'page' , type: Integer ) || 1
47
+ query = validate( params, required: false, var: 'query' , type: String )
48
+ name = validate( params, required: false, var: 'name' , type: String )
49
+
50
+ if(name.nil?)
51
+ api = []
52
+ api << format( 'perpage=%s', perpage ) unless( perpage.nil? )
53
+ api << format( 'page=%s', page ) unless( page.nil? )
54
+ api << format( 'query=%s', CGI.escape( query ) ) unless( query.nil? )
55
+
56
+ api = api.join( '&' )
57
+
58
+ endpoint = format('/api/teams/search?%s', api)
59
+ else
60
+ endpoint = format('/api/teams/search?name=%s',CGI.escape(name))
61
+ end
62
+
63
+ @logger.debug("Attempting to search for alerts (GET #{endpoint})") if @debug
64
+
65
+ r = get(endpoint)
66
+ r['status'] = 404 if( r.dig('totalCount').zero? )
67
+ r
68
+ end
69
+
70
+ # http://docs.grafana.org/http_api/team/#get-team-by-id
71
+ #
72
+ # Get Team By Id
73
+ # GET /api/teams/:id
74
+ #
75
+ #
76
+ def team( team_id )
77
+
78
+ if( team_id.is_a?(String) && team_id.is_a?(Integer) )
79
+ raise ArgumentError.new(format('wrong type. user \'team_id\' must be an String (for an Team name) or an Integer (for an Team Id), given \'%s\'', team_id.class.to_s))
80
+ end
81
+ raise ArgumentError.new('missing \'team_id\'') if( team_id.size.zero? )
82
+
83
+ v, mv = version.values
84
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
85
+
86
+ if(team_id.is_a?(String))
87
+ o_team = search_team(name: team_id)
88
+ status = o_team.dig('status')
89
+ total_count = o_team.dig('totalCount')
90
+
91
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
92
+
93
+ teams = o_team.dig('teams')
94
+ team = teams.detect { |x| x['name'] == team_id }
95
+ team_id = team.dig('id')
96
+ end
97
+
98
+ endpoint = format( '/api/teams/%s', team_id )
99
+
100
+ @logger.debug("Getting team by Id #{team_id} (GET #{endpoint})") if @debug
101
+ get(endpoint)
102
+ end
103
+
104
+ # http://docs.grafana.org/http_api/team/#add-team
105
+ #
106
+ # The Team name needs to be unique. name is required and email is optional.
107
+ # POST /api/teams
108
+ #
109
+ #
110
+ def add_team( params )
111
+
112
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
113
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
114
+
115
+ v, mv = version.values
116
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
117
+
118
+ name = validate( params, required: true, var: 'name' , type: String )
119
+ email = validate( params, required: false, var: 'email', type: String )
120
+
121
+ o_team = search_team(name: name)
122
+
123
+ status = o_team.dig('status')
124
+ total_count = o_team.dig('totalCount')
125
+ # teams = o_team.dig('teams')
126
+ # team = teams.detect { |x| x['name'] == name }
127
+
128
+ return { 'status' => 404, 'message' => format('team \'%s\' alread exists', name) } if(status == 200 && total_count > 0)
129
+
130
+ endpoint = '/api/teams'
131
+
132
+ payload = {
133
+ name: name,
134
+ email: email
135
+ }
136
+ payload.reject!{ |_, y| y.nil? }
137
+
138
+ @logger.debug("Creating team: #{name} (POST #{endpoint})") if @debug
139
+
140
+ post( endpoint, payload.to_json )
141
+ end
142
+
143
+ # http://docs.grafana.org/http_api/team/#update-team
144
+ #
145
+ # There are two fields that can be updated for a team: name and email.
146
+ # PUT /api/teams/:id
147
+ #
148
+ def update_team( params )
149
+
150
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
151
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
152
+
153
+ v, mv = version.values
154
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
155
+
156
+ team_id = validate( params, required: true , var: 'team_id' )
157
+ name = validate( params, required: false, var: 'name' , type: String )
158
+ email = validate( params, required: true , var: 'email', type: String )
159
+
160
+ if(team_id.is_a?(String))
161
+ o_team = search_team(name: team_id)
162
+ status = o_team.dig('status')
163
+ total_count = o_team.dig('totalCount')
164
+
165
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
166
+
167
+ teams = o_team.dig('teams')
168
+ team = teams.detect { |x| x['name'] == team_id }
169
+
170
+ team_id = team.dig('id')
171
+ end
172
+
173
+ payload = {
174
+ email: email,
175
+ name: name
176
+ }
177
+ payload.reject!{ |_, y| y.nil? }
178
+
179
+ endpoint = format( '/api/teams/%d', team_id )
180
+ @logger.debug("Updating team with Id #{team_id} (PUT #{endpoint})") if @debug
181
+
182
+ put( endpoint, payload.to_json )
183
+ end
184
+
185
+ # http://docs.grafana.org/http_api/team/#delete-team-by-id
186
+ #
187
+ # DELETE /api/teams/:id
188
+ #
189
+ #
190
+ #
191
+ #
192
+ #
193
+ #
194
+ def delete_team(team_id)
195
+
196
+ if( team_id.is_a?(String) && team_id.is_a?(Integer) )
197
+ raise ArgumentError.new(format('wrong type. user \'team_id\' must be an String (for an Team name) or an Integer (for an Team Id), given \'%s\'', team_id.class.to_s))
198
+ end
199
+ raise ArgumentError.new('missing \'team_id\'') if( team_id.size.zero? )
200
+
201
+ v, mv = version.values
202
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
203
+
204
+ if(team_id.is_a?(String))
205
+ o_team = search_team(name: team_id)
206
+
207
+ status = o_team.dig('status')
208
+ total_count = o_team.dig('totalCount')
209
+
210
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
211
+
212
+ teams = o_team.dig('teams')
213
+ team = teams.detect { |x| x['name'] == team_id }
214
+
215
+ team_id = team.dig('id')
216
+ end
217
+
218
+ endpoint = format( '/api/teams/%s', team_id )
219
+
220
+ @logger.debug("delete team Id #{team_id} (GET #{endpoint})") if @debug
221
+ delete(endpoint)
222
+ end
223
+
224
+ # http://docs.grafana.org/http_api/team/#get-team-members
225
+ #
226
+ # GET /api/teams/:teamId/members
227
+ #
228
+ #
229
+ #
230
+ #
231
+ def team_members(team_id)
232
+
233
+ if( team_id.is_a?(String) && team_id.is_a?(Integer) )
234
+ raise ArgumentError.new(format('wrong type. user \'team_id\' must be an String (for an Team name) or an Integer (for an Team Id), given \'%s\'', team_id.class.to_s))
235
+ end
236
+ raise ArgumentError.new('missing \'team_id\'') if( team_id.size.zero? )
237
+
238
+ v, mv = version.values
239
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
240
+
241
+ if(team_id.is_a?(String))
242
+ o_team = search_team(name: team_id)
243
+
244
+ status = o_team.dig('status')
245
+ total_count = o_team.dig('totalCount')
246
+
247
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
248
+
249
+ teams = o_team.dig('teams')
250
+ team = teams.detect { |x| x['name'] == team_id }
251
+
252
+ team_id = team.dig('id')
253
+ end
254
+
255
+ endpoint = format( '/api/teams/%s/members', team_id )
256
+
257
+ @logger.debug("Getting team by Id #{team_id} (GET #{endpoint})") if @debug
258
+
259
+ get(endpoint)
260
+ end
261
+
262
+ # http://docs.grafana.org/http_api/team/#add-team-member
263
+ #
264
+ # POST /api/teams/:teamId/members
265
+ #
266
+ #
267
+ #
268
+ def add_team_member(params)
269
+
270
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
271
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
272
+
273
+ v, mv = version.values
274
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
275
+
276
+ team_id = validate( params, required: true , var: 'team_id' )
277
+ user_id = validate( params, required: false, var: 'user_id' )
278
+
279
+ if( user_id.is_a?(String) && user_id.is_a?(Integer) )
280
+ raise ArgumentError.new(format('wrong type. user \'user_id\' must be an String (for an User name) or an Integer (for an User Id), given \'%s\'', user_id.class.to_s))
281
+ end
282
+ raise ArgumentError.new('missing \'user_id\'') if( user_id.size.zero? )
283
+
284
+ if(team_id.is_a?(String))
285
+ o_team = search_team(name: team_id)
286
+ status = o_team.dig('status')
287
+ total_count = o_team.dig('totalCount')
288
+
289
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
290
+
291
+ teams = o_team.dig('teams')
292
+ team = teams.detect { |x| x['name'] == team_id }
293
+
294
+ team_id = team.dig('id')
295
+ end
296
+
297
+ if(user_id.is_a?(String))
298
+ usr = user(user_id)
299
+ status = usr.dig('status')
300
+ user_id = usr.dig('id')
301
+
302
+ return { 'status' => 404, 'message' => format( 'No User \'%s\' found', user_id) } unless(status == 200)
303
+ end
304
+
305
+ payload = {
306
+ userId: user_id
307
+ }
308
+ payload.reject!{ |_, y| y.nil? }
309
+
310
+ endpoint = format( '/api/teams/%d/members', team_id )
311
+
312
+ post( endpoint, payload.to_json )
313
+ end
314
+
315
+ # http://docs.grafana.org/http_api/team/#remove-member-from-team
316
+ #
317
+ # DELETE /api/teams/:teamId/members/:userId
318
+ #
319
+ #
320
+ #
321
+ def remove_team_member(params)
322
+
323
+ raise ArgumentError.new(format('wrong type. \'params\' must be an Hash, given \'%s\'', params.class.to_s)) unless( params.is_a?(Hash) )
324
+ raise ArgumentError.new('missing \'params\'') if( params.size.zero? )
325
+
326
+ v, mv = version.values
327
+ return { 'status' => 404, 'message' => format( 'team has been supported in Grafana since version 5. you use version %s', v) } if(mv < 5)
328
+
329
+ team_id = validate( params, required: true , var: 'team_id' )
330
+ user_id = validate( params, required: false, var: 'user_id' )
331
+
332
+ if( user_id.is_a?(String) && user_id.is_a?(Integer) )
333
+ raise ArgumentError.new(format('wrong type. user \'user_id\' must be an String (for an User name) or an Integer (for an User Id), given \'%s\'', user_id.class.to_s))
334
+ end
335
+ raise ArgumentError.new('missing \'user_id\'') if( user_id.size.zero? )
336
+
337
+ if(team_id.is_a?(String))
338
+ o_team = search_team(name: team_id)
339
+ status = o_team.dig('status')
340
+ total_count = o_team.dig('totalCount')
341
+
342
+ return { 'status' => 404, 'message' => format( 'No Team \'%s\' found', team_id) } unless(status == 200 && total_count > 0)
343
+
344
+ teams = o_team.dig('teams')
345
+ team = teams.detect { |x| x['name'] == team_id }
346
+
347
+ team_id = team.dig('id')
348
+ end
349
+
350
+ if(user_id.is_a?(String))
351
+ usr = user(user_id)
352
+ status = usr.dig('status')
353
+ usr_id = usr.dig('id')
354
+
355
+ return { 'status' => 404, 'message' => format( 'No User \'%s\' found', user_id) } unless(status == 200)
356
+ end
357
+
358
+ endpoint = format( '/api/teams/%d/members/%d', team_id, usr_id )
359
+
360
+ delete(endpoint)
361
+ end
362
+
363
+ end
364
+ end