parseapi 0.1.0__py3-none-any.whl
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.
- parseapi/__init__.py +6 -0
- parseapi/_client.py +520 -0
- parseapi-0.1.0.dist-info/METADATA +126 -0
- parseapi-0.1.0.dist-info/RECORD +6 -0
- parseapi-0.1.0.dist-info/WHEEL +4 -0
- parseapi-0.1.0.dist-info/licenses/LICENSE +21 -0
parseapi/__init__.py
ADDED
parseapi/_client.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
VERSION = "0.1.0"
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
13
|
+
DEFAULT_TIMEOUT = 10.0
|
|
14
|
+
DEFAULT_RETRIES = 2
|
|
15
|
+
RETRY_STATUS = {429, 500, 502, 503, 504}
|
|
16
|
+
RETRY_AFTER_CAP = 5.0
|
|
17
|
+
|
|
18
|
+
Json = Dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParseAPIError(Exception):
|
|
22
|
+
"""Every non-2xx response from the API. Branch on `code`, never on the message."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, status: int, code: str, message: str, docs: Optional[str], request_id: Optional[str]):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.status = status
|
|
27
|
+
self.code = code
|
|
28
|
+
self.docs = docs
|
|
29
|
+
self.request_id = request_id
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _seg(value: Any) -> str:
|
|
33
|
+
return quote(str(value), safe="")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _retry_delay(attempt: int, retry_after: Optional[str]) -> float:
|
|
37
|
+
if retry_after:
|
|
38
|
+
try:
|
|
39
|
+
seconds = float(retry_after)
|
|
40
|
+
if seconds >= 0:
|
|
41
|
+
return min(seconds, RETRY_AFTER_CAP)
|
|
42
|
+
except ValueError:
|
|
43
|
+
pass
|
|
44
|
+
return random.random() * 0.25 * (2**attempt)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _error_from(response: httpx.Response) -> ParseAPIError:
|
|
48
|
+
try:
|
|
49
|
+
body = response.json()
|
|
50
|
+
except Exception:
|
|
51
|
+
body = {}
|
|
52
|
+
if not isinstance(body, dict):
|
|
53
|
+
body = {}
|
|
54
|
+
return ParseAPIError(
|
|
55
|
+
status=response.status_code,
|
|
56
|
+
code=body.get("code") if isinstance(body.get("code"), str) else "unknown_error",
|
|
57
|
+
message=body.get("message")
|
|
58
|
+
if isinstance(body.get("message"), str)
|
|
59
|
+
else f"Request failed with status {response.status_code}",
|
|
60
|
+
docs=body.get("docs") if isinstance(body.get("docs"), str) else None,
|
|
61
|
+
request_id=body.get("request_id") if isinstance(body.get("request_id"), str) else None,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _clean(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
66
|
+
return {name: value for name, value in params.items() if value is not None and value is not False}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _Config:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
api_key: Optional[str],
|
|
73
|
+
base_url: Optional[str],
|
|
74
|
+
timeout: Optional[float],
|
|
75
|
+
retries: Optional[int],
|
|
76
|
+
):
|
|
77
|
+
key = api_key or os.environ.get("PARSEAPI_KEY")
|
|
78
|
+
if not key:
|
|
79
|
+
raise ValueError("parseapi: missing API key. Pass one or set PARSEAPI_KEY.")
|
|
80
|
+
self.api_key = key
|
|
81
|
+
self.base_url = (base_url or os.environ.get("PARSEAPI_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
82
|
+
self.timeout = DEFAULT_TIMEOUT if timeout is None else timeout
|
|
83
|
+
self.retries = DEFAULT_RETRIES if retries is None else retries
|
|
84
|
+
|
|
85
|
+
def headers(self) -> Dict[str, str]:
|
|
86
|
+
return {"X-API-Key": self.api_key, "User-Agent": f"parseapi-python/{VERSION}"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ParseAPI:
|
|
90
|
+
"""Synchronous client. `parse = ParseAPI()` reads PARSEAPI_KEY from the env."""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
api_key: Optional[str] = None,
|
|
95
|
+
*,
|
|
96
|
+
base_url: Optional[str] = None,
|
|
97
|
+
timeout: Optional[float] = None,
|
|
98
|
+
retries: Optional[int] = None,
|
|
99
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
100
|
+
):
|
|
101
|
+
self._config = _Config(api_key, base_url, timeout, retries)
|
|
102
|
+
self._http = httpx.Client(
|
|
103
|
+
base_url=self._config.base_url,
|
|
104
|
+
timeout=self._config.timeout,
|
|
105
|
+
headers=self._config.headers(),
|
|
106
|
+
transport=transport,
|
|
107
|
+
)
|
|
108
|
+
self.ip = _IpSync(self)
|
|
109
|
+
self.continent = _ContinentSync(self)
|
|
110
|
+
self.country = _CountrySync(self)
|
|
111
|
+
self.state = _StateSync(self)
|
|
112
|
+
self.city = _CitySync(self)
|
|
113
|
+
self.postal = _PostalSync(self)
|
|
114
|
+
self.currency = _CurrencySync(self)
|
|
115
|
+
self.holiday = _HolidaySync(self)
|
|
116
|
+
self.emoji = _EmojiSync(self)
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
self._http.close()
|
|
120
|
+
|
|
121
|
+
def __enter__(self) -> "ParseAPI":
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, *exc: Any) -> None:
|
|
125
|
+
self.close()
|
|
126
|
+
|
|
127
|
+
def _get(self, path: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Json:
|
|
128
|
+
attempt = 0
|
|
129
|
+
while True:
|
|
130
|
+
try:
|
|
131
|
+
response = self._http.get(path, params=_clean(params or {}), headers=headers)
|
|
132
|
+
except httpx.HTTPError:
|
|
133
|
+
if attempt < self._config.retries:
|
|
134
|
+
time.sleep(_retry_delay(attempt, None))
|
|
135
|
+
attempt += 1
|
|
136
|
+
continue
|
|
137
|
+
raise
|
|
138
|
+
if response.is_success:
|
|
139
|
+
return response.json()
|
|
140
|
+
if response.status_code in RETRY_STATUS and attempt < self._config.retries:
|
|
141
|
+
time.sleep(_retry_delay(attempt, response.headers.get("Retry-After")))
|
|
142
|
+
attempt += 1
|
|
143
|
+
continue
|
|
144
|
+
raise _error_from(response)
|
|
145
|
+
|
|
146
|
+
# Plain methods (no subresources)
|
|
147
|
+
|
|
148
|
+
def district(self, code: str, *, country: Optional[str] = None) -> Json:
|
|
149
|
+
return self._get(f"/district/{_seg(code)}", {"country": country})
|
|
150
|
+
|
|
151
|
+
def email(self, email: str, *, deep: bool = False) -> Json:
|
|
152
|
+
return self._get(f"/email/{_seg(email)}", {"deep": deep})
|
|
153
|
+
|
|
154
|
+
def phone(self, number: str, *, country: Optional[str] = None, deep: bool = False) -> Json:
|
|
155
|
+
return self._get(f"/phone/{_seg(number)}", {"country": country, "deep": deep})
|
|
156
|
+
|
|
157
|
+
def domain(self, domain: str, *, deep: bool = False) -> Json:
|
|
158
|
+
return self._get(f"/domain/{_seg(domain)}", {"deep": deep})
|
|
159
|
+
|
|
160
|
+
def mx(self, domain: str) -> Json:
|
|
161
|
+
return self._get(f"/mx/{_seg(domain)}")
|
|
162
|
+
|
|
163
|
+
def useragent(self, ua: str, *, deep: bool = False) -> Json:
|
|
164
|
+
return self._get("/useragent", {"deep": deep}, headers={"User-Agent": ua})
|
|
165
|
+
|
|
166
|
+
def timezone(self, id: str, *, at: Optional[str] = None) -> Json:
|
|
167
|
+
return self._get(f"/timezone/{_seg(id)}", {"at": at})
|
|
168
|
+
|
|
169
|
+
def language(self, code: str) -> Json:
|
|
170
|
+
return self._get(f"/language/{_seg(code)}")
|
|
171
|
+
|
|
172
|
+
def elevation(self, lat: float, lon: float) -> Json:
|
|
173
|
+
return self._get("/elevation", {"lat": lat, "lon": lon})
|
|
174
|
+
|
|
175
|
+
def point(self, lat: float, lon: float, *, deep: bool = False) -> Json:
|
|
176
|
+
return self._get("/point", {"lat": lat, "lon": lon, "deep": deep})
|
|
177
|
+
|
|
178
|
+
def weather(self, lat: float, lon: float, *, deep: bool = False) -> Json:
|
|
179
|
+
return self._get("/weather", {"lat": lat, "lon": lon, "deep": deep})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class _IpSync:
|
|
183
|
+
def __init__(self, client: ParseAPI):
|
|
184
|
+
self._client = client
|
|
185
|
+
|
|
186
|
+
def __call__(self, ip: str, *, deep: bool = False) -> Json:
|
|
187
|
+
return self._client._get(f"/ip/{_seg(ip)}", {"deep": deep})
|
|
188
|
+
|
|
189
|
+
def self(self, *, deep: bool = False) -> Json:
|
|
190
|
+
return self._client._get("/ip", {"deep": deep})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _ContinentSync:
|
|
194
|
+
def __init__(self, client: ParseAPI):
|
|
195
|
+
self._client = client
|
|
196
|
+
|
|
197
|
+
def __call__(self, code: str) -> Json:
|
|
198
|
+
return self._client._get(f"/continent/{_seg(code)}")
|
|
199
|
+
|
|
200
|
+
def countries(self, code: str) -> Json:
|
|
201
|
+
return self._client._get(f"/continent/{_seg(code)}/countries")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _CountrySync:
|
|
205
|
+
def __init__(self, client: ParseAPI):
|
|
206
|
+
self._client = client
|
|
207
|
+
|
|
208
|
+
def __call__(self, code: str) -> Json:
|
|
209
|
+
return self._client._get(f"/country/{_seg(code)}")
|
|
210
|
+
|
|
211
|
+
def states(self, code: str) -> Json:
|
|
212
|
+
return self._client._get(f"/country/{_seg(code)}/states")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _StateSync:
|
|
216
|
+
def __init__(self, client: ParseAPI):
|
|
217
|
+
self._client = client
|
|
218
|
+
|
|
219
|
+
def __call__(self, code: str, *, country: str) -> Json:
|
|
220
|
+
return self._client._get(f"/state/{_seg(code)}", {"country": country})
|
|
221
|
+
|
|
222
|
+
def districts(self, code: str, *, country: str) -> Json:
|
|
223
|
+
return self._client._get(f"/state/{_seg(code)}/districts", {"country": country})
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _CitySync:
|
|
227
|
+
def __init__(self, client: ParseAPI):
|
|
228
|
+
self._client = client
|
|
229
|
+
|
|
230
|
+
def __call__(self, name: str, *, country: Optional[str] = None, state: Optional[str] = None) -> Json:
|
|
231
|
+
return self._client._get(f"/city/{_seg(name)}", {"country": country, "state": state})
|
|
232
|
+
|
|
233
|
+
def id(self, id: str) -> Json:
|
|
234
|
+
return self._client._get(f"/city/id/{_seg(id)}")
|
|
235
|
+
|
|
236
|
+
def search(
|
|
237
|
+
self,
|
|
238
|
+
q: str,
|
|
239
|
+
*,
|
|
240
|
+
country: Optional[str] = None,
|
|
241
|
+
state: Optional[str] = None,
|
|
242
|
+
limit: Optional[int] = None,
|
|
243
|
+
) -> Json:
|
|
244
|
+
return self._client._get("/city", {"q": q, "country": country, "state": state, "limit": limit})
|
|
245
|
+
|
|
246
|
+
def nearest(self, lat: float, lon: float) -> Json:
|
|
247
|
+
return self._client._get("/city", {"lat": lat, "lon": lon})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class _PostalSync:
|
|
251
|
+
def __init__(self, client: ParseAPI):
|
|
252
|
+
self._client = client
|
|
253
|
+
|
|
254
|
+
def __call__(self, code: str, *, country: str) -> Json:
|
|
255
|
+
return self._client._get(f"/postal/{_seg(code)}", {"country": country})
|
|
256
|
+
|
|
257
|
+
def nearby(
|
|
258
|
+
self,
|
|
259
|
+
code: str,
|
|
260
|
+
*,
|
|
261
|
+
country: str,
|
|
262
|
+
radius: Optional[float] = None,
|
|
263
|
+
unit: Optional[str] = None,
|
|
264
|
+
) -> Json:
|
|
265
|
+
return self._client._get(f"/postal/{_seg(code)}/nearby", {"country": country, "radius": radius, "unit": unit})
|
|
266
|
+
|
|
267
|
+
def distance(self, from_postal: str, to_postal: str, *, country: str) -> Json:
|
|
268
|
+
return self._client._get(f"/postal/{_seg(from_postal)}/distance/{_seg(to_postal)}", {"country": country})
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class _CurrencySync:
|
|
272
|
+
def __init__(self, client: ParseAPI):
|
|
273
|
+
self._client = client
|
|
274
|
+
|
|
275
|
+
def __call__(self, code: str) -> Json:
|
|
276
|
+
return self._client._get(f"/currency/{_seg(code)}")
|
|
277
|
+
|
|
278
|
+
def rate(self, base: str, quote_currency: str) -> Json:
|
|
279
|
+
return self._client._get(f"/currency/{_seg(base)}/{_seg(quote_currency)}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class _HolidaySync:
|
|
283
|
+
def __init__(self, client: ParseAPI):
|
|
284
|
+
self._client = client
|
|
285
|
+
|
|
286
|
+
def __call__(self, country: str, *, year: Optional[int] = None) -> Json:
|
|
287
|
+
return self._client._get(f"/holiday/{_seg(country)}", {"year": year})
|
|
288
|
+
|
|
289
|
+
def date(self, country: str, date: str) -> Json:
|
|
290
|
+
return self._client._get(f"/holiday/{_seg(country)}/{_seg(date)}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _EmojiSync:
|
|
294
|
+
def __init__(self, client: ParseAPI):
|
|
295
|
+
self._client = client
|
|
296
|
+
|
|
297
|
+
def __call__(self, emoji: str) -> Json:
|
|
298
|
+
return self._client._get(f"/emoji/{_seg(emoji)}")
|
|
299
|
+
|
|
300
|
+
def search(self, q: str, *, limit: Optional[int] = None) -> Json:
|
|
301
|
+
return self._client._get("/emoji", {"q": q, "limit": limit})
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class AsyncParseAPI:
|
|
305
|
+
"""Async client. `parse = AsyncParseAPI()` reads PARSEAPI_KEY from the env."""
|
|
306
|
+
|
|
307
|
+
def __init__(
|
|
308
|
+
self,
|
|
309
|
+
api_key: Optional[str] = None,
|
|
310
|
+
*,
|
|
311
|
+
base_url: Optional[str] = None,
|
|
312
|
+
timeout: Optional[float] = None,
|
|
313
|
+
retries: Optional[int] = None,
|
|
314
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
315
|
+
):
|
|
316
|
+
self._config = _Config(api_key, base_url, timeout, retries)
|
|
317
|
+
self._http = httpx.AsyncClient(
|
|
318
|
+
base_url=self._config.base_url,
|
|
319
|
+
timeout=self._config.timeout,
|
|
320
|
+
headers=self._config.headers(),
|
|
321
|
+
transport=transport,
|
|
322
|
+
)
|
|
323
|
+
self.ip = _IpAsync(self)
|
|
324
|
+
self.continent = _ContinentAsync(self)
|
|
325
|
+
self.country = _CountryAsync(self)
|
|
326
|
+
self.state = _StateAsync(self)
|
|
327
|
+
self.city = _CityAsync(self)
|
|
328
|
+
self.postal = _PostalAsync(self)
|
|
329
|
+
self.currency = _CurrencyAsync(self)
|
|
330
|
+
self.holiday = _HolidayAsync(self)
|
|
331
|
+
self.emoji = _EmojiAsync(self)
|
|
332
|
+
|
|
333
|
+
async def close(self) -> None:
|
|
334
|
+
await self._http.aclose()
|
|
335
|
+
|
|
336
|
+
async def __aenter__(self) -> "AsyncParseAPI":
|
|
337
|
+
return self
|
|
338
|
+
|
|
339
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
340
|
+
await self.close()
|
|
341
|
+
|
|
342
|
+
async def _get(
|
|
343
|
+
self, path: str, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None
|
|
344
|
+
) -> Json:
|
|
345
|
+
import asyncio
|
|
346
|
+
|
|
347
|
+
attempt = 0
|
|
348
|
+
while True:
|
|
349
|
+
try:
|
|
350
|
+
response = await self._http.get(path, params=_clean(params or {}), headers=headers)
|
|
351
|
+
except httpx.HTTPError:
|
|
352
|
+
if attempt < self._config.retries:
|
|
353
|
+
await asyncio.sleep(_retry_delay(attempt, None))
|
|
354
|
+
attempt += 1
|
|
355
|
+
continue
|
|
356
|
+
raise
|
|
357
|
+
if response.is_success:
|
|
358
|
+
return response.json()
|
|
359
|
+
if response.status_code in RETRY_STATUS and attempt < self._config.retries:
|
|
360
|
+
await asyncio.sleep(_retry_delay(attempt, response.headers.get("Retry-After")))
|
|
361
|
+
attempt += 1
|
|
362
|
+
continue
|
|
363
|
+
raise _error_from(response)
|
|
364
|
+
|
|
365
|
+
async def district(self, code: str, *, country: Optional[str] = None) -> Json:
|
|
366
|
+
return await self._get(f"/district/{_seg(code)}", {"country": country})
|
|
367
|
+
|
|
368
|
+
async def email(self, email: str, *, deep: bool = False) -> Json:
|
|
369
|
+
return await self._get(f"/email/{_seg(email)}", {"deep": deep})
|
|
370
|
+
|
|
371
|
+
async def phone(self, number: str, *, country: Optional[str] = None, deep: bool = False) -> Json:
|
|
372
|
+
return await self._get(f"/phone/{_seg(number)}", {"country": country, "deep": deep})
|
|
373
|
+
|
|
374
|
+
async def domain(self, domain: str, *, deep: bool = False) -> Json:
|
|
375
|
+
return await self._get(f"/domain/{_seg(domain)}", {"deep": deep})
|
|
376
|
+
|
|
377
|
+
async def mx(self, domain: str) -> Json:
|
|
378
|
+
return await self._get(f"/mx/{_seg(domain)}")
|
|
379
|
+
|
|
380
|
+
async def useragent(self, ua: str, *, deep: bool = False) -> Json:
|
|
381
|
+
return await self._get("/useragent", {"deep": deep}, headers={"User-Agent": ua})
|
|
382
|
+
|
|
383
|
+
async def timezone(self, id: str, *, at: Optional[str] = None) -> Json:
|
|
384
|
+
return await self._get(f"/timezone/{_seg(id)}", {"at": at})
|
|
385
|
+
|
|
386
|
+
async def language(self, code: str) -> Json:
|
|
387
|
+
return await self._get(f"/language/{_seg(code)}")
|
|
388
|
+
|
|
389
|
+
async def elevation(self, lat: float, lon: float) -> Json:
|
|
390
|
+
return await self._get("/elevation", {"lat": lat, "lon": lon})
|
|
391
|
+
|
|
392
|
+
async def point(self, lat: float, lon: float, *, deep: bool = False) -> Json:
|
|
393
|
+
return await self._get("/point", {"lat": lat, "lon": lon, "deep": deep})
|
|
394
|
+
|
|
395
|
+
async def weather(self, lat: float, lon: float, *, deep: bool = False) -> Json:
|
|
396
|
+
return await self._get("/weather", {"lat": lat, "lon": lon, "deep": deep})
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class _IpAsync:
|
|
400
|
+
def __init__(self, client: AsyncParseAPI):
|
|
401
|
+
self._client = client
|
|
402
|
+
|
|
403
|
+
async def __call__(self, ip: str, *, deep: bool = False) -> Json:
|
|
404
|
+
return await self._client._get(f"/ip/{_seg(ip)}", {"deep": deep})
|
|
405
|
+
|
|
406
|
+
async def self(self, *, deep: bool = False) -> Json:
|
|
407
|
+
return await self._client._get("/ip", {"deep": deep})
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class _ContinentAsync:
|
|
411
|
+
def __init__(self, client: AsyncParseAPI):
|
|
412
|
+
self._client = client
|
|
413
|
+
|
|
414
|
+
async def __call__(self, code: str) -> Json:
|
|
415
|
+
return await self._client._get(f"/continent/{_seg(code)}")
|
|
416
|
+
|
|
417
|
+
async def countries(self, code: str) -> Json:
|
|
418
|
+
return await self._client._get(f"/continent/{_seg(code)}/countries")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class _CountryAsync:
|
|
422
|
+
def __init__(self, client: AsyncParseAPI):
|
|
423
|
+
self._client = client
|
|
424
|
+
|
|
425
|
+
async def __call__(self, code: str) -> Json:
|
|
426
|
+
return await self._client._get(f"/country/{_seg(code)}")
|
|
427
|
+
|
|
428
|
+
async def states(self, code: str) -> Json:
|
|
429
|
+
return await self._client._get(f"/country/{_seg(code)}/states")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class _StateAsync:
|
|
433
|
+
def __init__(self, client: AsyncParseAPI):
|
|
434
|
+
self._client = client
|
|
435
|
+
|
|
436
|
+
async def __call__(self, code: str, *, country: str) -> Json:
|
|
437
|
+
return await self._client._get(f"/state/{_seg(code)}", {"country": country})
|
|
438
|
+
|
|
439
|
+
async def districts(self, code: str, *, country: str) -> Json:
|
|
440
|
+
return await self._client._get(f"/state/{_seg(code)}/districts", {"country": country})
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class _CityAsync:
|
|
444
|
+
def __init__(self, client: AsyncParseAPI):
|
|
445
|
+
self._client = client
|
|
446
|
+
|
|
447
|
+
async def __call__(self, name: str, *, country: Optional[str] = None, state: Optional[str] = None) -> Json:
|
|
448
|
+
return await self._client._get(f"/city/{_seg(name)}", {"country": country, "state": state})
|
|
449
|
+
|
|
450
|
+
async def id(self, id: str) -> Json:
|
|
451
|
+
return await self._client._get(f"/city/id/{_seg(id)}")
|
|
452
|
+
|
|
453
|
+
async def search(
|
|
454
|
+
self,
|
|
455
|
+
q: str,
|
|
456
|
+
*,
|
|
457
|
+
country: Optional[str] = None,
|
|
458
|
+
state: Optional[str] = None,
|
|
459
|
+
limit: Optional[int] = None,
|
|
460
|
+
) -> Json:
|
|
461
|
+
return await self._client._get("/city", {"q": q, "country": country, "state": state, "limit": limit})
|
|
462
|
+
|
|
463
|
+
async def nearest(self, lat: float, lon: float) -> Json:
|
|
464
|
+
return await self._client._get("/city", {"lat": lat, "lon": lon})
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class _PostalAsync:
|
|
468
|
+
def __init__(self, client: AsyncParseAPI):
|
|
469
|
+
self._client = client
|
|
470
|
+
|
|
471
|
+
async def __call__(self, code: str, *, country: str) -> Json:
|
|
472
|
+
return await self._client._get(f"/postal/{_seg(code)}", {"country": country})
|
|
473
|
+
|
|
474
|
+
async def nearby(
|
|
475
|
+
self,
|
|
476
|
+
code: str,
|
|
477
|
+
*,
|
|
478
|
+
country: str,
|
|
479
|
+
radius: Optional[float] = None,
|
|
480
|
+
unit: Optional[str] = None,
|
|
481
|
+
) -> Json:
|
|
482
|
+
return await self._client._get(
|
|
483
|
+
f"/postal/{_seg(code)}/nearby", {"country": country, "radius": radius, "unit": unit}
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
async def distance(self, from_postal: str, to_postal: str, *, country: str) -> Json:
|
|
487
|
+
return await self._client._get(f"/postal/{_seg(from_postal)}/distance/{_seg(to_postal)}", {"country": country})
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class _CurrencyAsync:
|
|
491
|
+
def __init__(self, client: AsyncParseAPI):
|
|
492
|
+
self._client = client
|
|
493
|
+
|
|
494
|
+
async def __call__(self, code: str) -> Json:
|
|
495
|
+
return await self._client._get(f"/currency/{_seg(code)}")
|
|
496
|
+
|
|
497
|
+
async def rate(self, base: str, quote_currency: str) -> Json:
|
|
498
|
+
return await self._client._get(f"/currency/{_seg(base)}/{_seg(quote_currency)}")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class _HolidayAsync:
|
|
502
|
+
def __init__(self, client: AsyncParseAPI):
|
|
503
|
+
self._client = client
|
|
504
|
+
|
|
505
|
+
async def __call__(self, country: str, *, year: Optional[int] = None) -> Json:
|
|
506
|
+
return await self._client._get(f"/holiday/{_seg(country)}", {"year": year})
|
|
507
|
+
|
|
508
|
+
async def date(self, country: str, date: str) -> Json:
|
|
509
|
+
return await self._client._get(f"/holiday/{_seg(country)}/{_seg(date)}")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class _EmojiAsync:
|
|
513
|
+
def __init__(self, client: AsyncParseAPI):
|
|
514
|
+
self._client = client
|
|
515
|
+
|
|
516
|
+
async def __call__(self, emoji: str) -> Json:
|
|
517
|
+
return await self._client._get(f"/emoji/{_seg(emoji)}")
|
|
518
|
+
|
|
519
|
+
async def search(self, q: str, *, limit: Optional[int] = None) -> Json:
|
|
520
|
+
return await self._client._get("/emoji", {"q": q, "limit": limit})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parseapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official parseAPI client for Python. One key, minimal JSON, fast.
|
|
5
|
+
Project-URL: Homepage, https://parseapi.com
|
|
6
|
+
Project-URL: Documentation, https://parseapi.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/parseapi/python
|
|
8
|
+
Author-email: parseAPI <hello@parseapi.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: currency,email validation,geolocation,ip,parseapi,phone validation,postal,timezone,weather
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: httpx>=0.24
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# parseapi
|
|
21
|
+
|
|
22
|
+
Official parseAPI client for Python.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install parseapi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from parseapi import ParseAPI
|
|
30
|
+
|
|
31
|
+
parse = ParseAPI("your-api-key")
|
|
32
|
+
country = parse.country("US")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEAPI_KEY` from the environment.
|
|
36
|
+
|
|
37
|
+
## Calls
|
|
38
|
+
|
|
39
|
+
One method per endpoint, named after the route.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
parse.ip("8.8.8.8")
|
|
43
|
+
parse.ip.self()
|
|
44
|
+
parse.email("hello@gmail.com")
|
|
45
|
+
parse.phone("+14155552671")
|
|
46
|
+
parse.postal("28202", country="US")
|
|
47
|
+
parse.postal.nearby("28202", country="US", radius=40)
|
|
48
|
+
parse.postal.distance("28202", "10001", country="US")
|
|
49
|
+
parse.city("charlotte", country="US")
|
|
50
|
+
parse.city.id("city_mb8mbqrkz8zb")
|
|
51
|
+
parse.city.search("char", country="US", limit=10)
|
|
52
|
+
parse.city.nearest(35.2271, -80.8431)
|
|
53
|
+
parse.country("US")
|
|
54
|
+
parse.country.states("US")
|
|
55
|
+
parse.state("NC", country="US")
|
|
56
|
+
parse.state.districts("NC", country="US")
|
|
57
|
+
parse.district("37081")
|
|
58
|
+
parse.continent("NA")
|
|
59
|
+
parse.continent.countries("NA")
|
|
60
|
+
parse.currency("USD")
|
|
61
|
+
parse.currency.rate("USD", "EUR")
|
|
62
|
+
parse.language("en")
|
|
63
|
+
parse.timezone("America/New_York")
|
|
64
|
+
parse.holiday("US", year=2026)
|
|
65
|
+
parse.holiday.date("US", "2026-12-25")
|
|
66
|
+
parse.elevation(35.2271, -80.8431)
|
|
67
|
+
parse.point(36.0726, -79.792)
|
|
68
|
+
parse.weather(40.7128, -74.006)
|
|
69
|
+
parse.domain("example.com")
|
|
70
|
+
parse.mx("example.com")
|
|
71
|
+
parse.useragent(ua_string)
|
|
72
|
+
parse.emoji("rocket")
|
|
73
|
+
parse.emoji.search("fire")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Responses are plain dicts, exactly the JSON the API returns.
|
|
77
|
+
|
|
78
|
+
## Async
|
|
79
|
+
|
|
80
|
+
Same surface, `await` everything.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from parseapi import AsyncParseAPI
|
|
84
|
+
|
|
85
|
+
parse = AsyncParseAPI("your-api-key")
|
|
86
|
+
country = await parse.country("US")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Deep
|
|
90
|
+
|
|
91
|
+
Pass `deep=True` to include the nested `deep` object with richer fields.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
ip = parse.ip("52.94.76.10", deep=True)
|
|
95
|
+
ip["deep"]["datacenter"] # True
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Errors
|
|
99
|
+
|
|
100
|
+
Every non-2xx response raises `ParseAPIError` with `status`, `code`, `docs`, and `request_id`. Branch on `code`.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from parseapi import ParseAPIError
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
parse.city("atlantis")
|
|
107
|
+
except ParseAPIError as err:
|
|
108
|
+
if err.code == "not_found":
|
|
109
|
+
... # no such city
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Options
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
parse = ParseAPI(
|
|
116
|
+
"your-api-key",
|
|
117
|
+
timeout=10.0, # per-attempt timeout in seconds
|
|
118
|
+
retries=2, # automatic retries on network errors, 429, and 5xx
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Requires Python 3.9 or later. One dependency (httpx).
|
|
123
|
+
|
|
124
|
+
## Docs
|
|
125
|
+
|
|
126
|
+
Full field reference for every endpoint: [parseapi.com/docs](https://parseapi.com/docs)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
parseapi/__init__.py,sha256=axNmGAESTPPh1WlmkULnS3oPONXhCclt1LtokECJwHA,199
|
|
2
|
+
parseapi/_client.py,sha256=zgQ5ZPq50BY3I5hjv31V4GZ72LEN0ME1NfY1Y0z5uaU,18240
|
|
3
|
+
parseapi-0.1.0.dist-info/METADATA,sha256=TBJNDHZYqmVeh3nzu8GeJSckFIV45lVQ6XpFFp0aeVQ,3183
|
|
4
|
+
parseapi-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
5
|
+
parseapi-0.1.0.dist-info/licenses/LICENSE,sha256=tdXjNJFfw9PIftBGjZIHyLHbRN6-Y7COFUVuyobBqWY,1065
|
|
6
|
+
parseapi-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|