razor-client 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/NEWS.md +18 -0
  2. data/bin/razor +9 -4
  3. data/lib/razor/cli.rb +10 -0
  4. data/lib/razor/cli/document.rb +59 -0
  5. data/lib/razor/cli/format.rb +70 -20
  6. data/lib/razor/cli/navigate.rb +114 -25
  7. data/lib/razor/cli/parse.rb +79 -10
  8. data/lib/razor/cli/transforms.rb +39 -0
  9. data/lib/razor/cli/version.rb +46 -0
  10. data/lib/razor/cli/views.rb +25 -0
  11. data/lib/razor/cli/views.yaml +189 -0
  12. data/spec/cli/format_spec.rb +77 -0
  13. data/spec/cli/navigate_spec.rb +102 -2
  14. data/spec/cli/parse_spec.rb +20 -0
  15. data/spec/fixtures/vcr/Razor_CLI_Navigate/argument_formatting/should_allow_spaces.yml +966 -0
  16. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_--help_command_.yml +99 -0
  17. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_-h_command_.yml +99 -0
  18. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_--help_.yml +99 -0
  19. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_-h_.yml +99 -0
  20. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_command_help_.yml +99 -0
  21. data/spec/fixtures/vcr/Razor_CLI_Navigate/for_command_help/should_provide_command_help_for_razor_help_command_.yml +99 -0
  22. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_authentication/should_preserve_that_across_navigation.yml +9 -42
  23. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_authentication/should_supply_that_to_the_API_service.yml +5 -5
  24. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_invalid_parameter/should_fail_with_bad_JSON.yml +228 -0
  25. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_invalid_parameter/should_fail_with_malformed_argument.yml +120 -0
  26. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_an_array_into_an_existing_array.yml +2006 -0
  27. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_the_arguments_as_an_array.yml +2006 -0
  28. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_array/should_merge_the_arguments_into_an_existing_array.yml +2006 -0
  29. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_construct_a_json_object.yml +234 -0
  30. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_fail_with_mixed_types_array_then_hash_.yml +228 -0
  31. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_multiple_arguments_with_same_name/combining_as_an_object/should_fail_with_mixed_types_hash_then_array_.yml +164 -0
  32. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_no_parameters/should_fail_with_bad_JSON.yml +36 -0
  33. data/spec/fixtures/vcr/Razor_CLI_Parse/_new/_help/should_print_a_list_of_known_endpoints.yml +5 -5
  34. data/spec/spec_helper.rb +12 -1
  35. data/spec/testing.md +16 -0
  36. data/spec/version_spec.rb +8 -0
  37. metadata +74 -66
  38. data/.gitignore +0 -7
  39. data/.yardopts +0 -2
  40. data/Gemfile +0 -35
  41. data/Gemfile.lock +0 -53
  42. data/Rakefile +0 -37
  43. data/lib/razor/cli/navigate.yaml +0 -28
  44. data/razor-client.gemspec +0 -32
  45. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_a_single_item_path/.yml +0 -69
  46. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_an_invalid_path/.yml +0 -36
  47. data/spec/fixtures/vcr/Razor_CLI_Navigate/with_no_path/.yml +0 -36
  48. data/spec/fixtures/vcr/Razor_CLI_Parse/_new/_help/.yml +0 -36
data/NEWS.md ADDED
@@ -0,0 +1,18 @@
1
+ # Razor Client Release Notes
2
+
3
+ ## 0.15.0 - 2014-05-22
4
+
5
+ Usability of the client has been greatly enhanced:
6
+
7
+ * Tabular views of most collections: things like 'razor nodes' now display
8
+ a table of results with important details about each node.
9
+ * Get help on commands via `razor help COMMAND`
10
+ * Output now includes hints on how to get more details on the things displayed
11
+ * No need to enter JSON on the command line for most commands (all but
12
+ create-tag)
13
+ + arrays can now be entered by repeating the same option, e.g. `razor
14
+ create-tag --name ... --tag t1 --tag t2`
15
+ + broker configuration is set using `razor create-broker --name
16
+ .. --configuration var1=value1 --configuration var2=value2 ...`
17
+ * Clearer error message when server responds with 'Unauthorized'
18
+ (RAZOR-175)
data/bin/razor CHANGED
@@ -27,7 +27,12 @@ rescue OptionParser::InvalidOption => e
27
27
  die e.message + "\nTry 'razor --help' for more information"
28
28
  end
29
29
 
30
- if parse.show_help?
30
+ if parse.show_version?
31
+ puts parse.version
32
+ exit 0
33
+ end
34
+
35
+ if parse.show_help? and not parse.show_command_help?
31
36
  puts parse.help
32
37
  exit 0
33
38
  end
@@ -35,9 +40,7 @@ end
35
40
  begin
36
41
  document = parse.navigate.get_document
37
42
  url = parse.navigate.last_url
38
- puts "From #{url}:\n\n#{format_document document}\n\n"
39
- rescue Razor::CLI::Error => e
40
- die "#{e}\n#{parse.help}\n\n"
43
+ puts "From #{url}:\n\n#{format_document document, parse}\n\n"
41
44
  rescue SocketError, Errno::ECONNREFUSED => e
42
45
  puts "Error: Could not connect to the server at #{parse.api_url}"
43
46
  puts " #{e}\n"
@@ -58,4 +61,6 @@ rescue RestClient::Exception => e
58
61
  puts r.body
59
62
  end
60
63
  die
64
+ rescue StandardError => e
65
+ die "#{e.message}\nTry 'razor --help' for more information\n\n"
61
66
  end
@@ -26,9 +26,19 @@ module Razor
26
26
  end
27
27
  end
28
28
 
29
+ class VersionCompatibilityError < Error
30
+ def initialize(reason)
31
+ super "Server version is not compatible with client version: #{reason}"
32
+ end
33
+ end
34
+
29
35
  end
30
36
  end
31
37
 
38
+ require_relative 'cli/version'
32
39
  require_relative 'cli/navigate'
33
40
  require_relative 'cli/parse'
34
41
  require_relative 'cli/format'
42
+ require_relative 'cli/document'
43
+ require_relative 'cli/views'
44
+ require_relative 'cli/transforms'
@@ -0,0 +1,59 @@
1
+ require 'forwardable'
2
+
3
+ module Razor::CLI
4
+ class Document
5
+ extend Forwardable
6
+ attr_reader 'spec', 'items', 'format_view', 'original_items'
7
+ def initialize(doc, format_type)
8
+ if doc['spec'].is_a?(Array)
9
+ @spec, @remaining_navigation = doc['spec']
10
+ else
11
+ @spec = doc['spec']
12
+ end
13
+ @command = doc['command']
14
+ if doc.has_key?('items')
15
+ @type = :list
16
+ else
17
+ @type = :single
18
+ end
19
+ @items = doc['items'] || Array[doc]
20
+ @format_view = Razor::CLI::Views.find_formatting(@spec, format_type, @remaining_navigation)
21
+
22
+ # Untransformed and unordered for displaying nested views.
23
+ @original_items = @items
24
+ @items = hide_or_transform_elements!(items, format_view)
25
+ end
26
+
27
+ def is_list?
28
+ @type == :list
29
+ end
30
+
31
+ private
32
+ # This method:
33
+ # - rearranges columns per Razor::CLI::Views.
34
+ # - hides columns per Razor::CLI::Views.
35
+ # - transforms data using both Razor::CLI::Views and its `TRANSFORMS`.
36
+ def hide_or_transform_elements!(items, format_view)
37
+ if format_view.has_key?('+show')
38
+ items.map do |item|
39
+ Hash[
40
+ format_view['+show'].map do |item_format_spec|
41
+ # Allow both '+column' as overrides.
42
+ item_spec = (item_format_spec[1] or {})
43
+ item_label = item_format_spec[0]
44
+ item_column = (item_spec['+column'] or item_label)
45
+ value = Razor::CLI::Views.transform(item[item_column], item_spec['+format'])
46
+
47
+ [item_label, value]
48
+ end
49
+ ].tap do |hash|
50
+ # Re-add the special 'command' key and value if the key isn't already there.
51
+ hash['command'] = @command if @command and not hash.has_key?('command')
52
+ end
53
+ end
54
+ else
55
+ items
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,8 +1,10 @@
1
+ require 'forwardable'
1
2
  require 'terminal-table'
2
3
 
3
4
  module Razor::CLI
4
5
  module Format
5
- PriorityKeys = %w[ id name ]
6
+ extend Forwardable
7
+ PriorityKeys = %w[ id name spec ]
6
8
  SpecNames = {
7
9
  "/spec/object/policy" => "Policy",
8
10
  "/spec/object/tag" => "Tag",
@@ -16,12 +18,41 @@ module Razor::CLI
16
18
  spec
17
19
  end
18
20
 
19
- def format_document(doc)
20
- case doc
21
- when Array then format_objects(doc)
22
- when Hash then format_object(doc)
23
- else doc.to_s
24
- end.chomp
21
+ def format_document(doc, parse = nil)
22
+ format = parse.format
23
+ arguments = parse.args
24
+ doc = Razor::CLI::Document.new(doc, format)
25
+
26
+ return "There are no items for this query." if doc.items.empty?
27
+ return format_objects(doc.items).chomp if parse.show_command_help?
28
+
29
+ case (doc.format_view['+layout'] or 'list')
30
+ when 'list'
31
+ format_objects(doc.items) + String(additional_details(doc, arguments)).chomp
32
+ when 'table'
33
+ case doc.items
34
+ when Array then
35
+ get_table(doc.items, doc.format_view) + String(additional_details(doc, arguments))
36
+ else doc.to_s
37
+ end
38
+ else
39
+ raise ArgumentError, "Unrecognized view format #{doc.format_view['+layout']}"
40
+ end
41
+ end
42
+
43
+ private
44
+ def get_table(doc, formatting)
45
+ # Use the formatting if it exists, otherwise build from the data.
46
+ headings = (formatting['+show'] and formatting['+show'].keys or [])
47
+ Terminal::Table.new do |t|
48
+ t.rows = doc.map do |page|
49
+ page.map do |item|
50
+ headings << item[0] unless headings.include? item[0]
51
+ item[1]
52
+ end
53
+ end
54
+ t.headings = headings
55
+ end.to_s
25
56
  end
26
57
 
27
58
  # We assume that all collections are homogenous
@@ -31,21 +62,16 @@ module Razor::CLI
31
62
  end.join "\n\n"
32
63
  end
33
64
 
34
- def format_reference_object(ref, indent = 0)
35
- output = ' '* indent + "#{ref['name']} => #{ref['id'].to_s.ljust 4}"
36
- end
37
-
38
-
39
65
  def format_object(object, indent = 0)
40
- if object.keys == ["id", "name"]
41
- format_reference_object(object, indent)
66
+ if object.has_key?('help') and object.has_key?('name')
67
+ object['help']['full']
42
68
  else
43
69
  format_default_object(object, indent)
44
70
  end
45
71
  end
46
72
 
47
73
  def format_default_object(object, indent = 0 )
48
- fields = (PriorityKeys & object.keys) + (object.keys - PriorityKeys)
74
+ fields = display_fields(object)
49
75
  key_indent = indent + fields.map {|f| f.length}.max
50
76
  output = ""
51
77
  fields.map do |f|
@@ -59,11 +85,13 @@ module Razor::CLI
59
85
  "\n" + format_object(value, key_indent + 4).rstrip
60
86
  end
61
87
  when Array
62
- if value.all? { |v| v.is_a?(String) }
63
- "[" + value.map(&:inspect).join(",") + "]"
64
- else
65
- "[\n" + format_objects(value, key_indent + 6) + ("\n"+' '*(key_indent+4)+"]")
66
- end
88
+ if value.all? { |v| v.is_a?(String) }
89
+ "[" + value.map(&:to_s).join(",") + "]"
90
+ else
91
+ "[\n" + format_objects(value, key_indent + 6) + ("\n"+' '*(key_indent+4)+"]")
92
+ end
93
+ when String
94
+ value
67
95
  else
68
96
  case f
69
97
  when "spec" then "\"#{Format.spec_name(value)}\""
@@ -72,5 +100,27 @@ module Razor::CLI
72
100
  end
73
101
  end.join "\n"
74
102
  end
103
+
104
+ def display_fields(object)
105
+ keys = object.respond_to?(:keys) ? object.keys : []
106
+ (PriorityKeys & keys) + (keys - PriorityKeys) - ['+spec']
107
+ end
108
+
109
+ def additional_details(doc, arguments)
110
+ objects = doc.original_items
111
+ # If every element has the 'name' key, it has nested elements.
112
+ if doc.is_list? and objects.all? { |it| it.is_a?(Hash) && it.has_key?('name')}
113
+ "\n\nQuery an entry by including its name, e.g. `razor #{arguments.join(' ')} #{objects.first['name']}`"
114
+ elsif objects.any?
115
+ object = objects.first
116
+ fields = display_fields(object) - PriorityKeys
117
+ list = fields.select do |f|
118
+ object[f].is_a?(Hash) or object[f].is_a?(Array)
119
+ end.sort
120
+ if list.any?
121
+ "\n\nQuery additional details via: `razor #{arguments.join(' ')} [#{list.join(', ')}]`"
122
+ end
123
+ end
124
+ end
75
125
  end
76
126
  end
@@ -1,9 +1,11 @@
1
1
  require 'rest-client'
2
2
  require 'multi_json'
3
3
  require 'yaml'
4
+ require 'forwardable'
4
5
 
5
6
  module Razor::CLI
6
7
  class Navigate
8
+ extend Forwardable
7
9
 
8
10
  def initialize(parse, segments)
9
11
  @parse = parse
@@ -28,6 +30,10 @@ module Razor::CLI
28
30
  entrypoint["commands"]
29
31
  end
30
32
 
33
+ def server_version
34
+ entrypoint.has_key?('version') and entrypoint['version']['server'] or 'Unknown'
35
+ end
36
+
31
37
  def query?
32
38
  collections.any? { |coll| coll["name"] == @segments.first }
33
39
  end
@@ -40,6 +46,8 @@ module Razor::CLI
40
46
  !! command(@segments.first)
41
47
  end
42
48
 
49
+ def_delegator '@parse', 'show_command_help?'
50
+
43
51
  def get_document
44
52
  if @segments.empty?
45
53
  entrypoint
@@ -48,15 +56,18 @@ module Razor::CLI
48
56
  while @segments.any?
49
57
  move_to @segments.shift
50
58
  end
59
+
60
+ # Get the next level if it's a list of objects.
61
+ if @doc.is_a?(Hash) and @doc['items'].is_a?(Array)
62
+ @doc['items'] = @doc['items'].map do |item|
63
+ item.is_a?(Hash) && item.has_key?('id') ? json_get(item['id']) : item
64
+ end
65
+ end
51
66
  @doc
52
67
  elsif command?
53
68
  # @todo lutter 2013-08-16: None of this has any tests, and error
54
69
  # handling is heinous at best
55
70
  cmd, body = extract_command
56
- if body.empty?
57
- raise Razor::CLI::Error,
58
- "No arguments for command (did you forget --json ?)"
59
- end
60
71
  # Ensure that we copy authentication data from our previous URL.
61
72
  url = cmd["id"]
62
73
  if @doc_url
@@ -64,7 +75,18 @@ module Razor::CLI
64
75
  url.userinfo = @doc_url.userinfo
65
76
  end
66
77
 
67
- json_post(url, body)
78
+ if show_command_help?
79
+ json_get(url)
80
+ else
81
+ if body.empty?
82
+ raise Razor::CLI::Error,
83
+ "No arguments for command (did you forget --json ?)"
84
+ end
85
+ result = json_post(url, body)
86
+ # Get actual object from the id.
87
+ result = result.merge(json_get(result['id'])) if result['id']
88
+ result
89
+ end
68
90
  else
69
91
  raise NavigationError.new(@doc_url, @segments, @doc)
70
92
  end
@@ -72,12 +94,36 @@ module Razor::CLI
72
94
 
73
95
  def extract_command
74
96
  cmd = command(@segments.shift)
97
+ @cmd_url = cmd['id']
75
98
  body = {}
76
99
  until @segments.empty?
77
- if @segments.shift =~ /\A--([a-z-]+)(=(\S+))?\Z/
100
+ argument = @segments.shift
101
+ if argument =~ /\A--([a-z-]+)(=(.+))?\Z/
102
+ # `--arg=value` or `--arg value`
78
103
  arg, value = [$1, $3]
79
104
  value = @segments.shift if value.nil? && @segments[0] !~ /^--/
80
- body[arg] = convert_arg(cmd["name"], arg, value)
105
+ if value =~ /\A([a-zA-Z._-]+)=(\S+)?\z/
106
+ # `--arg name=value`
107
+ unless body[arg].nil? or body[arg].is_a?(Hash)
108
+ # Error: `--arg value --arg name=value`
109
+ raise ArgumentError, "Cannot handle mixed types for argument #{arg}"
110
+ end
111
+ # Do not convert, assume the above is the conversion.
112
+ body[arg] = (body[arg].nil? ? {} : body[arg]).merge($1 => $2)
113
+ elsif body[arg].is_a?(Hash)
114
+ # Error: `--arg name=value --arg value`
115
+ raise ArgumentError, "Cannot handle mixed types for argument #{arg}"
116
+ else
117
+ value = convert_arg(cmd["name"], arg, value)
118
+ if body[arg].nil?
119
+ body[arg] = value
120
+ else
121
+ # Either/both `body[arg]` or/and `value` might be an array at this point.
122
+ body[arg] = Array(body[arg]) + Array(value)
123
+ end
124
+ end
125
+ else
126
+ raise ArgumentError, "Unexpected argument #{argument}"
81
127
  end
82
128
  end
83
129
 
@@ -89,15 +135,16 @@ module Razor::CLI
89
135
  raise Razor::CLI::Error, "File #{body["json"]} not found"
90
136
  rescue Errno::EACCES
91
137
  raise Razor::CLI::Error,
92
- "Permission to read file #{body["json"]} denied"
138
+ "Permission to read file #{body["json"]} denied"
93
139
  end
94
140
  [cmd, body]
95
141
  end
96
142
 
97
143
  def move_to(key)
98
- key = key.to_i if key.to_i.to_s == key
99
144
  if @doc.is_a? Array
100
145
  obj = @doc.find {|x| x.is_a?(Hash) and x["name"] == key }
146
+ elsif @doc.is_a?(Hash) && @doc['items'].is_a?(Array)
147
+ obj = @doc['items'].find {|x| x.is_a?(Hash) and x["name"] == key }
101
148
  elsif @doc.is_a? Hash
102
149
  obj = @doc[key]
103
150
  end
@@ -112,13 +159,27 @@ module Razor::CLI
112
159
  end
113
160
 
114
161
  @doc = json_get(url)
115
- # strip the wrapper around collections
116
- if @doc.is_a? Hash and @doc["items"].is_a? Array
117
- @doc = @doc["items"]
118
- end
119
162
  @doc_url = url
120
- elsif obj.is_a? Hash
163
+ elsif obj.is_a?(Hash) && obj['spec']
121
164
  @doc = obj
165
+ elsif obj.is_a?(Hash)
166
+ # No spec string; use parent's and remember extra navigation.
167
+ if @doc['+spec'].is_a?(Array)
168
+ # Something's been added.
169
+ @doc['+spec'] << key
170
+ elsif @doc['+spec'].nil? || @doc['+spec'].is_a?(String)
171
+ @doc['+spec'] = [@doc['spec'], key]
172
+ end
173
+ @doc = obj.merge({'+spec' => @doc['+spec']})
174
+ elsif obj.is_a?(Array)
175
+ # No spec string; use parent's and remember extra navigation.
176
+ if @doc['+spec'].is_a?(Array)
177
+ # Something's already been added.
178
+ @doc['+spec'] << key
179
+ elsif @doc['+spec'].nil? || @doc['+spec'].is_a?(String)
180
+ @doc['+spec'] = [@doc['spec'], key]
181
+ end
182
+ @doc = {'+spec' => @doc['+spec'], 'items' => obj}
122
183
  else
123
184
  @doc = nil
124
185
  end
@@ -133,7 +194,7 @@ module Razor::CLI
133
194
  def json_get(url, headers = {})
134
195
  response = get(url,headers.merge(:accept => :json))
135
196
  unless response.headers[:content_type] =~ /application\/json/
136
- raise "Received content type #{response.headers[:content_type]}"
197
+ raise "Received content type #{response.headers[:content_type]}"
137
198
  end
138
199
  MultiJson.load(response.body)
139
200
  end
@@ -152,25 +213,53 @@ module Razor::CLI
152
213
  end
153
214
 
154
215
  private
155
- def self.annotations
156
- @@annotations ||=
157
- YAML::load_file(File::join(File::dirname(__FILE__), "navigate.yaml"))
216
+ def cmd_schema(cmd_name)
217
+ cmd = json_get(@cmd_url)
218
+ cmd['schema'] or raise VersionCompatibilityError, 'Server must supply the expected datatypes for command arguments'
158
219
  end
159
220
 
160
- def self.arg_type(cmd_name, arg_name)
161
- cmd = annotations["commands"][cmd_name]
162
- cmd && cmd["args"][arg_name]
221
+ def arg_type(cmd_name, arg_name)
222
+ cmd = cmd_schema(cmd_name)
223
+ cmd && cmd[arg_name] && cmd[arg_name]['type'] or nil
163
224
  end
164
225
 
165
226
  def convert_arg(cmd_name, arg_name, value)
166
227
  value = nil if value == "null"
167
- case self.class.arg_type(cmd_name, arg_name)
168
- when "json"
169
- MultiJson::load(value)
228
+
229
+ argument_type = arg_type(cmd_name, arg_name)
230
+
231
+ # This might be helpful, since there's no other method for debug-level logging on the client.
232
+ puts "Formatting argument #{arg_name} with value #{value} as #{argument_type}\n" if @parse.dump_response?
233
+
234
+ case argument_type
235
+ when "array"
236
+ # 'array' datatype arguments will never fail. At worst, they'll be wrapped in an array.
237
+ begin
238
+ MultiJson::load(value)
239
+ rescue MultiJson::LoadError => _
240
+ Array(value)
241
+ end
242
+ when "object"
243
+ begin
244
+ MultiJson::load(value)
245
+ rescue MultiJson::LoadError => error
246
+ raise ArgumentError, "Invalid JSON for argument '#{arg_name}': #{error.message}"
247
+ end
170
248
  when "boolean"
171
249
  ["true", nil].include?(value)
172
- else
250
+ when "number"
251
+ begin
252
+ Integer(value)
253
+ rescue ArgumentError
254
+ raise ArgumentError, "Invalid integer for argument '#{arg_name}': #{value}"
255
+ end
256
+ when "null"
257
+ raise ArgumentError, "Expected nothing for argument '#{arg_name}', but was: '#{value}'" unless value.nil?
258
+ nil
259
+ when "string", nil # `nil` for 'might be an alias, send as-is'
173
260
  value
261
+ else
262
+ raise Razor::CLI::Error, "Unexpected datatype '#{argument_type}' for argument #{arg_name}"
174
263
  end
175
264
  end
176
265
  end