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 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
@@ -0,0 +1,3 @@
1
+ module ParseAPI
2
+ VERSION = '0.1.0'.freeze
3
+ 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: []