demografix 0.1.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/LICENSE +21 -0
- data/README.md +195 -0
- data/lib/demografix/client.rb +227 -0
- data/lib/demografix/errors.rb +34 -0
- data/lib/demografix/models.rb +29 -0
- data/lib/demografix/version.rb +5 -0
- data/lib/demografix.rb +13 -0
- metadata +94 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 68a9654603c29db7c24af02f7daf2716a3159060dd596a11b300b83abe093d17
|
|
4
|
+
data.tar.gz: 42f4a7f8d7c1cef1ebbfdc0990660d7a96b67fc5861744bb017880553e4ac080
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0c71e2fe5ea63acff9cc98c82061bf727bff9b4795252a31718698002cfa81fdc5628594a9efcf6af2fb8aa837e5c766ae7803fb2448c432006518536f68848c
|
|
7
|
+
data.tar.gz: 26415804ba554e64613ee930464a07a04c5e389b728582d39b7b2dda8de73496911b4f500f30f35c2a69030c56fbc8c227559312eb8abf8588ca2363c3dd5079
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Demografix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# demografix (Ruby)
|
|
2
|
+
|
|
3
|
+
Run demographic analysis over names — predicted gender, age, and nationality — from one Ruby client. The
|
|
4
|
+
gem covers genderize.io, agify.io, and nationalize.io.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
Add the gem to your Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "demografix"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then run `bundle install`. To install directly:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install demografix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The client uses the Ruby standard library (`net/http` and `json`) and has no runtime dependencies. It requires Ruby 3.2 or later.
|
|
21
|
+
|
|
22
|
+
## Authentication
|
|
23
|
+
|
|
24
|
+
An API key is required. Creating one is free and includes 2,500 requests per month. Generate a key in your
|
|
25
|
+
dashboard at genderize.io, agify.io, or nationalize.io. One key works across all three services.
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
Construct a client, run a batch over a list of names, read the predictions, and read the remaining quota.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "demografix"
|
|
33
|
+
|
|
34
|
+
client = Demografix::Client.new(api_key: ENV.fetch("DEMOGRAFIX_API_KEY"))
|
|
35
|
+
|
|
36
|
+
names = %w[michael matthew jane emily peter lois]
|
|
37
|
+
|
|
38
|
+
ages = client.agify_batch(names)
|
|
39
|
+
|
|
40
|
+
# Aggregate the predictions into an age distribution for the list.
|
|
41
|
+
known = ages.results.map(&:age).compact
|
|
42
|
+
average_age = known.sum.to_f / known.length
|
|
43
|
+
|
|
44
|
+
ages.quota.remaining # => 24987
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each call returns prediction fields plus a `quota`. Batch calls return `results` (one prediction per input
|
|
48
|
+
name, in input order) plus one `quota` for the response. Aggregate the results into a distribution.
|
|
49
|
+
|
|
50
|
+
## genderize
|
|
51
|
+
|
|
52
|
+
Predict gender from a name.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
result = client.genderize("peter")
|
|
56
|
+
result.gender # => "male"
|
|
57
|
+
result.probability # => 1.0
|
|
58
|
+
result.count # => 1352696
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Batch a list and reduce it to a gender split:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
batch = client.genderize_batch(%w[peter lois meg chris])
|
|
65
|
+
split = batch.results.each_with_object(Hash.new(0)) do |pred, counts|
|
|
66
|
+
counts[pred.gender || "unknown"] += 1
|
|
67
|
+
end
|
|
68
|
+
# => { "male" => 2, "female" => 2 }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`gender` is `"male"`, `"female"`, or `nil`. A name with no match returns `nil` gender, `0.0` probability,
|
|
72
|
+
and `0` count. That is a successful response, not an error.
|
|
73
|
+
|
|
74
|
+
## agify
|
|
75
|
+
|
|
76
|
+
Predict age from a name.
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
result = client.agify("michael")
|
|
80
|
+
result.age # => 57
|
|
81
|
+
result.count # => 311558
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Batch a list and reduce it to an age distribution:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
batch = client.agify_batch(%w[michael matthew jane])
|
|
88
|
+
ages = batch.results.map(&:age).compact
|
|
89
|
+
buckets = ages.group_by { |age| (age / 10) * 10 }
|
|
90
|
+
# => { 50 => [57], 40 => [48], ... }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`age` is an integer or `nil`. A name with no match returns `nil` age and `0` count.
|
|
94
|
+
|
|
95
|
+
## nationalize
|
|
96
|
+
|
|
97
|
+
Predict nationality from a name.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
result = client.nationalize("nguyen")
|
|
101
|
+
result.country.first.country_id # => "VN"
|
|
102
|
+
result.country.first.probability # => 0.891132
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Batch a list and reduce it to a nationality mix:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
batch = client.nationalize_batch(%w[nguyen schmidt rossi])
|
|
109
|
+
mix = batch.results.each_with_object(Hash.new(0)) do |pred, counts|
|
|
110
|
+
top = pred.country.first
|
|
111
|
+
counts[top ? top.country_id : "unknown"] += 1
|
|
112
|
+
end
|
|
113
|
+
# => { "VN" => 1, "DE" => 1, "IT" => 1 }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`country` holds up to five candidates in descending probability order. A name with no match returns an empty
|
|
117
|
+
`country` array.
|
|
118
|
+
|
|
119
|
+
## country_id
|
|
120
|
+
|
|
121
|
+
`genderize` and `agify` accept an optional `country_id` (ISO 3166-1 alpha-2) to scope the prediction to a
|
|
122
|
+
country. `nationalize` does not accept it.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
result = client.genderize("kim", country_id: "US")
|
|
126
|
+
result.country_id # => "US"
|
|
127
|
+
result.gender # => "female"
|
|
128
|
+
|
|
129
|
+
client.agify_batch(%w[andrea andrea], country_id: "IT")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The value is echoed back uppercase in `country_id` on each prediction. When the request sends no
|
|
133
|
+
`country_id`, the field is `nil`.
|
|
134
|
+
|
|
135
|
+
## Quota
|
|
136
|
+
|
|
137
|
+
Every result and every raised error carries a `quota` read from the response rate-limit headers:
|
|
138
|
+
|
|
139
|
+
| Field | Meaning |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `limit` | names allowed in the current window |
|
|
142
|
+
| `remaining` | names left in the current window |
|
|
143
|
+
| `reset` | seconds until the window resets |
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
result = client.genderize("peter")
|
|
147
|
+
result.quota.limit # => 25000
|
|
148
|
+
result.quota.remaining # => 24987
|
|
149
|
+
result.quota.reset # => 1314000
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Read quota off the returned value or a raised error. The client does not cache it.
|
|
153
|
+
|
|
154
|
+
## Errors
|
|
155
|
+
|
|
156
|
+
Every error subclasses `Demografix::Error` and carries `status`, `message`, and `quota` (when the response
|
|
157
|
+
included rate-limit headers).
|
|
158
|
+
|
|
159
|
+
| Error | Raised on |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `Demografix::AuthError` | 401, invalid or missing API key |
|
|
162
|
+
| `Demografix::SubscriptionError` | 402, subscription not active |
|
|
163
|
+
| `Demografix::ValidationError` | 422, invalid parameters; also client-side for a batch over 10 names |
|
|
164
|
+
| `Demografix::RateLimitError` | 429, request limit reached (quota always populated) |
|
|
165
|
+
| `Demografix::TransportError` | network failure, timeout, or non-JSON body |
|
|
166
|
+
| `Demografix::Error` | any other non-2xx status |
|
|
167
|
+
|
|
168
|
+
A batch of more than 10 names raises `ValidationError` before any HTTP call is made.
|
|
169
|
+
|
|
170
|
+
On a `RateLimitError`, read `quota.reset` for the seconds to wait before retrying:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
begin
|
|
174
|
+
client.genderize_batch(names)
|
|
175
|
+
rescue Demografix::RateLimitError => e
|
|
176
|
+
sleep(e.quota.reset)
|
|
177
|
+
retry
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Methods
|
|
182
|
+
|
|
183
|
+
| Method | Returns | country_id |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `genderize(name, country_id:)` | `GenderizeResult` | yes |
|
|
186
|
+
| `genderize_batch(names, country_id:)` | `Batch` of `GenderizePrediction` | yes |
|
|
187
|
+
| `agify(name, country_id:)` | `AgifyResult` | yes |
|
|
188
|
+
| `agify_batch(names, country_id:)` | `Batch` of `AgifyPrediction` | yes |
|
|
189
|
+
| `nationalize(name)` | `NationalizeResult` | no |
|
|
190
|
+
| `nationalize_batch(names)` | `Batch` of `NationalizePrediction` | no |
|
|
191
|
+
|
|
192
|
+
`Demografix::Client.new` requires `api_key:` and accepts `timeout:` (optional, default 10 seconds). The
|
|
193
|
+
host URLs and the User-Agent are fixed constants, not options.
|
|
194
|
+
|
|
195
|
+
Full API reference: https://genderize.io/documentation/api
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Demografix
|
|
8
|
+
# Synchronous client for the three Demografix APIs: genderize, agify, and
|
|
9
|
+
# nationalize. One instance covers all three services. Quota is read from
|
|
10
|
+
# the returned value or a raised error, never cached on the client.
|
|
11
|
+
class Client
|
|
12
|
+
# Per-service hosts. Hardcoded constants, not options.
|
|
13
|
+
HOSTS = {
|
|
14
|
+
genderize: "https://api.genderize.io",
|
|
15
|
+
agify: "https://api.agify.io",
|
|
16
|
+
nationalize: "https://api.nationalize.io"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
USER_AGENT = "demografix-ruby/#{VERSION}"
|
|
20
|
+
|
|
21
|
+
MAX_BATCH = 10
|
|
22
|
+
DEFAULT_TIMEOUT = 10
|
|
23
|
+
|
|
24
|
+
# @param api_key [String] required. The same key works across all three
|
|
25
|
+
# services. A missing or blank key raises ValidationError before any
|
|
26
|
+
# request is made.
|
|
27
|
+
# @param timeout [Numeric] request timeout in seconds.
|
|
28
|
+
def initialize(api_key:, timeout: DEFAULT_TIMEOUT)
|
|
29
|
+
key = api_key.to_s
|
|
30
|
+
raise ValidationError.new("api_key is required", status: 422) if key.strip.empty?
|
|
31
|
+
|
|
32
|
+
@api_key = key
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# --- genderize ---------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def genderize(name, country_id: nil)
|
|
39
|
+
body, quota = request(:genderize, [name], country_id: country_id, batch: false)
|
|
40
|
+
pred = parse_genderize(single(body))
|
|
41
|
+
GenderizeResult.new(**pred.to_h, quota: quota)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def genderize_batch(names, country_id: nil)
|
|
45
|
+
body, quota = request(:genderize, validate_batch(names), country_id: country_id, batch: true)
|
|
46
|
+
Batch.new(results: array(body).map { |o| parse_genderize(o) }, quota: quota)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# --- agify -------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def agify(name, country_id: nil)
|
|
52
|
+
body, quota = request(:agify, [name], country_id: country_id, batch: false)
|
|
53
|
+
pred = parse_agify(single(body))
|
|
54
|
+
AgifyResult.new(**pred.to_h, quota: quota)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def agify_batch(names, country_id: nil)
|
|
58
|
+
body, quota = request(:agify, validate_batch(names), country_id: country_id, batch: true)
|
|
59
|
+
Batch.new(results: array(body).map { |o| parse_agify(o) }, quota: quota)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- nationalize -------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def nationalize(name)
|
|
65
|
+
body, quota = request(:nationalize, [name], batch: false)
|
|
66
|
+
pred = parse_nationalize(single(body))
|
|
67
|
+
NationalizeResult.new(**pred.to_h, quota: quota)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def nationalize_batch(names)
|
|
71
|
+
body, quota = request(:nationalize, validate_batch(names), batch: true)
|
|
72
|
+
Batch.new(results: array(body).map { |o| parse_nationalize(o) }, quota: quota)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Validate the batch size client-side before any HTTP call.
|
|
78
|
+
def validate_batch(names)
|
|
79
|
+
list = Array(names)
|
|
80
|
+
if list.length > MAX_BATCH
|
|
81
|
+
raise ValidationError.new(
|
|
82
|
+
"A batch may contain at most #{MAX_BATCH} names, got #{list.length}.",
|
|
83
|
+
status: 422
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
list
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build and send the request, returning [parsed_body, quota].
|
|
90
|
+
def request(service, names, country_id: nil, batch:)
|
|
91
|
+
uri = URI(HOSTS.fetch(service))
|
|
92
|
+
uri.query = build_query(names, country_id: country_id, batch: batch)
|
|
93
|
+
send_request(uri)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build the query string: name=<v> for a single call, repeated name[]=<v>
|
|
97
|
+
# for a batch. apikey is always sent; country_id is added only when set.
|
|
98
|
+
def build_query(names, country_id:, batch:)
|
|
99
|
+
params = []
|
|
100
|
+
if batch
|
|
101
|
+
names.each { |n| params << ["name[]", n.to_s] }
|
|
102
|
+
else
|
|
103
|
+
params << ["name", names.first.to_s]
|
|
104
|
+
end
|
|
105
|
+
params << ["country_id", country_id.to_s] if country_id
|
|
106
|
+
params << ["apikey", @api_key]
|
|
107
|
+
URI.encode_www_form(params)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The internal transport seam. Tests stub Net::HTTP at the wire level
|
|
111
|
+
# (webmock); this method performs the real request and is the single point
|
|
112
|
+
# where the network is touched.
|
|
113
|
+
def send_request(uri)
|
|
114
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
115
|
+
http.use_ssl = (uri.scheme == "https")
|
|
116
|
+
http.open_timeout = @timeout
|
|
117
|
+
http.read_timeout = @timeout
|
|
118
|
+
|
|
119
|
+
req = Net::HTTP::Get.new(uri)
|
|
120
|
+
req["User-Agent"] = USER_AGENT
|
|
121
|
+
|
|
122
|
+
response = http.request(req)
|
|
123
|
+
handle(response)
|
|
124
|
+
rescue Timeout::Error, IOError, SocketError, SystemCallError, Net::HTTPBadResponse,
|
|
125
|
+
Net::ProtocolError, OpenSSL::SSL::SSLError => e
|
|
126
|
+
raise TransportError.new(e.message)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Map the HTTP response to a parsed body + quota, or raise a typed error.
|
|
130
|
+
def handle(response)
|
|
131
|
+
quota = parse_quota(response)
|
|
132
|
+
code = response.code.to_i
|
|
133
|
+
body = parse_json(response.body)
|
|
134
|
+
|
|
135
|
+
return [body, quota] if code.between?(200, 299)
|
|
136
|
+
|
|
137
|
+
message = body.is_a?(Hash) ? body["error"] : nil
|
|
138
|
+
message ||= "HTTP #{code}"
|
|
139
|
+
raise error_for(code).new(message, status: code, quota: quota)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Select the error class for a status code.
|
|
143
|
+
def error_for(code)
|
|
144
|
+
case code
|
|
145
|
+
when 401 then AuthError
|
|
146
|
+
when 402 then SubscriptionError
|
|
147
|
+
when 422 then ValidationError
|
|
148
|
+
when 429 then RateLimitError
|
|
149
|
+
else Error
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_json(raw)
|
|
154
|
+
return nil if raw.nil? || raw.empty?
|
|
155
|
+
|
|
156
|
+
JSON.parse(raw)
|
|
157
|
+
rescue JSON::ParserError => e
|
|
158
|
+
raise TransportError.new("Response body is not valid JSON: #{e.message}")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Read the rate-limit headers case-insensitively into a Quota. Net::HTTP
|
|
162
|
+
# already normalizes header names to lowercase for lookup.
|
|
163
|
+
def parse_quota(response)
|
|
164
|
+
limit = response["x-rate-limit-limit"]
|
|
165
|
+
remaining = response["x-rate-limit-remaining"]
|
|
166
|
+
reset = response["x-rate-limit-reset"]
|
|
167
|
+
return nil if limit.nil? && remaining.nil? && reset.nil?
|
|
168
|
+
|
|
169
|
+
Quota.new(
|
|
170
|
+
limit: to_i_or_nil(limit),
|
|
171
|
+
remaining: to_i_or_nil(remaining),
|
|
172
|
+
reset: to_i_or_nil(reset)
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def to_i_or_nil(value)
|
|
177
|
+
value.nil? ? nil : value.to_i
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def single(body)
|
|
181
|
+
unless body.is_a?(Hash)
|
|
182
|
+
raise TransportError.new("Expected a JSON object, got #{body.class}.")
|
|
183
|
+
end
|
|
184
|
+
body
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def array(body)
|
|
188
|
+
unless body.is_a?(Array)
|
|
189
|
+
raise TransportError.new("Expected a JSON array, got #{body.class}.")
|
|
190
|
+
end
|
|
191
|
+
body
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def parse_genderize(obj)
|
|
195
|
+
GenderizePrediction.new(
|
|
196
|
+
name: obj["name"],
|
|
197
|
+
gender: obj["gender"],
|
|
198
|
+
probability: obj["probability"],
|
|
199
|
+
count: obj["count"],
|
|
200
|
+
country_id: obj["country_id"]
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_agify(obj)
|
|
205
|
+
AgifyPrediction.new(
|
|
206
|
+
name: obj["name"],
|
|
207
|
+
age: obj["age"],
|
|
208
|
+
count: obj["count"],
|
|
209
|
+
country_id: obj["country_id"]
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parse_nationalize(obj)
|
|
214
|
+
countries = Array(obj["country"]).map do |c|
|
|
215
|
+
NationalizeCountry.new(
|
|
216
|
+
country_id: c["country_id"],
|
|
217
|
+
probability: c["probability"]
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
NationalizePrediction.new(
|
|
221
|
+
name: obj["name"],
|
|
222
|
+
country: countries,
|
|
223
|
+
count: obj["count"]
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demografix
|
|
4
|
+
# Base class for every error the SDK raises. Carries the HTTP status (when
|
|
5
|
+
# one is known), the server-provided message, and the quota parsed from the
|
|
6
|
+
# rate-limit headers (when present).
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :status, :quota
|
|
9
|
+
|
|
10
|
+
def initialize(message = nil, status: nil, quota: nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@status = status
|
|
13
|
+
@quota = quota
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# 401 — the API key is missing or invalid.
|
|
18
|
+
class AuthError < Error; end
|
|
19
|
+
|
|
20
|
+
# 402 — the subscription is not active (expired freebie or inactive plan).
|
|
21
|
+
class SubscriptionError < Error; end
|
|
22
|
+
|
|
23
|
+
# 422 — the request parameters are invalid. Also raised client-side, before
|
|
24
|
+
# any HTTP call, when a batch contains more than the maximum number of names.
|
|
25
|
+
class ValidationError < Error; end
|
|
26
|
+
|
|
27
|
+
# 429 — the request limit for the current window is reached. Read
|
|
28
|
+
# quota.reset for the seconds to wait before retrying.
|
|
29
|
+
class RateLimitError < Error; end
|
|
30
|
+
|
|
31
|
+
# Network failure, timeout, or a response body that is not valid JSON.
|
|
32
|
+
# status and quota may be absent.
|
|
33
|
+
class TransportError < Error; end
|
|
34
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demografix
|
|
4
|
+
# The rate-limit window, parsed from the x-rate-limit-* response headers.
|
|
5
|
+
Quota = Data.define(:limit, :remaining, :reset)
|
|
6
|
+
|
|
7
|
+
# One nationalize candidate: a country and its probability.
|
|
8
|
+
NationalizeCountry = Data.define(:country_id, :probability)
|
|
9
|
+
|
|
10
|
+
# A single genderize prediction. country_id is set only when the request
|
|
11
|
+
# carried one.
|
|
12
|
+
GenderizePrediction = Data.define(:name, :gender, :probability, :count, :country_id)
|
|
13
|
+
|
|
14
|
+
# A single agify prediction. country_id is set only when the request carried
|
|
15
|
+
# one.
|
|
16
|
+
AgifyPrediction = Data.define(:name, :age, :count, :country_id)
|
|
17
|
+
|
|
18
|
+
# A single nationalize prediction. country is an array of NationalizeCountry,
|
|
19
|
+
# descending by probability, possibly empty.
|
|
20
|
+
NationalizePrediction = Data.define(:name, :country, :count)
|
|
21
|
+
|
|
22
|
+
# A single-name result: every prediction field plus a quota reader.
|
|
23
|
+
GenderizeResult = Data.define(:name, :gender, :probability, :count, :country_id, :quota)
|
|
24
|
+
AgifyResult = Data.define(:name, :age, :count, :country_id, :quota)
|
|
25
|
+
NationalizeResult = Data.define(:name, :country, :count, :quota)
|
|
26
|
+
|
|
27
|
+
# A batch result: the per-name predictions plus one quota for the response.
|
|
28
|
+
Batch = Data.define(:results, :quota)
|
|
29
|
+
end
|
data/lib/demografix.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "demografix/version"
|
|
4
|
+
require_relative "demografix/errors"
|
|
5
|
+
require_relative "demografix/models"
|
|
6
|
+
require_relative "demografix/client"
|
|
7
|
+
|
|
8
|
+
# Demografix is the official Ruby client for the genderize, agify, and
|
|
9
|
+
# nationalize APIs. Construct a Demografix::Client and call genderize, agify,
|
|
10
|
+
# or nationalize (with their _batch forms) to predict gender, age, and
|
|
11
|
+
# nationality from names.
|
|
12
|
+
module Demografix
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: demografix
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Demografix
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webmock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.18'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.18'
|
|
54
|
+
description: One client for the three Demografix APIs — gender, age, and nationality
|
|
55
|
+
prediction from names — reporting the remaining quota carried on every response.
|
|
56
|
+
email:
|
|
57
|
+
- info@genderize.io
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- LICENSE
|
|
63
|
+
- README.md
|
|
64
|
+
- lib/demografix.rb
|
|
65
|
+
- lib/demografix/client.rb
|
|
66
|
+
- lib/demografix/errors.rb
|
|
67
|
+
- lib/demografix/models.rb
|
|
68
|
+
- lib/demografix/version.rb
|
|
69
|
+
homepage: https://github.com/DemografixGenderize/demografix-ruby
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata:
|
|
73
|
+
homepage_uri: https://github.com/DemografixGenderize/demografix-ruby
|
|
74
|
+
documentation_uri: https://genderize.io/documentation/api
|
|
75
|
+
source_code_uri: https://github.com/DemografixGenderize/demografix-ruby
|
|
76
|
+
bug_tracker_uri: https://github.com/DemografixGenderize/demografix-ruby/issues
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '3.2'
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 3.6.9
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Official Ruby client for the genderize, agify, and nationalize APIs.
|
|
94
|
+
test_files: []
|