parseapi 1.1.0__tar.gz → 1.4.0__tar.gz
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-1.4.0/CHANGELOG.md +13 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/PKG-INFO +19 -2
- {parseapi-1.1.0 → parseapi-1.4.0}/README.md +18 -1
- {parseapi-1.1.0 → parseapi-1.4.0}/api/public.json +46 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/pyproject.toml +1 -1
- {parseapi-1.1.0 → parseapi-1.4.0}/src/parseapi/__init__.py +1 -1
- {parseapi-1.1.0 → parseapi-1.4.0}/src/parseapi/_client.py +25 -3
- parseapi-1.4.0/tests/test_postal_localities.py +25 -0
- parseapi-1.4.0/tests/test_stack.py +251 -0
- parseapi-1.1.0/CHANGELOG.md +0 -5
- {parseapi-1.1.0 → parseapi-1.4.0}/.github/workflows/publish.yml +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/.github/workflows/test.yml +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/.gitignore +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/LICENSE +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/scripts/public_api.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/smoke/smoke.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/src/parseapi/py.typed +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_client.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_email_enrichment.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_language.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_name_formatting.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_public_api.py +0 -0
- {parseapi-1.1.0 → parseapi-1.4.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.4.0 - 2026-09-24
|
|
4
|
+
|
|
5
|
+
Adds Australian Postal suburb choices while preserving null, empty, and ambiguous results. Existing calls, compact nearby/distance responses, and API contract `2.0.0` remain unchanged.
|
|
6
|
+
|
|
7
|
+
## 1.3.0 - Unreleased
|
|
8
|
+
|
|
9
|
+
Adds Stack site inventory lookup with eight technology category arrays, nullable versions, and explicit page coverage. Stack uses a 35-second default attempt timeout while explicit caller settings remain honored. Existing calls and API contract `2.0.0` remain unchanged.
|
|
10
|
+
|
|
11
|
+
## 1.1.0 - 2026-09-20
|
|
12
|
+
|
|
13
|
+
Email deep results now include nullable suggested first name, no-reply flag, plus-address tag, mail provider, verification status and reason. Existing lookup calls, retry defaults and API contract `2.0.0` remain unchanged. Missing details remain unknown, and suggested names do not verify identity.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: parseapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Official ParseAPI client for Python. One key, minimal JSON, fast.
|
|
5
5
|
Project-URL: Homepage, https://parseapi.com
|
|
6
6
|
Project-URL: Documentation, https://parseapi.com/docs
|
|
@@ -36,7 +36,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
36
36
|
|
|
37
37
|
## API versions
|
|
38
38
|
|
|
39
|
-
Version 1.
|
|
39
|
+
Version 1.4.0 explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
40
40
|
|
|
41
41
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
42
42
|
|
|
@@ -185,6 +185,8 @@ Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` sele
|
|
|
185
185
|
|
|
186
186
|
## Place statistics and optional detail
|
|
187
187
|
|
|
188
|
+
Australian postal lookups include core `localities` with suburb choices (`city`, `state`, `state_name`) on every plan. Null or an omitted field means unknown, while `[]` means the reviewed reference has no eligible choices. `city` stays null when the source is ambiguous, even if there is only one eligible choice. Let the user select their suburb and keep manual entry available. These are geographic choices, not mailing-address verification. [G-NAF source, adaptations and licence](https://parseapi.com/legal/attribution#postal-au).
|
|
189
|
+
|
|
188
190
|
Postal and District paid profiles include `deep.property_tax` where supported. It contains `annual_median`, `currency` and `period`: median annual property tax payable on owner-occupied homes in the statistical area. The amount is adjusted to the final year of the reporting period (`YYYY-YYYY`). This is an area statistic, not a rate or an individual property bill. Unsupported, missing and censored estimates are null.
|
|
189
191
|
|
|
190
192
|
```python
|
|
@@ -293,3 +295,18 @@ basic = parse.time("America/New_York")
|
|
|
293
295
|
detail = parse.time("America/New_York", deep=True)
|
|
294
296
|
print(basic["at"], detail.get("deep", {}).get("next_dst"))
|
|
295
297
|
```
|
|
298
|
+
|
|
299
|
+
## Stack API
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
result = parse.stack("example.com")
|
|
303
|
+
# AsyncParseAPI exposes the same calls with await.
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Pass a public hostname without a scheme, path, port or IP address. Stack returns the checked URL and `checked_at` time, followed by `scope`, `pages` and `partial`. `scope` is `homepage` or `site`; `pages` counts successfully checked HTML pages. `partial` is true for homepage-only or incomplete bounded site checks. False means the known in-scope candidates were completed, not that every page on a website was visited. A homepage result has `scope: "homepage"`, `pages: 1` and `partial: true`.
|
|
307
|
+
|
|
308
|
+
`cms`, `servers`, `frameworks`, `ecommerce`, `analytics`, `chat`, `payments` and `hosting` are arrays because a site can use several technologies in each category. Each entry contains `technology`, `name` and nullable `version`. Technology codes are open strings. A successful check uses empty arrays for categories with no matches. When no HTML page could be checked, `checked_at` and all categories are null, `pages` is 0 and `partial` is null. Unknown or conflicting versions are null. Missing detections do not prove absence.
|
|
309
|
+
|
|
310
|
+
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.
|
|
311
|
+
|
|
312
|
+
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.
|
|
@@ -17,7 +17,7 @@ Get a key at [parseapi.com](https://parseapi.com). The client also reads `PARSEA
|
|
|
17
17
|
|
|
18
18
|
## API versions
|
|
19
19
|
|
|
20
|
-
Version 1.
|
|
20
|
+
Version 1.4.0 explicitly selects the API contract supported by this SDK. It sends `Parse-Version: 2.0.0` on every lookup so responses match the API contract supported by the package. Your key and the team's saved default stay the same.
|
|
21
21
|
|
|
22
22
|
Upgrade the dependency in staging, review the [release notes](https://parseapi.com/docs/releases), and test the application before deploying the same code and dependency version to production. Commit your dependency lockfile so the tested package travels with your deployment. Future major SDK upgrades can select a newer API contract.
|
|
23
23
|
|
|
@@ -166,6 +166,8 @@ Unit discovery accepts optional `query`, `type`, and `unit` filters. `unit` sele
|
|
|
166
166
|
|
|
167
167
|
## Place statistics and optional detail
|
|
168
168
|
|
|
169
|
+
Australian postal lookups include core `localities` with suburb choices (`city`, `state`, `state_name`) on every plan. Null or an omitted field means unknown, while `[]` means the reviewed reference has no eligible choices. `city` stays null when the source is ambiguous, even if there is only one eligible choice. Let the user select their suburb and keep manual entry available. These are geographic choices, not mailing-address verification. [G-NAF source, adaptations and licence](https://parseapi.com/legal/attribution#postal-au).
|
|
170
|
+
|
|
169
171
|
Postal and District paid profiles include `deep.property_tax` where supported. It contains `annual_median`, `currency` and `period`: median annual property tax payable on owner-occupied homes in the statistical area. The amount is adjusted to the final year of the reporting period (`YYYY-YYYY`). This is an area statistic, not a rate or an individual property bill. Unsupported, missing and censored estimates are null.
|
|
170
172
|
|
|
171
173
|
```python
|
|
@@ -274,3 +276,18 @@ basic = parse.time("America/New_York")
|
|
|
274
276
|
detail = parse.time("America/New_York", deep=True)
|
|
275
277
|
print(basic["at"], detail.get("deep", {}).get("next_dst"))
|
|
276
278
|
```
|
|
279
|
+
|
|
280
|
+
## Stack API
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
result = parse.stack("example.com")
|
|
284
|
+
# AsyncParseAPI exposes the same calls with await.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Pass a public hostname without a scheme, path, port or IP address. Stack returns the checked URL and `checked_at` time, followed by `scope`, `pages` and `partial`. `scope` is `homepage` or `site`; `pages` counts successfully checked HTML pages. `partial` is true for homepage-only or incomplete bounded site checks. False means the known in-scope candidates were completed, not that every page on a website was visited. A homepage result has `scope: "homepage"`, `pages: 1` and `partial: true`.
|
|
288
|
+
|
|
289
|
+
`cms`, `servers`, `frameworks`, `ecommerce`, `analytics`, `chat`, `payments` and `hosting` are arrays because a site can use several technologies in each category. Each entry contains `technology`, `name` and nullable `version`. Technology codes are open strings. A successful check uses empty arrays for categories with no matches. When no HTML page could be checked, `checked_at` and all categories are null, `pages` is 0 and `partial` is null. Unknown or conflicting versions are null. Missing detections do not prove absence.
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
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.
|
|
@@ -1224,6 +1224,29 @@
|
|
|
1224
1224
|
],
|
|
1225
1225
|
"returns": "Json"
|
|
1226
1226
|
},
|
|
1227
|
+
"stack": {
|
|
1228
|
+
"async": true,
|
|
1229
|
+
"parameters": [
|
|
1230
|
+
{
|
|
1231
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
1232
|
+
"name": "domain",
|
|
1233
|
+
"type": "str"
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"default": false,
|
|
1237
|
+
"kind": "KEYWORD_ONLY",
|
|
1238
|
+
"name": "deep",
|
|
1239
|
+
"type": "bool"
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
"default": false,
|
|
1243
|
+
"kind": "KEYWORD_ONLY",
|
|
1244
|
+
"name": "pretty",
|
|
1245
|
+
"type": "bool"
|
|
1246
|
+
}
|
|
1247
|
+
],
|
|
1248
|
+
"returns": "Json"
|
|
1249
|
+
},
|
|
1227
1250
|
"state": {
|
|
1228
1251
|
"async": true,
|
|
1229
1252
|
"parameters": [
|
|
@@ -2778,6 +2801,29 @@
|
|
|
2778
2801
|
],
|
|
2779
2802
|
"returns": "Json"
|
|
2780
2803
|
},
|
|
2804
|
+
"stack": {
|
|
2805
|
+
"async": false,
|
|
2806
|
+
"parameters": [
|
|
2807
|
+
{
|
|
2808
|
+
"kind": "POSITIONAL_OR_KEYWORD",
|
|
2809
|
+
"name": "domain",
|
|
2810
|
+
"type": "str"
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
"default": false,
|
|
2814
|
+
"kind": "KEYWORD_ONLY",
|
|
2815
|
+
"name": "deep",
|
|
2816
|
+
"type": "bool"
|
|
2817
|
+
},
|
|
2818
|
+
{
|
|
2819
|
+
"default": false,
|
|
2820
|
+
"kind": "KEYWORD_ONLY",
|
|
2821
|
+
"name": "pretty",
|
|
2822
|
+
"type": "bool"
|
|
2823
|
+
}
|
|
2824
|
+
],
|
|
2825
|
+
"returns": "Json"
|
|
2826
|
+
},
|
|
2781
2827
|
"state": {
|
|
2782
2828
|
"async": false,
|
|
2783
2829
|
"parameters": [
|
|
@@ -10,7 +10,7 @@ from urllib.parse import quote
|
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
12
12
|
|
|
13
|
-
VERSION = "1.
|
|
13
|
+
VERSION = "1.4.0"
|
|
14
14
|
_API_VERSION = "2.0.0"
|
|
15
15
|
DEFAULT_BASE_URL = "https://api.parseapi.com"
|
|
16
16
|
DEFAULT_TIMEOUT = 10.0
|
|
@@ -96,12 +96,16 @@ class _Config:
|
|
|
96
96
|
self.api_key = key
|
|
97
97
|
self.base_url = (base_url or os.environ.get("PARSEAPI_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
98
98
|
self.timeout = DEFAULT_TIMEOUT if timeout is None else timeout
|
|
99
|
+
self.timeout_explicit = timeout is not None
|
|
99
100
|
self.retries = retries
|
|
100
101
|
if type(self.timeout) not in (int, float) or not math.isfinite(self.timeout) or self.timeout <= 0:
|
|
101
102
|
raise ValueError("parseapi: timeout must be a finite positive number.")
|
|
102
103
|
if self.retries is not None and (type(self.retries) is not int or self.retries < 0):
|
|
103
104
|
raise ValueError("parseapi: retries must be a non-negative integer.")
|
|
104
105
|
|
|
106
|
+
def timeout_for(self, path: str) -> float:
|
|
107
|
+
return 35.0 if not self.timeout_explicit and path.startswith("/stack/") else self.timeout
|
|
108
|
+
|
|
105
109
|
def headers(self) -> Dict[str, str]:
|
|
106
110
|
return {"X-API-Key": self.api_key, "User-Agent": f"parseapi-python/{VERSION}", "Parse-Version": _API_VERSION}
|
|
107
111
|
|
|
@@ -157,7 +161,7 @@ class ParseAPI:
|
|
|
157
161
|
attempt = 0
|
|
158
162
|
while True:
|
|
159
163
|
try:
|
|
160
|
-
response = self._http.get(path, params=_clean(params or {}), headers=headers)
|
|
164
|
+
response = self._http.get(path, params=_clean(params or {}), headers=headers, timeout=self._config.timeout_for(path))
|
|
161
165
|
except httpx.HTTPError:
|
|
162
166
|
if attempt < retries:
|
|
163
167
|
time.sleep(_retry_delay(attempt, None))
|
|
@@ -230,6 +234,15 @@ class ParseAPI:
|
|
|
230
234
|
diagnostics within the same metered lookup. No automatic retries by default."""
|
|
231
235
|
return self._get(f"/hlr/{_seg(number)}", {"country": country, "deep": deep})
|
|
232
236
|
|
|
237
|
+
def stack(self, domain: str, *, deep: bool = False, pretty: bool = False) -> Json:
|
|
238
|
+
"""Identify website technologies and versions by category.
|
|
239
|
+
|
|
240
|
+
All categories are lists. Scope, pages and partial describe bounded coverage.
|
|
241
|
+
Lists are null when no page could be checked and empty for no matches.
|
|
242
|
+
Pass a hostname without a URL scheme or path.
|
|
243
|
+
"""
|
|
244
|
+
return self._get(f"/stack/{_seg(domain)}", {"deep": deep, "pretty": pretty})
|
|
245
|
+
|
|
233
246
|
def domain(self, domain: str, *, deep: bool = False) -> Json:
|
|
234
247
|
"""Check whether a domain is registered. Deep adds registration dates, registrar, status and DNSSEC on paid plans."""
|
|
235
248
|
return self._get(f"/domain/{_seg(domain)}", {"deep": deep})
|
|
@@ -531,7 +544,7 @@ class AsyncParseAPI:
|
|
|
531
544
|
attempt = 0
|
|
532
545
|
while True:
|
|
533
546
|
try:
|
|
534
|
-
response = await self._http.get(path, params=_clean(params or {}), headers=headers)
|
|
547
|
+
response = await self._http.get(path, params=_clean(params or {}), headers=headers, timeout=self._config.timeout_for(path))
|
|
535
548
|
except httpx.HTTPError:
|
|
536
549
|
if attempt < retries:
|
|
537
550
|
await asyncio.sleep(_retry_delay(attempt, None))
|
|
@@ -602,6 +615,15 @@ class AsyncParseAPI:
|
|
|
602
615
|
diagnostics within the same metered lookup. No automatic retries by default."""
|
|
603
616
|
return await self._get(f"/hlr/{_seg(number)}", {"country": country, "deep": deep})
|
|
604
617
|
|
|
618
|
+
async def stack(self, domain: str, *, deep: bool = False, pretty: bool = False) -> Json:
|
|
619
|
+
"""Identify website technologies and versions by category.
|
|
620
|
+
|
|
621
|
+
All categories are lists. Scope, pages and partial describe bounded coverage.
|
|
622
|
+
Lists are null when no page could be checked and empty for no matches.
|
|
623
|
+
Pass a hostname without a URL scheme or path.
|
|
624
|
+
"""
|
|
625
|
+
return await self._get(f"/stack/{_seg(domain)}", {"deep": deep, "pretty": pretty})
|
|
626
|
+
|
|
605
627
|
async def domain(self, domain: str, *, deep: bool = False) -> Json:
|
|
606
628
|
"""Check whether a domain is registered. Deep adds registration dates, registrar, status and DNSSEC on paid plans."""
|
|
607
629
|
return await self._get(f"/domain/{_seg(domain)}", {"deep": deep})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from parseapi import AsyncParseAPI, ParseAPI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("async_client", [False, True])
|
|
10
|
+
@pytest.mark.parametrize("extra", [
|
|
11
|
+
{}, {"localities": None}, {"localities": []},
|
|
12
|
+
{"localities": [{"city": "SYDNEY", "state": "NSW", "state_name": "New South Wales", "future": True}]},
|
|
13
|
+
{"localities": [{"city": "SYDNEY", "state": "NSW", "state_name": "New South Wales"}, {"city": "HAYMARKET", "state": "NSW", "state_name": "New South Wales"}]},
|
|
14
|
+
])
|
|
15
|
+
def test_postal_choices_preserve_observation_without_inferring_city(async_client, extra):
|
|
16
|
+
body = {"postal": "2000", "country": "AU", "city": None, **extra}
|
|
17
|
+
transport = httpx.MockTransport(lambda request: httpx.Response(200, json=body))
|
|
18
|
+
if async_client:
|
|
19
|
+
async def run():
|
|
20
|
+
async with AsyncParseAPI("test", transport=transport) as client:
|
|
21
|
+
assert await client.postal("2000", country="AU") == body
|
|
22
|
+
asyncio.run(run())
|
|
23
|
+
else:
|
|
24
|
+
with ParseAPI("test", transport=transport) as client:
|
|
25
|
+
assert client.postal("2000", country="AU") == body
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import httpx
|
|
4
|
+
from parseapi import ParseAPI, AsyncParseAPI
|
|
5
|
+
|
|
6
|
+
RECORDS = json.loads(r'''
|
|
7
|
+
[
|
|
8
|
+
{
|
|
9
|
+
"domain": "xn--bcher-kva.example",
|
|
10
|
+
"url": "https://xn--bcher-kva.example/",
|
|
11
|
+
"checked_at": null,
|
|
12
|
+
"scope": "homepage",
|
|
13
|
+
"pages": 0,
|
|
14
|
+
"partial": null,
|
|
15
|
+
"cms": null,
|
|
16
|
+
"servers": null,
|
|
17
|
+
"frameworks": null,
|
|
18
|
+
"ecommerce": null,
|
|
19
|
+
"analytics": null,
|
|
20
|
+
"chat": null,
|
|
21
|
+
"payments": null,
|
|
22
|
+
"hosting": null,
|
|
23
|
+
"future": true
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"domain": "xn--bcher-kva.example",
|
|
27
|
+
"url": "https://xn--bcher-kva.example/",
|
|
28
|
+
"checked_at": "2026-09-21T12:00:00Z",
|
|
29
|
+
"scope": "homepage",
|
|
30
|
+
"pages": 1,
|
|
31
|
+
"partial": true,
|
|
32
|
+
"cms": [],
|
|
33
|
+
"servers": [],
|
|
34
|
+
"frameworks": [],
|
|
35
|
+
"ecommerce": [],
|
|
36
|
+
"analytics": [],
|
|
37
|
+
"chat": [],
|
|
38
|
+
"payments": [],
|
|
39
|
+
"hosting": [],
|
|
40
|
+
"deep": {},
|
|
41
|
+
"future": true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"domain": "xn--bcher-kva.example",
|
|
45
|
+
"url": "https://xn--bcher-kva.example/",
|
|
46
|
+
"checked_at": "2026-09-21T12:00:00Z",
|
|
47
|
+
"scope": "homepage",
|
|
48
|
+
"pages": 1,
|
|
49
|
+
"partial": true,
|
|
50
|
+
"cms": [
|
|
51
|
+
{
|
|
52
|
+
"technology": "wordpress",
|
|
53
|
+
"name": "WordPress",
|
|
54
|
+
"version": "6.8.2"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"servers": [
|
|
58
|
+
{
|
|
59
|
+
"technology": "nginx",
|
|
60
|
+
"name": "nginx",
|
|
61
|
+
"version": null
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"frameworks": [
|
|
65
|
+
{
|
|
66
|
+
"technology": "react",
|
|
67
|
+
"name": "React",
|
|
68
|
+
"version": null
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"ecommerce": [],
|
|
72
|
+
"analytics": [],
|
|
73
|
+
"chat": [],
|
|
74
|
+
"payments": [],
|
|
75
|
+
"hosting": [],
|
|
76
|
+
"future": true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"domain": "xn--bcher-kva.example",
|
|
80
|
+
"url": "https://xn--bcher-kva.example/",
|
|
81
|
+
"checked_at": "2026-09-21T12:00:00Z",
|
|
82
|
+
"scope": "site",
|
|
83
|
+
"pages": 6,
|
|
84
|
+
"partial": false,
|
|
85
|
+
"cms": [
|
|
86
|
+
{
|
|
87
|
+
"technology": "wordpress",
|
|
88
|
+
"name": "WordPress",
|
|
89
|
+
"version": "6.8.2"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"technology": "ghost",
|
|
93
|
+
"name": "Ghost",
|
|
94
|
+
"version": null
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
"servers": [
|
|
98
|
+
{
|
|
99
|
+
"technology": "nginx",
|
|
100
|
+
"name": "nginx",
|
|
101
|
+
"version": null
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"technology": "apache",
|
|
105
|
+
"name": "Apache",
|
|
106
|
+
"version": null
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"frameworks": [
|
|
110
|
+
{
|
|
111
|
+
"technology": "nextjs",
|
|
112
|
+
"name": "Next.js",
|
|
113
|
+
"version": "15.0.0",
|
|
114
|
+
"future": true
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"technology": "react",
|
|
118
|
+
"name": "React",
|
|
119
|
+
"version": null
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"ecommerce": [
|
|
123
|
+
{
|
|
124
|
+
"technology": "woocommerce",
|
|
125
|
+
"name": "WooCommerce",
|
|
126
|
+
"version": null
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
"analytics": [
|
|
130
|
+
{
|
|
131
|
+
"technology": "google-analytics",
|
|
132
|
+
"name": "Google Analytics",
|
|
133
|
+
"version": null
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
"chat": [
|
|
137
|
+
{
|
|
138
|
+
"technology": "intercom",
|
|
139
|
+
"name": "Intercom",
|
|
140
|
+
"version": null
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"payments": [
|
|
144
|
+
{
|
|
145
|
+
"technology": "stripe",
|
|
146
|
+
"name": "Stripe",
|
|
147
|
+
"version": null
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"hosting": [
|
|
151
|
+
{
|
|
152
|
+
"technology": "vercel",
|
|
153
|
+
"name": "Vercel",
|
|
154
|
+
"version": null
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"future": true
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"domain": "xn--bcher-kva.example",
|
|
161
|
+
"url": "https://xn--bcher-kva.example/",
|
|
162
|
+
"checked_at": "2026-09-21T12:00:00Z",
|
|
163
|
+
"scope": "site",
|
|
164
|
+
"pages": 3,
|
|
165
|
+
"partial": true,
|
|
166
|
+
"cms": [],
|
|
167
|
+
"servers": [],
|
|
168
|
+
"frameworks": [
|
|
169
|
+
{
|
|
170
|
+
"technology": "nextjs",
|
|
171
|
+
"name": "Next.js",
|
|
172
|
+
"version": null,
|
|
173
|
+
"future": true
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"ecommerce": [],
|
|
177
|
+
"analytics": [],
|
|
178
|
+
"chat": [],
|
|
179
|
+
"payments": [],
|
|
180
|
+
"hosting": [],
|
|
181
|
+
"deep": {},
|
|
182
|
+
"future": true
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"domain": "xn--bcher-kva.example",
|
|
186
|
+
"url": "https://xn--bcher-kva.example/",
|
|
187
|
+
"checked_at": null,
|
|
188
|
+
"scope": "site",
|
|
189
|
+
"pages": 0,
|
|
190
|
+
"partial": null,
|
|
191
|
+
"cms": null,
|
|
192
|
+
"servers": null,
|
|
193
|
+
"frameworks": null,
|
|
194
|
+
"ecommerce": null,
|
|
195
|
+
"analytics": null,
|
|
196
|
+
"chat": null,
|
|
197
|
+
"payments": null,
|
|
198
|
+
"hosting": null,
|
|
199
|
+
"deep": {},
|
|
200
|
+
"future": true
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
''')
|
|
204
|
+
|
|
205
|
+
def test_stack_sync_and_async_preserve_site_inventory_shapes_and_encoding():
|
|
206
|
+
async def run_async(record):
|
|
207
|
+
calls = []
|
|
208
|
+
async def handler(request):
|
|
209
|
+
calls.append(request)
|
|
210
|
+
return httpx.Response(200, json=record)
|
|
211
|
+
async with AsyncParseAPI("test_key", transport=httpx.MockTransport(handler)) as client:
|
|
212
|
+
assert await client.stack("bücher.example", deep=True, pretty=True) == record
|
|
213
|
+
assert await client.stack("example.com") == record
|
|
214
|
+
check(calls)
|
|
215
|
+
for record in RECORDS:
|
|
216
|
+
calls = []
|
|
217
|
+
def handler(request):
|
|
218
|
+
calls.append(request)
|
|
219
|
+
return httpx.Response(200, json=record)
|
|
220
|
+
with ParseAPI("test_key", transport=httpx.MockTransport(handler)) as client:
|
|
221
|
+
assert client.stack("bücher.example", deep=True, pretty=True) == record
|
|
222
|
+
assert client.stack("example.com") == record
|
|
223
|
+
check(calls)
|
|
224
|
+
asyncio.run(run_async(record))
|
|
225
|
+
|
|
226
|
+
def check(calls):
|
|
227
|
+
assert str(calls[0].url) == "https://api.parseapi.com/stack/b%C3%BCcher.example?deep=true&pretty=true"
|
|
228
|
+
assert str(calls[1].url) == "https://api.parseapi.com/stack/example.com"
|
|
229
|
+
assert all(request.headers["Parse-Version"] == "2.0.0" for request in calls)
|
|
230
|
+
|
|
231
|
+
def test_stack_deadline_defaults_and_explicit_sync_async_settings():
|
|
232
|
+
for explicit in [None, 10.0, 1.2, 45.0]:
|
|
233
|
+
expected = 35.0 if explicit is None else explicit
|
|
234
|
+
calls = []
|
|
235
|
+
def handler(request):
|
|
236
|
+
calls.append(request.extensions["timeout"])
|
|
237
|
+
return httpx.Response(200, json={**RECORDS[0], "deep": {}})
|
|
238
|
+
with ParseAPI("test_key", timeout=explicit, transport=httpx.MockTransport(handler)) as client:
|
|
239
|
+
assert client.stack("example.com")["deep"] == {}
|
|
240
|
+
client.domain("example.com")
|
|
241
|
+
client.stack("example.com", deep=True)
|
|
242
|
+
assert [call["read"] for call in calls] == [expected, 10.0 if explicit is None else explicit, expected]
|
|
243
|
+
assert all(len(set(call.values())) == 1 for call in calls)
|
|
244
|
+
async def run():
|
|
245
|
+
calls.clear()
|
|
246
|
+
async with AsyncParseAPI("test_key", timeout=explicit, transport=httpx.MockTransport(handler)) as client:
|
|
247
|
+
assert (await client.stack("example.com"))["deep"] == {}
|
|
248
|
+
await client.domain("example.com")
|
|
249
|
+
await client.stack("example.com", deep=True)
|
|
250
|
+
assert [call["read"] for call in calls] == [expected, 10.0 if explicit is None else explicit, expected]
|
|
251
|
+
asyncio.run(run())
|
parseapi-1.1.0/CHANGELOG.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.1.0 - 2026-09-20
|
|
4
|
-
|
|
5
|
-
Email deep results now include nullable suggested first name, no-reply flag, plus-address tag, mail provider, verification status and reason. Existing lookup calls, retry defaults and API contract `2.0.0` remain unchanged. Missing details remain unknown, and suggested names do not verify identity.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|