eco-helpers 3.0.14 → 3.0.16

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -2
  3. data/eco-helpers.gemspec +15 -14
  4. data/lib/eco/api/common/people/default_parsers/date_parser.rb +6 -0
  5. data/lib/eco/api/common/session/mailer/aws_provider.rb +85 -0
  6. data/lib/eco/api/common/session/mailer/provider_base.rb +61 -0
  7. data/lib/eco/api/common/session/mailer/sendgrid_provider.rb +117 -0
  8. data/lib/eco/api/common/session/mailer.rb +42 -71
  9. data/lib/eco/api/microcases/people_refresh.rb +1 -1
  10. data/lib/eco/api/session/batch/errors.rb +2 -2
  11. data/lib/eco/api/session/batch.rb +83 -37
  12. data/lib/eco/api/session/config/api.rb +96 -37
  13. data/lib/eco/api/session/config/apis/enviro_spaces.rb +106 -0
  14. data/lib/eco/api/session/config/apis/one_off.rb +94 -0
  15. data/lib/eco/api/session/config/apis/service_up.rb +37 -0
  16. data/lib/eco/api/session/config/apis/space_helpers.rb +41 -0
  17. data/lib/eco/api/session/config/apis.rb +81 -132
  18. data/lib/eco/api/session/config.rb +21 -3
  19. data/lib/eco/api/usecases/default_cases/samples/sftp_case.rb +1 -1
  20. data/lib/eco/api/usecases/graphql/helpers/base/error_handling.rb +19 -8
  21. data/lib/eco/api/usecases/graphql/helpers/base/graphql_env.rb +1 -0
  22. data/lib/eco/api/usecases/graphql/samples/location/command/dsl.rb +3 -7
  23. data/lib/eco/api/usecases/graphql/samples/location/command/service/tree_update.rb +6 -2
  24. data/lib/eco/cli/config/options_set.rb +10 -7
  25. data/lib/eco/cli/scripting/args_helpers.rb +18 -9
  26. data/lib/eco/cli_default/options.rb +8 -0
  27. data/lib/eco/cli_default/people.rb +3 -3
  28. data/lib/eco/language/basic_logger.rb +4 -2
  29. data/lib/eco/version.rb +1 -1
  30. metadata +37 -16
@@ -0,0 +1,37 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Config
5
+ class Apis
6
+ module ServiceUp
7
+ class << self
8
+ def included(base)
9
+ target_class = Eco::API::Session::Config::Apis
10
+ msg = "To be included in #{target_class}. "
11
+ msg << "Included in #{base}"
12
+ raise msg unless base <= target_class
13
+
14
+ super
15
+ end
16
+ end
17
+
18
+ def service_up?
19
+ @api_test ||=
20
+ Session::Config::Api.
21
+ api_class(active_api.version).
22
+ new(
23
+ 'foobar',
24
+ host: active_api.host,
25
+ logger: ::Logger.new(IO::NULL)
26
+ )
27
+
28
+ status = @api_test.client.get('/policy_groups').status
29
+ # 401 Unauthorized "Permission denied. API key may be invalid."
30
+ status == 401
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ module Eco
2
+ module API
3
+ class Session
4
+ class Config
5
+ class Apis
6
+ module SpaceHelpers
7
+ def space_option?
8
+ SCR.get_arg('-space')
9
+ end
10
+
11
+ def space_option
12
+ :default.then do |default|
13
+ value = SCR.get_arg('-space', with_param: true)
14
+
15
+ value = nil if value.to_s.strip.empty?
16
+ value = to_space(value) unless value.nil?
17
+ next default unless value
18
+
19
+ value
20
+ end
21
+ end
22
+
23
+ def set_options_space!(space)
24
+ ASSETS.cli.options.deep_merge!(api: {space: space})
25
+ end
26
+
27
+ private
28
+
29
+ def to_space(...)
30
+ Session::Config::Api.to_space(...)
31
+ end
32
+
33
+ def full_name(...)
34
+ Session::Config::Api.full_name(...)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -3,182 +3,131 @@ module Eco
3
3
  class Session
4
4
  class Config
5
5
  class Apis < BaseConfig
6
- def apis
7
- self["apis"] ||= {}
8
- end
9
-
10
- def apis?
11
- apis.keys.length > 0
12
- end
13
-
14
- def enviros
15
- apis.keys
16
- end
6
+ class UndefinedApi < ArgumentError; end
7
+
8
+ require_relative 'apis/space_helpers'
9
+ require_relative 'apis/enviro_spaces'
10
+ require_relative 'apis/one_off'
11
+ require_relative 'apis/service_up'
12
+
13
+ include EnviroSpaces
14
+ include OneOff
15
+ include ServiceUp
16
+
17
+ def add(
18
+ name,
19
+ key:,
20
+ host:,
21
+ space: nil,
22
+ version: :internal,
23
+ mode: :local,
24
+ user_key: nil,
25
+ external_key: nil,
26
+ email: nil,
27
+ pass: nil,
28
+ org_id: nil
29
+ )
30
+ # ensure that space_option isn't used
31
+ space = to_space(space)
32
+
33
+ msg = "WARNING: Overriding #{full_name(name, space: space)} API credentials"
34
+ puts msg if self.defined?(name, space: space)
35
+
36
+ apis(space)[name] = Session::Config::Api.new(
37
+ name,
38
+ space: space,
39
+ key: key,
40
+ host: host,
41
+ version: version,
42
+ mode: mode,
43
+ root: self,
44
+ user_key: user_key,
45
+ external_key: external_key,
46
+ email: email,
47
+ pass: pass,
48
+ org_id: org_id
49
+ )
17
50
 
18
- def defined?(name)
19
- apis.key?(name)
51
+ self
20
52
  end
21
53
 
22
- def any_defined?(*names)
23
- [names].flatten.any? do |name|
24
- self.defined?(name)
25
- end
54
+ # the active environment
55
+ def active_root_name
56
+ active_name
26
57
  end
27
58
 
28
- def add(name, key:, host:, version: :internal,
29
- mode: :local, user_key: nil, external_key: nil,
30
- email: nil, pass: nil, org_id: nil)
31
- apis[name] = Session::Config::Api.new(
32
- name,
33
- key: key,
34
- host: host,
35
- version: version,
36
- mode: mode,
37
- root: self,
38
- user_key: user_key,
39
- external_key: external_key,
40
- email: email,
41
- pass: pass,
42
- org_id: org_id
43
- )
44
- self
59
+ def active_name
60
+ self['active-name']
45
61
  end
46
62
 
47
63
  def active_api
48
- self["active-api"]
64
+ self['active-api']
49
65
  end
50
66
 
51
67
  def active_name=(name)
52
- raise "'#{name}' Api environment not defined" if !apis[name]
53
- self["active-name"] = name
54
- self["active-api"] = apis[name]
55
- self
68
+ set_active_name(name)
56
69
  end
57
70
 
58
- def active_name
59
- self["active-name"]
60
- end
71
+ def set_active_name(name, space: space_option)
72
+ space ||= space_option
61
73
 
62
- # the active environment
63
- def active_root_name
64
- active_name
65
- end
74
+ msg = missing_api_message(name, space: space)
75
+ raise UndefinedApi, msg unless self.api?(name, space: space)
66
76
 
67
- def service_up?
68
- @api_test ||= Session::Config::Api.api_class(active_api.version).new("foobar", host: active_api.host, logger: ::Logger.new(IO::NULL))
69
- status = @api_test.client.get("/policy_groups").status
70
- # 401 Unauthorized "Permission denied. API key may be invalid."
71
- status == 401
77
+ self['active-name'] = name
78
+ self['active-api'] = apis(space)[name]
79
+ set_options_space!(active_space)
80
+
81
+ self
72
82
  end
73
83
 
74
84
  def api(logger = ::Logger.new(IO::NULL), version: nil)
75
- unless active_api
76
- raise "There's no 'active_api'. Use apis.active_name='api_enviro_name' to set the active api"
77
- end
85
+ msg = "There's no 'active_api'. "
86
+ msg << "To set the target environment, please use either of:\n"
87
+ msg << " * apis.set_active_name('api_enviro_name', space: 'the-space')\n"
88
+ msg << " * apis.active_name='api_enviro_name'\n"
89
+ raise msg unless active_api
90
+
78
91
  active_api.api(version: version, logger: logger)
79
92
  end
80
93
 
81
94
  def default_user_key=(key)
82
- self["user_key"] = key
95
+ self['user_key'] = key
83
96
  end
84
97
 
85
98
  def default_user_key
86
- self["user_key"]
99
+ self['user_key']
87
100
  end
88
101
 
89
102
  def default_email=(email)
90
- self["default_email"] = email
103
+ self['default_email'] = email
91
104
  end
92
105
 
93
106
  def default_email
94
- self["default_email"] || ENV['USER_EMAIL']
107
+ self['default_email'] || ENV['USER_EMAIL']
95
108
  end
96
109
 
97
110
  def default_pass=(pass)
98
- self["default_pass"] = pass
111
+ self['default_pass'] = pass
99
112
  end
100
113
 
101
114
  def default_pass
102
- self["default_pass"] || ENV['USER_PASS']
115
+ self['default_pass'] || ENV['USER_PASS']
103
116
  end
104
117
 
105
118
  # Method to support CLI one-off API requests
106
119
  def one_off
107
- if one_off?
108
- add(one_off_org, key: one_off_key, host: "#{one_off_enviro}.ecoportal.com")
109
- return one_off_org
110
- end
111
- end
112
-
113
- private
114
-
115
- def one_off?
116
- @is_one_off ||= SCR.get_arg("-api-key") || SCR.get_arg("-one-off")
117
- end
120
+ return unless one_off?
118
121
 
119
- def one_off_key
120
- return @one_off_key if instance_variable_defined?(:@one_off_key)
121
- if one_off?
122
- Dotenv.load("./.env_one_off")
123
- SCR.get_arg("-api-key", with_param: true).yield_self do |key|
124
- one_off_key_env(key)
125
- end
126
- end
127
- end
128
-
129
- def one_off_key_env(key)
130
- if one_off?
131
- if key
132
- env_file_set_var("./.env_one_off", one_off_key_env_var, key)
133
- key
134
- else
135
- Dotenv.load("./.env_one_off")
136
- ENV[one_off_key_env_var].tap do |k|
137
- raise "At least the first time, you should provide the -api-key" unless k
138
- end
139
- end
140
- end
141
- end
142
-
143
- def one_off_key_env_var
144
- @one_off_key_env_var ||= "#{one_off_org}_KEY"
145
- end
122
+ name = one_off_org
146
123
 
147
- def one_off_org
148
- return @one_off_org if instance_variable_defined?(:@one_off_org)
149
- unless org = SCR.get_arg("-org", with_param: true)
150
- raise("You should specify -org NAME when using -api-key or -one-off")
151
- end
152
- @one_off_org ||= "#{org.downcase.split(/[^a-z]+/).join("_")}_#{one_off_enviro.gsub(".", "_")}".to_sym
153
- end
154
-
155
- def one_off_enviro
156
- return @one_off_enviro if instance_variable_defined?(:@one_off_enviro)
157
- enviro = SCR.get_arg("-enviro") ? SCR.get_arg("-enviro", with_param: true) : "live"
158
- @one_off_enviro ||= enviro.downcase
159
- end
124
+ add(
125
+ name,
126
+ key: one_off_key,
127
+ host: "#{one_off_enviro}.ecoportal.com"
128
+ )
160
129
 
161
- def env_file_set_var(file, var, value)
162
- begin
163
- pattern = /"#{var}=(?<value>[^ \r\n]+)"/
164
- File.open(file, "w+") do |fd|
165
- found = false
166
- fd.each_line do |line|
167
- if match = line.match(pattern)
168
- found = true
169
- # IO::SEEK_CUR => Seeks to _amount_ plus current position
170
- fd.seek(-(line.length + 1), IO::SEEK_CUR)
171
- fd.write line.gsub(match[:value], value)
172
- end
173
- end
174
-
175
- fd << "#{var}=#{value}" unless found
176
- end
177
- rescue StandardError => e
178
- puts "#{e}"
179
- return false
180
- end
181
- return true
130
+ name
182
131
  end
183
132
  end
184
133
  end
@@ -97,18 +97,31 @@ module Eco
97
97
  apis.apis?
98
98
  end
99
99
 
100
+ ApiDefChain = Struct.new(:root, :name, :params) do
101
+ def add_space(space, **kargs, &block)
102
+ kargs = (params || {}).merge(kargs).merge(space: space)
103
+ root.add_api(name, **kargs, &block)
104
+ end
105
+ end
106
+
100
107
  # @param (see Eco::API::Session::Config::Apis#add)
101
108
  # @return [Eco::API::Session::Config] this configuration
102
109
  def add_api(name, **kargs)
103
110
  apis.add(name, **kargs)
104
- self
111
+
112
+ if block_given?
113
+ params = kargs.merge({space: :sub_other})
114
+ yield(ApiDefChain.new(self, name, params))
115
+ end
116
+
117
+ ApiDefChain.new(self, name, {space: :other})
105
118
  end
106
119
 
107
120
  # Set the active api by `name`
108
121
  # @see Eco::API::Session::Config::Apis#active_api=
109
122
  # @return [Eco::API::Session::Config] this configuration
110
- def active_api(name)
111
- apis.active_name = name
123
+ def active_api(name, space: nil)
124
+ apis.set_active_name(name, space: space)
112
125
  self
113
126
  end
114
127
 
@@ -117,6 +130,11 @@ module Eco
117
130
  apis.active_root_name
118
131
  end
119
132
 
133
+ # @see Eco::API::Session::Config::Apis#active_space
134
+ def active_enviro_space
135
+ apis.active_space
136
+ end
137
+
120
138
  # @see Eco::API::Session::Config::Apis#api
121
139
  # @return [Eco::API::Session::Config::Api] the currently active api
122
140
  def api(logger = ::Logger.new(IO::NULL), version: nil)
@@ -71,7 +71,7 @@ class Eco::API::UseCases::DefaultCases::Samples::Sftp < Eco::API::Common::Loader
71
71
  end
72
72
 
73
73
  def to_remote_path(file)
74
- File.join(remote_folder, file)
74
+ [remote_folder, file].compact.join('/')
75
75
  end
76
76
 
77
77
  def local_folder
@@ -3,7 +3,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Base
3
3
  module ErrorHandling
4
4
  include Eco::Language::AuxiliarLogger
5
5
 
6
- attr_reader :exception
6
+ attr_reader :exception, :exiting
7
7
 
8
8
  private
9
9
 
@@ -19,6 +19,14 @@ module Eco::API::UseCases::GraphQL::Helpers::Base
19
19
  exception && !interrupted?
20
20
  end
21
21
 
22
+ def exiting?
23
+ @exiting
24
+ end
25
+
26
+ def exception?
27
+ error_raised? || exiting?
28
+ end
29
+
22
30
  def rescued
23
31
  yield
24
32
  rescue StandardError => err
@@ -27,24 +35,27 @@ module Eco::API::UseCases::GraphQL::Helpers::Base
27
35
  unless exception_already_captured?(err)
28
36
  log(:error) { err.patch_full_message }
29
37
  end
38
+ rescue SystemExit
39
+ @exiting = true
40
+ raise
30
41
  end
31
42
 
32
43
  def with_error_handling
33
44
  @exception = nil
34
45
  yield
35
- rescue SystemExit => sext
36
- @exception = sext
37
- exit sext.status
38
- rescue *interrupt_errors => int
46
+ rescue StandardError, SignalException => err
47
+ @exception = err
48
+ raise
49
+ rescue SystemExit
50
+ @exiting = true
51
+ raise
52
+ rescue *interrupt_errors
39
53
  @exception = int
40
54
  raise
41
55
  rescue SystemStackError
42
56
  puts $! # rubocop:disable Style/SpecialGlobalVars
43
57
  puts caller[0..100]
44
58
  raise
45
- rescue StandardError, SignalException => err
46
- @exception = err
47
- raise
48
59
  end
49
60
 
50
61
  def interrupt_errors
@@ -8,6 +8,7 @@ module Eco::API::UseCases::GraphQL::Helpers::Base
8
8
  def graphql
9
9
  msg = "The credentials or basic graphql config are missing for the active environment"
10
10
  raise msg unless session.api?(version: :graphql)
11
+
11
12
  @graphql ||= session.api(version: :graphql)
12
13
  end
13
14
  end
@@ -49,15 +49,11 @@ class Eco::API::UseCases::GraphQL::Samples::Location
49
49
  end
50
50
 
51
51
  break if error
52
+ rescue StandardError => err
53
+ log(:error) { err.patch_full_message }
54
+ raise
52
55
  end
53
56
  end
54
- rescue SystemStackError
55
- puts $! # rubocop:disable Style/SpecialGlobalVars
56
- puts caller[0..100]
57
- raise
58
- rescue StandardError => err
59
- log(:error) { err.patch_full_message }
60
- raise
61
57
  ensure
62
58
  rescued { self.tags_remap_csv_file = generate_tags_remap_csv }
63
59
  rescued { close_handling_tags_remap_csv }
@@ -21,7 +21,7 @@ class Eco::API::UseCases::GraphQL::Samples::Location
21
21
  super
22
22
  end
23
23
  ensure
24
- rescued { re_archive } unless error_raised?
24
+ rescued { re_archive } unless exception?
25
25
  rescued { email_digest('TagTree Update') }
26
26
  end
27
27
 
@@ -30,7 +30,7 @@ class Eco::API::UseCases::GraphQL::Samples::Location
30
30
  # @note this is an additional necessary step
31
31
  def re_archive
32
32
  return if simulate?
33
- return if error_raised?
33
+ return if exception?
34
34
 
35
35
  stage = :rearchive
36
36
 
@@ -90,8 +90,12 @@ class Eco::API::UseCases::GraphQL::Samples::Location
90
90
 
91
91
  digest_msgs = logger.cache.logs(level: %i[info error warn])
92
92
  str_exception = exception ? " - Exception!" : ''
93
+ str_exception = " - Aborted!" if exiting?
94
+
93
95
  subject = "#{config.active_enviro} - #{title}#{str_exception}"
94
96
 
97
+ digest_msgs << "\n#{exception.patch_full_message(trace_count: 3)}" if exception
98
+
95
99
  session.mail(subject: subject, body: digest_msgs.join).tap do
96
100
  options.deep_merge!({worfklow: {no_email: true}})
97
101
  end
@@ -21,9 +21,11 @@ module Eco
21
21
 
22
22
  ["The following are the available options#{refinement}:"].then do |lines|
23
23
  max_len = keys_max_len(options_args(spaces)) + indent
24
+
24
25
  spaces.each do |namespace|
25
26
  is_general = (namespace == :general)
26
27
  str_indent = is_general ? "" : " " * indent
28
+
27
29
  lines << help_line(namespace, "", max_len) unless is_general
28
30
 
29
31
  options_set(namespace).select do |_arg, option| # rubocop:disable Style/HashEachMethods
@@ -32,6 +34,7 @@ module Eco
32
34
  lines << help_line("#{str_indent}#{option.name}", option.description, max_len)
33
35
  end
34
36
  end
37
+
35
38
  lines
36
39
  end.join("\n")
37
40
  end
@@ -63,13 +66,13 @@ module Eco
63
66
  raise "Missing block to define the options builder" unless block_given?
64
67
 
65
68
  opts = [option].flatten.compact
66
- unless opts.empty?
67
- callback = block
68
- opts.each do |opt|
69
- msg = "Overriding CLI option '#{option}' in '#{namespace}' CLI case / namespace"
70
- puts msg if option_exists?(opt, namespace)
71
- options_set(namespace)[opt] = OptConfig.new(opt, namespace, desc, callback)
72
- end
69
+ return self if opts.empty?
70
+
71
+ callback = block
72
+ opts.each do |opt|
73
+ msg = "Overriding CLI option '#{option}' in '#{namespace}' CLI case / namespace"
74
+ puts msg if option_exists?(opt, namespace)
75
+ options_set(namespace)[opt] = OptConfig.new(opt, namespace, desc, callback)
73
76
  end
74
77
 
75
78
  self
@@ -78,15 +78,24 @@ module Eco
78
78
 
79
79
  # @return [String, Boolean] the argument value if `with_param` or a `Boolean` if not.
80
80
  def get_arg(key, with_param: false, valid: true)
81
- # track what a known option looks like
82
- known_argument(key, with_param: with_param)
83
- return nil unless (index = get_arg_index(key))
84
- return true unless with_param
85
-
86
- value = argv[index + 1]
87
- #puts "modifier argument: #{value}"
88
- value = nil if valid && is_modifier?(value)
89
- value
81
+ case key
82
+ when Array
83
+ key.reduce(nil) do |value, k|
84
+ next value unless value.nil?
85
+
86
+ get_arg(k, with_param: with_param, valid: valid)
87
+ end
88
+ else
89
+ # track what a known option looks like
90
+ known_argument(key, with_param: with_param)
91
+ return nil unless (index = get_arg_index(key))
92
+ return true unless with_param
93
+
94
+ value = argv[index + 1]
95
+ #puts "modifier argument: #{value}"
96
+ value = nil if valid && is_modifier?(value)
97
+ value
98
+ end
90
99
  end
91
100
 
92
101
  # @return [String] the filename.
@@ -153,6 +153,14 @@ ASSETS.cli.config do |cnf| # rubocop:disable Metrics/BlockLength
153
153
  session.config.dry_run!
154
154
  end
155
155
 
156
+ desc = "Specifies the target API key space (i.e. uat, dev, etc.). "
157
+ desc << "Use with CAUTION !!!"
158
+ options_set.add('-space', desc) do |options, session|
159
+ next unless space = SCR.get_arg('-space', with_param: true)
160
+
161
+ options.deep_merge!(api: {space: space.to_sym})
162
+ end
163
+
156
164
  desc = "It specifies the type of batch to be used (default: ':batch')"
157
165
  options_set.add("-batch-mode", desc) do |options, session|
158
166
  mode_in = SCR.get_arg("-batch-mode", with_param: true)
@@ -23,7 +23,8 @@ ASSETS.cli.config do |cnf|
23
23
 
24
24
  msg = "(Optimization) "
25
25
  msg << "Switching from partial to full people download. "
26
- msg << "Input (#{input.count}) surpases MAX_GET_PARTIAL (#{MAX_GET_PARTIAL}) entries."
26
+ msg << "Input (#{input.count}) surpases MAX_GET_PARTIAL "
27
+ msg << "(#{MAX_GET_PARTIAL}) entries."
27
28
  session.log(:info) { msg }
28
29
 
29
30
  options.deep_merge!(people: {
@@ -46,8 +47,6 @@ ASSETS.cli.config do |cnf|
46
47
  elsif get_by_file
47
48
  # -people-from-backup
48
49
  session.micro.people_load(get[:file], modifier: :file)
49
- #people = JSON.parse(File.read(get[:file]))
50
- #Eco::API::Organization::People.new(people)
51
50
  else
52
51
  options.deep_merge!(people: {
53
52
  get: {
@@ -67,6 +66,7 @@ ASSETS.cli.config do |cnf|
67
66
  })
68
67
  people = session.micro.people_cache
69
68
  end
69
+
70
70
  people
71
71
  end
72
72
  end
@@ -24,6 +24,7 @@ module Eco
24
24
  def initialize(level: ::Logger::INFO, timestamp: false)
25
25
  @level = level
26
26
  self.timestamp = timestamp
27
+
27
28
  loggers[:console] = ::Logger.new($stdout).tap do |logger|
28
29
  logger.formatter = format_proc(console: true)
29
30
  logger.level = level
@@ -51,13 +52,14 @@ module Eco
51
52
  end
52
53
 
53
54
  def console_timestamp(datetime)
54
- return nil unless timestamp?
55
+ return unless timestamp?
55
56
 
56
57
  timestamp(datetime)
57
58
  end
58
59
 
59
60
  def timestamp(datetime)
60
- return nil unless datetime
61
+ return unless datetime
62
+
61
63
  str_date = datetime.strftime(self.class::TIMESTAMP_PATTERN)
62
64
  "#{str_date} > "
63
65
  end
data/lib/eco/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Eco
2
- VERSION = '3.0.14'.freeze
2
+ VERSION = '3.0.16'.freeze
3
3
  end