parseapi 1.1.0__tar.gz → 1.3.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.
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 - Unreleased
4
+
5
+ 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.
6
+
3
7
  ## 1.1.0 - 2026-09-20
4
8
 
5
9
  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.1.0
3
+ Version: 1.3.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.1.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.
39
+ Version 1.3.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
 
@@ -293,3 +293,18 @@ basic = parse.time("America/New_York")
293
293
  detail = parse.time("America/New_York", deep=True)
294
294
  print(basic["at"], detail.get("deep", {}).get("next_dst"))
295
295
  ```
296
+
297
+ ## Stack API
298
+
299
+ ```python
300
+ result = parse.stack("example.com")
301
+ # AsyncParseAPI exposes the same calls with await.
302
+ ```
303
+
304
+ 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`.
305
+
306
+ `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.
307
+
308
+ 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.
309
+
310
+ 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.1.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.
20
+ Version 1.3.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
 
@@ -274,3 +274,18 @@ basic = parse.time("America/New_York")
274
274
  detail = parse.time("America/New_York", deep=True)
275
275
  print(basic["at"], detail.get("deep", {}).get("next_dst"))
276
276
  ```
277
+
278
+ ## Stack API
279
+
280
+ ```python
281
+ result = parse.stack("example.com")
282
+ # AsyncParseAPI exposes the same calls with await.
283
+ ```
284
+
285
+ 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`.
286
+
287
+ `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.
288
+
289
+ 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.
290
+
291
+ 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": [
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "parseapi"
7
- version = "1.1.0"
7
+ version = "1.3.0"
8
8
  description = "Official ParseAPI client for Python. One key, minimal JSON, fast."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from ._client import AsyncParseAPI, ParseAPI, ParseAPIError
4
4
 
5
- __version__ = "1.1.0"
5
+ __version__ = "1.3.0"
6
6
  __all__ = ["ParseAPI", "AsyncParseAPI", "ParseAPIError", "__version__"]
@@ -10,7 +10,7 @@ from urllib.parse import quote
10
10
 
11
11
  import httpx
12
12
 
13
- VERSION = "1.1.0"
13
+ VERSION = "1.3.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,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())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes