sports-odds-api 1.0.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 +7 -0
- data/.ignore +2 -0
- data/CHANGELOG.md +10 -0
- data/README.md +186 -0
- data/SECURITY.md +27 -0
- data/lib/sports_odds_api/client.rb +112 -0
- data/lib/sports_odds_api/errors.rb +228 -0
- data/lib/sports_odds_api/file_part.rb +55 -0
- data/lib/sports_odds_api/internal/next_cursor_page.rb +86 -0
- data/lib/sports_odds_api/internal/transport/base_client.rb +580 -0
- data/lib/sports_odds_api/internal/transport/pooled_net_requester.rb +201 -0
- data/lib/sports_odds_api/internal/type/array_of.rb +168 -0
- data/lib/sports_odds_api/internal/type/base_model.rb +534 -0
- data/lib/sports_odds_api/internal/type/base_page.rb +55 -0
- data/lib/sports_odds_api/internal/type/boolean.rb +77 -0
- data/lib/sports_odds_api/internal/type/converter.rb +327 -0
- data/lib/sports_odds_api/internal/type/enum.rb +131 -0
- data/lib/sports_odds_api/internal/type/file_input.rb +108 -0
- data/lib/sports_odds_api/internal/type/hash_of.rb +188 -0
- data/lib/sports_odds_api/internal/type/request_parameters.rb +42 -0
- data/lib/sports_odds_api/internal/type/union.rb +243 -0
- data/lib/sports_odds_api/internal/type/unknown.rb +81 -0
- data/lib/sports_odds_api/internal/util.rb +914 -0
- data/lib/sports_odds_api/internal.rb +20 -0
- data/lib/sports_odds_api/models/account_get_usage_params.rb +14 -0
- data/lib/sports_odds_api/models/account_usage.rb +91 -0
- data/lib/sports_odds_api/models/event.rb +686 -0
- data/lib/sports_odds_api/models/event_get_params.rb +195 -0
- data/lib/sports_odds_api/models/league.rb +39 -0
- data/lib/sports_odds_api/models/league_get_params.rb +30 -0
- data/lib/sports_odds_api/models/league_get_response.rb +8 -0
- data/lib/sports_odds_api/models/player.rb +128 -0
- data/lib/sports_odds_api/models/player_get_params.rb +58 -0
- data/lib/sports_odds_api/models/rate_limit_interval.rb +92 -0
- data/lib/sports_odds_api/models/sport.rb +197 -0
- data/lib/sports_odds_api/models/sport_get_params.rb +14 -0
- data/lib/sports_odds_api/models/sport_get_response.rb +8 -0
- data/lib/sports_odds_api/models/stat.rb +144 -0
- data/lib/sports_odds_api/models/stat_get_params.rb +43 -0
- data/lib/sports_odds_api/models/stat_get_response.rb +8 -0
- data/lib/sports_odds_api/models/stream_events_params.rb +38 -0
- data/lib/sports_odds_api/models/stream_events_response.rb +120 -0
- data/lib/sports_odds_api/models/team.rb +162 -0
- data/lib/sports_odds_api/models/team_get_params.rb +58 -0
- data/lib/sports_odds_api/models.rb +76 -0
- data/lib/sports_odds_api/request_options.rb +78 -0
- data/lib/sports_odds_api/resources/account.rb +33 -0
- data/lib/sports_odds_api/resources/events.rb +94 -0
- data/lib/sports_odds_api/resources/leagues.rb +39 -0
- data/lib/sports_odds_api/resources/players.rb +48 -0
- data/lib/sports_odds_api/resources/sports.rb +33 -0
- data/lib/sports_odds_api/resources/stats.rb +44 -0
- data/lib/sports_odds_api/resources/stream.rb +40 -0
- data/lib/sports_odds_api/resources/teams.rb +48 -0
- data/lib/sports_odds_api/version.rb +5 -0
- data/lib/sports_odds_api.rb +82 -0
- data/manifest.yaml +15 -0
- data/rbi/sports_odds_api/client.rbi +83 -0
- data/rbi/sports_odds_api/errors.rbi +205 -0
- data/rbi/sports_odds_api/file_part.rbi +37 -0
- data/rbi/sports_odds_api/internal/next_cursor_page.rbi +22 -0
- data/rbi/sports_odds_api/internal/transport/base_client.rbi +305 -0
- data/rbi/sports_odds_api/internal/transport/pooled_net_requester.rbi +80 -0
- data/rbi/sports_odds_api/internal/type/array_of.rbi +104 -0
- data/rbi/sports_odds_api/internal/type/base_model.rbi +310 -0
- data/rbi/sports_odds_api/internal/type/base_page.rbi +43 -0
- data/rbi/sports_odds_api/internal/type/boolean.rbi +58 -0
- data/rbi/sports_odds_api/internal/type/converter.rbi +225 -0
- data/rbi/sports_odds_api/internal/type/enum.rbi +82 -0
- data/rbi/sports_odds_api/internal/type/file_input.rbi +59 -0
- data/rbi/sports_odds_api/internal/type/hash_of.rbi +104 -0
- data/rbi/sports_odds_api/internal/type/request_parameters.rbi +31 -0
- data/rbi/sports_odds_api/internal/type/union.rbi +128 -0
- data/rbi/sports_odds_api/internal/type/unknown.rbi +58 -0
- data/rbi/sports_odds_api/internal/util.rbi +487 -0
- data/rbi/sports_odds_api/internal.rbi +18 -0
- data/rbi/sports_odds_api/models/account_get_usage_params.rbi +32 -0
- data/rbi/sports_odds_api/models/account_usage.rbi +173 -0
- data/rbi/sports_odds_api/models/event.rbi +1269 -0
- data/rbi/sports_odds_api/models/event_get_params.rbi +286 -0
- data/rbi/sports_odds_api/models/league.rbi +74 -0
- data/rbi/sports_odds_api/models/league_get_params.rbi +60 -0
- data/rbi/sports_odds_api/models/league_get_response.rbi +11 -0
- data/rbi/sports_odds_api/models/player.rbi +247 -0
- data/rbi/sports_odds_api/models/player_get_params.rbi +95 -0
- data/rbi/sports_odds_api/models/rate_limit_interval.rbi +176 -0
- data/rbi/sports_odds_api/models/sport.rbi +371 -0
- data/rbi/sports_odds_api/models/sport_get_params.rbi +29 -0
- data/rbi/sports_odds_api/models/sport_get_response.rbi +11 -0
- data/rbi/sports_odds_api/models/stat.rbi +273 -0
- data/rbi/sports_odds_api/models/stat_get_params.rbi +72 -0
- data/rbi/sports_odds_api/models/stat_get_response.rbi +11 -0
- data/rbi/sports_odds_api/models/stream_events_params.rbi +71 -0
- data/rbi/sports_odds_api/models/stream_events_response.rbi +247 -0
- data/rbi/sports_odds_api/models/team.rbi +305 -0
- data/rbi/sports_odds_api/models/team_get_params.rbi +92 -0
- data/rbi/sports_odds_api/models.rbi +35 -0
- data/rbi/sports_odds_api/request_options.rbi +59 -0
- data/rbi/sports_odds_api/resources/account.rbi +21 -0
- data/rbi/sports_odds_api/resources/events.rbi +96 -0
- data/rbi/sports_odds_api/resources/leagues.rbi +29 -0
- data/rbi/sports_odds_api/resources/players.rbi +41 -0
- data/rbi/sports_odds_api/resources/sports.rbi +21 -0
- data/rbi/sports_odds_api/resources/stats.rbi +34 -0
- data/rbi/sports_odds_api/resources/stream.rbi +32 -0
- data/rbi/sports_odds_api/resources/teams.rbi +39 -0
- data/rbi/sports_odds_api/version.rbi +5 -0
- data/sig/sports_odds_api/client.rbs +45 -0
- data/sig/sports_odds_api/errors.rbs +117 -0
- data/sig/sports_odds_api/file_part.rbs +21 -0
- data/sig/sports_odds_api/internal/next_cursor_page.rbs +13 -0
- data/sig/sports_odds_api/internal/transport/base_client.rbs +133 -0
- data/sig/sports_odds_api/internal/transport/pooled_net_requester.rbs +45 -0
- data/sig/sports_odds_api/internal/type/array_of.rbs +48 -0
- data/sig/sports_odds_api/internal/type/base_model.rbs +104 -0
- data/sig/sports_odds_api/internal/type/base_page.rbs +24 -0
- data/sig/sports_odds_api/internal/type/boolean.rbs +26 -0
- data/sig/sports_odds_api/internal/type/converter.rbs +79 -0
- data/sig/sports_odds_api/internal/type/enum.rbs +32 -0
- data/sig/sports_odds_api/internal/type/file_input.rbs +25 -0
- data/sig/sports_odds_api/internal/type/hash_of.rbs +48 -0
- data/sig/sports_odds_api/internal/type/request_parameters.rbs +19 -0
- data/sig/sports_odds_api/internal/type/union.rbs +52 -0
- data/sig/sports_odds_api/internal/type/unknown.rbs +26 -0
- data/sig/sports_odds_api/internal/util.rbs +185 -0
- data/sig/sports_odds_api/internal.rbs +10 -0
- data/sig/sports_odds_api/models/account_get_usage_params.rbs +15 -0
- data/sig/sports_odds_api/models/account_usage.rbs +116 -0
- data/sig/sports_odds_api/models/event.rbs +860 -0
- data/sig/sports_odds_api/models/event_get_params.rbs +168 -0
- data/sig/sports_odds_api/models/league.rbs +50 -0
- data/sig/sports_odds_api/models/league_get_params.rbs +32 -0
- data/sig/sports_odds_api/models/league_get_response.rbs +7 -0
- data/sig/sports_odds_api/models/player.rbs +162 -0
- data/sig/sports_odds_api/models/player_get_params.rbs +56 -0
- data/sig/sports_odds_api/models/rate_limit_interval.rbs +67 -0
- data/sig/sports_odds_api/models/sport.rbs +241 -0
- data/sig/sports_odds_api/models/sport_get_params.rbs +15 -0
- data/sig/sports_odds_api/models/sport_get_response.rbs +7 -0
- data/sig/sports_odds_api/models/stat.rbs +166 -0
- data/sig/sports_odds_api/models/stat_get_params.rbs +38 -0
- data/sig/sports_odds_api/models/stat_get_response.rbs +7 -0
- data/sig/sports_odds_api/models/stream_events_params.rbs +38 -0
- data/sig/sports_odds_api/models/stream_events_response.rbs +151 -0
- data/sig/sports_odds_api/models/team.rbs +201 -0
- data/sig/sports_odds_api/models/team_get_params.rbs +56 -0
- data/sig/sports_odds_api/models.rbs +33 -0
- data/sig/sports_odds_api/request_options.rbs +36 -0
- data/sig/sports_odds_api/resources/account.rbs +11 -0
- data/sig/sports_odds_api/resources/events.rbs +32 -0
- data/sig/sports_odds_api/resources/leagues.rbs +13 -0
- data/sig/sports_odds_api/resources/players.rbs +16 -0
- data/sig/sports_odds_api/resources/sports.rbs +11 -0
- data/sig/sports_odds_api/resources/stats.rbs +14 -0
- data/sig/sports_odds_api/resources/stream.rbs +14 -0
- data/sig/sports_odds_api/resources/teams.rbs +16 -0
- data/sig/sports_odds_api/version.rbs +3 -0
- metadata +215 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SportsOddsAPI
|
|
4
|
+
module Internal
|
|
5
|
+
# @generic Elem
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# if next_cursor_page.has_next?
|
|
9
|
+
# next_cursor_page = next_cursor_page.next_page
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# next_cursor_page.auto_paging_each do |event|
|
|
14
|
+
# puts(event)
|
|
15
|
+
# end
|
|
16
|
+
class NextCursorPage
|
|
17
|
+
include SportsOddsAPI::Internal::Type::BasePage
|
|
18
|
+
|
|
19
|
+
# @return [Array<generic<Elem>>, nil]
|
|
20
|
+
attr_accessor :data
|
|
21
|
+
|
|
22
|
+
# @return [String]
|
|
23
|
+
attr_accessor :next_cursor
|
|
24
|
+
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def next_page?
|
|
27
|
+
!data.to_a.empty? && !next_cursor.to_s.empty?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @raise [SportsOddsAPI::HTTP::Error]
|
|
31
|
+
# @return [self]
|
|
32
|
+
def next_page
|
|
33
|
+
unless next_page?
|
|
34
|
+
message = "No more pages available. Please check #next_page? before calling ##{__method__}"
|
|
35
|
+
raise RuntimeError.new(message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
req = SportsOddsAPI::Internal::Util.deep_merge(@req, {query: {cursor: next_cursor}})
|
|
39
|
+
@client.request(req)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param blk [Proc]
|
|
43
|
+
#
|
|
44
|
+
# @yieldparam [generic<Elem>]
|
|
45
|
+
def auto_paging_each(&blk)
|
|
46
|
+
unless block_given?
|
|
47
|
+
raise ArgumentError.new("A block must be given to ##{__method__}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
page = self
|
|
51
|
+
loop do
|
|
52
|
+
page.data&.each(&blk)
|
|
53
|
+
|
|
54
|
+
break unless page.next_page?
|
|
55
|
+
page = page.next_page
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @api private
|
|
60
|
+
#
|
|
61
|
+
# @param client [SportsOddsAPI::Internal::Transport::BaseClient]
|
|
62
|
+
# @param req [Hash{Symbol=>Object}]
|
|
63
|
+
# @param headers [Hash{String=>String}]
|
|
64
|
+
# @param page_data [Hash{Symbol=>Object}]
|
|
65
|
+
def initialize(client:, req:, headers:, page_data:)
|
|
66
|
+
super
|
|
67
|
+
|
|
68
|
+
case page_data
|
|
69
|
+
in {data: Array => data}
|
|
70
|
+
@data = data.map { SportsOddsAPI::Internal::Type::Converter.coerce(@model, _1) }
|
|
71
|
+
else
|
|
72
|
+
end
|
|
73
|
+
@next_cursor = page_data[:nextCursor]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @api private
|
|
77
|
+
#
|
|
78
|
+
# @return [String]
|
|
79
|
+
def inspect
|
|
80
|
+
model = SportsOddsAPI::Internal::Type::Converter.inspect(@model, depth: 1)
|
|
81
|
+
|
|
82
|
+
"#<#{self.class}[#{model}]:0x#{object_id.to_s(16)} next_cursor=#{next_cursor.inspect}>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SportsOddsAPI
|
|
4
|
+
module Internal
|
|
5
|
+
module Transport
|
|
6
|
+
# @api private
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
class BaseClient
|
|
10
|
+
extend SportsOddsAPI::Internal::Util::SorbetRuntimeSupport
|
|
11
|
+
|
|
12
|
+
# from whatwg fetch spec
|
|
13
|
+
MAX_REDIRECTS = 20
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Style/MutableConstant
|
|
16
|
+
PLATFORM_HEADERS =
|
|
17
|
+
{
|
|
18
|
+
"x-stainless-arch" => SportsOddsAPI::Internal::Util.arch,
|
|
19
|
+
"x-stainless-lang" => "ruby",
|
|
20
|
+
"x-stainless-os" => SportsOddsAPI::Internal::Util.os,
|
|
21
|
+
"x-stainless-package-version" => SportsOddsAPI::VERSION,
|
|
22
|
+
"x-stainless-runtime" => ::RUBY_ENGINE,
|
|
23
|
+
"x-stainless-runtime-version" => ::RUBY_ENGINE_VERSION
|
|
24
|
+
}
|
|
25
|
+
# rubocop:enable Style/MutableConstant
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @api private
|
|
29
|
+
#
|
|
30
|
+
# @param req [Hash{Symbol=>Object}]
|
|
31
|
+
#
|
|
32
|
+
# @raise [ArgumentError]
|
|
33
|
+
def validate!(req)
|
|
34
|
+
keys = [:method, :path, :query, :headers, :body, :unwrap, :page, :stream, :model, :options]
|
|
35
|
+
case req
|
|
36
|
+
in Hash
|
|
37
|
+
req.each_key do |k|
|
|
38
|
+
unless keys.include?(k)
|
|
39
|
+
raise ArgumentError.new("Request `req` keys must be one of #{keys}, got #{k.inspect}")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError.new("Request `req` must be a Hash or RequestOptions, got #{req.inspect}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @api private
|
|
48
|
+
#
|
|
49
|
+
# @param status [Integer]
|
|
50
|
+
# @param headers [Hash{String=>String}]
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
def should_retry?(status, headers:)
|
|
54
|
+
coerced = SportsOddsAPI::Internal::Util.coerce_boolean(headers["x-should-retry"])
|
|
55
|
+
case [coerced, status]
|
|
56
|
+
in [true | false, _]
|
|
57
|
+
coerced
|
|
58
|
+
in [_, 408 | 409 | 429 | (500..)]
|
|
59
|
+
# retry on:
|
|
60
|
+
# 408: timeouts
|
|
61
|
+
# 409: locks
|
|
62
|
+
# 429: rate limits
|
|
63
|
+
# 500+: unknown errors
|
|
64
|
+
true
|
|
65
|
+
else
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @api private
|
|
71
|
+
#
|
|
72
|
+
# @param request [Hash{Symbol=>Object}] .
|
|
73
|
+
#
|
|
74
|
+
# @option request [Symbol] :method
|
|
75
|
+
#
|
|
76
|
+
# @option request [URI::Generic] :url
|
|
77
|
+
#
|
|
78
|
+
# @option request [Hash{String=>String}] :headers
|
|
79
|
+
#
|
|
80
|
+
# @option request [Object] :body
|
|
81
|
+
#
|
|
82
|
+
# @option request [Integer] :max_retries
|
|
83
|
+
#
|
|
84
|
+
# @option request [Float] :timeout
|
|
85
|
+
#
|
|
86
|
+
# @param status [Integer]
|
|
87
|
+
#
|
|
88
|
+
# @param response_headers [Hash{String=>String}]
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash{Symbol=>Object}]
|
|
91
|
+
def follow_redirect(request, status:, response_headers:)
|
|
92
|
+
method, url, headers = request.fetch_values(:method, :url, :headers)
|
|
93
|
+
location =
|
|
94
|
+
Kernel.then do
|
|
95
|
+
URI.join(url, response_headers["location"])
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
message = "Server responded with status #{status} but no valid location header."
|
|
98
|
+
raise SportsOddsAPI::Errors::APIConnectionError.new(
|
|
99
|
+
url: url,
|
|
100
|
+
response: response_headers,
|
|
101
|
+
message: message
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
request = {**request, url: location}
|
|
106
|
+
|
|
107
|
+
case [url.scheme, location.scheme]
|
|
108
|
+
in ["https", "http"]
|
|
109
|
+
message = "Tried to redirect to a insecure URL"
|
|
110
|
+
raise SportsOddsAPI::Errors::APIConnectionError.new(
|
|
111
|
+
url: url,
|
|
112
|
+
response: response_headers,
|
|
113
|
+
message: message
|
|
114
|
+
)
|
|
115
|
+
else
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# from whatwg fetch spec
|
|
120
|
+
case [status, method]
|
|
121
|
+
in [301 | 302, :post] | [303, _]
|
|
122
|
+
drop = %w[content-encoding content-language content-length content-location content-type]
|
|
123
|
+
request = {
|
|
124
|
+
**request,
|
|
125
|
+
method: method == :head ? :head : :get,
|
|
126
|
+
headers: headers.except(*drop),
|
|
127
|
+
body: nil
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# from undici
|
|
133
|
+
if SportsOddsAPI::Internal::Util.uri_origin(url) != SportsOddsAPI::Internal::Util.uri_origin(location)
|
|
134
|
+
drop = %w[authorization cookie host proxy-authorization]
|
|
135
|
+
request = {**request, headers: request.fetch(:headers).except(*drop)}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
request
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @api private
|
|
142
|
+
#
|
|
143
|
+
# @param status [Integer, SportsOddsAPI::Errors::APIConnectionError]
|
|
144
|
+
# @param stream [Enumerable<String>, nil]
|
|
145
|
+
def reap_connection!(status, stream:)
|
|
146
|
+
case status
|
|
147
|
+
in (..199) | (300..499)
|
|
148
|
+
stream&.each { next }
|
|
149
|
+
in SportsOddsAPI::Errors::APIConnectionError | (500..)
|
|
150
|
+
SportsOddsAPI::Internal::Util.close_fused!(stream)
|
|
151
|
+
else
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [URI::Generic]
|
|
157
|
+
attr_reader :base_url
|
|
158
|
+
|
|
159
|
+
# @return [Float]
|
|
160
|
+
attr_reader :timeout
|
|
161
|
+
|
|
162
|
+
# @return [Integer]
|
|
163
|
+
attr_reader :max_retries
|
|
164
|
+
|
|
165
|
+
# @return [Float]
|
|
166
|
+
attr_reader :initial_retry_delay
|
|
167
|
+
|
|
168
|
+
# @return [Float]
|
|
169
|
+
attr_reader :max_retry_delay
|
|
170
|
+
|
|
171
|
+
# @return [Hash{String=>String}]
|
|
172
|
+
attr_reader :headers
|
|
173
|
+
|
|
174
|
+
# @return [String, nil]
|
|
175
|
+
attr_reader :idempotency_header
|
|
176
|
+
|
|
177
|
+
# @api private
|
|
178
|
+
# @return [SportsOddsAPI::Internal::Transport::PooledNetRequester]
|
|
179
|
+
attr_reader :requester
|
|
180
|
+
|
|
181
|
+
# @api private
|
|
182
|
+
#
|
|
183
|
+
# @param base_url [String]
|
|
184
|
+
# @param timeout [Float]
|
|
185
|
+
# @param max_retries [Integer]
|
|
186
|
+
# @param initial_retry_delay [Float]
|
|
187
|
+
# @param max_retry_delay [Float]
|
|
188
|
+
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
|
|
189
|
+
# @param idempotency_header [String, nil]
|
|
190
|
+
def initialize(
|
|
191
|
+
base_url:,
|
|
192
|
+
timeout: 0.0,
|
|
193
|
+
max_retries: 0,
|
|
194
|
+
initial_retry_delay: 0.0,
|
|
195
|
+
max_retry_delay: 0.0,
|
|
196
|
+
headers: {},
|
|
197
|
+
idempotency_header: nil
|
|
198
|
+
)
|
|
199
|
+
@requester = SportsOddsAPI::Internal::Transport::PooledNetRequester.new
|
|
200
|
+
@headers = SportsOddsAPI::Internal::Util.normalized_headers(
|
|
201
|
+
self.class::PLATFORM_HEADERS,
|
|
202
|
+
{
|
|
203
|
+
"accept" => "application/json",
|
|
204
|
+
"content-type" => "application/json"
|
|
205
|
+
},
|
|
206
|
+
headers
|
|
207
|
+
)
|
|
208
|
+
@base_url_components = SportsOddsAPI::Internal::Util.parse_uri(base_url)
|
|
209
|
+
@base_url = SportsOddsAPI::Internal::Util.unparse_uri(@base_url_components)
|
|
210
|
+
@idempotency_header = idempotency_header&.to_s&.downcase
|
|
211
|
+
@timeout = timeout
|
|
212
|
+
@max_retries = max_retries
|
|
213
|
+
@initial_retry_delay = initial_retry_delay
|
|
214
|
+
@max_retry_delay = max_retry_delay
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @api private
|
|
218
|
+
#
|
|
219
|
+
# @return [Hash{String=>String}]
|
|
220
|
+
private def auth_headers = {}
|
|
221
|
+
|
|
222
|
+
# @api private
|
|
223
|
+
#
|
|
224
|
+
# @return [Hash{String=>String}]
|
|
225
|
+
private def auth_query = {}
|
|
226
|
+
|
|
227
|
+
# @api private
|
|
228
|
+
#
|
|
229
|
+
# @return [String]
|
|
230
|
+
private def generate_idempotency_key = "stainless-ruby-retry-#{SecureRandom.uuid}"
|
|
231
|
+
|
|
232
|
+
# @api private
|
|
233
|
+
#
|
|
234
|
+
# @param req [Hash{Symbol=>Object}] .
|
|
235
|
+
#
|
|
236
|
+
# @option req [Symbol] :method
|
|
237
|
+
#
|
|
238
|
+
# @option req [String, Array<String>] :path
|
|
239
|
+
#
|
|
240
|
+
# @option req [Hash{String=>Array<String>, String, nil}, nil] :query
|
|
241
|
+
#
|
|
242
|
+
# @option req [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil] :headers
|
|
243
|
+
#
|
|
244
|
+
# @option req [Object, nil] :body
|
|
245
|
+
#
|
|
246
|
+
# @option req [Symbol, Integer, Array<Symbol, Integer>, Proc, nil] :unwrap
|
|
247
|
+
#
|
|
248
|
+
# @option req [Class<SportsOddsAPI::Internal::Type::BasePage>, nil] :page
|
|
249
|
+
#
|
|
250
|
+
# @option req [Class<SportsOddsAPI::Internal::Type::BaseStream>, nil] :stream
|
|
251
|
+
#
|
|
252
|
+
# @option req [SportsOddsAPI::Internal::Type::Converter, Class, nil] :model
|
|
253
|
+
#
|
|
254
|
+
# @param opts [Hash{Symbol=>Object}] .
|
|
255
|
+
#
|
|
256
|
+
# @option opts [String, nil] :idempotency_key
|
|
257
|
+
#
|
|
258
|
+
# @option opts [Hash{String=>Array<String>, String, nil}, nil] :extra_query
|
|
259
|
+
#
|
|
260
|
+
# @option opts [Hash{String=>String, nil}, nil] :extra_headers
|
|
261
|
+
#
|
|
262
|
+
# @option opts [Object, nil] :extra_body
|
|
263
|
+
#
|
|
264
|
+
# @option opts [Integer, nil] :max_retries
|
|
265
|
+
#
|
|
266
|
+
# @option opts [Float, nil] :timeout
|
|
267
|
+
#
|
|
268
|
+
# @return [Hash{Symbol=>Object}]
|
|
269
|
+
private def build_request(req, opts)
|
|
270
|
+
method, uninterpolated_path = req.fetch_values(:method, :path)
|
|
271
|
+
|
|
272
|
+
path = SportsOddsAPI::Internal::Util.interpolate_path(uninterpolated_path)
|
|
273
|
+
|
|
274
|
+
query = SportsOddsAPI::Internal::Util.deep_merge(
|
|
275
|
+
auth_query,
|
|
276
|
+
req[:query].to_h,
|
|
277
|
+
opts[:extra_query].to_h
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
headers = SportsOddsAPI::Internal::Util.normalized_headers(
|
|
281
|
+
@headers,
|
|
282
|
+
auth_headers,
|
|
283
|
+
req[:headers].to_h,
|
|
284
|
+
opts[:extra_headers].to_h
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if @idempotency_header &&
|
|
288
|
+
!headers.key?(@idempotency_header) &&
|
|
289
|
+
(!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key))
|
|
290
|
+
headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
unless headers.key?("x-stainless-retry-count")
|
|
294
|
+
headers["x-stainless-retry-count"] = "0"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..)
|
|
298
|
+
unless headers.key?("x-stainless-timeout") || timeout.zero?
|
|
299
|
+
headers["x-stainless-timeout"] = timeout.to_s
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
headers.reject! { |_, v| v.to_s.empty? }
|
|
303
|
+
|
|
304
|
+
body =
|
|
305
|
+
case method
|
|
306
|
+
in :get | :head | :options | :trace
|
|
307
|
+
nil
|
|
308
|
+
else
|
|
309
|
+
SportsOddsAPI::Internal::Util.deep_merge(*[req[:body], opts[:extra_body]].compact)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
url = SportsOddsAPI::Internal::Util.join_parsed_uri(
|
|
313
|
+
@base_url_components,
|
|
314
|
+
{**req, path: path, query: query}
|
|
315
|
+
)
|
|
316
|
+
headers, encoded = SportsOddsAPI::Internal::Util.encode_content(headers, body)
|
|
317
|
+
{
|
|
318
|
+
method: method,
|
|
319
|
+
url: url,
|
|
320
|
+
headers: headers,
|
|
321
|
+
body: encoded,
|
|
322
|
+
max_retries: opts.fetch(:max_retries, @max_retries),
|
|
323
|
+
timeout: timeout
|
|
324
|
+
}
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @api private
|
|
328
|
+
#
|
|
329
|
+
# @param headers [Hash{String=>String}]
|
|
330
|
+
# @param retry_count [Integer]
|
|
331
|
+
#
|
|
332
|
+
# @return [Float]
|
|
333
|
+
private def retry_delay(headers, retry_count:)
|
|
334
|
+
# Non-standard extension
|
|
335
|
+
span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
|
|
336
|
+
return span if span
|
|
337
|
+
|
|
338
|
+
retry_header = headers["retry-after"]
|
|
339
|
+
return span if (span = Float(retry_header, exception: false))
|
|
340
|
+
|
|
341
|
+
span = retry_header&.then do
|
|
342
|
+
Time.httpdate(_1) - Time.now
|
|
343
|
+
rescue ArgumentError
|
|
344
|
+
nil
|
|
345
|
+
end
|
|
346
|
+
return span if span
|
|
347
|
+
|
|
348
|
+
scale = retry_count**2
|
|
349
|
+
jitter = 1 - (0.25 * rand)
|
|
350
|
+
(@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# @api private
|
|
354
|
+
#
|
|
355
|
+
# @param request [Hash{Symbol=>Object}] .
|
|
356
|
+
#
|
|
357
|
+
# @option request [Symbol] :method
|
|
358
|
+
#
|
|
359
|
+
# @option request [URI::Generic] :url
|
|
360
|
+
#
|
|
361
|
+
# @option request [Hash{String=>String}] :headers
|
|
362
|
+
#
|
|
363
|
+
# @option request [Object] :body
|
|
364
|
+
#
|
|
365
|
+
# @option request [Integer] :max_retries
|
|
366
|
+
#
|
|
367
|
+
# @option request [Float] :timeout
|
|
368
|
+
#
|
|
369
|
+
# @param redirect_count [Integer]
|
|
370
|
+
#
|
|
371
|
+
# @param retry_count [Integer]
|
|
372
|
+
#
|
|
373
|
+
# @param send_retry_header [Boolean]
|
|
374
|
+
#
|
|
375
|
+
# @raise [SportsOddsAPI::Errors::APIError]
|
|
376
|
+
# @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
|
|
377
|
+
def send_request(request, redirect_count:, retry_count:, send_retry_header:)
|
|
378
|
+
url, headers, max_retries, timeout = request.fetch_values(:url, :headers, :max_retries, :timeout)
|
|
379
|
+
input = {**request.except(:timeout), deadline: SportsOddsAPI::Internal::Util.monotonic_secs + timeout}
|
|
380
|
+
|
|
381
|
+
if send_retry_header
|
|
382
|
+
headers["x-stainless-retry-count"] = retry_count.to_s
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
begin
|
|
386
|
+
status, response, stream = @requester.execute(input)
|
|
387
|
+
rescue SportsOddsAPI::Errors::APIConnectionError => e
|
|
388
|
+
status = e
|
|
389
|
+
end
|
|
390
|
+
headers = SportsOddsAPI::Internal::Util.normalized_headers(response&.each_header&.to_h)
|
|
391
|
+
|
|
392
|
+
case status
|
|
393
|
+
in ..299
|
|
394
|
+
[status, response, stream]
|
|
395
|
+
in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
|
|
396
|
+
self.class.reap_connection!(status, stream: stream)
|
|
397
|
+
|
|
398
|
+
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
|
|
399
|
+
raise SportsOddsAPI::Errors::APIConnectionError.new(
|
|
400
|
+
url: url,
|
|
401
|
+
response: response,
|
|
402
|
+
message: message
|
|
403
|
+
)
|
|
404
|
+
in 300..399
|
|
405
|
+
self.class.reap_connection!(status, stream: stream)
|
|
406
|
+
|
|
407
|
+
request = self.class.follow_redirect(request, status: status, response_headers: headers)
|
|
408
|
+
send_request(
|
|
409
|
+
request,
|
|
410
|
+
redirect_count: redirect_count + 1,
|
|
411
|
+
retry_count: retry_count,
|
|
412
|
+
send_retry_header: send_retry_header
|
|
413
|
+
)
|
|
414
|
+
in SportsOddsAPI::Errors::APIConnectionError if retry_count >= max_retries
|
|
415
|
+
raise status
|
|
416
|
+
in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: headers)
|
|
417
|
+
decoded = Kernel.then do
|
|
418
|
+
SportsOddsAPI::Internal::Util.decode_content(headers, stream: stream, suppress_error: true)
|
|
419
|
+
ensure
|
|
420
|
+
self.class.reap_connection!(status, stream: stream)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
raise SportsOddsAPI::Errors::APIStatusError.for(
|
|
424
|
+
url: url,
|
|
425
|
+
status: status,
|
|
426
|
+
headers: headers,
|
|
427
|
+
body: decoded,
|
|
428
|
+
request: nil,
|
|
429
|
+
response: response
|
|
430
|
+
)
|
|
431
|
+
in (400..) | SportsOddsAPI::Errors::APIConnectionError
|
|
432
|
+
self.class.reap_connection!(status, stream: stream)
|
|
433
|
+
|
|
434
|
+
delay = retry_delay(response || {}, retry_count: retry_count)
|
|
435
|
+
sleep(delay)
|
|
436
|
+
|
|
437
|
+
send_request(
|
|
438
|
+
request,
|
|
439
|
+
redirect_count: redirect_count,
|
|
440
|
+
retry_count: retry_count + 1,
|
|
441
|
+
send_retry_header: send_retry_header
|
|
442
|
+
)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Execute the request specified by `req`. This is the method that all resource
|
|
447
|
+
# methods call into.
|
|
448
|
+
#
|
|
449
|
+
# @overload request(method, path, query: {}, headers: {}, body: nil, unwrap: nil, page: nil, stream: nil, model: SportsOddsAPI::Internal::Type::Unknown, options: {})
|
|
450
|
+
#
|
|
451
|
+
# @param method [Symbol]
|
|
452
|
+
#
|
|
453
|
+
# @param path [String, Array<String>]
|
|
454
|
+
#
|
|
455
|
+
# @param query [Hash{String=>Array<String>, String, nil}, nil]
|
|
456
|
+
#
|
|
457
|
+
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil]
|
|
458
|
+
#
|
|
459
|
+
# @param body [Object, nil]
|
|
460
|
+
#
|
|
461
|
+
# @param unwrap [Symbol, Integer, Array<Symbol, Integer>, Proc, nil]
|
|
462
|
+
#
|
|
463
|
+
# @param page [Class<SportsOddsAPI::Internal::Type::BasePage>, nil]
|
|
464
|
+
#
|
|
465
|
+
# @param stream [Class<SportsOddsAPI::Internal::Type::BaseStream>, nil]
|
|
466
|
+
#
|
|
467
|
+
# @param model [SportsOddsAPI::Internal::Type::Converter, Class, nil]
|
|
468
|
+
#
|
|
469
|
+
# @param options [SportsOddsAPI::RequestOptions, Hash{Symbol=>Object}, nil] .
|
|
470
|
+
#
|
|
471
|
+
# @option options [String, nil] :idempotency_key
|
|
472
|
+
#
|
|
473
|
+
# @option options [Hash{String=>Array<String>, String, nil}, nil] :extra_query
|
|
474
|
+
#
|
|
475
|
+
# @option options [Hash{String=>String, nil}, nil] :extra_headers
|
|
476
|
+
#
|
|
477
|
+
# @option options [Object, nil] :extra_body
|
|
478
|
+
#
|
|
479
|
+
# @option options [Integer, nil] :max_retries
|
|
480
|
+
#
|
|
481
|
+
# @option options [Float, nil] :timeout
|
|
482
|
+
#
|
|
483
|
+
# @raise [SportsOddsAPI::Errors::APIError]
|
|
484
|
+
# @return [Object]
|
|
485
|
+
def request(req)
|
|
486
|
+
self.class.validate!(req)
|
|
487
|
+
model = req.fetch(:model) { SportsOddsAPI::Internal::Type::Unknown }
|
|
488
|
+
opts = req[:options].to_h
|
|
489
|
+
unwrap = req[:unwrap]
|
|
490
|
+
SportsOddsAPI::RequestOptions.validate!(opts)
|
|
491
|
+
request = build_request(req.except(:options), opts)
|
|
492
|
+
url = request.fetch(:url)
|
|
493
|
+
|
|
494
|
+
# Don't send the current retry count in the headers if the caller modified the header defaults.
|
|
495
|
+
send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
|
|
496
|
+
status, response, stream = send_request(
|
|
497
|
+
request,
|
|
498
|
+
redirect_count: 0,
|
|
499
|
+
retry_count: 0,
|
|
500
|
+
send_retry_header: send_retry_header
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
headers = SportsOddsAPI::Internal::Util.normalized_headers(response.each_header.to_h)
|
|
504
|
+
decoded = SportsOddsAPI::Internal::Util.decode_content(headers, stream: stream)
|
|
505
|
+
case req
|
|
506
|
+
in {stream: Class => st}
|
|
507
|
+
st.new(
|
|
508
|
+
model: model,
|
|
509
|
+
url: url,
|
|
510
|
+
status: status,
|
|
511
|
+
headers: headers,
|
|
512
|
+
response: response,
|
|
513
|
+
unwrap: unwrap,
|
|
514
|
+
stream: decoded
|
|
515
|
+
)
|
|
516
|
+
in {page: Class => page}
|
|
517
|
+
page.new(client: self, req: req, headers: headers, page_data: decoded)
|
|
518
|
+
else
|
|
519
|
+
unwrapped = SportsOddsAPI::Internal::Util.dig(decoded, unwrap)
|
|
520
|
+
SportsOddsAPI::Internal::Type::Converter.coerce(model, unwrapped)
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# @api private
|
|
525
|
+
#
|
|
526
|
+
# @return [String]
|
|
527
|
+
def inspect
|
|
528
|
+
# rubocop:disable Layout/LineLength
|
|
529
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} max_retries=#{@max_retries} timeout=#{@timeout}>"
|
|
530
|
+
# rubocop:enable Layout/LineLength
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
define_sorbet_constant!(:RequestComponents) do
|
|
534
|
+
T.type_alias do
|
|
535
|
+
{
|
|
536
|
+
method: Symbol,
|
|
537
|
+
path: T.any(String, T::Array[String]),
|
|
538
|
+
query: T.nilable(T::Hash[String, T.nilable(T.any(T::Array[String], String))]),
|
|
539
|
+
headers: T.nilable(
|
|
540
|
+
T::Hash[String,
|
|
541
|
+
T.nilable(
|
|
542
|
+
T.any(
|
|
543
|
+
String,
|
|
544
|
+
Integer,
|
|
545
|
+
T::Array[T.nilable(T.any(String, Integer))]
|
|
546
|
+
)
|
|
547
|
+
)]
|
|
548
|
+
),
|
|
549
|
+
body: T.nilable(T.anything),
|
|
550
|
+
unwrap: T.nilable(
|
|
551
|
+
T.any(
|
|
552
|
+
Symbol,
|
|
553
|
+
Integer,
|
|
554
|
+
T::Array[T.any(Symbol, Integer)],
|
|
555
|
+
T.proc.params(arg0: T.anything).returns(T.anything)
|
|
556
|
+
)
|
|
557
|
+
),
|
|
558
|
+
page: T.nilable(T::Class[SportsOddsAPI::Internal::Type::BasePage[SportsOddsAPI::Internal::Type::BaseModel]]),
|
|
559
|
+
stream: T.nilable(T::Class[T.anything]),
|
|
560
|
+
model: T.nilable(SportsOddsAPI::Internal::Type::Converter::Input),
|
|
561
|
+
options: T.nilable(SportsOddsAPI::RequestOptions::OrHash)
|
|
562
|
+
}
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
define_sorbet_constant!(:RequestInput) do
|
|
566
|
+
T.type_alias do
|
|
567
|
+
{
|
|
568
|
+
method: Symbol,
|
|
569
|
+
url: URI::Generic,
|
|
570
|
+
headers: T::Hash[String, String],
|
|
571
|
+
body: T.anything,
|
|
572
|
+
max_retries: Integer,
|
|
573
|
+
timeout: Float
|
|
574
|
+
}
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
end
|