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.
Files changed (158) hide show
  1. checksums.yaml +7 -0
  2. data/.ignore +2 -0
  3. data/CHANGELOG.md +10 -0
  4. data/README.md +186 -0
  5. data/SECURITY.md +27 -0
  6. data/lib/sports_odds_api/client.rb +112 -0
  7. data/lib/sports_odds_api/errors.rb +228 -0
  8. data/lib/sports_odds_api/file_part.rb +55 -0
  9. data/lib/sports_odds_api/internal/next_cursor_page.rb +86 -0
  10. data/lib/sports_odds_api/internal/transport/base_client.rb +580 -0
  11. data/lib/sports_odds_api/internal/transport/pooled_net_requester.rb +201 -0
  12. data/lib/sports_odds_api/internal/type/array_of.rb +168 -0
  13. data/lib/sports_odds_api/internal/type/base_model.rb +534 -0
  14. data/lib/sports_odds_api/internal/type/base_page.rb +55 -0
  15. data/lib/sports_odds_api/internal/type/boolean.rb +77 -0
  16. data/lib/sports_odds_api/internal/type/converter.rb +327 -0
  17. data/lib/sports_odds_api/internal/type/enum.rb +131 -0
  18. data/lib/sports_odds_api/internal/type/file_input.rb +108 -0
  19. data/lib/sports_odds_api/internal/type/hash_of.rb +188 -0
  20. data/lib/sports_odds_api/internal/type/request_parameters.rb +42 -0
  21. data/lib/sports_odds_api/internal/type/union.rb +243 -0
  22. data/lib/sports_odds_api/internal/type/unknown.rb +81 -0
  23. data/lib/sports_odds_api/internal/util.rb +914 -0
  24. data/lib/sports_odds_api/internal.rb +20 -0
  25. data/lib/sports_odds_api/models/account_get_usage_params.rb +14 -0
  26. data/lib/sports_odds_api/models/account_usage.rb +91 -0
  27. data/lib/sports_odds_api/models/event.rb +686 -0
  28. data/lib/sports_odds_api/models/event_get_params.rb +195 -0
  29. data/lib/sports_odds_api/models/league.rb +39 -0
  30. data/lib/sports_odds_api/models/league_get_params.rb +30 -0
  31. data/lib/sports_odds_api/models/league_get_response.rb +8 -0
  32. data/lib/sports_odds_api/models/player.rb +128 -0
  33. data/lib/sports_odds_api/models/player_get_params.rb +58 -0
  34. data/lib/sports_odds_api/models/rate_limit_interval.rb +92 -0
  35. data/lib/sports_odds_api/models/sport.rb +197 -0
  36. data/lib/sports_odds_api/models/sport_get_params.rb +14 -0
  37. data/lib/sports_odds_api/models/sport_get_response.rb +8 -0
  38. data/lib/sports_odds_api/models/stat.rb +144 -0
  39. data/lib/sports_odds_api/models/stat_get_params.rb +43 -0
  40. data/lib/sports_odds_api/models/stat_get_response.rb +8 -0
  41. data/lib/sports_odds_api/models/stream_events_params.rb +38 -0
  42. data/lib/sports_odds_api/models/stream_events_response.rb +120 -0
  43. data/lib/sports_odds_api/models/team.rb +162 -0
  44. data/lib/sports_odds_api/models/team_get_params.rb +58 -0
  45. data/lib/sports_odds_api/models.rb +76 -0
  46. data/lib/sports_odds_api/request_options.rb +78 -0
  47. data/lib/sports_odds_api/resources/account.rb +33 -0
  48. data/lib/sports_odds_api/resources/events.rb +94 -0
  49. data/lib/sports_odds_api/resources/leagues.rb +39 -0
  50. data/lib/sports_odds_api/resources/players.rb +48 -0
  51. data/lib/sports_odds_api/resources/sports.rb +33 -0
  52. data/lib/sports_odds_api/resources/stats.rb +44 -0
  53. data/lib/sports_odds_api/resources/stream.rb +40 -0
  54. data/lib/sports_odds_api/resources/teams.rb +48 -0
  55. data/lib/sports_odds_api/version.rb +5 -0
  56. data/lib/sports_odds_api.rb +82 -0
  57. data/manifest.yaml +15 -0
  58. data/rbi/sports_odds_api/client.rbi +83 -0
  59. data/rbi/sports_odds_api/errors.rbi +205 -0
  60. data/rbi/sports_odds_api/file_part.rbi +37 -0
  61. data/rbi/sports_odds_api/internal/next_cursor_page.rbi +22 -0
  62. data/rbi/sports_odds_api/internal/transport/base_client.rbi +305 -0
  63. data/rbi/sports_odds_api/internal/transport/pooled_net_requester.rbi +80 -0
  64. data/rbi/sports_odds_api/internal/type/array_of.rbi +104 -0
  65. data/rbi/sports_odds_api/internal/type/base_model.rbi +310 -0
  66. data/rbi/sports_odds_api/internal/type/base_page.rbi +43 -0
  67. data/rbi/sports_odds_api/internal/type/boolean.rbi +58 -0
  68. data/rbi/sports_odds_api/internal/type/converter.rbi +225 -0
  69. data/rbi/sports_odds_api/internal/type/enum.rbi +82 -0
  70. data/rbi/sports_odds_api/internal/type/file_input.rbi +59 -0
  71. data/rbi/sports_odds_api/internal/type/hash_of.rbi +104 -0
  72. data/rbi/sports_odds_api/internal/type/request_parameters.rbi +31 -0
  73. data/rbi/sports_odds_api/internal/type/union.rbi +128 -0
  74. data/rbi/sports_odds_api/internal/type/unknown.rbi +58 -0
  75. data/rbi/sports_odds_api/internal/util.rbi +487 -0
  76. data/rbi/sports_odds_api/internal.rbi +18 -0
  77. data/rbi/sports_odds_api/models/account_get_usage_params.rbi +32 -0
  78. data/rbi/sports_odds_api/models/account_usage.rbi +173 -0
  79. data/rbi/sports_odds_api/models/event.rbi +1269 -0
  80. data/rbi/sports_odds_api/models/event_get_params.rbi +286 -0
  81. data/rbi/sports_odds_api/models/league.rbi +74 -0
  82. data/rbi/sports_odds_api/models/league_get_params.rbi +60 -0
  83. data/rbi/sports_odds_api/models/league_get_response.rbi +11 -0
  84. data/rbi/sports_odds_api/models/player.rbi +247 -0
  85. data/rbi/sports_odds_api/models/player_get_params.rbi +95 -0
  86. data/rbi/sports_odds_api/models/rate_limit_interval.rbi +176 -0
  87. data/rbi/sports_odds_api/models/sport.rbi +371 -0
  88. data/rbi/sports_odds_api/models/sport_get_params.rbi +29 -0
  89. data/rbi/sports_odds_api/models/sport_get_response.rbi +11 -0
  90. data/rbi/sports_odds_api/models/stat.rbi +273 -0
  91. data/rbi/sports_odds_api/models/stat_get_params.rbi +72 -0
  92. data/rbi/sports_odds_api/models/stat_get_response.rbi +11 -0
  93. data/rbi/sports_odds_api/models/stream_events_params.rbi +71 -0
  94. data/rbi/sports_odds_api/models/stream_events_response.rbi +247 -0
  95. data/rbi/sports_odds_api/models/team.rbi +305 -0
  96. data/rbi/sports_odds_api/models/team_get_params.rbi +92 -0
  97. data/rbi/sports_odds_api/models.rbi +35 -0
  98. data/rbi/sports_odds_api/request_options.rbi +59 -0
  99. data/rbi/sports_odds_api/resources/account.rbi +21 -0
  100. data/rbi/sports_odds_api/resources/events.rbi +96 -0
  101. data/rbi/sports_odds_api/resources/leagues.rbi +29 -0
  102. data/rbi/sports_odds_api/resources/players.rbi +41 -0
  103. data/rbi/sports_odds_api/resources/sports.rbi +21 -0
  104. data/rbi/sports_odds_api/resources/stats.rbi +34 -0
  105. data/rbi/sports_odds_api/resources/stream.rbi +32 -0
  106. data/rbi/sports_odds_api/resources/teams.rbi +39 -0
  107. data/rbi/sports_odds_api/version.rbi +5 -0
  108. data/sig/sports_odds_api/client.rbs +45 -0
  109. data/sig/sports_odds_api/errors.rbs +117 -0
  110. data/sig/sports_odds_api/file_part.rbs +21 -0
  111. data/sig/sports_odds_api/internal/next_cursor_page.rbs +13 -0
  112. data/sig/sports_odds_api/internal/transport/base_client.rbs +133 -0
  113. data/sig/sports_odds_api/internal/transport/pooled_net_requester.rbs +45 -0
  114. data/sig/sports_odds_api/internal/type/array_of.rbs +48 -0
  115. data/sig/sports_odds_api/internal/type/base_model.rbs +104 -0
  116. data/sig/sports_odds_api/internal/type/base_page.rbs +24 -0
  117. data/sig/sports_odds_api/internal/type/boolean.rbs +26 -0
  118. data/sig/sports_odds_api/internal/type/converter.rbs +79 -0
  119. data/sig/sports_odds_api/internal/type/enum.rbs +32 -0
  120. data/sig/sports_odds_api/internal/type/file_input.rbs +25 -0
  121. data/sig/sports_odds_api/internal/type/hash_of.rbs +48 -0
  122. data/sig/sports_odds_api/internal/type/request_parameters.rbs +19 -0
  123. data/sig/sports_odds_api/internal/type/union.rbs +52 -0
  124. data/sig/sports_odds_api/internal/type/unknown.rbs +26 -0
  125. data/sig/sports_odds_api/internal/util.rbs +185 -0
  126. data/sig/sports_odds_api/internal.rbs +10 -0
  127. data/sig/sports_odds_api/models/account_get_usage_params.rbs +15 -0
  128. data/sig/sports_odds_api/models/account_usage.rbs +116 -0
  129. data/sig/sports_odds_api/models/event.rbs +860 -0
  130. data/sig/sports_odds_api/models/event_get_params.rbs +168 -0
  131. data/sig/sports_odds_api/models/league.rbs +50 -0
  132. data/sig/sports_odds_api/models/league_get_params.rbs +32 -0
  133. data/sig/sports_odds_api/models/league_get_response.rbs +7 -0
  134. data/sig/sports_odds_api/models/player.rbs +162 -0
  135. data/sig/sports_odds_api/models/player_get_params.rbs +56 -0
  136. data/sig/sports_odds_api/models/rate_limit_interval.rbs +67 -0
  137. data/sig/sports_odds_api/models/sport.rbs +241 -0
  138. data/sig/sports_odds_api/models/sport_get_params.rbs +15 -0
  139. data/sig/sports_odds_api/models/sport_get_response.rbs +7 -0
  140. data/sig/sports_odds_api/models/stat.rbs +166 -0
  141. data/sig/sports_odds_api/models/stat_get_params.rbs +38 -0
  142. data/sig/sports_odds_api/models/stat_get_response.rbs +7 -0
  143. data/sig/sports_odds_api/models/stream_events_params.rbs +38 -0
  144. data/sig/sports_odds_api/models/stream_events_response.rbs +151 -0
  145. data/sig/sports_odds_api/models/team.rbs +201 -0
  146. data/sig/sports_odds_api/models/team_get_params.rbs +56 -0
  147. data/sig/sports_odds_api/models.rbs +33 -0
  148. data/sig/sports_odds_api/request_options.rbs +36 -0
  149. data/sig/sports_odds_api/resources/account.rbs +11 -0
  150. data/sig/sports_odds_api/resources/events.rbs +32 -0
  151. data/sig/sports_odds_api/resources/leagues.rbs +13 -0
  152. data/sig/sports_odds_api/resources/players.rbs +16 -0
  153. data/sig/sports_odds_api/resources/sports.rbs +11 -0
  154. data/sig/sports_odds_api/resources/stats.rbs +14 -0
  155. data/sig/sports_odds_api/resources/stream.rbs +14 -0
  156. data/sig/sports_odds_api/resources/teams.rbs +16 -0
  157. data/sig/sports_odds_api/version.rbs +3 -0
  158. 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