parseapi 1.7.0 → 1.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6598fb8f4a05494418599738f387ab8137099ddb12ac09f63ca42410d6b4b89
4
- data.tar.gz: 9de6f2540a617508c1c9a298361db8218e5f0460fb4a03e1e2c04a85179badca
3
+ metadata.gz: 3bd44c95f15db2d0248e3b9afebf94580ef381cd5df317b668d92bd6ba8e5f3a
4
+ data.tar.gz: 51258c88b79a3c61ce685fbc5c4fa4606adc86ee1a64a22574f9cd41a690b418
5
5
  SHA512:
6
- metadata.gz: 2599c0832bed2f4306ac4f1b5eb31a2d4757546a7349178a8c7bd693605ee091b107cec97c2005d0ed002afa9e2758f0b22d86c679fd15efffad1bd6fa0453d4
7
- data.tar.gz: 3d6a7f151089a85c0e76624f84cdd788df87a329371669505efd3179ec98c682017248e969072949f3975c70687ae4d9dfc766fec0949211df22a89374c80533
6
+ metadata.gz: 9bd2d3e93c4c1f5aa2668146a4af0ad4a18379a483ca77578971980d11dbc64be2942c3a50522723f4ff2492cd23b523fb077769b38c6f4b77aa1f1670a8c71c
7
+ data.tar.gz: 1d06c9bf515839953e35f563bc808a177fde1bdac02a4f7a095b90a6d34b2013dfdbec32d4b7b95d0f046cdb514094b7fc370e6c2581bfe1fefe31713be7de1d
data/README.md CHANGED
@@ -53,9 +53,9 @@ parse.ip('8.8.8.8')
53
53
  parse.ip_self
54
54
  parse.email('hello@gmail.com')
55
55
  parse.vat('DE136695976')
56
- parse.iban('DE89370400440532013000')
57
- parse.bin('424242')
58
- parse.npi('1881018208')
56
+ parse.bank('DE89370400440532013000')
57
+ parse.card('424242')
58
+ parse.provider('1881018208')
59
59
  parse.phone('+14155552671')
60
60
  parse.carrier('+14155552671')
61
61
  parse.caller('+14155552671')
@@ -107,16 +107,18 @@ parse.mx('example.com')
107
107
  parse.dns('example.com')
108
108
  parse.dns('_dmarc.example.com', type: 'TXT')
109
109
  parse.useragent(ua_string)
110
- parse.vin('1HGCM82633A004352')
111
- parse.naics('541511')
112
- parse.naics_search('coffee shop', limit: 5)
110
+ parse.vehicle('1HGCM82633A004352')
111
+ parse.industry('541511')
112
+ parse.industry_search('coffee shop', limit: 5)
113
113
  parse.tariff('8471.30.01.00', origin: 'CN', deep: true)
114
114
  parse.tariff_search('sunglasses')
115
115
  parse.emoji('rocket')
116
116
  parse.emoji_search('fire')
117
117
  ```
118
118
 
119
- NAICS paid deep records include classification `deep.exclusions`, each with a description and linked codes. Generic exclusions can have no linked codes. Omitted or null exclusions in older responses remain unknown. Search results also include `match`: the matched `field` (`name`, `term` or `naics`) and `text`, plus `corrections` with `from` and `to` tokens for typo fallback. Corrections are empty for exact, plural and prefix matches. Direct code lookups omit `match`. Older responses may omit it.
119
+ The existing NAICS lookup and search methods remain available as compatibility names for Industry.
120
+
121
+ Industry paid deep records include classification `deep.exclusions`, each with a description and linked codes. Generic exclusions can have no linked codes. Omitted or null exclusions in older responses remain unknown. Search results also include `match`: the matched `field` (`name`, `term` or `naics`) and `text`, plus `corrections` with `from` and `to` tokens for typo fallback. Corrections are empty for exact, plural and prefix matches. Direct code lookups omit `match`. Older responses may omit it.
120
122
 
121
123
  Each lookup returns a plain hash with string keys. Related lookups are separate calls, such as `country_states('US')`. Reading the result makes no further requests. New response fields and `nil` values are preserved.
122
124
 
@@ -235,6 +237,32 @@ Address search uses context from the form: prefer postal, or city and state. An
235
237
 
236
238
  HLR reports status at the last check. `live` means assigned and `connected` means reachable at that check. Cached results may be returned. Null means unconfirmed. Deep diagnostics stay within the same metered lookup.
237
239
 
240
+ Bank returns core `checks` for input, country, length, structure, checksum and national rules, plus an `issues` list. States are `passed`, `failed`, `not_checked` or `not_supported`. Unsupported national checking is not a failure. `valid` covers the implemented format and checksum rules, not account existence, ownership or payment reachability. Directory names and BICs may be null independently. Older responses may omit `checks` and `issues`, and future states and issue codes remain strings. Pass the original input unchanged so the API can report invalid characters. Deep `account` remains the BBAN remainder.
241
+
242
+ Bank inputs use `POST /bank` JSON bodies, keeping IBAN and account values out of request URLs. Pass original strings; the server owns normalization and validation. Avoid logging request bodies. IBAN deep can include `directory` with the immutable `edition`, resolved `country` and actual `match` grain (`bank`, `branch`, `prefix` or `none`); it is absent if no directory lookup ran. A match does not prove complete country coverage or payment reachability.
243
+
244
+ Use country requirements to build supported input fields. US ACH has an explicit helper with no deep option. It checks the routing format/ABA checksum and account-field syntax; `account_checksum` is `not_supported`. It preserves account characters and leading zeros. A nullable bank name is routing-directory identity, not account existence, ownership or ACH eligibility. The examples below are synthetic test inputs, not payment instructions.
245
+
246
+ ```ruby
247
+ parse.bank_requirements('US', format: 'us_ach')
248
+ parse.bank_us_ach(routing: '011000015', account: '0001234567')
249
+ ```
250
+
251
+ ## Provider lookup
252
+
253
+ ```ruby
254
+ provider = parse.provider('1881018208')
255
+ profile = parse.provider('1881018208', deep: true)
256
+ ```
257
+
258
+ Pass the original NPI as a string. `valid` checks its format and checksum; `registered` means a match in the stored NPPES snapshot. `active` reflects recorded NPI deactivation, not licensure. `excluded` is an NPI-only OIG LEIE match; `false` is not a complete exclusion clearance. These directory facts do not verify credentials, current practice contact or payment eligibility.
259
+
260
+ Invalid input returns `valid: false` with unknown provider fields. A checksum-valid number missing from the snapshot returns `registered: false`; unavailable storage remains an API error. Preserve `null` as unknown.
261
+
262
+ The default pooled lookup includes provider identity, specialty and practice contact where held. Paid `deep` adds `deactivated_at`, `medicare`, `opt_out` and `enrollments` from stored source files, with no separate check meter or live verification. `enrollments: null` means unavailable; `[]` means no enrollment rows are returned. The API omits unrequested `deep` and returns `{}` when requested on Free.
263
+
264
+ Paid Deep also returns `taxonomies` in published order, with taxonomy code, specialty label, primary flag and provider-reported license number/state, plus `enumerated_at`, `updated_at` and `reactivated_at` record dates. Reported licenses are not verified licenses. Null lists mean unavailable; empty lists mean the edition contains no entries. Core `sources` is available on every plan: NPPES, LEIE, PECOS and opt-out each have nullable edition metadata (`edition`, `published_at`, `through`, `imported_at`). Provider record dates are separate from source publication and completed import dates. Older responses may omit these additions. Edition details remain null until a verified source is served.
265
+
238
266
  ## Deep
239
267
 
240
268
  Choose enrichment for the question you need answered.
@@ -245,10 +273,11 @@ Choose enrichment for the question you need answered.
245
273
  | Domain | Registration dates, registrar, status and DNSSEC, included with a paid plan. Use `dns` for DNS records and `mx` for mail routing. |
246
274
  | Email | A metered mailbox check with deliverability, catch-all, status, reason and address hints, using included email checks or enabled on-demand usage. |
247
275
  | VAT | A metered registry check where supported, using included VAT checks or enabled on-demand usage. |
248
- | Phone, Time, Date, Currency, Language, Emoji, IBAN, Point | Optional detail in the same pooled request on every plan. |
276
+ | Phone, Time, Date, Currency, Language, Emoji, Bank, Point | Optional detail in the same pooled request on every plan. |
249
277
  | Country, State, District, City, Postal | The place profile on paid plans, including demographic and tax facts where held. |
250
- | Name, NAICS | Name evidence or the industry definition profile on paid plans. |
251
- | VIN, NPI, Tariff, Company | The complete product detail bag on paid plans. |
278
+ | Name, Industry | Name evidence or the industry definition profile on paid plans. |
279
+ | NPI | Deactivation date, Medicare enrollment, opt-out and enrollment rows from stored sources on paid plans. Exclusion evidence stays core. |
280
+ | Vehicle, Tariff, Company | The complete product detail bag on paid plans. |
252
281
  | Weather | Specialist current measurements and the existing forecast, alert, air and history bag on paid plans. |
253
282
  | Carrier, HLR | Optional diagnostic detail within the same metered core unit, including Free allowance units. No second gate or additional check. |
254
283
 
@@ -267,7 +296,7 @@ ip.dig('deep', 'datacenter') # true, false, or nil
267
296
 
268
297
  ## Errors
269
298
 
270
- Every non-2xx response raises `ParseAPI::Error` with `status`, `code`, `docs`, and `request_id`. Branch on `code`.
299
+ Every non-2xx response raises `ParseAPI::Error` with `status`, `code`, `docs`, and `request_id`, plus nullable `retry_after` header metadata. Branch on `code`.
271
300
 
272
301
  ```ruby
273
302
  begin
@@ -292,11 +321,13 @@ Ordinary lookups retry network errors and HTTP 429, 500, 502, 503, and 504 up to
292
321
 
293
322
  Pass `retries: 0` to make every lookup a single attempt. An explicit count such as `retries: 2` applies to every lookup, including paid ones. A retried request can count toward usage even when the first response was lost. Omit `retries` or pass `nil` to use the defaults above.
294
323
 
324
+ Automatic retries honor numeric and HTTP-date `Retry-After` values up to five seconds. A longer server wait returns the original API error immediately, with the raw header in `retry_after`, so the application can schedule a later attempt. Missing or invalid headers use ordinary backoff.
325
+
295
326
  Reuse one client for successive lookups. Call `parse.close` to release its connection when finished. A later lookup opens a new connection. Use a separate client in each concurrent thread.
296
327
 
297
328
  Network failures raise native Ruby exceptions. An invalid JSON response raises `JSON::ParserError`.
298
329
 
299
- For testing or instrumentation, pass a callable as `transport:`. It receives the URL and request-header hash and returns `[status, lowercase_response_headers, body]`. Custom transports should use the supplied headers and keep redirect following disabled.
330
+ For testing or instrumentation, pass a callable as `transport:`. For GET it receives the URL and request-header hash. For Bank POST it also receives a third `"POST"` argument and fourth serialized JSON body argument. Accept optional method/body parameters when supplying a custom transport. It returns `[status, lowercase_response_headers, body]`. Custom transports should use the supplied headers and keep redirect following disabled.
300
331
 
301
332
  Requires Ruby 3.0 or later. Standard library only, zero dependencies.
302
333
 
@@ -304,12 +335,34 @@ Requires Ruby 3.0 or later. Standard library only, zero dependencies.
304
335
 
305
336
  Full field reference for every endpoint: [parseapi.com/docs](https://parseapi.com/docs)
306
337
 
307
- BIN lookup accepts 6-11 digits as a string, including leading zeros. Spaces and hyphens are accepted. `prefix` is the actual longest match and can be shorter than the input. Unknown reference fields are null. `deep` adds an empty object on every plan.
338
+ ## Card
339
+
340
+ Send 2–11 leading digits as a string. Core returns `bin`, `brand`, `brand_name`
341
+ and a CDN SVG `logo`. Brand detection uses reviewed network rules independently
342
+ of issuer records. Unknown or ambiguous prefixes return null brand fields and a
343
+ generic logo; a known network without reviewed artwork also uses the generic logo.
308
344
 
345
+ Optional Deep adds `prefix`, `issuer`, `country`, `type` and `prepaid`, included
346
+ in the same pooled request on every plan. Six or more digits enable directory
347
+ matching. Fewer digits return all-null Deep fields. Compare `deep.prefix` with
348
+ `bin`: equal is an exact recorded match; shorter is broader; null is no match.
349
+ The longest row wins, including null fields. `prepaid: null` means unknown, not
350
+ false. This is partial reference data, not card validity or payment acceptance.
351
+
352
+ ```ruby
353
+ card = parse.card("51")
354
+ puts card["logo"]
355
+ details = parse.card("43737400", deep: true)
356
+ issuer = details["deep"]["issuer"]
357
+ ```
358
+
359
+ Leading zeros are preserved. Only ASCII spaces, tabs, CR, LF and hyphens are
360
+ removed; raw input is limited to 64 characters. Invalid prefixes are rejected
361
+ before dispatch, accepted input is forwarded unchanged. Never send a full card number.
309
362
 
310
363
  ## Optional detail
311
364
 
312
- The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, NAICS and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep`; only the source has `deep.next_dst`.
365
+ The default response answers the common task. Ask for `deep` when you need more detail about that same result. Core fields stay equal. City, Industry and Emoji searches put detail inside each result. Postal nearby and distance put metropolitan detail beside the entity it describes. Time conversion keeps target detail in `to.deep`; only the source has `deep.next_dst`.
313
366
 
314
367
  ```ruby
315
368
  basic = parse.time('America/New_York')
@@ -330,3 +383,5 @@ Pass a public hostname without a scheme, path, port or IP address. Stack returns
330
383
  Successful checks may be reused for up to 24 hours. `pretty` optionally formats the wire JSON. Stack uses your plan's request allowance and API version 2.0.0 selected by this client.
331
384
 
332
385
  Stack defaults to a 35-second transport timeout so a first scan has time to finish. Other lookups retain their 10-second default. An explicit client timeout takes precedence.
386
+
387
+ Vehicle lookups use `vin` as the input and response field. Existing VIN methods remain available for compatibility.
@@ -6,14 +6,15 @@ require 'time'
6
6
  module ParseAPI
7
7
  # Every non-2xx response from the API. Branch on +code+, never on the message.
8
8
  class Error < StandardError
9
- attr_reader :status, :code, :docs, :request_id
9
+ attr_reader :status, :code, :docs, :request_id, :retry_after
10
10
 
11
- def initialize(status:, code:, message:, docs: nil, request_id: nil)
11
+ def initialize(status:, code:, message:, docs: nil, request_id: nil, retry_after: nil)
12
12
  super(message)
13
13
  @status = status
14
14
  @code = code
15
15
  @docs = docs
16
16
  @request_id = request_id
17
+ @retry_after = retry_after
17
18
  end
18
19
  end
19
20
 
@@ -173,16 +174,39 @@ module ParseAPI
173
174
  get("/iban/#{seg(iban)}", country: country, deep: deep)
174
175
  end
175
176
 
176
- # Look up a 6-11 digit card prefix, preserving leading zeros.
177
177
  def bin(bin, deep: false)
178
178
  get("/bin/#{seg(bin)}", deep: deep)
179
179
  end
180
180
 
181
-
182
181
  def npi(npi, deep: false, lang: nil)
183
182
  get("/npi/#{seg(npi)}", deep: deep, lang: lang)
184
183
  end
185
184
 
185
+ def bank(iban, country: nil, deep: false)
186
+ get("/bank", {}, {}, { iban: iban, country: country, deep: deep }.reject { |_key, value| value.nil? })
187
+ end
188
+
189
+ # Look up a 2-11 digit card prefix, preserving leading zeros.
190
+ def card(bin, deep: false)
191
+ raise ArgumentError, 'parseapi: Card requires a 2-11 digit prefix string.' unless bin.is_a?(String) && bin.length <= 64 && /\A[0-9]{2,11}\z/.match?(bin.delete(" \t\r\n-"))
192
+ get("/card/#{seg(bin)}", deep: deep)
193
+ end
194
+
195
+
196
+ # US routing/account syntax only; not account or ACH eligibility verification.
197
+ def bank_us_ach(routing:, account:)
198
+ get('/bank', {}, {}, { format: 'us_ach', country: 'US', routing: routing, account: account })
199
+ end
200
+
201
+ # Describe accepted fields and check scope, not directory completeness.
202
+ def bank_requirements(country, format: nil)
203
+ get('/bank/requirements', country: country, format: format)
204
+ end
205
+
206
+ def provider(npi, deep: false, lang: nil)
207
+ get("/provider/#{seg(npi)}", deep: deep, lang: lang)
208
+ end
209
+
186
210
  # Parse a phone number and its formats. Pass country for national numbers when needed. Deep
187
211
  # adds numbering-plan geography on every plan. Carrier, caller, and HLR are separate metered lookups.
188
212
  def phone(number, country: nil, deep: false)
@@ -240,18 +264,31 @@ module ParseAPI
240
264
  get('/useragent', { deep: deep }, { 'User-Agent' => ua })
241
265
  end
242
266
 
267
+ def vehicle(vin, deep: false)
268
+ get("/vehicle/#{seg(vin)}", deep: deep)
269
+ end
270
+
243
271
  def vin(vin, deep: false)
244
272
  get("/vin/#{seg(vin)}", deep: deep)
245
273
  end
246
274
 
247
275
  # US NAICS 2022 definition and hierarchy.
276
+ # Compatibility names for Industry.
248
277
  def naics(code, deep: false)
249
- get("/naics/#{seg(code)}", deep: deep)
278
+ industry(code, deep: deep)
250
279
  end
251
280
 
252
- # Keyword search. Limit defaults to 10 and accepts 1-50.
253
281
  def naics_search(query, limit: nil, deep: false)
254
- get('/naics', q: query, limit: limit, deep: deep)
282
+ industry_search(query, limit: limit, deep: deep)
283
+ end
284
+
285
+ def industry(code, deep: false)
286
+ get("/industry/#{seg(code)}", deep: deep)
287
+ end
288
+
289
+ # Keyword search. Limit defaults to 10 and accepts 1-50.
290
+ def industry_search(query, limit: nil, deep: false)
291
+ get('/industry', q: query, limit: limit, deep: deep)
255
292
  end
256
293
 
257
294
  # Look up the general US duty schedule line. Paid deep adds units and the special and other
@@ -393,17 +430,22 @@ module ParseAPI
393
430
  URI.encode_www_form_component(value.to_s).gsub('+', '%20')
394
431
  end
395
432
 
396
- def get(path, params = {}, headers = {})
433
+ def get(path, params = {}, headers = {}, json = nil)
397
434
  retries = retries_for(path, params)
398
435
  query = params.reject { |_name, value| value.nil? || value == false }
399
436
  uri = @base_url.dup
400
437
  uri.path = path
401
438
  uri.query = URI.encode_www_form(query) unless query.empty?
402
439
 
440
+ encoded = json.nil? ? nil : JSON.generate(json)
403
441
  attempt = 0
404
442
  loop do
405
443
  begin
406
- status, response_headers, body = execute(uri, request_headers(headers))
444
+ status, response_headers, body = if encoded.nil?
445
+ execute(uri, request_headers(headers))
446
+ else
447
+ execute(uri, request_headers(headers.merge('Content-Type' => 'application/json')), 'POST', encoded)
448
+ end
407
449
  rescue *NETWORK_ERRORS
408
450
  raise if attempt >= retries
409
451
 
@@ -414,13 +456,14 @@ module ParseAPI
414
456
 
415
457
  return JSON.parse(body) if (200..299).cover?(status)
416
458
 
417
- if RETRY_STATUS.include?(status) && attempt < retries
418
- sleep(retry_delay(attempt, response_headers['retry-after']))
459
+ retry_after = response_headers['retry-after']
460
+ if RETRY_STATUS.include?(status) && attempt < retries && (wait = retry_delay(attempt, retry_after))
461
+ sleep(wait)
419
462
  attempt += 1
420
463
  next
421
464
  end
422
465
 
423
- raise build_error(status, body)
466
+ raise build_error(status, body, retry_after)
424
467
  end
425
468
  end
426
469
 
@@ -429,12 +472,13 @@ module ParseAPI
429
472
  end
430
473
 
431
474
  # Returns [status, headers_hash, body_string]. Overridden in tests.
432
- def execute(uri, headers)
433
- return @transport.call(uri.to_s, headers) if @transport
475
+ def execute(uri, headers, method = 'GET', body = nil)
476
+ return (method == 'GET' ? @transport.call(uri.to_s, headers) : @transport.call(uri.to_s, headers, method, body)) if @transport
434
477
 
435
478
  timeout = !@timeout_explicit && uri.path.start_with?('/stack/') ? 35 : @timeout
436
479
  http = connection(timeout)
437
- request = Net::HTTP::Get.new(uri.request_uri)
480
+ request = (method == 'POST' ? Net::HTTP::Post : Net::HTTP::Get).new(uri.request_uri)
481
+ request.body = body unless body.nil?
438
482
  headers.each { |name, value| request[name] = value }
439
483
  response = http.request(request)
440
484
  header_hash = {}
@@ -459,15 +503,18 @@ module ParseAPI
459
503
 
460
504
  def retry_delay(attempt, retry_after)
461
505
  if retry_after
462
- seconds = Float(retry_after, exception: false)
463
- return [seconds, RETRY_AFTER_CAP].min if seconds && seconds.finite? && seconds >= 0
506
+ if /\A[0-9]+(?:\.[0-9]+)?\z/.match?(retry_after.strip)
507
+ seconds = Float(retry_after, exception: false)
508
+ return seconds && seconds.finite? && seconds <= RETRY_AFTER_CAP ? seconds : nil
509
+ end
464
510
  begin
465
- return [[Time.httpdate(retry_after) - Time.now, 0].max, RETRY_AFTER_CAP].min
511
+ seconds = [Time.httpdate(retry_after) - Time.now, 0].max
512
+ return seconds > RETRY_AFTER_CAP ? nil : seconds
466
513
  rescue ArgumentError
467
514
  # Fall back to jitter when the header is not a delay or HTTP date.
468
515
  end
469
516
  end
470
- rand * 0.25 * (2**attempt)
517
+ rand * [0.25 * (2**[attempt, 5].min), RETRY_AFTER_CAP].min
471
518
  end
472
519
 
473
520
  def retries_for(path, params)
@@ -478,7 +525,7 @@ module ParseAPI
478
525
  metered ? 0 : DEFAULT_RETRIES
479
526
  end
480
527
 
481
- def build_error(status, body)
528
+ def build_error(status, body, retry_after = nil)
482
529
  parsed = begin
483
530
  JSON.parse(body)
484
531
  rescue JSON::ParserError
@@ -490,7 +537,8 @@ module ParseAPI
490
537
  code: parsed['code'].is_a?(String) ? parsed['code'] : 'unknown_error',
491
538
  message: parsed['message'].is_a?(String) ? parsed['message'] : "Request failed with status #{status}",
492
539
  docs: parsed['docs'].is_a?(String) ? parsed['docs'] : nil,
493
- request_id: parsed['request_id'].is_a?(String) ? parsed['request_id'] : nil
540
+ request_id: parsed['request_id'].is_a?(String) ? parsed['request_id'] : nil,
541
+ retry_after: retry_after
494
542
  )
495
543
  end
496
544
  end
@@ -1,3 +1,3 @@
1
1
  module ParseAPI
2
- VERSION = '1.7.0'.freeze
2
+ VERSION = '1.8.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parseapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ParseAPI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-24 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: