ecoportal-api 0.10.3 → 0.10.4

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: dde26458f8ba991809f2218f6a4594101350f861a811d1bf4c6b508b2194ef67
4
- data.tar.gz: f9bd67ceaea38ec6ea82d9a53ae0651c36fb7f74e417b37029acd7d72fd00d97
3
+ metadata.gz: 28e9d05f838bf52d038a6d9533a8bcbb98f7b66b5ffa6fa7e99a4c30b2d0e486
4
+ data.tar.gz: a6586e72297c5e109ed4f2e8a04beb32e1e435ea3eb0332803b4ba9fd91e8fb3
5
5
  SHA512:
6
- metadata.gz: '09dd4b2cde2bd8f6c6689433c7472fedb6110d076603afcd8edc39f5275aed0296470e57900eaceb335dc875f31554d515925c6b1eae060786a055ac706f6a66'
7
- data.tar.gz: 3363216e775f38601146617ee80abab13625163c46739f536d77f94c17247fcce139331cad8af02b4cbcc0848a352211a34c476d0d766082e18b5d4b222f43cf
6
+ metadata.gz: b8ecb9337ed3b2f11913ae5823bdb3b4771d1bcb364a50afc68bba38518dff39fd7986de641a4030030cfeeb0019a5960d9ac2440a10ee2bb49ed15aad77f0a8
7
+ data.tar.gz: 1f3ca204cb6c8b4a6cab2396285bf50644cb955f1120c15253f1d81ac1663bf2874f2b3706f5dedcfbdc34bf7c67d5a0520f1e62c0e8891259b337d92394cbc3
data/CHANGELOG.md CHANGED
@@ -2,7 +2,7 @@
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
5
+ ## [0.10.5] - 2024-10-xx
6
6
 
7
7
  ### Added
8
8
 
@@ -10,6 +10,21 @@ All notable changes to this project will be documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [0.10.4] - 2024-10-15
14
+
15
+ ### Added
16
+
17
+ - `Ecoportal::API::Common::Client::RateThrottling`
18
+ - See: <https://github.com/zombocom/rate_throttle_client?tab=readme-ov-file#ratethrottleclient>
19
+ - Add adapter to the RateLimit, may it be present.
20
+ - **Default:** `:proportional_decrease`
21
+
22
+ ### Changed
23
+
24
+ - `Ecoportal::API::Common::Client::TimeOut`
25
+ - Const `MIN_SIZE` changed from `10` to `5` to speed up small requests.
26
+ - Full revamp of `Job` endpoint
27
+
13
28
  ## [0.10.3] - 2024-10-01
14
29
 
15
30
  ### Changed
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  'oscar@ecoportal.co.nz'
14
14
  ]
15
15
 
16
- spec.summary = %q{A collection of helpers for interacting with the ecoPortal MS's various APIs}
16
+ spec.summary = "A collection of helpers for interacting with the ecoPortal MS's various APIs"
17
17
  spec.homepage = "https://www.ecoportal.com"
18
18
  spec.licenses = %w[MIT]
19
19
 
@@ -28,17 +28,18 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
- spec.add_development_dependency 'pry' , '~> 0.14'
31
+ spec.add_development_dependency 'pry', '~> 0.14'
32
32
  spec.add_development_dependency 'rake', '>= 13.0.3', '< 14'
33
33
  spec.add_development_dependency 'redcarpet', '>= 3.6.0', '< 4'
34
34
  spec.add_development_dependency 'rspec', '>= 3.12.0', '< 4'
35
35
  spec.add_development_dependency 'rubocop', '~> 1'
36
36
  spec.add_development_dependency 'rubocop-rake', '~> 0'
37
- spec.add_development_dependency 'yard', '~> 0.9'
37
+ spec.add_development_dependency 'yard', '~> 0.9'
38
38
 
39
39
  spec.add_dependency 'dotenv', '~> 3'
40
40
  spec.add_dependency 'elastic-apm', '>= 4.7', "< 5"
41
41
  spec.add_dependency 'http', '~> 5.1', "< 6"
42
+ spec.add_dependency 'rate_throttle_client', '~> 0.1'
42
43
  end
43
44
 
44
45
  # rubocop:enable Gemspec/DevelopmentDependencies
@@ -0,0 +1,43 @@
1
+ require 'rate_throttle_client'
2
+
3
+ module Ecoportal
4
+ module API
5
+ module Common
6
+ class Client
7
+ module RateThrottling
8
+ private
9
+
10
+ def rate_throttling(strategy: nil, &block)
11
+ throttle(strategy).call(&block)
12
+ end
13
+
14
+ def throttle(strategy = nil)
15
+ @throttle ||= {}
16
+
17
+ return @throttle[strategy] if @throttle.key?(strategy)
18
+
19
+ @throttle[strategy] = throttle_class(strategy).new
20
+ end
21
+
22
+ def throttle_class(strategy = default_throttle_strategy)
23
+ case strategy
24
+ when :gradual_decrease
25
+ RateThrottleClient::ExponentialIncreaseGradualDecrease
26
+ when :proportional_decrease
27
+ RateThrottleClient::ExponentialIncreaseProportionalDecrease
28
+ when :remaining_decrease
29
+ # requires RateLimit-Remaining in the response
30
+ RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease
31
+ else # :backoff
32
+ RateThrottleClient::ExponentialBackoff
33
+ end
34
+ end
35
+
36
+ def default_throttle_strategy
37
+ :proportional_decrease
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,119 @@
1
+ module Ecoportal
2
+ module API
3
+ module Common
4
+ class Client
5
+ class Throughput
6
+ class Stats
7
+ DEFAULT_SD = 1.0
8
+ DUMMY_DIFF = 0.001
9
+ CONFIDENCE = {
10
+ _90: 1645,
11
+ _95: 1.96,
12
+ _98: 2.326,
13
+ _99: 2.576,
14
+ _995: 2.807
15
+ }.freeze
16
+ DEFAULT_CONFI = :_99
17
+
18
+ attr_reader :average, :standard_deviation, :count
19
+ attr_reader :default_margin_error
20
+
21
+ def initialize(average, margin: DEFAULT_CONFI)
22
+ @average = average.to_f
23
+ @standard_deviation = default_sd
24
+ @count = 1
25
+ @default_margin_error = margin || DEFAULT_CONFI
26
+ end
27
+
28
+ def empty?
29
+ count <= 1
30
+ end
31
+
32
+ def max(value = default_margin_error)
33
+ confidence_interval(value).max
34
+ end
35
+
36
+ def min(value = default_margin_error)
37
+ confidence_interval(value).min
38
+ end
39
+
40
+ def record!(value)
41
+ if count == 1
42
+ @average = value
43
+ else
44
+ @average = ((average * count) + value) / (count + 1)
45
+ end
46
+
47
+ @standard_deviation = new_sd(value)
48
+ @count += 1
49
+ average
50
+ end
51
+
52
+ alias_method :<<, :record!
53
+
54
+ # @return [Interval]
55
+ def confidence_interval(value = default_margin_error)
56
+ me = margin_error(value)
57
+ pair = [me * -1, me].map {|err| average + err}
58
+ (pair.first..pair.last)
59
+ end
60
+
61
+ protected
62
+
63
+ def margin_error(value = default_margin_error)
64
+ to_z(value) * (standard_deviation / Math.sqrt(count))
65
+ end
66
+
67
+ private
68
+
69
+ def new_sd(value)
70
+ pre_sum = (sd_per_item ** 2) * count
71
+ cur_sum = pre_sum + ((value - average) ** 2)
72
+ Math.sqrt(cur_sum / count)
73
+ end
74
+
75
+ # (diff^2) * N
76
+ # SD^2 = -------------
77
+ # N - 1
78
+ #
79
+ # (diff^2) * N = SD^2 * (N - 1)
80
+ #
81
+ # --------------
82
+ # diff = 2 / SD^2 * (N - 1)
83
+ # V -------------
84
+ # N
85
+ def sd_per_item
86
+ base = (sd_for_calcs ** 2) * (count - 1) / count.to_f
87
+ Math.sqrt(base)
88
+ end
89
+
90
+ def sd_for_calcs
91
+ return standard_deviation unless empty?
92
+
93
+ standard_deviation + dummy_diff
94
+ end
95
+
96
+ def dummy_diff
97
+ self.class::DUMMY_DIFF
98
+ end
99
+
100
+ def to_z(value)
101
+ value = default_margin_error unless confidences.key?(value)
102
+
103
+ confidences[value]
104
+ end
105
+
106
+ def confidences
107
+ self.class::CONFIDENCE
108
+ end
109
+
110
+ def default_sd
111
+ default = self.class::DEFAULT_SD
112
+ default * average / 10
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,93 @@
1
+ require 'ecoportal/api/common/client/throughput/stats'
2
+
3
+ module Ecoportal
4
+ module API
5
+ module Common
6
+ class Client
7
+ class Throughput
8
+ MIN_THROUGHPUT = 0.2 # records per second
9
+ MAX_THROUGHPUT = 12
10
+
11
+ attr_reader :default_approach
12
+
13
+ def initialize(approach = :conservative)
14
+ @default_approach = approach || :conservative
15
+ end
16
+
17
+ def eta_for(count, approach: default_approach, ratio: self.ratio(approach))
18
+ to_seconds(count, ratio: ratio)
19
+ end
20
+
21
+ def ratio(approach = default_approach)
22
+ case approach
23
+ when :min
24
+ min_throughput
25
+ when :last
26
+ @last_throughput || stats.average
27
+ when :average
28
+ stats.average
29
+ when :optimistic
30
+ [stats.max, max_throughput].min
31
+ else # :conservative
32
+ [stats.min, min_throughput].max
33
+ end.then do |relation|
34
+ next relation if relation < max_throughput
35
+
36
+ max_throughput
37
+ end
38
+ end
39
+
40
+ def record!(secs = nil, count: nil)
41
+ return ratio if secs.to_f.zero? || count.to_i.zero?
42
+
43
+ (count / secs.to_f).round(3).tap do |last|
44
+ stats << @last_throughput = last
45
+ end
46
+ end
47
+
48
+ # Keeps track of the current processing speed
49
+ # @note it needs to be called in specific spots
50
+ def push(value)
51
+ stats.record!(value)
52
+ end
53
+ alias_method :<<, :push
54
+
55
+ def around_min_throughput?(margin)
56
+ rate = ratio
57
+ return true if rate == min_throughput
58
+
59
+ right_under = min_throughput * (1 - margin)
60
+ return false if rate < right_under
61
+
62
+ right_above = min_throughput * (1 + margin)
63
+ return false if rate > right_above
64
+
65
+ true
66
+ end
67
+
68
+ protected
69
+
70
+ def stats
71
+ @stats ||= Stats.new(min_throughput)
72
+ end
73
+
74
+ def to_seconds(count, ratio: min_throughput)
75
+ return 1 unless count
76
+
77
+ (count.ceil / ratio.to_f).ceil
78
+ end
79
+
80
+ private
81
+
82
+ def min_throughput
83
+ self.class::MIN_THROUGHPUT
84
+ end
85
+
86
+ def max_throughput
87
+ self.class::MAX_THROUGHPUT
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,23 +3,65 @@ module Ecoportal
3
3
  module Common
4
4
  class Client
5
5
  module TimeOut
6
- MIN_THROUGHPUT = 0.2 # people per second
7
- MIN_SIZE = 10
6
+ TIMEOUT_MARGIN = 0.15 # adaptative timeout with margin
7
+ MIN_SIZE = 5 # requests
8
+
9
+ protected
10
+
11
+ attr_writer :throughput
12
+
13
+ # Keeps track of the current processing speed
14
+ # @note it needs to be called in specific spots
15
+ def throughput!(secs = nil, count: nil)
16
+ throughput.record!(secs, count: count)
17
+ end
18
+
19
+ def throughput
20
+ @throughput ||= Throughput.new
21
+ end
22
+
23
+ def to_count(value)
24
+ value ||= 0
25
+ return min_size if value.positive? && value < min_size
26
+ return value if value.positive?
27
+
28
+ 1
29
+ end
8
30
 
9
31
  private
10
32
 
11
- def min_throughput
12
- self.class::MIN_THROUGHPUT
33
+ def eta_for(count, approach: :conservative)
34
+ kargs = {}.tap do |params|
35
+ next params.merge!({ratio: rectified_throughput}) if approach == :conservative
36
+
37
+ params.merge!({approach: approach})
38
+ end
39
+
40
+ throughput.eta_for(to_count(count), **kargs)
13
41
  end
14
42
 
15
- def min_size
16
- self.class::MIN_SIZE
43
+ def timeout_for(count, waited: 0, max_wait: nil, approach: :conservative)
44
+ (waited + eta_for(count, approach: approach)).then do |time|
45
+ next time unless max_wait
46
+
47
+ [max_wait, time].min
48
+ end
17
49
  end
18
50
 
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
51
+ def rectified_throughput
52
+ throughput.ratio * (1 - timeout_margin)
53
+ end
54
+
55
+ def around_min_throughput?
56
+ throughput.around_min_throughput?(timeout_margin)
57
+ end
58
+
59
+ def timeout_margin
60
+ self.class::TIMEOUT_MARGIN
61
+ end
62
+
63
+ def min_size
64
+ self.class::MIN_SIZE
23
65
  end
24
66
  end
25
67
  end
@@ -2,6 +2,8 @@ require 'http'
2
2
 
3
3
  require 'ecoportal/api/common/client/error'
4
4
  require 'ecoportal/api/common/client/elastic_apm_integration'
5
+ require 'ecoportal/api/common/client/rate_throttling'
6
+ require 'ecoportal/api/common/client/throughput'
5
7
  require 'ecoportal/api/common/client/time_out'
6
8
  require 'ecoportal/api/common/client/with_retry'
7
9
 
@@ -23,6 +25,9 @@ module Ecoportal
23
25
  # @attr_reader host [String] the remote target server.
24
26
  class Client
25
27
  include WithRetry
28
+ include RateThrottling
29
+
30
+ DEFAULT_HOST = 'live.ecoportal.com'.freeze
26
31
 
27
32
  attr_accessor :logger
28
33
  attr_reader :host
@@ -34,7 +39,7 @@ module Ecoportal
34
39
  # @param logger [Logger] an object with `Logger` interface to generate logs.
35
40
  # @param deep_logging [Boolean] whether or not batch responses should be logged
36
41
  # @return [Client] an object that holds the configuration of the api connection.
37
- def initialize(api_key:, version: "v1", host: "live.ecoportal.com", logger: nil, deep_logging: false)
42
+ def initialize(api_key:, version: "v1", host: DEFAULT_HOST, logger: nil, deep_logging: false)
38
43
  @version = version
39
44
  @api_key = api_key
40
45
  @logger = logger
@@ -163,7 +168,9 @@ module Ecoportal
163
168
  log(:debug) { "Data: #{JSON.pretty_generate(data)}" }
164
169
  end
165
170
 
166
- with_retry(&block).tap do |result|
171
+ with_retry do
172
+ rate_throttling(&block)
173
+ end.tap do |result|
167
174
  next unless deep_logging?
168
175
 
169
176
  end_time = Time.now.to_f
@@ -0,0 +1,115 @@
1
+ module Ecoportal
2
+ module API
3
+ class V1
4
+ class Job
5
+ class Awaiter
6
+ include Common::Client::TimeOut
7
+
8
+ DELAY_STATUS_CHECK = 4
9
+ MIN_STATUS_CHECK = 2
10
+
11
+ TIMEOUT_APPROACH = :conservative # adaptative timeout
12
+ TIMEOUT_FALLBACK = :min
13
+
14
+ attr_reader :job, :job_id, :total
15
+ attr_accessor :timeout_approach
16
+
17
+ def initialize(job, job_id:, total:)
18
+ @job = job
19
+ @job_id = job_id
20
+ @total = total
21
+ @checked = false
22
+ self.timeout_approach = self.class::TIMEOUT_APPROACH
23
+ end
24
+
25
+ # Allows to preserve the learned throughput
26
+ def new(**kargs)
27
+ self.class.new(job, **kargs).tap do |out|
28
+ out.throughput = throughput
29
+ end
30
+ end
31
+
32
+ def await_completion! # rubocop:disable Metrics/AbcSize
33
+ max_timeout = timeout_for(total, approach: timeout_approach)
34
+
35
+ # timeout library is evil. So we make poor-man timeout.
36
+ # https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
37
+ before = Time.now
38
+ delay_status_check = nil
39
+
40
+ loop do
41
+ status = job.status(job_id)
42
+ waited = Time.now - before
43
+
44
+ adapted = waited
45
+ adapted = waited - (delay_status_check / 2) if delay_status_check
46
+ ratio = throughput!(adapted, count: status.progress)
47
+
48
+ break status if status.complete?(total)
49
+
50
+ pending = status.pending(total)
51
+ left = max_timeout - waited
52
+
53
+ timeout!(status, timeout: max_timeout) unless left.positive?
54
+
55
+ delay_status_check = status_check_in(pending, timeout_in: left)
56
+
57
+ msg = " ... Awaiting #{delay_status_check} s. -- "
58
+ msg << " TimeOut: #{left.round(2)} s. "
59
+ msg << "(job '#{job_id}') "
60
+ msg << "Done: #{status.progress} (est. #{ratio} rec/s) "
61
+ msg << " \r"
62
+
63
+ print msg
64
+ $stdout.flush
65
+
66
+ sleep(delay_status_check)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def timeout!(status, timeout:)
73
+ self.timeout_approach = self.class::TIMEOUT_FALLBACK
74
+
75
+ msg = "Job '#{job_id}' not complete (size: #{total}).\n"
76
+ msg << " Timed out after #{timeout} seconds.\n"
77
+ msg << " Current status: #{status}"
78
+
79
+ raise API::Errors::TimeOut, msg
80
+ end
81
+
82
+ def checked?
83
+ @checked
84
+ end
85
+
86
+ def status_check_in(pending, timeout_in:)
87
+ unless checked?
88
+ @checked = true
89
+ return min_delay_status_check
90
+ end
91
+
92
+ return default_delay_status_check if around_min_throughput?
93
+
94
+ eta = eta_for(pending, approach: :optimistic)
95
+ check_in_max = [eta, timeout_in].min * 0.90
96
+
97
+ check_in_best = check_in_max / 2.0
98
+ default_5 = default_delay_status_check * 5
99
+
100
+ top_check = [default_5, check_in_best].min.ceil
101
+ [top_check, min_delay_status_check].max
102
+ end
103
+
104
+ def default_delay_status_check
105
+ self.class::DELAY_STATUS_CHECK
106
+ end
107
+
108
+ def min_delay_status_check
109
+ self.class::MIN_STATUS_CHECK
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,42 @@
1
+ module Ecoportal
2
+ module API
3
+ class V1
4
+ class Job
5
+ class Status
6
+ attr_reader :id, :progress
7
+
8
+ def initialize(id, complete, errored, progress)
9
+ @id = id
10
+ @complete = complete
11
+ @errored = errored
12
+ @progress = progress
13
+ end
14
+
15
+ def complete?(total = nil)
16
+ return @complete if total.nil?
17
+
18
+ progress >= total
19
+ end
20
+
21
+ def pending(total)
22
+ return 1 unless total
23
+ return 0 if total <= progress
24
+
25
+ total - progress
26
+ end
27
+
28
+ def errored?
29
+ @errored
30
+ end
31
+
32
+ def to_s
33
+ msg = complete? ? "Completed" : "In progress"
34
+ msg = "Errored" if errored?
35
+ msg << " with #{progress} done."
36
+ msg
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,131 @@
1
+ require 'ecoportal/api/v1/job/status'
2
+ require 'ecoportal/api/v1/job/awaiter'
3
+
4
+ module Ecoportal
5
+ module API
6
+ class V1
7
+ class Job
8
+ attr_reader :client, :person_class
9
+
10
+ # @param client [Common::Client] a `Common::Client` object that holds the configuration of the api connection.
11
+ # @return [People] an instance object ready to make people api requests.
12
+ def initialize(client, person_class:)
13
+ @client = client
14
+ @person_class = person_class
15
+ @created = false
16
+ end
17
+
18
+ # Allows to preserve the learned througoutput
19
+ def new
20
+ self.class.new(client, person_class: person_class).tap do |out|
21
+ out.awaiter = @awaiter
22
+ end
23
+ end
24
+
25
+ def created?
26
+ @created
27
+ end
28
+
29
+ # @return [Ecoportal::API::Common::Response] the results of the batch job
30
+ def batch(recover: false)
31
+ raise_if_already_launched! unless recover
32
+
33
+ @operation ||= Common::BatchOperation.new(
34
+ "/people",
35
+ person_class,
36
+ logger: client.logger
37
+ )
38
+
39
+ yield operation unless recover
40
+
41
+ total = operation.count
42
+ job_id = create(operation, recover: recover)
43
+ stat = awaiter(
44
+ job_id: job_id,
45
+ total: total
46
+ ).await_completion!
47
+
48
+ job_result(job_id, operation) if stat&.complete?(total)
49
+ end
50
+
51
+ def status(job_id)
52
+ response = client.get("/people/job/#{CGI.escape(job_id)}/status")
53
+ body = response && body_data(response.body)
54
+
55
+ msg = "Status error (#{response.status}) - "
56
+ msg << "Errors: #{body}"
57
+ raise msg unless response.success?
58
+
59
+ Status.new(*body.values_at(*%w[id complete errored progress]))
60
+ end
61
+
62
+ protected
63
+
64
+ attr_writer :awaiter
65
+
66
+ def awaiter(job_id:, total:, preserve: true)
67
+ return @awaiter = Awaiter.new(self, job_id: job_id, total: total) unless preserve && @awaiter
68
+
69
+ @awaiter = @awaiter.new(job_id: job_id, total: total)
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :operation
75
+
76
+ def operations
77
+ @operations ||= {}
78
+ end
79
+
80
+ def job_operation(job_id)
81
+ operations[job_id]
82
+ end
83
+
84
+ def raise_if_already_launched!
85
+ return unless created?
86
+
87
+ msg = "Missusage: job was already created."
88
+ msg << " Can't call batch more than once"
89
+ raise msg
90
+ end
91
+
92
+ # @return [Ecoportal::API::Common::Response] the results of the batch job
93
+ def job_result(job_id, operation)
94
+ client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
95
+ operation.process_response(response)
96
+ end
97
+ end
98
+
99
+ # @return [String] the `id` of the created batch job
100
+ def create(operation, recover: false)
101
+ raise_if_already_launched! unless recover
102
+
103
+ job_id = nil
104
+
105
+ client.post("/people/job", data: operation.as_json).tap do |response|
106
+ job_id = body_data(response.body)["id"] if response.success?
107
+
108
+ next if job_id
109
+
110
+ msg = "Could not create job - Error (#{response.status}): "
111
+ msg << body_data(response.body).to_s
112
+ raise msg
113
+ end
114
+
115
+ if job_id
116
+ @created = true
117
+ operations[job_id] = operation
118
+ end
119
+
120
+ job_id
121
+ end
122
+
123
+ # Hook for other api versions to obtain the raw data of a response
124
+ # @note this was introduced to allow `v2` to reuse this class
125
+ def body_data(body)
126
+ body
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -6,11 +6,8 @@ module Ecoportal
6
6
  class People
7
7
  extend Common::BaseClass
8
8
  include Common::DocHelpers
9
- include Common::Client::TimeOut
10
9
  include Enumerable
11
10
 
12
- DELAY_STATUS_CHECK = 5
13
-
14
11
  class_resolver :person_class, "Ecoportal::API::V1::Person"
15
12
 
16
13
  attr_reader :client
@@ -143,7 +140,7 @@ module Ecoportal
143
140
  # @param job_mode [Boolean] whether or not it should use batch jobs
144
141
  # @return [Ecoportal::API::Common::Response] the results of the batch
145
142
  def batch(job_mode: true, &block)
146
- return job(&block) if job_mode
143
+ return job.batch(&block) if job_mode
147
144
 
148
145
  operation = Common::BatchOperation.new(
149
146
  "/people",
@@ -159,33 +156,11 @@ module Ecoportal
159
156
  end
160
157
  end
161
158
 
162
- # @return [Ecoportal::API::Common::Response] the results of the batch job
163
- def job
164
- operation = Common::BatchOperation.new(
165
- "/people",
166
- person_class,
167
- logger: client.logger
168
- )
169
-
170
- yield operation
171
-
172
- total = operation.count
173
- timeout = timeout_for(total)
174
-
175
- job_id = create_job(operation)
176
- status = wait_for_job_completion(job_id, timeout: timeout, total: total)
159
+ # @return [Ecoportal::API::V1::Job]
160
+ def job(preserve_stats: true)
161
+ return @last_job = Job.new(client, person_class: person_class) unless preserve_stats && @last_job
177
162
 
178
- # @todo
179
- # if total == status.progress
180
- if status&.complete?(total)
181
- job_result(job_id, operation)
182
- else
183
- msg = "Job '#{job_id}' not complete (size: #{total}).\n"
184
- msg << " Probably timeout after #{timeout} seconds.\n"
185
- msg << " Current status: #{status}"
186
-
187
- raise API::Errors::TimeOut, msg
188
- end
163
+ @last_job = @last_job.new
189
164
  end
190
165
 
191
166
  # Creates a new `Person` object.
@@ -196,59 +171,6 @@ module Ecoportal
196
171
 
197
172
  private
198
173
 
199
- def job_status(job_id)
200
- response = client.get("/people/job/#{CGI.escape(job_id)}/status")
201
- body = response && body_data(response.body)
202
-
203
- msg = "Status error (#{response.status}) - "
204
- msg << "Errors: #{body}"
205
- raise msg unless response.success?
206
-
207
- JobStatus.new(*body.values_at(*%w[id complete errored progress]))
208
- end
209
-
210
- # @return [Ecoportal::API::Common::Response] the results of the batch job
211
- def job_result(job_id, operation)
212
- client.get("/people/job/#{CGI.escape(job_id)}").tap do |response|
213
- operation.process_response(response)
214
- end
215
- end
216
-
217
- def wait_for_job_completion(job_id, timeout:, total:)
218
- # timeout library is evil. So we make poor-man timeout.
219
- # https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
220
- before = Time.now
221
-
222
- loop do
223
- status = job_status(job_id)
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
236
-
237
- sleep(DELAY_STATUS_CHECK)
238
- status
239
- end
240
- end
241
-
242
- # @return [String] the `id` of the created batch job
243
- def create_job(operation)
244
- job_id = nil
245
- client.post("/people/job", data: operation.as_json).tap do |response|
246
- job_id = body_data(response.body)["id"] if response.success?
247
- raise "Could not create job - Error (#{response.status}): #{body_data(response.body)}" unless job_id
248
- end
249
- job_id
250
- end
251
-
252
174
  # Hook for other api versions to obtain the raw data of a response
253
175
  # @note this was introduced to allow `v2` to reuse this class
254
176
  def body_data(body)
@@ -262,3 +184,4 @@ end
262
184
  require 'ecoportal/api/v1/schema_field_value'
263
185
  require 'ecoportal/api/v1/person_details'
264
186
  require 'ecoportal/api/v1/person'
187
+ require 'ecoportal/api/v1/job'
@@ -46,6 +46,5 @@ module Ecoportal
46
46
  end
47
47
  end
48
48
 
49
- require 'ecoportal/api/v1/job_status'
50
49
  require 'ecoportal/api/v1/person_schemas'
51
50
  require 'ecoportal/api/v1/people'
@@ -1,5 +1,5 @@
1
1
  module Ecoportal
2
2
  module API
3
- VERSION = '0.10.3'.freeze
3
+ VERSION = '0.10.4'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecoportal-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 0.10.4
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-09-30 00:00:00.000000000 Z
11
+ date: 2024-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pry
@@ -180,6 +180,20 @@ dependencies:
180
180
  - - "<"
181
181
  - !ruby/object:Gem::Version
182
182
  version: '6'
183
+ - !ruby/object:Gem::Dependency
184
+ name: rate_throttle_client
185
+ requirement: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - "~>"
188
+ - !ruby/object:Gem::Version
189
+ version: '0.1'
190
+ type: :runtime
191
+ prerelease: false
192
+ version_requirements: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - "~>"
195
+ - !ruby/object:Gem::Version
196
+ version: '0.1'
183
197
  description:
184
198
  email:
185
199
  - tapio@ecoportal.co.nz
@@ -212,6 +226,9 @@ files:
212
226
  - lib/ecoportal/api/common/client/elastic_apm_integration.rb
213
227
  - lib/ecoportal/api/common/client/error.rb
214
228
  - lib/ecoportal/api/common/client/error/checks.rb
229
+ - lib/ecoportal/api/common/client/rate_throttling.rb
230
+ - lib/ecoportal/api/common/client/throughput.rb
231
+ - lib/ecoportal/api/common/client/throughput/stats.rb
215
232
  - lib/ecoportal/api/common/client/time_out.rb
216
233
  - lib/ecoportal/api/common/client/with_retry.rb
217
234
  - lib/ecoportal/api/common/doc_helpers.rb
@@ -239,7 +256,9 @@ files:
239
256
  - lib/ecoportal/api/internal/schema_field_value.rb
240
257
  - lib/ecoportal/api/logger.rb
241
258
  - lib/ecoportal/api/v1.rb
242
- - lib/ecoportal/api/v1/job_status.rb
259
+ - lib/ecoportal/api/v1/job.rb
260
+ - lib/ecoportal/api/v1/job/awaiter.rb
261
+ - lib/ecoportal/api/v1/job/status.rb
243
262
  - lib/ecoportal/api/v1/people.rb
244
263
  - lib/ecoportal/api/v1/person.rb
245
264
  - lib/ecoportal/api/v1/person_details.rb
@@ -1,33 +0,0 @@
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