parseapi 0.2.1 → 0.3.2

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: e4c6ab4b71acb2d8fbfd29ee797b05b3c731be5bd57f00df8a3147fadc40b9aa
4
- data.tar.gz: 176cf120dac886d1cfdfca1622ad6b0ffc575fc6730606bf43d6c0e9c4562561
3
+ metadata.gz: 69e639b49f6986257ea0f5cf3b5424867fd563efd4d6762e06657c0349e2071b
4
+ data.tar.gz: 7212244b17194211f3a5a400a932df526dc4ff6ce04d7fc28ed94ede12486ebd
5
5
  SHA512:
6
- metadata.gz: d17692b3a20373508da5a33fa90dfbbbdfc7f4c9b8f7d033e305962177c158254e81e09444294e72ea05cea355f3499ddeba3325be697122a52bbe720bc42c82
7
- data.tar.gz: 90ad0d41b12be8866972c02bc8b484898b8009237e9d34584689e7d57fa8da836f664e037c3594666240f149364aab0126558b0a75fc08606d828a4fc912c46c
6
+ metadata.gz: 255d9d9ae3fddb1bd3e35ba6fd5d9fb06a74c8d8a603bd24d29bc558f5d0b2587fdb5a60df2cf2bae3be703234febfa72c61c22e508d72b1e5ebc045eef74ab0
7
+ data.tar.gz: 6438c6a8761ecb4b5cbb77af884ebfb20b2e755d51f52ce1832d553e845322804b6c4184498eb49203c1d78ecdca608cc263a5a9606bc69cf91c9ea388a2c205
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 parseAPI
3
+ Copyright (c) 2026 ParseAPI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,3 @@
1
- # parseapi
2
-
3
- Official parseAPI client for Ruby.
4
-
5
1
  ```bash
6
2
  gem install parseapi
7
3
  ```
@@ -34,6 +30,9 @@ parse.postal('SW1A 1AA')
34
30
  parse.postal('28202', country: 'US')
35
31
  parse.postal_nearby('28202', country: 'US', radius: 40)
36
32
  parse.postal_distance('28202', '10001', country: 'US')
33
+ parse.address('1600 Pennsylvania Ave NW, Washington, DC 20500', country: 'US')
34
+ parse.address_search('123 main', country: 'US', postal: '27401')
35
+ parse.company('732829320', country: 'FR')
37
36
  parse.city('charlotte', country: 'US')
38
37
  parse.city_id('city_mb8mbqrkz8zb')
39
38
  parse.city_search('char', country: 'US', limit: 10)
@@ -47,25 +46,36 @@ parse.state_districts('NC', country: 'US')
47
46
  parse.district('37081')
48
47
  parse.continent('NA')
49
48
  parse.continent_countries('NA')
49
+ parse.bloc('EU')
50
+ parse.bloc_countries('SCHENGEN')
50
51
  parse.currency('USD')
51
52
  parse.currency_rate('USD', 'EUR')
52
53
  parse.language('en')
53
54
  parse.name('BILLY OSHALL')
54
55
  parse.timezone('America/New_York')
56
+ parse.timezone('America/New_York', at: '2026-09-05T15:00', to: 'Europe/London')
57
+ parse.timezone_at(35.2271, -80.8431)
58
+ parse.date('03/04/2026', format: 'mdy')
59
+ parse.date_today(to: '2026-12-25')
55
60
  parse.holiday('US', year: 2026)
56
61
  parse.holiday_date('US', '2026-12-25')
57
62
  parse.elevation(35.2271, -80.8431)
58
63
  parse.point(36.0726, -79.792)
59
64
  parse.weather(40.7128, -74.006)
65
+ parse.weather(40.7128, -74.006, deep: true, date: '2026-09-01')
60
66
  parse.domain('example.com')
67
+ parse.asn('AS13335')
68
+ parse.mac('00:1B:63:84:45:E6')
61
69
  parse.mx('example.com')
62
70
  parse.useragent(ua_string)
63
71
  parse.vin('1HGCM82633A004352')
72
+ parse.tariff('8471.30.01.00', origin: 'CN', deep: true)
73
+ parse.tariff_search('sunglasses')
64
74
  parse.emoji('rocket')
65
75
  parse.emoji_search('fire')
66
76
  ```
67
77
 
68
- Responses are plain hashes, exactly the JSON the API returns.
78
+ 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.
69
79
 
70
80
  ## Deep
71
81
 
@@ -95,11 +105,20 @@ end
95
105
  ```ruby
96
106
  parse = ParseAPI.new(
97
107
  'your-api-key',
98
- timeout: 10, # per-attempt timeout in seconds
99
- retries: 2 # automatic retries on network errors, 429, and 5xx
108
+ timeout: 10 # connect, read, and write timeout in seconds
100
109
  )
101
110
  ```
102
111
 
112
+ Ordinary lookups retry network errors and HTTP 429, 500, 502, 503, and 504 up to twice. Carrier, caller, and HLR lookups, plus email and VAT with `deep: true`, make one attempt by default because repeating them can repeat paid usage. Address with `deep: true` also uses one attempt, reserving that behavior for future verification. This does not mean address deep currently has a separate charge.
113
+
114
+ 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.
115
+
116
+ 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.
117
+
118
+ Network failures raise native Ruby exceptions. An invalid JSON response raises `JSON::ParserError`.
119
+
120
+ 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.
121
+
103
122
  Requires Ruby 3.0 or later. Standard library only, zero dependencies.
104
123
 
105
124
  ## Docs
@@ -1,6 +1,7 @@
1
1
  require 'json'
2
2
  require 'net/http'
3
3
  require 'uri'
4
+ require 'time'
4
5
 
5
6
  module ParseAPI
6
7
  # Every non-2xx response from the API. Branch on +code+, never on the message.
@@ -22,21 +23,37 @@ module ParseAPI
22
23
  DEFAULT_RETRIES = 2
23
24
  RETRY_STATUS = [429, 500, 502, 503, 504].freeze
24
25
  RETRY_AFTER_CAP = 5.0
26
+ METERED_CORE = %w[carrier caller hlr litigator reassigned].freeze
25
27
  NETWORK_ERRORS = [
26
28
  Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ETIMEDOUT,
27
29
  Net::OpenTimeout, Net::ReadTimeout, IOError, EOFError, SocketError
28
30
  ].freeze
29
31
 
30
- def initialize(api_key = nil, base_url: nil, timeout: nil, retries: nil)
32
+ def initialize(api_key = nil, base_url: nil, timeout: nil, retries: nil, transport: nil)
31
33
  @api_key = api_key || ENV['PARSEAPI_KEY']
32
34
  raise ArgumentError, 'parseapi: missing API key. Pass one or set PARSEAPI_KEY.' if @api_key.nil? || @api_key.empty?
33
35
 
34
36
  @base_url = URI((base_url || ENV['PARSEAPI_BASE_URL'] || DEFAULT_BASE_URL).sub(%r{/+\z}, ''))
35
37
  @timeout = timeout || DEFAULT_TIMEOUT
36
- @retries = retries || DEFAULT_RETRIES
38
+ @retries = retries
39
+ raise ArgumentError, 'parseapi: timeout must be a finite positive number.' unless @timeout.is_a?(Numeric) && @timeout.finite? && @timeout > 0
40
+ raise ArgumentError, 'parseapi: retries must be a non-negative integer or nil.' unless @retries.nil? || (@retries.is_a?(Integer) && @retries >= 0)
41
+ raise ArgumentError, 'parseapi: transport must be callable.' unless transport.nil? || transport.respond_to?(:call)
42
+ @transport = transport
37
43
  @http = nil
38
44
  end
39
45
 
46
+ # Release the connection. A later lookup opens a new one.
47
+ def close
48
+ @http.finish if @http&.started?
49
+ @http = nil
50
+ nil
51
+ end
52
+
53
+ def inspect
54
+ "#<#{self.class} api_key=[REDACTED] timeout=#{@timeout} retries=#{@retries.nil? ? 'auto' : @retries}>"
55
+ end
56
+
40
57
  # --- Lookup methods (one per endpoint, named after the route) ---
41
58
 
42
59
  def ip(ip, deep: false)
@@ -91,8 +108,8 @@ module ParseAPI
91
108
  get("/city/id/#{seg(id)}")
92
109
  end
93
110
 
94
- def city_search(q, country: nil, state: nil, limit: nil)
95
- get('/city', q: q, country: country, state: state, limit: limit)
111
+ def city_search(query, country: nil, state: nil, limit: nil)
112
+ get('/city', q: query, country: country, state: state, limit: limit)
96
113
  end
97
114
 
98
115
  def city_nearest(lat, lon)
@@ -115,6 +132,18 @@ module ParseAPI
115
132
  get("/postal/#{seg(from)}/distance/#{seg(to)}", country: country)
116
133
  end
117
134
 
135
+ def address(address, country: nil, deep: false)
136
+ get("/address/#{seg(address)}", country: country, deep: deep)
137
+ end
138
+
139
+ def address_search(query, country: nil, postal: nil, city: nil, state: nil, ip: nil)
140
+ get('/address', q: query, country: country, postal: postal, city: city, state: state, ip: ip)
141
+ end
142
+
143
+ def company(number, country: nil, deep: false)
144
+ get("/company/#{seg(number)}", country: country, deep: deep)
145
+ end
146
+
118
147
  def email(email, deep: false)
119
148
  get("/email/#{seg(email)}", deep: deep)
120
149
  end
@@ -151,6 +180,14 @@ module ParseAPI
151
180
  get("/domain/#{seg(domain)}", deep: deep)
152
181
  end
153
182
 
183
+ def asn(asn)
184
+ get("/asn/#{seg(asn)}")
185
+ end
186
+
187
+ def mac(mac)
188
+ get("/mac/#{seg(mac)}")
189
+ end
190
+
154
191
  def mx(domain)
155
192
  get("/mx/#{seg(domain)}")
156
193
  end
@@ -167,8 +204,8 @@ module ParseAPI
167
204
  get("/tariff/#{seg(code)}", deep: deep, origin: origin)
168
205
  end
169
206
 
170
- def tariff_search(q)
171
- get('/tariff', q: q)
207
+ def tariff_search(query)
208
+ get('/tariff', q: query)
172
209
  end
173
210
 
174
211
  def currency(code)
@@ -187,8 +224,20 @@ module ParseAPI
187
224
  get("/name/#{seg(name)}")
188
225
  end
189
226
 
190
- def timezone(id, at: nil)
191
- get("/timezone/#{seg(id)}", at: at)
227
+ def timezone(id, at: nil, to: nil)
228
+ get("/timezone/#{seg(id)}", at: at, to: to)
229
+ end
230
+
231
+ def timezone_at(lat, lon, at: nil)
232
+ get('/timezone', lat: lat, lon: lon, at: at)
233
+ end
234
+
235
+ def date(date, format: nil, to: nil)
236
+ get("/date/#{seg(date)}", format: format, to: to)
237
+ end
238
+
239
+ def date_today(to: nil)
240
+ get('/date', to: to)
192
241
  end
193
242
 
194
243
  def holiday(country, year: nil)
@@ -207,16 +256,16 @@ module ParseAPI
207
256
  get('/point', lat: lat, lon: lon, deep: deep)
208
257
  end
209
258
 
210
- def weather(lat, lon, deep: false)
211
- get('/weather', lat: lat, lon: lon, deep: deep)
259
+ def weather(lat, lon, deep: false, date: nil)
260
+ get('/weather', lat: lat, lon: lon, deep: deep, date: date)
212
261
  end
213
262
 
214
263
  def emoji(emoji)
215
264
  get("/emoji/#{seg(emoji)}")
216
265
  end
217
266
 
218
- def emoji_search(q, limit: nil)
219
- get('/emoji', q: q, limit: limit)
267
+ def emoji_search(query, limit: nil)
268
+ get('/emoji', q: query, limit: limit)
220
269
  end
221
270
 
222
271
  private
@@ -226,6 +275,7 @@ module ParseAPI
226
275
  end
227
276
 
228
277
  def get(path, params = {}, headers = {})
278
+ retries = retries_for(path, params)
229
279
  query = params.reject { |_name, value| value.nil? || value == false }
230
280
  uri = @base_url.dup
231
281
  uri.path = path
@@ -236,7 +286,7 @@ module ParseAPI
236
286
  begin
237
287
  status, response_headers, body = execute(uri, request_headers(headers))
238
288
  rescue *NETWORK_ERRORS
239
- raise if attempt >= @retries
289
+ raise if attempt >= retries
240
290
 
241
291
  sleep(retry_delay(attempt, nil))
242
292
  attempt += 1
@@ -245,7 +295,7 @@ module ParseAPI
245
295
 
246
296
  return JSON.parse(body) if (200..299).cover?(status)
247
297
 
248
- if RETRY_STATUS.include?(status) && attempt < @retries
298
+ if RETRY_STATUS.include?(status) && attempt < retries
249
299
  sleep(retry_delay(attempt, response_headers['retry-after']))
250
300
  attempt += 1
251
301
  next
@@ -261,6 +311,8 @@ module ParseAPI
261
311
 
262
312
  # Returns [status, headers_hash, body_string]. Overridden in tests.
263
313
  def execute(uri, headers)
314
+ return @transport.call(uri.to_s, headers) if @transport
315
+
264
316
  http = connection
265
317
  request = Net::HTTP::Get.new(uri.request_uri)
266
318
  headers.each { |name, value| request[name] = value }
@@ -276,6 +328,9 @@ module ParseAPI
276
328
  @http.use_ssl = @base_url.scheme == 'https'
277
329
  @http.open_timeout = @timeout
278
330
  @http.read_timeout = @timeout
331
+ @http.write_timeout = @timeout
332
+ # The client owns retries, including retries: 0.
333
+ @http.max_retries = 0
279
334
  @http.keep_alive_timeout = 30
280
335
  end
281
336
  @http.start unless @http.started?
@@ -285,11 +340,24 @@ module ParseAPI
285
340
  def retry_delay(attempt, retry_after)
286
341
  if retry_after
287
342
  seconds = Float(retry_after, exception: false)
288
- return [seconds, RETRY_AFTER_CAP].min if seconds && seconds >= 0
343
+ return [seconds, RETRY_AFTER_CAP].min if seconds && seconds.finite? && seconds >= 0
344
+ begin
345
+ return [[Time.httpdate(retry_after) - Time.now, 0].max, RETRY_AFTER_CAP].min
346
+ rescue ArgumentError
347
+ # Fall back to jitter when the header is not a delay or HTTP date.
348
+ end
289
349
  end
290
350
  rand * 0.25 * (2**attempt)
291
351
  end
292
352
 
353
+ def retries_for(path, params)
354
+ return @retries unless @retries.nil?
355
+
356
+ product = path.split('/', 3)[1]
357
+ metered = METERED_CORE.include?(product) || (%w[email vat address].include?(product) && params[:deep] == true)
358
+ metered ? 0 : DEFAULT_RETRIES
359
+ end
360
+
293
361
  def build_error(status, body)
294
362
  parsed = begin
295
363
  JSON.parse(body)
@@ -1,3 +1,3 @@
1
1
  module ParseAPI
2
- VERSION = '0.2.1'.freeze
2
+ VERSION = '0.3.2'.freeze
3
3
  end
data/lib/parseapi.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require_relative 'parseapi/version'
2
2
  require_relative 'parseapi/client'
3
3
 
4
- # Official parseAPI client for Ruby.
4
+ # Official ParseAPI client for Ruby.
5
5
  #
6
6
  # parse = ParseAPI.new('your-api-key')
7
7
  # parse.country('US')
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: 0.2.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
- - parseAPI
7
+ - ParseAPI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-02 00:00:00.000000000 Z
11
+ date: 2026-09-07 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -47,5 +47,5 @@ requirements: []
47
47
  rubygems_version: 3.5.22
48
48
  signing_key:
49
49
  specification_version: 4
50
- summary: Official parseAPI client for Ruby. One key, minimal JSON, fast.
50
+ summary: Official ParseAPI client for Ruby. One key, minimal JSON, fast.
51
51
  test_files: []