ecoportal-api 0.10.1 → 0.10.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84d530d29757e28159e985e70e8a0da65f16d831ea2658fcafbe94f42e98f882
4
- data.tar.gz: a4778b377de727e3c648fa79d674b044b32d452c7cc4f024d5fbb383185a3097
3
+ metadata.gz: dde26458f8ba991809f2218f6a4594101350f861a811d1bf4c6b508b2194ef67
4
+ data.tar.gz: f9bd67ceaea38ec6ea82d9a53ae0651c36fb7f74e417b37029acd7d72fd00d97
5
5
  SHA512:
6
- metadata.gz: cb0dad40ddd3d5b0b621ff61b063bf2e92522b09156035009a15e782d946d43aa9db28c0066c3db1c805c78f29a2ee550fd7c358020b7216a776624c3d31f43e
7
- data.tar.gz: c84332d17ddd19fbfbbd69f287c2110cad88064ebce801f232fcaeeccc5e6db5ec2c5ce359267d43df6bd8f4bf08238b0c242f30ef39789b92321030a914b09f
6
+ metadata.gz: '09dd4b2cde2bd8f6c6689433c7472fedb6110d076603afcd8edc39f5275aed0296470e57900eaceb335dc875f31554d515925c6b1eae060786a055ac706f6a66'
7
+ data.tar.gz: 3363216e775f38601146617ee80abab13625163c46739f536d77f94c17247fcce139331cad8af02b4cbcc0848a352211a34c476d0d766082e18b5d4b222f43cf
data/.rubocop.yml CHANGED
@@ -30,6 +30,11 @@ Style/ConditionalAssignment:
30
30
  Style/BlockDelimiters:
31
31
  BracesRequiredMethods: ['log']
32
32
  AllowedPatterns: ['proc', 'new']
33
+ Style/HashSyntax:
34
+ EnforcedShorthandSyntax: either
35
+ EnforcedStyle: no_mixed_keys
36
+ Style/ArgumentsForwarding:
37
+ UseAnonymousForwarding: false
33
38
  Style/ClassAndModuleChildren:
34
39
  Enabled: false
35
40
  Style/FrozenStringLiteralComment:
@@ -93,3 +98,5 @@ Naming/MethodParameterName:
93
98
  AllowedNames: ['x', 'y', 'i', 'j', 'id', 'io', 'to']
94
99
  Naming/RescuedExceptionsVariableName:
95
100
  Enabled: false
101
+ Naming/BlockForwarding:
102
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -2,19 +2,45 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.10.4] - 2024-10-xx
6
+
7
+ ### Added
8
+
9
+ ### Changed
10
+
11
+ ### Fixed
12
+
13
+ ## [0.10.3] - 2024-10-01
14
+
15
+ ### Changed
16
+
17
+ - refactored Client
18
+ - initialized with `deep_logging` named argumnet
19
+ - previously `response_logging`
20
+
21
+ ## [0.10.2] - 2024-09-27
22
+
23
+ ### Added
24
+
25
+ - `Ecoportal::API::Common::TimeOut` to calculate adaptive time outs.
26
+
27
+ ### Changed
28
+
29
+ - `Ecoportal::API::V1::Person#job`
30
+ - added **adaptative await**
31
+ - evaluate `true` to `complete?` if `progress` is that of total count.
32
+
5
33
  ## [0.10.1] - 2024-08-01
6
34
 
7
35
  ### Added
8
36
 
9
- - `Ecoportal::V1::Person#brand_id`
10
- - `Ecoportal::V1::Person#archived`
37
+ - `Ecoportal::API::V1::Person#brand_id`
38
+ - `Ecoportal::API::V1::Person#archived`
11
39
 
12
40
  ### Changed
13
41
 
14
42
  - require `ruby 3`
15
43
 
16
- ### Fixed
17
-
18
44
  ## [0.9.8] - 2024-05-15
19
45
 
20
46
  ### Added
@@ -344,5 +370,3 @@ All notable changes to this project will be documented in this file.
344
370
 
345
371
  - this `CHANGELOG.md` file
346
372
  - person model: `freemium` core property
347
-
348
-
@@ -1,12 +1,17 @@
1
+ # rubocop:disable Gemspec/DevelopmentDependencies
1
2
  lib = File.expand_path('lib', __dir__)
2
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "ecoportal/api/version"
4
+
5
+ require 'ecoportal/api/version'
4
6
 
5
7
  Gem::Specification.new do |spec|
6
8
  spec.name = "ecoportal-api"
7
9
  spec.version = Ecoportal::API::VERSION
8
- spec.authors = ["Tapio Saarinen"]
9
- spec.email = ["tapio@ecoportal.co.nz", "rien@ecoportal.co.nz", "oscar@ecoportal.co.nz", "bozydar@ecoportal.co.nz"]
10
+ spec.authors = ['Tapio Saarinen']
11
+ spec.email = [
12
+ 'tapio@ecoportal.co.nz',
13
+ 'oscar@ecoportal.co.nz'
14
+ ]
10
15
 
11
16
  spec.summary = %q{A collection of helpers for interacting with the ecoPortal MS's various APIs}
12
17
  spec.homepage = "https://www.ecoportal.com"
@@ -19,17 +24,21 @@ Gem::Specification.new do |spec|
19
24
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
25
  f.match(%r{^(test|spec|features)/})
21
26
  end
22
- spec.bindir = "exe"
27
+ spec.bindir = 'exe'
23
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
- spec.require_paths = ["lib"]
29
+ spec.require_paths = ['lib']
25
30
 
26
- spec.add_development_dependency "rspec", ">= 3.12.0", "< 4"
27
- spec.add_development_dependency "rake", ">= 13.1.0", "< 14"
28
- spec.add_development_dependency "yard", ">= 0.9.34", "< 1"
29
- spec.add_development_dependency "redcarpet", ">= 3.6.0", "< 4"
30
- spec.add_development_dependency "pry" , "~> 0.14"
31
+ spec.add_development_dependency 'pry' , '~> 0.14'
32
+ spec.add_development_dependency 'rake', '>= 13.0.3', '< 14'
33
+ spec.add_development_dependency 'redcarpet', '>= 3.6.0', '< 4'
34
+ spec.add_development_dependency 'rspec', '>= 3.12.0', '< 4'
35
+ spec.add_development_dependency 'rubocop', '~> 1'
36
+ spec.add_development_dependency 'rubocop-rake', '~> 0'
37
+ spec.add_development_dependency 'yard', '~> 0.9'
31
38
 
32
39
  spec.add_dependency 'dotenv', '~> 3'
33
40
  spec.add_dependency 'elastic-apm', '>= 4.7', "< 5"
34
41
  spec.add_dependency 'http', '~> 5.1', "< 6"
35
42
  end
43
+
44
+ # rubocop:enable Gemspec/DevelopmentDependencies
@@ -4,11 +4,16 @@ module Ecoportal
4
4
  class BatchOperation
5
5
  include Common::DocHelpers
6
6
 
7
- def initialize(base_path, wrapper, logger: nil)
8
- @base_path = base_path
9
- @wrapper = wrapper
10
- @operations = []
11
- @logger = logger
7
+ def initialize(base_path, wrapper, logger: nil, deep_logging: false)
8
+ @base_path = base_path
9
+ @wrapper = wrapper
10
+ @operations = []
11
+ @logger = logger
12
+ @deep_logging = deep_logging
13
+ end
14
+
15
+ def count
16
+ @operations.count
12
17
  end
13
18
 
14
19
  def as_json
@@ -21,11 +26,12 @@ module Ecoportal
21
26
 
22
27
  def process_response(response)
23
28
  unless response.success?
24
- log(:error) { "Total failure in batch operation." }
25
- raise "Total failure in batch operation"
29
+ msg = "Error: total failure in batch operation."
30
+ log(:debug) { msg }
31
+ raise msg
26
32
  end
27
33
 
28
- log(:info) { "Processing batch responses" }
34
+ log(:debug) { "Processing batch responses" } if deep_logging?
29
35
 
30
36
  body_data(response.body).each.with_index do |subresponse, idx|
31
37
  status = subresponse["status"]
@@ -106,14 +112,19 @@ module Ecoportal
106
112
  end
107
113
 
108
114
  def log_batch_response(operation, response)
109
- log(:info) { "BATCH #{operation[:method]} #{operation[:path]}" }
110
- log(:info) { "Status #{response.status}" }
115
+ return unless deep_logging?
116
+
117
+ log(:debug) { "BATCH #{operation[:method]} #{operation[:path]}" }
118
+ log(:debug) { "Status #{response.status}" }
119
+ log(:debug) { "Response: #{JSON.pretty_generate(response.body)}" }
120
+ end
111
121
 
112
- level = response.success?? :debug : :warn
113
- log(level) { "Response: #{JSON.pretty_generate(response.body)}" }
122
+ def deep_logging?
123
+ @deep_logging
114
124
  end
115
125
 
116
126
  def log(level, &block)
127
+ puts "(#{level}) #{yield}"
117
128
  @logger&.send(level, &block)
118
129
  end
119
130
  end
@@ -0,0 +1,111 @@
1
+ require 'elastic-apm'
2
+ module Ecoportal
3
+ module API
4
+ module Common
5
+ class Client
6
+ module ElasticApmIntegration
7
+ include Ecoportal::API::Common::Client::Error::Checks
8
+
9
+ APM_SERVICE_NAME = 'ecoportal-api-gem'.freeze
10
+
11
+ # Log only errors that are only server's responsibility
12
+ def log_unexpected_server_error(response)
13
+ msg = "Expecting Ecoportal::API::Common::Response. Given: #{response.class}"
14
+ raise msg unless response.is_a?(Common::Response)
15
+
16
+ return unless elastic_apm_service
17
+ return unless unexpected_server_error_code?(response.status)
18
+ return unless ElasticAPM.running?
19
+
20
+ ElasticAPM.report(
21
+ Ecoportal::API::Common::Client::Error::UnexpectedServerError.new(
22
+ response.body,
23
+ code: response.status
24
+ )
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ # finalizer to stop the agent
31
+ close_elastic_apm = proc do |_id|
32
+ next unless ElasticAPM.running?
33
+
34
+ puts "Stopping ElasticAPM service"
35
+ ElasticAPM.stop
36
+ rescue StandardError
37
+ # Silent
38
+ end
39
+
40
+ ObjectSpace.define_finalizer("ElasticAPM", close_elastic_apm)
41
+
42
+ def elastic_apm_service
43
+ return false if @disable_apm
44
+
45
+ ElasticAPM.start(**elastic_apm_options) unless ElasticAPM.running?
46
+ rescue StandardError => err
47
+ @disable_apm = true
48
+ puts "ElasticAPM services not available: #{err}"
49
+ end
50
+
51
+ def elastic_apm_options
52
+ {
53
+ service_name: APM_SERVICE_NAME,
54
+ server_url: elastic_apm_url,
55
+ secret_token: elastic_apm_key,
56
+ environment: environment,
57
+ # http_compression: false,
58
+ transaction_sample_rate: 0.1,
59
+ transaction_max_spans: 100,
60
+ span_frames_min_duration: "5ms"
61
+ }.tap do |options|
62
+ # next unless false
63
+
64
+ options.merge!({
65
+ log_level: Logger::DEBUG,
66
+ log_path: File.join(__dir__, "elastic_apm.log")
67
+ })
68
+ end
69
+ end
70
+
71
+ def elastic_apm_url
72
+ @elastic_apm_url ||= "https://".tap do |url|
73
+ url << elastic_apm_account_id.to_s
74
+ url << ".#{elastic_apm_base_url}"
75
+ url << ":#{elastic_apm_port}"
76
+ end
77
+ end
78
+
79
+ def elastic_apm_key
80
+ @elastic_apm_key ||= ENV['ELASTIC_APM_KEY']
81
+ end
82
+
83
+ def elastic_apm_account_id
84
+ @elastic_apm_account_id ||= ENV['ELASTIC_APM_ACCOUNT_ID']
85
+ end
86
+
87
+ def elastic_apm_base_url
88
+ @elastic_apm_base_url ||= "apm.#{elastic_apm_region}.aws.cloud.es.io"
89
+ end
90
+
91
+ def elastic_apm_region
92
+ @elastic_apm_region ||= ENV['ELASTIC_APM_REGION'] || "ap-southeast-2"
93
+ end
94
+
95
+ def elastic_apm_port
96
+ @elastic_apm_port ||= ENV['ELASTIC_APM_PORT'] || "443"
97
+ end
98
+
99
+ def environment
100
+ @environment ||= "unknown".tap do |value|
101
+ next unless instance_variable_defined?(:@host)
102
+ next unless (env = @host.gsub(".ecoportal.com", ''))
103
+
104
+ value.clear << env
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,39 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ class Client
5
+ module Error
6
+ module Checks
7
+ private
8
+
9
+ def unexpected_server_error_code?(code)
10
+ return true unless code
11
+ return true if (code >= 500) && (code <= 599)
12
+
13
+ code <= 99
14
+ end
15
+
16
+ # Sometimes response body is wrong but status code
17
+ # doesn't reflect. Let it retry
18
+ def some_unexpected_error?(response)
19
+ return true if unexpected_server_error_code?(response.status)
20
+
21
+ unexpected_body?(response)
22
+ end
23
+
24
+ def unexpected_body?(response)
25
+ response.body.nil?.tap do |wrong|
26
+ next unless wrong
27
+
28
+ msg = "Received non json body in response "
29
+ msg << "(#{response.src_body.class}):\n "
30
+ msg << response.src_body
31
+ puts
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ require 'ecoportal/api/common/client/error/checks'
2
+
3
+ module Ecoportal
4
+ module API
5
+ module Common
6
+ class Client
7
+ module Error
8
+ class UnexpectedServerError < StandardError
9
+ def initialize(msg, code:)
10
+ super("Code: #{code} -- Error: #{msg}")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ class Client
5
+ module TimeOut
6
+ MIN_THROUGHPUT = 0.2 # people per second
7
+ MIN_SIZE = 10
8
+
9
+ private
10
+
11
+ def min_throughput
12
+ self.class::MIN_THROUGHPUT
13
+ end
14
+
15
+ def min_size
16
+ self.class::MIN_SIZE
17
+ end
18
+
19
+ def timeout_for(count)
20
+ count = 1 unless count&.positive?
21
+ count = min_size if count < min_size
22
+ (count.ceil / min_throughput).ceil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,86 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ class Client
5
+ module WithRetry
6
+ DELAY_REQUEST_RETRY = 5
7
+ RETRY_ATTEMPTS = 5
8
+ HANDLED_CONNECTION_ERRORS = [
9
+ HTTP::ConnectionError,
10
+ IOError
11
+ ].freeze
12
+
13
+ include Ecoportal::API::Common::Client::ElasticApmIntegration
14
+
15
+ private
16
+
17
+ # Helper to ensure unexpected server errors do not bring
18
+ # client scripts immediately down
19
+ # @note it manages limited range of errors, the rest
20
+ # are not handled.
21
+ def with_retry(
22
+ attempts = retry_attemps,
23
+ delay = delay_request_retry,
24
+ error_safe: true,
25
+ &block
26
+ )
27
+ response = nil
28
+
29
+ attempts.times do |i|
30
+ remaining = attempts - i - 1
31
+
32
+ response = with_connection_error_handling(
33
+ remaining,
34
+ error_safe: error_safe,
35
+ callback: block
36
+ ) do
37
+ block.call
38
+ end
39
+
40
+ return response unless some_unexpected_error?(response)
41
+
42
+ # handle server errors (5xx) & server bugs (i.e. empty body)
43
+ msg = "re-attempting (remaining: "
44
+ msg << "#{remaining} attempts out of #{attempts})"
45
+ log(:debug) { msg }
46
+
47
+ log_unexpected_server_error(response)
48
+
49
+ msg = "Got server error (#{response.status}): #{response.body}\n"
50
+ msg << "Going to retry (##{i} of #{attempts})"
51
+ log(:debug) { msg }
52
+
53
+ sleep(delay) if i < attempts
54
+ end
55
+
56
+ response
57
+ end
58
+
59
+ def with_connection_error_handling(remaining, callback:, error_safe: true)
60
+ yield
61
+ rescue *handled_connection_errors => err
62
+ raise unless error_safe && remaining.positive?
63
+
64
+ msg = "Got #{err.class}: #{err.message}"
65
+ log(:debug) { msg }
66
+
67
+ with_retry(remaining, error_safe: error_safe, &callback)
68
+ end
69
+
70
+ # Add here other connection errors
71
+ def handled_connection_errors
72
+ self.class::HANDLED_CONNECTION_ERRORS
73
+ end
74
+
75
+ def retry_attemps
76
+ self.class::RETRY_ATTEMPTS
77
+ end
78
+
79
+ def delay_request_retry
80
+ self.class::DELAY_REQUEST_RETRY
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,4 +1,10 @@
1
1
  require 'http'
2
+
3
+ require 'ecoportal/api/common/client/error'
4
+ require 'ecoportal/api/common/client/elastic_apm_integration'
5
+ require 'ecoportal/api/common/client/time_out'
6
+ require 'ecoportal/api/common/client/with_retry'
7
+
2
8
  module Ecoportal
3
9
  module API
4
10
  module Common
@@ -16,9 +22,7 @@ module Ecoportal
16
22
  # @attr_reader logger [Logger] the logger.
17
23
  # @attr_reader host [String] the remote target server.
18
24
  class Client
19
- include Common::ElasticApmIntegration
20
- DELAY_REQUEST_RETRY = 5
21
- RETRY_ATTEMPTS = 5
25
+ include WithRetry
22
26
 
23
27
  attr_accessor :logger
24
28
  attr_reader :host
@@ -28,40 +32,32 @@ module Ecoportal
28
32
  # @param version [String] it is part of the base url and will determine the api version we query against.
29
33
  # @param host [String] api server domain.
30
34
  # @param logger [Logger] an object with `Logger` interface to generate logs.
31
- # @param response_logging [Boolean] whether or not batch responses should be logged
35
+ # @param deep_logging [Boolean] whether or not batch responses should be logged
32
36
  # @return [Client] an object that holds the configuration of the api connection.
33
- def initialize(api_key:, version: "v1", host: "live.ecoportal.com", logger: nil, response_logging: false)
34
- @version = version
35
- @api_key = api_key
36
- @logger = logger
37
- @host = host
38
- @response_logging_enabled = response_logging
37
+ def initialize(api_key:, version: "v1", host: "live.ecoportal.com", logger: nil, deep_logging: false)
38
+ @version = version
39
+ @api_key = api_key
40
+ @logger = logger
41
+ @host = host
42
+ @deep_logging = deep_logging
39
43
 
40
44
  if host.match(/^localhost|^127\.0\.0\.1/)
41
45
  @base_uri = "http://#{host}/api/"
42
46
  else
43
47
  @base_uri = "https://#{host}/api/"
44
48
  end
45
- log(:info) { "#{version} client initialized pointing at #{host}" }
49
+
50
+ if deep_logging?
51
+ log(:debug) {
52
+ "#{version} client initialized pointing at #{host}"
53
+ }
54
+ end
46
55
 
47
56
  return unless @api_key.nil? || @api_key.match(/\A\W*\z/)
48
57
 
49
58
  log(:error) { "Api-key missing!" }
50
59
  end
51
60
 
52
- # Logger interface.
53
- # @example:
54
- # log(:info) {"General information on what's going on"}
55
- # log(:warn) {"This is a warning that something is likely to have gone amiss"}
56
- # log(:error) {"Something went wrong"}
57
- # log(:fatal) {"An unrecoverable error has happend"}
58
- # @param level [Symbol] the level that the message should be logged.
59
- # @yield [] generates the message.
60
- # @yieldreturn [String] the generated message.
61
- def log(level, &block)
62
- logger&.send(level, &block)
63
- end
64
-
65
61
  # Sends an http `GET` request against the api version using `path` to complete the base url,
66
62
  # and adding the key_value pairs of `params` in the http _header_.
67
63
  # @param path [String] the tail that completes the url of the request.
@@ -152,7 +148,7 @@ module Ecoportal
152
148
  # @param path [String] the tail that completes the url of the request.
153
149
  # @return [String] the final url.
154
150
  def url_for(path)
155
- @base_uri+@version+path
151
+ "#{@base_uri}#{@version}#{path}"
156
152
  end
157
153
 
158
154
  private
@@ -161,71 +157,43 @@ module Ecoportal
161
157
  raise "Expected block" unless block_given?
162
158
 
163
159
  start_time = Time.now.to_f
164
- log(:info) { "#{method} #{url_for(path)}" }
165
- log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
160
+
161
+ if deep_logging?
162
+ log(:debug) { "#{method} #{url_for(path)}" }
163
+ log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
164
+ end
166
165
 
167
166
  with_retry(&block).tap do |result|
167
+ next unless deep_logging?
168
+
168
169
  end_time = Time.now.to_f
169
- log(result.success?? :info : :warn) {
170
+
171
+ log(:debug) {
170
172
  "Took %.2fs, Status #{result.status}" % (end_time - start_time) # rubocop:disable Style/FormatString
171
173
  }
172
174
 
173
- next unless @response_logging_enabled
174
-
175
- log(result.success?? :debug : :warn) {
175
+ log(:debug) {
176
176
  "Response: #{JSON.pretty_generate(result.body)}"
177
177
  }
178
178
  end
179
179
  end
180
180
 
181
- # Helper to ensure unexpected server errors do not bring client scripts immediately down
182
- def with_retry(attempts = RETRY_ATTEMPTS, delay = DELAY_REQUEST_RETRY, error_safe: true, &block)
183
- response = nil
184
- attempts.times do |i|
185
- remaining = attempts - i - 1
186
-
187
- begin
188
- response = block.call
189
- rescue HTTP::ConnectionError => e
190
- raise unless error_safe && remaining.positive?
191
- log(:error) { "Got connection error: #{e.message}" }
192
- response = with_retry(remaining, error_safe: error_safe, &block)
193
- rescue IOError => e
194
- raise unless error_safe && remaining.positive?
195
- log(:error) { "Got IO error: #{e.message}" }
196
- response = with_retry(remaining, error_safe: error_safe, &block)
197
- end
198
-
199
- return response unless some_unexpected_error?(response)
200
-
201
- puts "re-attempting (remaining: #{remaining} attempts out of #{attempts})"
202
-
203
- log_unexpected_server_error(response)
204
-
205
- msg = "Got server error (#{response.status}): #{response.body}\n"
206
- msg += "Going to retry (#{i} out of #{attempts})"
207
- log(:error) { msg }
208
-
209
- sleep(delay) if i < attempts
210
- end
211
- response
212
- end
213
-
214
- # Sometimes response body is wrong but status code
215
- # doesn't reflect. Let it retry
216
- def some_unexpected_error?(response)
217
- unexpected_server_error?(response.status) ||
218
- unexpected_body?(response)
181
+ def deep_logging?
182
+ @deep_logging
219
183
  end
220
184
 
221
- def unexpected_body?(response)
222
- response.body.nil?.tap do |wrong|
223
- next unless wrong
224
-
225
- msg = "Received non json body in response "
226
- msg << "(#{response.src_body.class}):\n #{response.src_body}"
227
- puts
228
- end
185
+ # Logger interface.
186
+ # @example:
187
+ # log(:info) {"General information on what's going on"}
188
+ # log(:warn) {"This is a warning that something is likely to have gone amiss"}
189
+ # log(:error) {"Something went wrong"}
190
+ # log(:fatal) {"An unrecoverable error has happend"}
191
+ # @param level [Symbol] the level that the message should be logged.
192
+ # @yield [] generates the message.
193
+ # @yieldreturn [String] the generated message.
194
+ def log(level, &block)
195
+ puts "(#{level}) #{yield}"
196
+ logger&.send(level, &block)
229
197
  end
230
198
  end
231
199
  end
@@ -12,7 +12,7 @@ module Ecoportal
12
12
  doc
13
13
  end
14
14
  end
15
-
15
+
16
16
  def get_id(doc)
17
17
  id = nil
18
18
  id ||= doc.id if doc.respond_to?(:id)
@@ -10,7 +10,6 @@ require 'ecoportal/api/common/hash_diff'
10
10
  require 'ecoportal/api/common/base_model'
11
11
  require 'ecoportal/api/common/doc_helpers'
12
12
  require 'ecoportal/api/common/logging'
13
- require 'ecoportal/api/common/elastic_apm_integration'
14
13
  require 'ecoportal/api/common/client'
15
14
  require 'ecoportal/api/common/response'
16
15
  require 'ecoportal/api/common/wrapped_response'
@@ -0,0 +1,33 @@
1
+ module Ecoportal
2
+ module API
3
+ class V1
4
+ class JobStatus
5
+ attr_reader :id, :progress
6
+
7
+ def initialize(id, complete, errored, progress)
8
+ @id = id
9
+ @complete = complete
10
+ @errored = errored
11
+ @progress = progress
12
+ end
13
+
14
+ def complete?(total = nil)
15
+ return @complete if total.nil?
16
+
17
+ progress >= total
18
+ end
19
+
20
+ def errored?
21
+ @errored
22
+ end
23
+
24
+ def to_s
25
+ msg = complete? ? "Completed" : "In progress"
26
+ msg = "Errored" if errored?
27
+ msg << " with #{progress} done."
28
+ msg
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -6,9 +6,9 @@ module Ecoportal
6
6
  class People
7
7
  extend Common::BaseClass
8
8
  include Common::DocHelpers
9
+ include Common::Client::TimeOut
9
10
  include Enumerable
10
11
 
11
- JOB_TIMEOUT = 240
12
12
  DELAY_STATUS_CHECK = 5
13
13
 
14
14
  class_resolver :person_class, "Ecoportal::API::V1::Person"
@@ -169,15 +169,20 @@ module Ecoportal
169
169
 
170
170
  yield operation
171
171
 
172
- job_id = create_job(operation)
173
- status = wait_for_job_completion(job_id)
172
+ total = operation.count
173
+ timeout = timeout_for(total)
174
174
 
175
- if status&.complete?
175
+ job_id = create_job(operation)
176
+ status = wait_for_job_completion(job_id, timeout: timeout, total: total)
177
+
178
+ # @todo
179
+ # if total == status.progress
180
+ if status&.complete?(total)
176
181
  job_result(job_id, operation)
177
182
  else
178
- msg = "Job `#{job_id}` not complete. "
179
- msg << "Probably timeout after #{JOB_TIMEOUT} seconds. "
180
- msg << "Current status: #{status}"
183
+ msg = "Job '#{job_id}' not complete (size: #{total}).\n"
184
+ msg << " Probably timeout after #{timeout} seconds.\n"
185
+ msg << " Current status: #{status}"
181
186
 
182
187
  raise API::Errors::TimeOut, msg
183
188
  end
@@ -191,8 +196,6 @@ module Ecoportal
191
196
 
192
197
  private
193
198
 
194
- JobStatus = Struct.new(:id, :complete?, :errored?, :progress)
195
-
196
199
  def job_status(job_id)
197
200
  response = client.get("/people/job/#{CGI.escape(job_id)}/status")
198
201
  body = response && body_data(response.body)
@@ -201,12 +204,7 @@ module Ecoportal
201
204
  msg << "Errors: #{body}"
202
205
  raise msg unless response.success?
203
206
 
204
- JobStatus.new(
205
- body["id"],
206
- body["complete"],
207
- body["errored"],
208
- body["progress"]
209
- )
207
+ JobStatus.new(*body.values_at(*%w[id complete errored progress]))
210
208
  end
211
209
 
212
210
  # @return [Ecoportal::API::Common::Response] the results of the batch job
@@ -216,15 +214,25 @@ module Ecoportal
216
214
  end
217
215
  end
218
216
 
219
- def wait_for_job_completion(job_id)
217
+ def wait_for_job_completion(job_id, timeout:, total:)
220
218
  # timeout library is evil. So we make poor-man timeout.
221
219
  # https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
222
220
  before = Time.now
223
221
 
224
222
  loop do
225
223
  status = job_status(job_id)
226
- break status if status.complete?
227
- break status if Time.now >= before + JOB_TIMEOUT
224
+ break status if status.complete?(total)
225
+
226
+ left = (before + timeout) - Time.now
227
+ break status unless left.positive?
228
+ # break status if Time.now >= before + timeout
229
+
230
+ msg = " ... Await job "
231
+ msg << "('#{job_id}'; done: #{status.progress}): "
232
+ msg << "#{left.ceil} sec. \r"
233
+
234
+ print msg
235
+ $stdout.flush
228
236
 
229
237
  sleep(DELAY_STATUS_CHECK)
230
238
  status
@@ -46,5 +46,6 @@ module Ecoportal
46
46
  end
47
47
  end
48
48
 
49
+ require 'ecoportal/api/v1/job_status'
49
50
  require 'ecoportal/api/v1/person_schemas'
50
51
  require 'ecoportal/api/v1/people'
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- VERSION = "0.10.1".freeze
3
+ VERSION = '0.10.3'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,42 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tapio Saarinen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-01 00:00:00.000000000 Z
11
+ date: 2024-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
14
+ name: pry
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 3.12.0
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '4'
19
+ version: '0.14'
23
20
  type: :development
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 3.12.0
30
- - - "<"
24
+ - - "~>"
31
25
  - !ruby/object:Gem::Version
32
- version: '4'
26
+ version: '0.14'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: rake
35
29
  requirement: !ruby/object:Gem::Requirement
36
30
  requirements:
37
31
  - - ">="
38
32
  - !ruby/object:Gem::Version
39
- version: 13.1.0
33
+ version: 13.0.3
40
34
  - - "<"
41
35
  - !ruby/object:Gem::Version
42
36
  version: '14'
@@ -46,37 +40,37 @@ dependencies:
46
40
  requirements:
47
41
  - - ">="
48
42
  - !ruby/object:Gem::Version
49
- version: 13.1.0
43
+ version: 13.0.3
50
44
  - - "<"
51
45
  - !ruby/object:Gem::Version
52
46
  version: '14'
53
47
  - !ruby/object:Gem::Dependency
54
- name: yard
48
+ name: redcarpet
55
49
  requirement: !ruby/object:Gem::Requirement
56
50
  requirements:
57
51
  - - ">="
58
52
  - !ruby/object:Gem::Version
59
- version: 0.9.34
53
+ version: 3.6.0
60
54
  - - "<"
61
55
  - !ruby/object:Gem::Version
62
- version: '1'
56
+ version: '4'
63
57
  type: :development
64
58
  prerelease: false
65
59
  version_requirements: !ruby/object:Gem::Requirement
66
60
  requirements:
67
61
  - - ">="
68
62
  - !ruby/object:Gem::Version
69
- version: 0.9.34
63
+ version: 3.6.0
70
64
  - - "<"
71
65
  - !ruby/object:Gem::Version
72
- version: '1'
66
+ version: '4'
73
67
  - !ruby/object:Gem::Dependency
74
- name: redcarpet
68
+ name: rspec
75
69
  requirement: !ruby/object:Gem::Requirement
76
70
  requirements:
77
71
  - - ">="
78
72
  - !ruby/object:Gem::Version
79
- version: 3.6.0
73
+ version: 3.12.0
80
74
  - - "<"
81
75
  - !ruby/object:Gem::Version
82
76
  version: '4'
@@ -86,24 +80,52 @@ dependencies:
86
80
  requirements:
87
81
  - - ">="
88
82
  - !ruby/object:Gem::Version
89
- version: 3.6.0
83
+ version: 3.12.0
90
84
  - - "<"
91
85
  - !ruby/object:Gem::Version
92
86
  version: '4'
93
87
  - !ruby/object:Gem::Dependency
94
- name: pry
88
+ name: rubocop
95
89
  requirement: !ruby/object:Gem::Requirement
96
90
  requirements:
97
91
  - - "~>"
98
92
  - !ruby/object:Gem::Version
99
- version: '0.14'
93
+ version: '1'
100
94
  type: :development
101
95
  prerelease: false
102
96
  version_requirements: !ruby/object:Gem::Requirement
103
97
  requirements:
104
98
  - - "~>"
105
99
  - !ruby/object:Gem::Version
106
- version: '0.14'
100
+ version: '1'
101
+ - !ruby/object:Gem::Dependency
102
+ name: rubocop-rake
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: yard
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '0.9'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '0.9'
107
129
  - !ruby/object:Gem::Dependency
108
130
  name: dotenv
109
131
  requirement: !ruby/object:Gem::Requirement
@@ -161,9 +183,7 @@ dependencies:
161
183
  description:
162
184
  email:
163
185
  - tapio@ecoportal.co.nz
164
- - rien@ecoportal.co.nz
165
186
  - oscar@ecoportal.co.nz
166
- - bozydar@ecoportal.co.nz
167
187
  executables: []
168
188
  extensions: []
169
189
  extra_rdoc_files: []
@@ -189,8 +209,12 @@ files:
189
209
  - lib/ecoportal/api/common/batch_operation.rb
190
210
  - lib/ecoportal/api/common/batch_response.rb
191
211
  - lib/ecoportal/api/common/client.rb
212
+ - lib/ecoportal/api/common/client/elastic_apm_integration.rb
213
+ - lib/ecoportal/api/common/client/error.rb
214
+ - lib/ecoportal/api/common/client/error/checks.rb
215
+ - lib/ecoportal/api/common/client/time_out.rb
216
+ - lib/ecoportal/api/common/client/with_retry.rb
192
217
  - lib/ecoportal/api/common/doc_helpers.rb
193
- - lib/ecoportal/api/common/elastic_apm_integration.rb
194
218
  - lib/ecoportal/api/common/hash_diff.rb
195
219
  - lib/ecoportal/api/common/logging.rb
196
220
  - lib/ecoportal/api/common/response.rb
@@ -215,6 +239,7 @@ files:
215
239
  - lib/ecoportal/api/internal/schema_field_value.rb
216
240
  - lib/ecoportal/api/logger.rb
217
241
  - lib/ecoportal/api/v1.rb
242
+ - lib/ecoportal/api/v1/job_status.rb
218
243
  - lib/ecoportal/api/v1/people.rb
219
244
  - lib/ecoportal/api/v1/person.rb
220
245
  - lib/ecoportal/api/v1/person_details.rb
@@ -243,7 +268,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
268
  - !ruby/object:Gem::Version
244
269
  version: '0'
245
270
  requirements: []
246
- rubygems_version: 3.5.6
271
+ rubygems_version: 3.5.18
247
272
  signing_key:
248
273
  specification_version: 4
249
274
  summary: A collection of helpers for interacting with the ecoPortal MS's various APIs
@@ -1,112 +0,0 @@
1
- require 'elastic-apm'
2
- module Ecoportal
3
- module API
4
- module Common
5
- module ElasticApmIntegration
6
-
7
- class UnexpectedServerError < StandardError
8
- def initialize(code, msg)
9
- super("Code: #{code} -- Error: #{msg}")
10
- end
11
- end
12
-
13
- APM_SERVICE_NAME = 'ecoportal-api-gem'
14
-
15
- # Log only errors that are only server's responsibility
16
- def log_unexpected_server_error(response)
17
- raise "Expecting Ecoportal::API::Common::Response. Given: #{response.class}" unless response.is_a?(Common::Response)
18
- return nil unless elastic_apm_service
19
- return nil unless unexpected_server_error?(response.status)
20
- if ElasticAPM.running?
21
- ElasticAPM.report(UnexpectedServerError.new(response.status, response.body))
22
- end
23
- end
24
-
25
- private
26
-
27
- def unexpected_server_error?(code)
28
- !code || ((code >= 500) && (code <= 599)) || (code <= 99)
29
- end
30
-
31
- # finalizer to stop the agent
32
- close_elastic_apm = Proc.new do |id|
33
- begin
34
- if ElasticAPM.running?
35
- puts "Stopping ElasticAPM service"
36
- ElasticAPM.stop
37
- end
38
- rescue StandardError => e
39
- # Silent
40
- end
41
- end
42
- ObjectSpace.define_finalizer("ElasticAPM", close_elastic_apm)
43
-
44
- def elastic_apm_service
45
- return false if @disable_apm
46
- begin
47
- ElasticAPM.start(**elastic_apm_options) unless ElasticAPM.running?
48
- rescue StandardError => e
49
- @disable_apm = true
50
- puts "ElasticAPM services not available: #{e}"
51
- end
52
- end
53
-
54
- def elastic_apm_options
55
- {
56
- service_name: APM_SERVICE_NAME,
57
- server_url: elastic_apm_url,
58
- secret_token: elastic_apm_key,
59
- environment: environment,
60
- #http_compression: false,
61
- transaction_sample_rate: 0.1,
62
- transaction_max_spans: 100,
63
- span_frames_min_duration: "5ms"
64
- }.tap do |options|
65
- options.merge!({
66
- log_level: Logger::DEBUG,
67
- log_path: File.join(__dir__, "elastic_apm.log")
68
- }) if false
69
- end
70
- end
71
-
72
- def elastic_apm_url
73
- @elastic_apm_url ||= "https://".tap do |url|
74
- url << "#{elastic_apm_account_id}"
75
- url << ".#{elastic_apm_base_url}"
76
- url << ":#{elastic_apm_port}"
77
- end
78
- end
79
-
80
- def elastic_apm_key
81
- @elastic_apm_key ||= ENV['ELASTIC_APM_KEY']
82
- end
83
-
84
- def elastic_apm_account_id
85
- @elastic_apm_account_id ||= ENV['ELASTIC_APM_ACCOUNT_ID']
86
- end
87
-
88
- def elastic_apm_base_url
89
- @elastic_apm_base_url ||= "apm.#{elastic_apm_region}.aws.cloud.es.io"
90
- end
91
-
92
- def elastic_apm_region
93
- @elastic_apm_region ||= ENV['ELASTIC_APM_REGION'] || "ap-southeast-2"
94
- end
95
-
96
-
97
- def elastic_apm_port
98
- @elastic_apm_port ||= ENV['ELASTIC_APM_PORT'] || "443"
99
- end
100
-
101
- def environment
102
- @environment ||= "unknown".tap do |value|
103
- if instance_variable_defined?(:@host) && env = @host.gsub(".ecoportal.com", '')
104
- value.clear << env
105
- end
106
- end
107
- end
108
-
109
- end
110
- end
111
- end
112
- end