genderapi 1.0.5 → 2.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +193 -219
- data/lib/genderapi/client.rb +279 -180
- data/lib/genderapi/errors.rb +236 -0
- data/lib/genderapi/models.rb +278 -0
- data/lib/genderapi/validation.rb +158 -0
- data/lib/genderapi/version.rb +2 -2
- data/lib/genderapi.rb +8 -2
- metadata +19 -56
data/lib/genderapi/client.rb
CHANGED
|
@@ -1,215 +1,314 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "httparty"
|
|
4
3
|
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "zlib"
|
|
5
8
|
|
|
6
9
|
module GenderAPI
|
|
7
|
-
|
|
8
|
-
# Ruby SDK for GenderAPI.io
|
|
10
|
+
# Client for the GenderAPI.io V2 API (https://api.genderapi.io/api/v2).
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# - email addresses
|
|
13
|
-
# - social media usernames
|
|
12
|
+
# Server-side only: keep the API key in your server environment and never
|
|
13
|
+
# embed it in browser or mobile code.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
15
|
+
# Safety behaviour:
|
|
16
|
+
# * No automatic retries, ever (a lost response may still have been billed),
|
|
17
|
+
# including on HTTP 429: {RateLimitError#retry_after} is exposed instead.
|
|
18
|
+
# * Redirects are never followed; a 3xx raises {RedirectError}.
|
|
19
|
+
# * Timeout defaults to 10 seconds for each connect, write and read operation.
|
|
20
|
+
# * HTTPS only; plain http is accepted only for localhost/127.0.0.1/::1 (tests).
|
|
21
|
+
# * Constructing a client makes no request. The API key is sent only in the
|
|
22
|
+
# Authorization header, never in a URL, and is never logged or inspected.
|
|
19
23
|
#
|
|
24
|
+
# Without an API key the server applies the shared IP trial (10 credits per
|
|
25
|
+
# 24 hours per public IP, batches of at most 10 items). The client has no
|
|
26
|
+
# trial logic of its own; the server decides.
|
|
20
27
|
class Client
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.genderapi.io/api/v2"
|
|
29
|
+
DEFAULT_TIMEOUT = 10
|
|
30
|
+
ENV_API_KEY = "GENDERAPI_API_KEY"
|
|
31
|
+
LOCAL_HOSTS = %w[localhost 127.0.0.1 ::1 [::1]].freeze
|
|
32
|
+
ACCEPT = "application/json, application/problem+json"
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
# @return [String] normalised base URL without trailing slash
|
|
35
|
+
attr_reader :base_url
|
|
36
|
+
# @return [Numeric] timeout in seconds for each connect, write and read operation
|
|
37
|
+
attr_reader :timeout
|
|
38
|
+
|
|
39
|
+
# @param api_key [String, nil] API key. Defaults to ENV["GENDERAPI_API_KEY"].
|
|
40
|
+
# Without a key the server applies the shared IP trial.
|
|
41
|
+
# @param base_url [String] API base URL. Must be https, except for
|
|
42
|
+
# http://localhost, http://127.0.0.1 or http://[::1] (tests).
|
|
43
|
+
# @param timeout [Numeric] seconds for each connect, write and read operation (default 10)
|
|
44
|
+
# @param user_agent [String, nil] text appended to the default User-Agent
|
|
45
|
+
# @param require_api_key_access [Boolean] when a key is configured, raise
|
|
46
|
+
# {UnexpectedAccessModeError} if a response reports IP-trial or
|
|
47
|
+
# unauthenticated access (the key was not accepted). Default true.
|
|
48
|
+
def initialize(api_key: ENV.fetch(ENV_API_KEY, nil), base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT,
|
|
49
|
+
user_agent: nil, require_api_key_access: true)
|
|
50
|
+
@api_key = normalize_key(api_key)
|
|
51
|
+
@base_url, @base_uri = normalize_base_url(base_url)
|
|
52
|
+
unless timeout.is_a?(Numeric) && timeout.positive? && timeout.finite?
|
|
53
|
+
raise ValidationError.new("timeout must be a positive number of seconds", field: "timeout")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@timeout = timeout
|
|
57
|
+
if !user_agent.nil? && (!user_agent.is_a?(String) || user_agent.match?(/[\x00-\x1f\x7f]/))
|
|
58
|
+
raise ValidationError.new("user_agent must be a String without control characters", field: "user_agent")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@user_agent = ["genderapi-ruby/#{VERSION}", "(Ruby #{RUBY_VERSION})", user_agent].compact.join(" ")
|
|
62
|
+
@require_api_key_access = require_api_key_access ? true : false
|
|
37
63
|
end
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# @param country [String, nil] Optional two-letter country code (e.g. "US").
|
|
44
|
-
# @param ask_to_ai [Boolean] Whether to force AI lookup. Default: false
|
|
45
|
-
# @param force_to_genderize [Boolean] Whether to analyze nicknames or emojis. Default: false
|
|
46
|
-
#
|
|
47
|
-
# @return [Hash] JSON response as a Ruby Hash.
|
|
48
|
-
#
|
|
49
|
-
def get_gender_by_name(name:, country: nil, ask_to_ai: false, force_to_genderize: false)
|
|
50
|
-
payload = {
|
|
51
|
-
name: name,
|
|
52
|
-
country: country,
|
|
53
|
-
askToAI: ask_to_ai,
|
|
54
|
-
forceToGenderize: force_to_genderize
|
|
55
|
-
}
|
|
65
|
+
# @return [Boolean] whether an API key is configured
|
|
66
|
+
def api_key?
|
|
67
|
+
!@api_key.nil?
|
|
68
|
+
end
|
|
56
69
|
|
|
57
|
-
|
|
70
|
+
# Never reveals the API key.
|
|
71
|
+
def inspect
|
|
72
|
+
"#<#{self.class.name} base_url=#{@base_url.inspect} timeout=#{@timeout} api_key=#{api_key? ? '[FILTERED]' : 'nil'}>"
|
|
58
73
|
end
|
|
74
|
+
alias to_s inspect
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# @
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
# Predict gender for one name, email or username (POST /gender).
|
|
77
|
+
#
|
|
78
|
+
# Every call is a new, billable operation. Unknown results are successful
|
|
79
|
+
# and billed. Single requests default to ai_mode "fallback" on the server
|
|
80
|
+
# (1 credit total); "always" costs 2; force_to_genderize true costs 1 for a
|
|
81
|
+
# dataset hit, otherwise 2 total.
|
|
82
|
+
#
|
|
83
|
+
# @param type [String, Symbol] "name", "email" or "username"
|
|
84
|
+
# @param value [String] 1-254 characters, no control characters
|
|
85
|
+
# @param country [String, nil] uppercase ISO 3166-1 alpha-2 code, e.g. "US"
|
|
86
|
+
# @param ai_mode [String, Symbol, nil] "off", "fallback" or "always" (sent as options.ai_mode)
|
|
87
|
+
# @param force_to_genderize [Boolean, nil] sent as forceToGenderize; not with ai_mode off/always
|
|
88
|
+
# @param id [String, nil] optional 1-64 character correlation id
|
|
89
|
+
# @return [GenderResult]
|
|
90
|
+
def gender(type, value, country: nil, ai_mode: nil, force_to_genderize: nil, id: nil)
|
|
91
|
+
body = Validation.gender_item(type, value, country: country, ai_mode: ai_mode,
|
|
92
|
+
force_to_genderize: force_to_genderize, id: id)
|
|
93
|
+
request(:post, "/gender", body: body, auth: true) { |raw, ctx| check_prediction(raw, ctx) && GenderResult.new(raw) }
|
|
94
|
+
end
|
|
75
95
|
|
|
76
|
-
|
|
96
|
+
# @see #gender
|
|
97
|
+
def name(value, **options)
|
|
98
|
+
gender("name", value, **options)
|
|
77
99
|
end
|
|
78
100
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# @param country [String, nil] Optional two-letter country code (e.g. "US").
|
|
84
|
-
# @param ask_to_ai [Boolean] Whether to force AI lookup. Default: false
|
|
85
|
-
# @param force_to_genderize [Boolean] Whether to analyze nicknames or emojis. Default: false
|
|
86
|
-
#
|
|
87
|
-
# @return [Hash] JSON response as a Ruby Hash.
|
|
88
|
-
#
|
|
89
|
-
def get_gender_by_username(username:, country: nil, ask_to_ai: false, force_to_genderize: false)
|
|
90
|
-
payload = {
|
|
91
|
-
username: username,
|
|
92
|
-
country: country,
|
|
93
|
-
askToAI: ask_to_ai,
|
|
94
|
-
forceToGenderize: force_to_genderize
|
|
95
|
-
}
|
|
101
|
+
# @see #gender
|
|
102
|
+
def email(value, **options)
|
|
103
|
+
gender("email", value, **options)
|
|
104
|
+
end
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
# @see #gender
|
|
107
|
+
def username(value, **options)
|
|
108
|
+
gender("username", value, **options)
|
|
98
109
|
end
|
|
99
110
|
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# @
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
_post_request("/api/name/multi/country", payload)
|
|
111
|
+
# Predict 1-50 items in one request (POST /gender/batch). The server may
|
|
112
|
+
# allow fewer (10 on the IP trial). Batch items default to ai_mode "off".
|
|
113
|
+
#
|
|
114
|
+
# Each item is a Hash with :type, :value and optional :country, :id,
|
|
115
|
+
# :ai_mode and :force_to_genderize (wire names "forceToGenderize" and
|
|
116
|
+
# "options" => {"ai_mode" => ...} are accepted too). Ids must be unique.
|
|
117
|
+
#
|
|
118
|
+
# Partial success is returned, not raised: inspect {BatchResult#failed_items}.
|
|
119
|
+
# If every executed item fails the server answers with an error status and
|
|
120
|
+
# an {APIError} is raised whose {APIError#items} holds the item outcomes.
|
|
121
|
+
#
|
|
122
|
+
# @param items [Array<Hash>]
|
|
123
|
+
# @return [BatchResult]
|
|
124
|
+
def gender_batch(items)
|
|
125
|
+
wire = Validation.batch_items(items)
|
|
126
|
+
request(:post, "/gender/batch", body: { "items" => wire }, auth: true) do |raw, ctx|
|
|
127
|
+
check_batch(raw, ctx, wire) && BatchResult.new(raw)
|
|
128
|
+
end
|
|
119
129
|
end
|
|
120
130
|
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# This method is designed for scenarios such as bulk database
|
|
126
|
-
# cleaning, analytics, or personalization tasks where email
|
|
127
|
-
# data is available and high throughput is required.
|
|
128
|
-
#
|
|
129
|
-
# Each email object can contain:
|
|
130
|
-
# - email [String] The email address to analyze. (Required)
|
|
131
|
-
# - country [String, nil] Optional two-letter country code (e.g. "US").
|
|
132
|
-
# - id [String, Integer, nil] Optional custom identifier to correlate input and output.
|
|
133
|
-
#
|
|
134
|
-
# @param data [Array<Hash>] Array of email data hashes.
|
|
135
|
-
#
|
|
136
|
-
# @return [Hash] JSON response as a Ruby Hash.
|
|
137
|
-
#
|
|
138
|
-
def get_gender_by_email_bulk(data:)
|
|
139
|
-
payload = { data: data }
|
|
140
|
-
_post_request("/api/email/multi/country", payload)
|
|
131
|
+
# Current balance and trial quota (GET /usage). Free.
|
|
132
|
+
# @return [UsageResult]
|
|
133
|
+
def usage
|
|
134
|
+
request(:get, "/usage", auth: true) { |raw, ctx| check_envelope(raw, ctx) && UsageResult.new(raw) }
|
|
141
135
|
end
|
|
142
136
|
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
137
|
+
# Validate a phone number's structure (POST /phone/validate). Costs 1 credit,
|
|
138
|
+
# including invalid numbers. It does not check subscriber existence.
|
|
139
|
+
#
|
|
140
|
+
# @param number [String] 3-32 characters: digits, spaces, parentheses, hyphens, optional leading +
|
|
141
|
+
# @param country [String, nil] ISO alpha-2 code; required unless number starts with +
|
|
142
|
+
# @return [PhoneResult]
|
|
143
|
+
def validate_phone(number, country: nil)
|
|
144
|
+
body = Validation.phone(number, country)
|
|
145
|
+
request(:post, "/phone/validate", body: body, auth: true) { |raw, ctx| check_envelope(raw, ctx) && PhoneResult.new(raw) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Deployment version, limits and AI availability (GET /). No key is sent.
|
|
149
|
+
# @return [Hash] parsed JSON
|
|
150
|
+
def capabilities
|
|
151
|
+
request(:get, "", auth: false) { |raw, _ctx| raw }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Public machine-readable error catalog (GET /errors). No key is sent.
|
|
155
|
+
# @return [Hash] parsed JSON ({"errors" => {code => {...}}})
|
|
156
|
+
def error_catalog
|
|
157
|
+
request(:get, "/errors", auth: false) { |raw, _ctx| raw }
|
|
163
158
|
end
|
|
164
159
|
|
|
165
160
|
private
|
|
166
161
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
#
|
|
176
|
-
# @param endpoint [String] API endpoint path (e.g. "/api")
|
|
177
|
-
# @param payload [Hash] Request body data.
|
|
178
|
-
#
|
|
179
|
-
# @return [Hash] JSON response as Ruby Hash.
|
|
180
|
-
#
|
|
181
|
-
def _post_request(endpoint, payload)
|
|
182
|
-
# Remove nil values from payload
|
|
183
|
-
cleaned_payload = payload.reject { |_k, v| v.nil? }
|
|
184
|
-
|
|
185
|
-
response = self.class.post(
|
|
186
|
-
endpoint,
|
|
187
|
-
headers: @headers,
|
|
188
|
-
body: JSON.generate(cleaned_payload)
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
# Handle server-side errors or timeouts
|
|
192
|
-
if [500, 502, 503, 504, 408].include?(response.code)
|
|
193
|
-
raise "GenderAPI Server Error or Timeout: HTTP #{response.code} - #{response.body}"
|
|
194
|
-
else
|
|
195
|
-
begin
|
|
196
|
-
parse_json(response.body)
|
|
197
|
-
rescue JSON::ParserError
|
|
198
|
-
raise "GenderAPI Response is not valid JSON"
|
|
199
|
-
end
|
|
162
|
+
def normalize_key(key)
|
|
163
|
+
return nil if key.nil?
|
|
164
|
+
raise ValidationError.new("api_key must be a String", field: "api_key") unless key.is_a?(String)
|
|
165
|
+
|
|
166
|
+
key = key.strip
|
|
167
|
+
return nil if key.empty?
|
|
168
|
+
if key.match?(/[\x00-\x20\x7f]/) || !key.ascii_only?
|
|
169
|
+
raise ValidationError.new("api_key contains invalid characters", field: "api_key")
|
|
200
170
|
end
|
|
201
|
-
|
|
202
|
-
|
|
171
|
+
|
|
172
|
+
key
|
|
203
173
|
end
|
|
204
174
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
175
|
+
def normalize_base_url(url)
|
|
176
|
+
raise ValidationError.new("base_url must be a String", field: "base_url") unless url.is_a?(String)
|
|
177
|
+
|
|
178
|
+
uri = begin
|
|
179
|
+
URI.parse(url.strip)
|
|
180
|
+
rescue URI::InvalidURIError
|
|
181
|
+
raise ValidationError.new("base_url is not a valid URL", field: "base_url")
|
|
182
|
+
end
|
|
183
|
+
unless uri.is_a?(URI::HTTP) && uri.host && !uri.host.empty? && uri.userinfo.nil? && uri.query.nil? && uri.fragment.nil?
|
|
184
|
+
raise ValidationError.new("base_url must be an absolute http(s) URL without credentials, query or fragment",
|
|
185
|
+
field: "base_url")
|
|
186
|
+
end
|
|
187
|
+
if uri.scheme == "http" && !LOCAL_HOSTS.include?(uri.host.downcase)
|
|
188
|
+
raise ValidationError.new("base_url must use https (plain http is only allowed for localhost tests)",
|
|
189
|
+
field: "base_url")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
uri.path = uri.path.sub(%r{/+\z}, "")
|
|
193
|
+
[uri.to_s, uri]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def request(method, path, auth:, body: nil)
|
|
197
|
+
uri = @base_uri.dup
|
|
198
|
+
uri.path = "#{@base_uri.path}#{path}"
|
|
199
|
+
uri.path = "/" if uri.path.empty?
|
|
200
|
+
req = method == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
|
|
201
|
+
req["Accept"] = ACCEPT
|
|
202
|
+
req["User-Agent"] = @user_agent
|
|
203
|
+
req["Authorization"] = "Bearer #{@api_key}" if auth && @api_key
|
|
204
|
+
if body
|
|
205
|
+
req["Content-Type"] = "application/json"
|
|
206
|
+
req.body = JSON.generate(body)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
response = perform(uri, req)
|
|
210
|
+
handle(response, auth: auth) { |raw, ctx| yield raw, ctx }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def perform(uri, req)
|
|
214
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
215
|
+
http.use_ssl = uri.scheme == "https"
|
|
216
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if http.use_ssl?
|
|
217
|
+
http.open_timeout = @timeout
|
|
218
|
+
http.read_timeout = @timeout
|
|
219
|
+
http.write_timeout = @timeout
|
|
220
|
+
http.ssl_timeout = @timeout
|
|
221
|
+
http.max_retries = 0 # Net::HTTP would otherwise retry idempotent requests once.
|
|
222
|
+
http.start { |conn| conn.request(req) }
|
|
223
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, ::Timeout::Error => e
|
|
224
|
+
raise TimeoutError.new(original: e)
|
|
225
|
+
rescue SystemCallError, IOError, SocketError, OpenSSL::SSL::SSLError, Net::HTTPBadResponse,
|
|
226
|
+
Net::ProtocolError, EOFError, Zlib::Error => e
|
|
227
|
+
raise TransportError.new(original: e)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def handle(response, auth:)
|
|
231
|
+
status = response.code.to_i
|
|
232
|
+
headers = response.each_header.to_h { |k, v| [k.downcase, v] }
|
|
233
|
+
raw_body = response.body
|
|
234
|
+
if status.between?(300, 399)
|
|
235
|
+
raise RedirectError.new(status: status, headers: headers, raw_body: raw_body)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
parsed = parse_json(raw_body, headers)
|
|
239
|
+
if status >= 400
|
|
240
|
+
raise GenderAPI.api_error_class(status).new(status: status, headers: headers, body: parsed, raw_body: raw_body)
|
|
241
|
+
end
|
|
242
|
+
unless status.between?(200, 299)
|
|
243
|
+
raise InvalidResponseError.new("Unexpected HTTP #{status}; completion and billing are unknown.",
|
|
244
|
+
status: status, headers: headers, raw_body: raw_body)
|
|
245
|
+
end
|
|
246
|
+
unless parsed.is_a?(Hash)
|
|
247
|
+
raise InvalidResponseError.new("Expected a JSON object; completion and billing are unknown. " \
|
|
248
|
+
"Check usage before another submission.",
|
|
249
|
+
status: status, headers: headers, raw_body: raw_body)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
ctx = { status: status, headers: headers, raw_body: raw_body }
|
|
253
|
+
result = yield parsed, ctx
|
|
254
|
+
check_access(result, ctx) if auth
|
|
255
|
+
result
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def parse_json(raw_body, headers)
|
|
259
|
+
return nil if raw_body.nil? || raw_body.empty?
|
|
260
|
+
return nil unless headers["content-type"].to_s.downcase.include?("json")
|
|
261
|
+
|
|
262
|
+
text = raw_body.dup.force_encoding(Encoding::UTF_8)
|
|
263
|
+
return nil unless text.valid_encoding?
|
|
264
|
+
|
|
265
|
+
JSON.parse(text)
|
|
266
|
+
rescue JSON::ParserError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def check_access(result, ctx)
|
|
271
|
+
return unless @api_key && @require_api_key_access && result.is_a?(Result)
|
|
272
|
+
|
|
273
|
+
mode = result.meta.access&.mode
|
|
274
|
+
return if mode.nil? || mode == "api_key"
|
|
275
|
+
|
|
276
|
+
raise UnexpectedAccessModeError.new(result: result, **ctx)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def invalid!(message, raw, ctx)
|
|
280
|
+
raise InvalidResponseError.new("#{message} Keep the request ID and check usage before another submission.",
|
|
281
|
+
body: raw, **ctx)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def check_envelope(raw, ctx)
|
|
285
|
+
invalid!("Response is missing data.", raw, ctx) unless raw.key?("data")
|
|
286
|
+
invalid!("Response is missing meta.", raw, ctx) unless raw["meta"].is_a?(Hash)
|
|
287
|
+
true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def check_prediction(raw, ctx)
|
|
291
|
+
check_envelope(raw, ctx)
|
|
292
|
+
invalid!("Response data is not a prediction object.", raw, ctx) unless raw["data"].is_a?(Hash)
|
|
293
|
+
true
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Structural checks only: every submitted item must come back once, in
|
|
297
|
+
# order, with its id, and with exactly one of data or error.
|
|
298
|
+
def check_batch(raw, ctx, wire)
|
|
299
|
+
check_envelope(raw, ctx)
|
|
300
|
+
rows = raw["data"]
|
|
301
|
+
unless rows.is_a?(Array) && rows.length == wire.length
|
|
302
|
+
invalid!("Batch result count does not match the submitted items.", raw, ctx)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
rows.each_with_index do |row, index|
|
|
306
|
+
unless row.is_a?(Hash) && row["index"] == index && row["id"] == wire[index]["id"]
|
|
307
|
+
invalid!("Batch result mapping does not match the submitted items.", raw, ctx)
|
|
308
|
+
end
|
|
309
|
+
invalid!("Batch item must have exactly one of data or error.", raw, ctx) if row.key?("data") == row.key?("error")
|
|
310
|
+
end
|
|
311
|
+
true
|
|
213
312
|
end
|
|
214
313
|
end
|
|
215
314
|
end
|