lago-ruby-client 1.46.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/lago/api/client.rb +8 -1
- data/lib/lago/api/connection.rb +44 -49
- data/lib/lago/api/logging_rate_limit_observer.rb +54 -0
- data/lib/lago/api/rate_limit_info.rb +49 -0
- data/lib/lago/api/resources/base.rb +1 -0
- data/lib/lago/api/resources/customer.rb +5 -0
- data/lib/lago/api/resources/event.rb +1 -0
- data/lib/lago/api/resources/nested.rb +1 -0
- data/lib/lago/version.rb +1 -1
- data/lib/lago-ruby-client.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1ce7eaf525a2785545ea91a30464c1da9da45a09c95d01a6b062cd4b51a85f5
|
|
4
|
+
data.tar.gz: 6d8be436f3b5b4155d8e72e28aea99082fd40dfa1070a18758e40842ede13d34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c10616c41e59442007247c9245bb91be08881744dbf91f9112dbe4043e7d19c33539aa50c8367f77a06a6d3fd49efa5d5a3add3f10842eaa05cc0d0ab0f2643d
|
|
7
|
+
data.tar.gz: adc378ba5f462f08880bbd3ff8b841b9432660a378f3dec36aeab4b4065e0ecaf07608a27ead50a49cea6eab701c250e0f2e89915d1d13df016f22c252dc95de
|
data/lib/lago/api/client.rb
CHANGED
|
@@ -53,7 +53,13 @@ module Lago
|
|
|
53
53
|
API_PATH = 'api/v1/'
|
|
54
54
|
|
|
55
55
|
class Client
|
|
56
|
-
attr_reader :api_key,
|
|
56
|
+
attr_reader :api_key,
|
|
57
|
+
:api_url,
|
|
58
|
+
:use_ingest_service,
|
|
59
|
+
:ingest_api_url,
|
|
60
|
+
:max_retries,
|
|
61
|
+
:retry_on_rate_limit,
|
|
62
|
+
:on_rate_limit_info
|
|
57
63
|
|
|
58
64
|
def initialize(api_key: nil, api_url: nil, use_ingest_service: false, ingest_api_url: nil, **options)
|
|
59
65
|
@api_key = api_key
|
|
@@ -62,6 +68,7 @@ module Lago
|
|
|
62
68
|
@ingest_api_url = ingest_api_url
|
|
63
69
|
@max_retries = options.fetch(:max_retries, 3)
|
|
64
70
|
@retry_on_rate_limit = options.fetch(:retry_on_rate_limit, true)
|
|
71
|
+
@on_rate_limit_info = options[:on_rate_limit_info]
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def base_api_url
|
data/lib/lago/api/connection.rb
CHANGED
|
@@ -11,84 +11,67 @@ module Lago
|
|
|
11
11
|
BACKOFF_MULTIPLIER = 2
|
|
12
12
|
MAX_RETRY_DELAY = 20
|
|
13
13
|
|
|
14
|
-
def initialize(
|
|
14
|
+
def initialize(
|
|
15
|
+
api_key,
|
|
16
|
+
uri,
|
|
17
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
18
|
+
retry_on_rate_limit: true,
|
|
19
|
+
on_rate_limit_info: nil
|
|
20
|
+
)
|
|
15
21
|
@api_key = api_key
|
|
16
22
|
@uri = uri
|
|
17
23
|
@max_retries = max_retries
|
|
18
24
|
@retry_on_rate_limit = retry_on_rate_limit
|
|
25
|
+
@on_rate_limit_info = on_rate_limit_info
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
def post(body, path = uri.path)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
path,
|
|
26
|
-
prepare_payload(body),
|
|
27
|
-
headers
|
|
28
|
-
)
|
|
29
|
+
method = 'POST'
|
|
30
|
+
execute_request(method:) do
|
|
31
|
+
http_client.send_request(method, path, prepare_payload(body), headers)
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def put(path = uri.path, identifier:, body:)
|
|
36
|
+
method = 'PUT'
|
|
33
37
|
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
|
|
34
|
-
execute_request do
|
|
35
|
-
http_client.send_request(
|
|
36
|
-
'PUT',
|
|
37
|
-
uri_path,
|
|
38
|
-
prepare_payload(body),
|
|
39
|
-
headers
|
|
40
|
-
)
|
|
38
|
+
execute_request(method:) do
|
|
39
|
+
http_client.send_request(method, uri_path, prepare_payload(body), headers)
|
|
41
40
|
end
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def patch(path = uri.path, identifier:, body:)
|
|
44
|
+
method = 'PATCH'
|
|
45
45
|
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
|
|
46
|
-
execute_request do
|
|
47
|
-
http_client.send_request(
|
|
48
|
-
'PATCH',
|
|
49
|
-
uri_path,
|
|
50
|
-
prepare_payload(body),
|
|
51
|
-
headers
|
|
52
|
-
)
|
|
46
|
+
execute_request(method:) do
|
|
47
|
+
http_client.send_request(method, uri_path, prepare_payload(body), headers)
|
|
53
48
|
end
|
|
54
49
|
end
|
|
55
50
|
|
|
56
51
|
def get(path = uri.path, identifier:)
|
|
52
|
+
method = 'GET'
|
|
57
53
|
uri_path = identifier.nil? ? path : "#{path}/#{CGI.escapeURIComponent(identifier)}"
|
|
58
|
-
execute_request do
|
|
59
|
-
http_client.send_request(
|
|
60
|
-
'GET',
|
|
61
|
-
uri_path,
|
|
62
|
-
prepare_payload(nil),
|
|
63
|
-
headers
|
|
64
|
-
)
|
|
54
|
+
execute_request(method:) do
|
|
55
|
+
http_client.send_request(method, uri_path, prepare_payload(nil), headers)
|
|
65
56
|
end
|
|
66
57
|
end
|
|
67
58
|
|
|
68
59
|
def destroy(path = uri.path, identifier:, options: nil)
|
|
60
|
+
method = 'DELETE'
|
|
69
61
|
uri_path = path
|
|
70
62
|
uri_path += "/#{CGI.escapeURIComponent(identifier)}" if identifier
|
|
71
63
|
uri_path += "?#{URI.encode_www_form(options)}" unless options.nil?
|
|
72
|
-
execute_request do
|
|
73
|
-
http_client.send_request(
|
|
74
|
-
'DELETE',
|
|
75
|
-
uri_path,
|
|
76
|
-
prepare_payload(nil),
|
|
77
|
-
headers
|
|
78
|
-
)
|
|
64
|
+
execute_request(method:) do
|
|
65
|
+
http_client.send_request(method, uri_path, prepare_payload(nil), headers)
|
|
79
66
|
end
|
|
80
67
|
end
|
|
81
68
|
|
|
82
69
|
def get_all(options, path = uri.path)
|
|
70
|
+
method = 'GET'
|
|
83
71
|
uri_path = options.empty? ? path : "#{path}?#{URI.encode_www_form(options)}"
|
|
84
72
|
|
|
85
|
-
execute_request do
|
|
86
|
-
http_client.send_request(
|
|
87
|
-
'GET',
|
|
88
|
-
uri_path,
|
|
89
|
-
prepare_payload(nil),
|
|
90
|
-
headers
|
|
91
|
-
)
|
|
73
|
+
execute_request(method:) do
|
|
74
|
+
http_client.send_request(method, uri_path, prepare_payload(nil), headers)
|
|
92
75
|
end
|
|
93
76
|
end
|
|
94
77
|
|
|
@@ -104,29 +87,41 @@ module Lago
|
|
|
104
87
|
}
|
|
105
88
|
end
|
|
106
89
|
|
|
107
|
-
def execute_request(retry_count = 0, &block)
|
|
90
|
+
def execute_request(retry_count = 0, method: nil, &block)
|
|
108
91
|
response = yield
|
|
109
|
-
handle_response(response, retry_count, block)
|
|
92
|
+
handle_response(response, retry_count, block, method:)
|
|
110
93
|
end
|
|
111
94
|
|
|
112
|
-
def handle_response(response, retry_count, block)
|
|
95
|
+
def handle_response(response, retry_count, block, method: nil)
|
|
113
96
|
code = response.code.to_i
|
|
114
97
|
|
|
115
98
|
if code == 429 && @retry_on_rate_limit && retry_count < @max_retries
|
|
116
|
-
handle_rate_limit(response, retry_count, block)
|
|
99
|
+
handle_rate_limit(response, retry_count, block, method:)
|
|
117
100
|
elsif !RESPONSE_SUCCESS_CODES.include?(code)
|
|
118
101
|
raise_error(response)
|
|
119
102
|
else
|
|
103
|
+
emit_rate_limit_info(response, method:)
|
|
120
104
|
parse_response_body(response)
|
|
121
105
|
end
|
|
122
106
|
rescue JSON::ParserError
|
|
123
107
|
response.body
|
|
124
108
|
end
|
|
125
109
|
|
|
126
|
-
def handle_rate_limit(response, retry_count, block)
|
|
110
|
+
def handle_rate_limit(response, retry_count, block, method: nil)
|
|
127
111
|
reset_seconds = extract_reset_seconds(response, retry_count)
|
|
128
112
|
sleep(reset_seconds)
|
|
129
|
-
execute_request(retry_count + 1, &block)
|
|
113
|
+
execute_request(retry_count + 1, method:, &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def emit_rate_limit_info(response, method: nil)
|
|
117
|
+
return if @on_rate_limit_info.nil?
|
|
118
|
+
|
|
119
|
+
info = Lago::Api::RateLimitInfo.parse(response, method:, url: uri.to_s)
|
|
120
|
+
return if info.nil?
|
|
121
|
+
|
|
122
|
+
@on_rate_limit_info.call(info)
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
warn("Lago: on_rate_limit_info callback raised: #{e.class}: #{e.message}")
|
|
130
125
|
end
|
|
131
126
|
|
|
132
127
|
def parse_response_body(response)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Lago
|
|
6
|
+
module Api
|
|
7
|
+
# Ready-to-use +on_rate_limit_info+ callable that logs a warning each time
|
|
8
|
+
# rate limit usage crosses one of the configured thresholds.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# client = Lago::Api::Client.new(
|
|
12
|
+
# api_key: '...',
|
|
13
|
+
# on_rate_limit_info: Lago::Api::LoggingRateLimitObserver.new,
|
|
14
|
+
# )
|
|
15
|
+
class LoggingRateLimitObserver
|
|
16
|
+
DEFAULT_THRESHOLDS = [0.80, 0.90, 0.95].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(thresholds: DEFAULT_THRESHOLDS, logger: nil, level: Logger::WARN)
|
|
19
|
+
@thresholds = thresholds.sort.reverse
|
|
20
|
+
@logger = logger || default_logger
|
|
21
|
+
@level = level
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(info)
|
|
25
|
+
pct = info.usage_pct
|
|
26
|
+
return if pct.nil?
|
|
27
|
+
|
|
28
|
+
return unless @thresholds.any? { |threshold| pct >= threshold }
|
|
29
|
+
|
|
30
|
+
@logger.add(
|
|
31
|
+
@level,
|
|
32
|
+
format(
|
|
33
|
+
'Lago rate limit at %<pct>.0f%% (limit=%<limit>s, remaining=%<remaining>s, ' \
|
|
34
|
+
'reset=%<reset>ss, %<method>s %<url>s)',
|
|
35
|
+
pct: pct * 100,
|
|
36
|
+
limit: info.limit.inspect,
|
|
37
|
+
remaining: info.remaining.inspect,
|
|
38
|
+
reset: info.reset.inspect,
|
|
39
|
+
method: info.method,
|
|
40
|
+
url: info.url,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def default_logger
|
|
48
|
+
logger = Logger.new($stderr)
|
|
49
|
+
logger.progname = 'lago_ruby_client.rate_limit'
|
|
50
|
+
logger
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lago
|
|
4
|
+
module Api
|
|
5
|
+
# Parsed rate limit headers from a Lago API response.
|
|
6
|
+
#
|
|
7
|
+
# Delivered to the +on_rate_limit_info+ callback after every successful
|
|
8
|
+
# request so callers can build observability around the rate limit
|
|
9
|
+
# (warn at thresholds, emit metrics, etc.).
|
|
10
|
+
class RateLimitInfo
|
|
11
|
+
attr_reader :limit, :remaining, :reset, :method, :url
|
|
12
|
+
|
|
13
|
+
# Parses x-ratelimit-* headers from a Net::HTTPResponse-like object.
|
|
14
|
+
# Returns +nil+ when no rate limit headers are present.
|
|
15
|
+
def self.parse(response, method:, url:)
|
|
16
|
+
limit = response['x-ratelimit-limit']
|
|
17
|
+
remaining = response['x-ratelimit-remaining']
|
|
18
|
+
reset = response['x-ratelimit-reset']
|
|
19
|
+
|
|
20
|
+
return nil if limit.nil? && remaining.nil? && reset.nil?
|
|
21
|
+
|
|
22
|
+
new(
|
|
23
|
+
limit: limit&.to_i,
|
|
24
|
+
remaining: remaining&.to_i,
|
|
25
|
+
reset: reset&.to_i,
|
|
26
|
+
method:,
|
|
27
|
+
url:,
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(limit:, remaining:, reset:, method:, url:)
|
|
32
|
+
@limit = limit
|
|
33
|
+
@remaining = remaining
|
|
34
|
+
@reset = reset
|
|
35
|
+
@method = method
|
|
36
|
+
@url = url
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns the fraction of the rate limit currently used as a Float in
|
|
40
|
+
# [0.0, 1.0], or +nil+ when the headers aren't usable (missing limit,
|
|
41
|
+
# zero limit, missing remaining).
|
|
42
|
+
def usage_pct
|
|
43
|
+
return nil if limit.nil? || remaining.nil? || limit.to_i <= 0
|
|
44
|
+
|
|
45
|
+
1.0 - (remaining.to_f / limit)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -20,10 +20,15 @@ module Lago
|
|
|
20
20
|
|
|
21
21
|
def current_usage( # rubocop:disable Metrics/ParameterLists
|
|
22
22
|
external_customer_id, external_subscription_id, apply_taxes: nil,
|
|
23
|
+
charge_id: nil, charge_code: nil, billable_metric_code: nil, group: nil,
|
|
23
24
|
filter_by_charge_id: nil, filter_by_charge_code: nil, filter_by_group: nil, full_usage: nil
|
|
24
25
|
)
|
|
25
26
|
query_params = { external_subscription_id: external_subscription_id }
|
|
26
27
|
query_params[:apply_taxes] = apply_taxes unless apply_taxes.nil?
|
|
28
|
+
query_params[:charge_id] = charge_id unless charge_id.nil?
|
|
29
|
+
query_params[:charge_code] = charge_code unless charge_code.nil?
|
|
30
|
+
query_params[:billable_metric_code] = billable_metric_code unless billable_metric_code.nil?
|
|
31
|
+
group&.each { |k, v| query_params[:"group[#{k}]"] = v }
|
|
27
32
|
query_params[:filter_by_charge_id] = filter_by_charge_id unless filter_by_charge_id.nil?
|
|
28
33
|
query_params[:filter_by_charge_code] = filter_by_charge_code unless filter_by_charge_code.nil?
|
|
29
34
|
filter_by_group&.each { |k, v| query_params[:"filter_by_group[#{k}]"] = v }
|
data/lib/lago/version.rb
CHANGED
data/lib/lago-ruby-client.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lago-ruby-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.47.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lovro Colic
|
|
@@ -313,7 +313,9 @@ files:
|
|
|
313
313
|
- lib/lago/api/client.rb
|
|
314
314
|
- lib/lago/api/connection.rb
|
|
315
315
|
- lib/lago/api/http_error.rb
|
|
316
|
+
- lib/lago/api/logging_rate_limit_observer.rb
|
|
316
317
|
- lib/lago/api/rate_limit_error.rb
|
|
318
|
+
- lib/lago/api/rate_limit_info.rb
|
|
317
319
|
- lib/lago/api/resources/activity_log.rb
|
|
318
320
|
- lib/lago/api/resources/add_on.rb
|
|
319
321
|
- lib/lago/api/resources/api_log.rb
|