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 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Demografix
4
+ VERSION = "0.1.0"
5
+ 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: []