parseapi 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 +96 -0
- data/lib/parseapi/client.rb +257 -0
- data/lib/parseapi/version.rb +3 -0
- data/lib/parseapi.rb +13 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: db102dc0d021fa26f14dfdb337e38ead8d770d6a49b99d9b185ff4caf1c1cc17
|
|
4
|
+
data.tar.gz: d78a937301d8084f9a3ddfb0004419931309448223dbbe98302effcef101ba8c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 16e8e88149664cb0c07e72fda814f32ae22ca630ddc67e0f950e12d4a4d4542f41169b4b73f0cfe73565f36318eadbe13e64dc9be8dfaccdec2721d583d51938
|
|
7
|
+
data.tar.gz: b90d2a5589adc9c5c97e0ff8499cdd847e8a332133aec087dcdd26cb51233faca40434342245658c894231ab0126c4b5466d8523f61969243e4813aece87e1e7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 parseAPI
|
|
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,96 @@
|
|
|
1
|
+
# parseapi
|
|
2
|
+
|
|
3
|
+
Official parseAPI client for Ruby.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
gem install parseapi
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require 'parseapi'
|
|
11
|
+
|
|
12
|
+
parse = ParseAPI.new('your-api-key')
|
|
13
|
+
country = parse.country('US')
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEAPI_KEY` from the environment.
|
|
17
|
+
|
|
18
|
+
## Calls
|
|
19
|
+
|
|
20
|
+
One method per endpoint, named after the route.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
parse.ip('8.8.8.8')
|
|
24
|
+
parse.ip_self
|
|
25
|
+
parse.email('hello@gmail.com')
|
|
26
|
+
parse.phone('+14155552671')
|
|
27
|
+
parse.postal('28202', country: 'US')
|
|
28
|
+
parse.postal_nearby('28202', country: 'US', radius: 40)
|
|
29
|
+
parse.postal_distance('28202', '10001', country: 'US')
|
|
30
|
+
parse.city('charlotte', country: 'US')
|
|
31
|
+
parse.city_id('city_mb8mbqrkz8zb')
|
|
32
|
+
parse.city_search('char', country: 'US', limit: 10)
|
|
33
|
+
parse.city_nearest(35.2271, -80.8431)
|
|
34
|
+
parse.country('US')
|
|
35
|
+
parse.country_states('US')
|
|
36
|
+
parse.state('NC', country: 'US')
|
|
37
|
+
parse.state_districts('NC', country: 'US')
|
|
38
|
+
parse.district('37081')
|
|
39
|
+
parse.continent('NA')
|
|
40
|
+
parse.continent_countries('NA')
|
|
41
|
+
parse.currency('USD')
|
|
42
|
+
parse.currency_rate('USD', 'EUR')
|
|
43
|
+
parse.language('en')
|
|
44
|
+
parse.timezone('America/New_York')
|
|
45
|
+
parse.holiday('US', year: 2026)
|
|
46
|
+
parse.holiday_date('US', '2026-12-25')
|
|
47
|
+
parse.elevation(35.2271, -80.8431)
|
|
48
|
+
parse.point(36.0726, -79.792)
|
|
49
|
+
parse.weather(40.7128, -74.006)
|
|
50
|
+
parse.domain('example.com')
|
|
51
|
+
parse.mx('example.com')
|
|
52
|
+
parse.useragent(ua_string)
|
|
53
|
+
parse.emoji('rocket')
|
|
54
|
+
parse.emoji_search('fire')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Responses are plain hashes, exactly the JSON the API returns.
|
|
58
|
+
|
|
59
|
+
## Deep
|
|
60
|
+
|
|
61
|
+
Pass `deep: true` to include the nested `deep` object with richer fields.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
ip = parse.ip('52.94.76.10', deep: true)
|
|
65
|
+
ip['deep']['datacenter'] # true
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Errors
|
|
69
|
+
|
|
70
|
+
Every non-2xx response raises `ParseAPI::Error` with `status`, `code`, `docs`, and `request_id`. Branch on `code`.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
begin
|
|
74
|
+
parse.city('atlantis')
|
|
75
|
+
rescue ParseAPI::Error => e
|
|
76
|
+
if e.code == 'not_found'
|
|
77
|
+
# no such city
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Options
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
parse = ParseAPI.new(
|
|
86
|
+
'your-api-key',
|
|
87
|
+
timeout: 10, # per-attempt timeout in seconds
|
|
88
|
+
retries: 2 # automatic retries on network errors, 429, and 5xx
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Requires Ruby 3.0 or later. Standard library only, zero dependencies.
|
|
93
|
+
|
|
94
|
+
## Docs
|
|
95
|
+
|
|
96
|
+
Full field reference for every endpoint: [parseapi.com/docs](https://parseapi.com/docs)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module ParseAPI
|
|
6
|
+
# Every non-2xx response from the API. Branch on +code+, never on the message.
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
attr_reader :status, :code, :docs, :request_id
|
|
9
|
+
|
|
10
|
+
def initialize(status:, code:, message:, docs: nil, request_id: nil)
|
|
11
|
+
super(message)
|
|
12
|
+
@status = status
|
|
13
|
+
@code = code
|
|
14
|
+
@docs = docs
|
|
15
|
+
@request_id = request_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Client
|
|
20
|
+
DEFAULT_BASE_URL = 'https://api.parseapi.com'.freeze
|
|
21
|
+
DEFAULT_TIMEOUT = 10
|
|
22
|
+
DEFAULT_RETRIES = 2
|
|
23
|
+
RETRY_STATUS = [429, 500, 502, 503, 504].freeze
|
|
24
|
+
RETRY_AFTER_CAP = 5.0
|
|
25
|
+
NETWORK_ERRORS = [
|
|
26
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ETIMEDOUT,
|
|
27
|
+
Net::OpenTimeout, Net::ReadTimeout, IOError, EOFError, SocketError
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
def initialize(api_key = nil, base_url: nil, timeout: nil, retries: nil)
|
|
31
|
+
@api_key = api_key || ENV['PARSEAPI_KEY']
|
|
32
|
+
raise ArgumentError, 'parseapi: missing API key. Pass one or set PARSEAPI_KEY.' if @api_key.nil? || @api_key.empty?
|
|
33
|
+
|
|
34
|
+
@base_url = URI((base_url || ENV['PARSEAPI_BASE_URL'] || DEFAULT_BASE_URL).sub(%r{/+\z}, ''))
|
|
35
|
+
@timeout = timeout || DEFAULT_TIMEOUT
|
|
36
|
+
@retries = retries || DEFAULT_RETRIES
|
|
37
|
+
@http = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# --- Lookup methods (one per endpoint, named after the route) ---
|
|
41
|
+
|
|
42
|
+
def ip(ip, deep: false)
|
|
43
|
+
get("/ip/#{seg(ip)}", deep: deep)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ip_self(deep: false)
|
|
47
|
+
get('/ip', deep: deep)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def continent(code)
|
|
51
|
+
get("/continent/#{seg(code)}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def continent_countries(code)
|
|
55
|
+
get("/continent/#{seg(code)}/countries")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def country(code)
|
|
59
|
+
get("/country/#{seg(code)}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def country_states(code)
|
|
63
|
+
get("/country/#{seg(code)}/states")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def state(code, country:)
|
|
67
|
+
get("/state/#{seg(code)}", country: country)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def state_districts(code, country:)
|
|
71
|
+
get("/state/#{seg(code)}/districts", country: country)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def district(code, country: nil)
|
|
75
|
+
get("/district/#{seg(code)}", country: country)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def city(name, country: nil, state: nil)
|
|
79
|
+
get("/city/#{seg(name)}", country: country, state: state)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def city_id(id)
|
|
83
|
+
get("/city/id/#{seg(id)}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def city_search(q, country: nil, state: nil, limit: nil)
|
|
87
|
+
get('/city', q: q, country: country, state: state, limit: limit)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def city_nearest(lat, lon)
|
|
91
|
+
get('/city', lat: lat, lon: lon)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def postal(code, country:)
|
|
95
|
+
get("/postal/#{seg(code)}", country: country)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def postal_nearby(code, country:, radius: nil, unit: nil)
|
|
99
|
+
get("/postal/#{seg(code)}/nearby", country: country, radius: radius, unit: unit)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def postal_distance(from, to, country:)
|
|
103
|
+
get("/postal/#{seg(from)}/distance/#{seg(to)}", country: country)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def email(email, deep: false)
|
|
107
|
+
get("/email/#{seg(email)}", deep: deep)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def phone(number, country: nil, deep: false)
|
|
111
|
+
get("/phone/#{seg(number)}", country: country, deep: deep)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def domain(domain, deep: false)
|
|
115
|
+
get("/domain/#{seg(domain)}", deep: deep)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mx(domain)
|
|
119
|
+
get("/mx/#{seg(domain)}")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def useragent(ua, deep: false)
|
|
123
|
+
get('/useragent', { deep: deep }, { 'User-Agent' => ua })
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def currency(code)
|
|
127
|
+
get("/currency/#{seg(code)}")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def currency_rate(base, quote)
|
|
131
|
+
get("/currency/#{seg(base)}/#{seg(quote)}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def language(code)
|
|
135
|
+
get("/language/#{seg(code)}")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def timezone(id, at: nil)
|
|
139
|
+
get("/timezone/#{seg(id)}", at: at)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def holiday(country, year: nil)
|
|
143
|
+
get("/holiday/#{seg(country)}", year: year)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def holiday_date(country, date)
|
|
147
|
+
get("/holiday/#{seg(country)}/#{seg(date)}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def elevation(lat, lon)
|
|
151
|
+
get('/elevation', lat: lat, lon: lon)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def point(lat, lon, deep: false)
|
|
155
|
+
get('/point', lat: lat, lon: lon, deep: deep)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def weather(lat, lon, deep: false)
|
|
159
|
+
get('/weather', lat: lat, lon: lon, deep: deep)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def emoji(emoji)
|
|
163
|
+
get("/emoji/#{seg(emoji)}")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def emoji_search(q, limit: nil)
|
|
167
|
+
get('/emoji', q: q, limit: limit)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def seg(value)
|
|
173
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def get(path, params = {}, headers = {})
|
|
177
|
+
query = params.reject { |_name, value| value.nil? || value == false }
|
|
178
|
+
uri = @base_url.dup
|
|
179
|
+
uri.path = path
|
|
180
|
+
uri.query = URI.encode_www_form(query) unless query.empty?
|
|
181
|
+
|
|
182
|
+
attempt = 0
|
|
183
|
+
loop do
|
|
184
|
+
begin
|
|
185
|
+
status, response_headers, body = execute(uri, request_headers(headers))
|
|
186
|
+
rescue *NETWORK_ERRORS
|
|
187
|
+
raise if attempt >= @retries
|
|
188
|
+
|
|
189
|
+
sleep(retry_delay(attempt, nil))
|
|
190
|
+
attempt += 1
|
|
191
|
+
next
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return JSON.parse(body) if (200..299).cover?(status)
|
|
195
|
+
|
|
196
|
+
if RETRY_STATUS.include?(status) && attempt < @retries
|
|
197
|
+
sleep(retry_delay(attempt, response_headers['retry-after']))
|
|
198
|
+
attempt += 1
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
raise build_error(status, body)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def request_headers(extra)
|
|
207
|
+
{ 'X-API-Key' => @api_key, 'User-Agent' => "parseapi-ruby/#{VERSION}" }.merge(extra)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Returns [status, headers_hash, body_string]. Overridden in tests.
|
|
211
|
+
def execute(uri, headers)
|
|
212
|
+
http = connection
|
|
213
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
214
|
+
headers.each { |name, value| request[name] = value }
|
|
215
|
+
response = http.request(request)
|
|
216
|
+
header_hash = {}
|
|
217
|
+
response.each_header { |name, value| header_hash[name.downcase] = value }
|
|
218
|
+
[response.code.to_i, header_hash, response.body || '']
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def connection
|
|
222
|
+
if @http.nil?
|
|
223
|
+
@http = Net::HTTP.new(@base_url.host, @base_url.port)
|
|
224
|
+
@http.use_ssl = @base_url.scheme == 'https'
|
|
225
|
+
@http.open_timeout = @timeout
|
|
226
|
+
@http.read_timeout = @timeout
|
|
227
|
+
@http.keep_alive_timeout = 30
|
|
228
|
+
end
|
|
229
|
+
@http.start unless @http.started?
|
|
230
|
+
@http
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def retry_delay(attempt, retry_after)
|
|
234
|
+
if retry_after
|
|
235
|
+
seconds = Float(retry_after, exception: false)
|
|
236
|
+
return [seconds, RETRY_AFTER_CAP].min if seconds && seconds >= 0
|
|
237
|
+
end
|
|
238
|
+
rand * 0.25 * (2**attempt)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_error(status, body)
|
|
242
|
+
parsed = begin
|
|
243
|
+
JSON.parse(body)
|
|
244
|
+
rescue JSON::ParserError
|
|
245
|
+
{}
|
|
246
|
+
end
|
|
247
|
+
parsed = {} unless parsed.is_a?(Hash)
|
|
248
|
+
Error.new(
|
|
249
|
+
status: status,
|
|
250
|
+
code: parsed['code'].is_a?(String) ? parsed['code'] : 'unknown_error',
|
|
251
|
+
message: parsed['message'].is_a?(String) ? parsed['message'] : "Request failed with status #{status}",
|
|
252
|
+
docs: parsed['docs'].is_a?(String) ? parsed['docs'] : nil,
|
|
253
|
+
request_id: parsed['request_id'].is_a?(String) ? parsed['request_id'] : nil
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
data/lib/parseapi.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative 'parseapi/version'
|
|
2
|
+
require_relative 'parseapi/client'
|
|
3
|
+
|
|
4
|
+
# Official parseAPI client for Ruby.
|
|
5
|
+
#
|
|
6
|
+
# parse = ParseAPI.new('your-api-key')
|
|
7
|
+
# parse.country('US')
|
|
8
|
+
module ParseAPI
|
|
9
|
+
# Sugar so `ParseAPI.new` builds a Client.
|
|
10
|
+
def self.new(api_key = nil, **options)
|
|
11
|
+
Client.new(api_key, **options)
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: parseapi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- parseAPI
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description:
|
|
14
|
+
email:
|
|
15
|
+
- hello@parseapi.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/parseapi.rb
|
|
23
|
+
- lib/parseapi/client.rb
|
|
24
|
+
- lib/parseapi/version.rb
|
|
25
|
+
homepage: https://parseapi.com
|
|
26
|
+
licenses:
|
|
27
|
+
- MIT
|
|
28
|
+
metadata:
|
|
29
|
+
homepage_uri: https://parseapi.com
|
|
30
|
+
source_code_uri: https://github.com/parseapi/ruby
|
|
31
|
+
documentation_uri: https://parseapi.com/docs
|
|
32
|
+
post_install_message:
|
|
33
|
+
rdoc_options: []
|
|
34
|
+
require_paths:
|
|
35
|
+
- lib
|
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0'
|
|
46
|
+
requirements: []
|
|
47
|
+
rubygems_version: 3.0.3.1
|
|
48
|
+
signing_key:
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Official parseAPI client for Ruby. One key, minimal JSON, fast.
|
|
51
|
+
test_files: []
|