checkanysite 0.1.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.
Files changed (35) hide show
  1. checkanysite-0.1.0/.gitignore +11 -0
  2. checkanysite-0.1.0/CHANGELOG.md +16 -0
  3. checkanysite-0.1.0/LICENSE +21 -0
  4. checkanysite-0.1.0/PKG-INFO +245 -0
  5. checkanysite-0.1.0/README.md +216 -0
  6. checkanysite-0.1.0/examples/quickstart.py +48 -0
  7. checkanysite-0.1.0/openapi.json +2305 -0
  8. checkanysite-0.1.0/pyproject.toml +97 -0
  9. checkanysite-0.1.0/src/checkanysite/__init__.py +48 -0
  10. checkanysite-0.1.0/src/checkanysite/_base_client.py +268 -0
  11. checkanysite-0.1.0/src/checkanysite/_client.py +130 -0
  12. checkanysite-0.1.0/src/checkanysite/_constants.py +49 -0
  13. checkanysite-0.1.0/src/checkanysite/_retries.py +60 -0
  14. checkanysite-0.1.0/src/checkanysite/_version.py +3 -0
  15. checkanysite-0.1.0/src/checkanysite/errors.py +228 -0
  16. checkanysite-0.1.0/src/checkanysite/models/__init__.py +33 -0
  17. checkanysite-0.1.0/src/checkanysite/models/_base.py +37 -0
  18. checkanysite-0.1.0/src/checkanysite/models/_generated.py +1329 -0
  19. checkanysite-0.1.0/src/checkanysite/models/batch.py +82 -0
  20. checkanysite-0.1.0/src/checkanysite/models/meta.py +119 -0
  21. checkanysite-0.1.0/src/checkanysite/py.typed +0 -0
  22. checkanysite-0.1.0/src/checkanysite/resources/__init__.py +19 -0
  23. checkanysite-0.1.0/src/checkanysite/resources/_params.py +38 -0
  24. checkanysite-0.1.0/src/checkanysite/resources/scans.py +338 -0
  25. checkanysite-0.1.0/src/checkanysite/resources/sites.py +120 -0
  26. checkanysite-0.1.0/src/checkanysite/resources/usage.py +28 -0
  27. checkanysite-0.1.0/tests/conftest.py +50 -0
  28. checkanysite-0.1.0/tests/support.py +118 -0
  29. checkanysite-0.1.0/tests/test_async.py +180 -0
  30. checkanysite-0.1.0/tests/test_client.py +132 -0
  31. checkanysite-0.1.0/tests/test_errors.py +169 -0
  32. checkanysite-0.1.0/tests/test_models.py +172 -0
  33. checkanysite-0.1.0/tests/test_retries.py +272 -0
  34. checkanysite-0.1.0/tests/test_scans.py +271 -0
  35. checkanysite-0.1.0/tests/test_sites.py +166 -0
@@ -0,0 +1,11 @@
1
+ # The Python SDK's local environment, caches and build output (the repository's own .gitignore
2
+ # covers the rest). Kept here so the source package, which carries this file, names only these.
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ __pycache__/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `checkanysite` Python library. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [semantic versioning](https://semver.org).
4
+
5
+ ## 0.1.0 (unreleased)
6
+
7
+ The first version, for the CheckAnySite API v1.
8
+
9
+ - `CheckAnySite` and `AsyncCheckAnySite`, configured by argument or by `CHECKANYSITE_API_KEY` and `CHECKANYSITE_BASE_URL`.
10
+ - The Lookup API: `sites.retrieve` and `sites.batch`.
11
+ - The Scan API: `scans.create`, `scans.retrieve` and `scans.batch`; `scans.wait`, `scans.create_and_wait` and `scans.batch_and_wait`, which poll as often as `Retry-After` asks and raise `ScanTimeoutError` at their timeout.
12
+ - `usage.retrieve`.
13
+ - Typed answers generated from the API's OpenAPI document, keeping fields and values added later; `.meta` with the request id, credits and rate limit from the headers.
14
+ - Errors by status (`BadRequestError` … `InternalServerError`), connection errors and timeouts, each with the API's code, message, parameter and request id.
15
+ - Retries with exponential backoff and jitter, honouring `Retry-After`; an automatic `Idempotency-Key` on every POST, kept across its retries.
16
+ - A client never closed closes its connections when it is garbage collected, so it leaves no `ResourceWarning`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CheckAnySite
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.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.5
2
+ Name: checkanysite
3
+ Version: 0.1.0
4
+ Summary: The official Python library for the CheckAnySite API: what is this website, with evidence.
5
+ Project-URL: Homepage, https://checkanysite.com
6
+ Project-URL: Documentation, https://docs.checkanysite.com
7
+ Author-email: CheckAnySite <hello@checkanysite.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,api,checkanysite,dns,sdk,tech stack,website
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: httpx<1,>=0.23.0
27
+ Requires-Dist: pydantic<3,>=2.5.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # CheckAnySite Python library
31
+
32
+ The official Python library for the [CheckAnySite API](https://docs.checkanysite.com): what is this website? Ask about any public site and get, with evidence, the traces of AI website builders, its AI readiness, popularity, technology, DNS records, trust signals, a summary and its history.
33
+
34
+ It gives typed answers (pydantic models), a sync and an async client, retries with backoff, idempotent POSTs and the credits each call cost. Python 3.9 or newer; its only dependencies are httpx and pydantic.
35
+
36
+ ## Install
37
+
38
+ ```sh
39
+ pip install checkanysite
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ Get a key free at [checkanysite.com/account/keys](https://checkanysite.com/account/keys) and put it in the environment:
45
+
46
+ ```sh
47
+ export CHECKANYSITE_API_KEY="cas_live_…"
48
+ ```
49
+
50
+ ```python
51
+ from checkanysite import CheckAnySite
52
+
53
+ client = CheckAnySite() # reads CHECKANYSITE_API_KEY
54
+
55
+ site = client.sites.retrieve("stripe.com", checks=["ai", "tech"], max_age_days=30)
56
+ print(site.ai.level) # "high", "medium" or "none"
57
+ for technology in site.tech.technologies:
58
+ print(technology.name, technology.category)
59
+
60
+ print(site.meta.credits_cost, site.meta.credits_remaining, site.meta.request_id)
61
+ ```
62
+
63
+ There are two APIs behind one key:
64
+
65
+ - **Lookup** (`client.sites`) answers from stored results at once and never scans. A found result costs each check asked at its stored price; a miss (`no_result`) is free.
66
+ - **Scan** (`client.scans`) scans a site live. A scan answers asynchronously: start it, then poll it, or let `create_and_wait` poll for you.
67
+
68
+ ```python
69
+ scan = client.scans.create("example.com", checks=["readiness"]) # a Scan, maybe still queued
70
+ scan = client.scans.retrieve(scan.id) # read it again, free
71
+ scan = client.scans.wait(scan) # poll it until it is complete, free
72
+
73
+ scan = client.scans.create_and_wait("example.com", checks=["readiness"], timeout=120)
74
+ if scan.status == "completed":
75
+ print(scan.result.readiness.score)
76
+ else:
77
+ print(scan.failure.code, scan.failure.message)
78
+
79
+ usage = client.usage.retrieve()
80
+ print(usage.remaining, "of", usage.allowance, "credits left this month")
81
+ ```
82
+
83
+ The checks are `ai`, `readiness`, `popularity`, `tech`, `dns`, `trust`, `summary` and `history`; leave `checks` out to ask every check. A scan runs only what its checks need, so fewer checks are faster and cost less.
84
+
85
+ `wait` and `create_and_wait` wait as long as each answer's `Retry-After` asks (3 seconds today) and return the scan once it is complete: completed with every slow source in, or failed. When their `timeout` (120 seconds by default) comes first, they raise `ScanTimeoutError`, which names the scan's id and holds the scan as last read; the scan goes on, so read it later with `client.scans.retrieve(error.scan_id)` or wait again with `client.scans.wait(error.scan_id)`.
86
+
87
+ ## Batches
88
+
89
+ ```python
90
+ batch = client.sites.batch(["stripe.com", "lovable.dev"], checks=["ai"])
91
+ for item in batch.data: # one item per unique domain, in the order first sent
92
+ if item.result:
93
+ print(item.domain, item.result.ai.level)
94
+ else:
95
+ print(item.input, item.error.code) # no_result, invalid_domain, …
96
+
97
+ from checkanysite.models import Scan
98
+
99
+ scans = client.scans.batch_and_wait(["stripe.com", "lovable.dev"], checks=["ai"])
100
+ for item in scans.data:
101
+ if not isinstance(item, Scan): # a ScanBatchFailure: this one couldn't be scanned
102
+ print(item.input, item.error.code)
103
+ elif item.status == "completed":
104
+ print(item.domain, item.result.ai.level)
105
+ else: # failed
106
+ print(item.domain, item.failure.code)
107
+ ```
108
+
109
+ A lookup batch takes 1 to 50 domains or URLs, a scan batch 1 to 10. `scans.batch` starts the scans and answers at once; `batch_and_wait` also waits until every one is complete, within one `timeout` for them all, and keeps the batch's `meta` (what it cost).
110
+
111
+ ## What each answer's headers said
112
+
113
+ Every object a method returns has `.meta`, read from the answer's headers:
114
+
115
+ | Attribute | Header |
116
+ | -------------------------------------------------------------- | ------------------------------------------ |
117
+ | `request_id` | `X-Request-Id`: quote it when you write us |
118
+ | `credits_cost`, `credits_remaining`, `credits_reset` | `X-Credits-*` (on calls that cost credits) |
119
+ | `rate_limit_limit`, `rate_limit_remaining`, `rate_limit_reset` | `X-RateLimit-*` |
120
+ | `retry_after_seconds` | `Retry-After` |
121
+ | `idempotent_replayed` | `Idempotent-Replayed` |
122
+ | `status_code`, `etag`, `headers` | the status, `ETag`, every header |
123
+
124
+ ## Errors
125
+
126
+ Every error is an `APIError`. When the API answers with an error, the class comes from the status, and `.code` says exactly what happened:
127
+
128
+ ```python
129
+ import checkanysite
130
+
131
+ domain = "example.com"
132
+ try:
133
+ site = client.sites.retrieve(domain, checks=["ai"])
134
+ except checkanysite.NotFoundError as error:
135
+ if error.code == "no_result": # nothing stored for this site yet: scan it
136
+ scan = client.scans.create_and_wait(domain, checks=["ai"])
137
+ except checkanysite.RateLimitError as error:
138
+ print(error.code) # rate_limited, or a limit used up, such as quota_exceeded
139
+ except checkanysite.APIStatusError as error:
140
+ print(error.status_code, error.code, error.message, error.param, error.request_id, error.docs)
141
+ except checkanysite.APIConnectionError:
142
+ print("No answer from the API")
143
+ ```
144
+
145
+ | Status | Class | Codes |
146
+ | ---------- | -------------------------- | --------------------------------------------------------------------------- |
147
+ | 400 | `BadRequestError` | `invalid_json`, `invalid_parameter`, `invalid_domain` |
148
+ | 401 | `AuthenticationError` | `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key` |
149
+ | 403 | `PermissionDeniedError` | `read_only_key`, `browser_request` |
150
+ | 404 | `NotFoundError` | `not_found`, `no_result`, `scan_not_found` |
151
+ | 409 | `ConflictError` | `idempotency_key_in_use` |
152
+ | 422 | `UnprocessableEntityError` | `site_refused`, `idempotency_key_reused` |
153
+ | 429 | `RateLimitError` | `rate_limited`, `quota_exceeded`, `key_limit_reached`, `scan_limit_reached` |
154
+ | 500 and up | `InternalServerError` | `internal_error`, `capacity` |
155
+ | other | `APIStatusError` | `method_not_allowed` (405), `request_too_large` (413), … |
156
+ | no answer | `APIConnectionError` | `APITimeoutError` when the request timed out |
157
+
158
+ New codes may appear within v1; they land in the class for their status. Every code is explained on the [errors page](https://docs.checkanysite.com/errors).
159
+
160
+ ## Retries
161
+
162
+ A failure that may pass is retried twice by default, with exponential backoff (0.5, 1, 2 … up to 8 seconds) and jitter, waiting as long as `Retry-After` asks when the API sends it:
163
+
164
+ - no answer (a connection error or a timeout);
165
+ - 409 `idempotency_key_in_use`;
166
+ - 429 `rate_limited`, but never `quota_exceeded` or `key_limit_reached` (the month's credits are spent) or `scan_limit_reached` (the day's new scans are started);
167
+ - 500, 502, 503 and 504.
168
+
169
+ A `Retry-After` longer than 60 seconds isn't waited out: the error is raised so you can decide.
170
+
171
+ ```python
172
+ client = CheckAnySite(max_retries=5) # or 0 to turn retrying off
173
+ ```
174
+
175
+ ## Idempotency
176
+
177
+ Every POST carries an `Idempotency-Key`, a new UUID per call, kept across that call's retries: if an answer is lost and the call is retried, the API replays the first answer instead of scanning (and charging) twice. Pass your own key to make a call safe to repeat across runs:
178
+
179
+ ```python
180
+ scan = client.scans.create("example.com", idempotency_key="order-1234")
181
+ scan.meta.idempotent_replayed # True when this is the stored first answer
182
+ ```
183
+
184
+ A key is kept 24 hours. The same key with a different body raises `UnprocessableEntityError` (`idempotency_key_reused`).
185
+
186
+ ## Timeouts and configuration
187
+
188
+ ```python
189
+ import httpx
190
+ from checkanysite import CheckAnySite
191
+
192
+ client = CheckAnySite(
193
+ api_key="cas_live_…", # default: CHECKANYSITE_API_KEY
194
+ base_url="https://api.checkanysite.com/v1", # default: CHECKANYSITE_BASE_URL, else this
195
+ timeout=30.0, # seconds per request, or an httpx.Timeout
196
+ max_retries=2,
197
+ http_client=httpx.Client(proxy="http://proxy.internal:8080"), # optional; you close it
198
+ )
199
+ ```
200
+
201
+ Use the client as a context manager (`with CheckAnySite() as client:`) or call `client.close()` when done.
202
+
203
+ ## Async
204
+
205
+ `AsyncCheckAnySite` has the same methods, awaited:
206
+
207
+ ```python
208
+ import asyncio
209
+ from checkanysite import AsyncCheckAnySite
210
+
211
+
212
+ async def main() -> None:
213
+ async with AsyncCheckAnySite() as client:
214
+ sites = await asyncio.gather(
215
+ client.sites.retrieve("stripe.com", checks=["ai"]),
216
+ client.sites.retrieve("lovable.dev", checks=["ai"]),
217
+ )
218
+ scan = await client.scans.create_and_wait("example.com", checks=["dns"])
219
+
220
+
221
+ asyncio.run(main())
222
+ ```
223
+
224
+ ## Models
225
+
226
+ The answers are pydantic models generated from the API's OpenAPI document, in `checkanysite.models`. Attributes are snake_case (`site.scanned_at`); `model_dump(by_alias=True)` gives the API's camelCase JSON. Within v1 the API may add fields and values, never remove or rename them, so the models keep what they don't know: new fields are in `model.model_extra`, and new values of a field such as `level` arrive as strings.
227
+
228
+ ## Versioning
229
+
230
+ This library follows [semantic versioning](https://semver.org) and the API v1. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
231
+
232
+ ## Development
233
+
234
+ From `sdks/python/`, with [uv](https://docs.astral.sh/uv/):
235
+
236
+ ```sh
237
+ uv run ruff format --check . && uv run ruff check . && uv run mypy && uv run pytest
238
+ uv build # an sdist and a wheel in dist/
239
+ ```
240
+
241
+ The models come from the server's schemas. After an API change, from the repository root:
242
+
243
+ ```sh
244
+ pnpm sdk:python:models # exports openapi.json, then regenerates src/checkanysite/models/_generated.py
245
+ ```
@@ -0,0 +1,216 @@
1
+ # CheckAnySite Python library
2
+
3
+ The official Python library for the [CheckAnySite API](https://docs.checkanysite.com): what is this website? Ask about any public site and get, with evidence, the traces of AI website builders, its AI readiness, popularity, technology, DNS records, trust signals, a summary and its history.
4
+
5
+ It gives typed answers (pydantic models), a sync and an async client, retries with backoff, idempotent POSTs and the credits each call cost. Python 3.9 or newer; its only dependencies are httpx and pydantic.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install checkanysite
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ Get a key free at [checkanysite.com/account/keys](https://checkanysite.com/account/keys) and put it in the environment:
16
+
17
+ ```sh
18
+ export CHECKANYSITE_API_KEY="cas_live_…"
19
+ ```
20
+
21
+ ```python
22
+ from checkanysite import CheckAnySite
23
+
24
+ client = CheckAnySite() # reads CHECKANYSITE_API_KEY
25
+
26
+ site = client.sites.retrieve("stripe.com", checks=["ai", "tech"], max_age_days=30)
27
+ print(site.ai.level) # "high", "medium" or "none"
28
+ for technology in site.tech.technologies:
29
+ print(technology.name, technology.category)
30
+
31
+ print(site.meta.credits_cost, site.meta.credits_remaining, site.meta.request_id)
32
+ ```
33
+
34
+ There are two APIs behind one key:
35
+
36
+ - **Lookup** (`client.sites`) answers from stored results at once and never scans. A found result costs each check asked at its stored price; a miss (`no_result`) is free.
37
+ - **Scan** (`client.scans`) scans a site live. A scan answers asynchronously: start it, then poll it, or let `create_and_wait` poll for you.
38
+
39
+ ```python
40
+ scan = client.scans.create("example.com", checks=["readiness"]) # a Scan, maybe still queued
41
+ scan = client.scans.retrieve(scan.id) # read it again, free
42
+ scan = client.scans.wait(scan) # poll it until it is complete, free
43
+
44
+ scan = client.scans.create_and_wait("example.com", checks=["readiness"], timeout=120)
45
+ if scan.status == "completed":
46
+ print(scan.result.readiness.score)
47
+ else:
48
+ print(scan.failure.code, scan.failure.message)
49
+
50
+ usage = client.usage.retrieve()
51
+ print(usage.remaining, "of", usage.allowance, "credits left this month")
52
+ ```
53
+
54
+ The checks are `ai`, `readiness`, `popularity`, `tech`, `dns`, `trust`, `summary` and `history`; leave `checks` out to ask every check. A scan runs only what its checks need, so fewer checks are faster and cost less.
55
+
56
+ `wait` and `create_and_wait` wait as long as each answer's `Retry-After` asks (3 seconds today) and return the scan once it is complete: completed with every slow source in, or failed. When their `timeout` (120 seconds by default) comes first, they raise `ScanTimeoutError`, which names the scan's id and holds the scan as last read; the scan goes on, so read it later with `client.scans.retrieve(error.scan_id)` or wait again with `client.scans.wait(error.scan_id)`.
57
+
58
+ ## Batches
59
+
60
+ ```python
61
+ batch = client.sites.batch(["stripe.com", "lovable.dev"], checks=["ai"])
62
+ for item in batch.data: # one item per unique domain, in the order first sent
63
+ if item.result:
64
+ print(item.domain, item.result.ai.level)
65
+ else:
66
+ print(item.input, item.error.code) # no_result, invalid_domain, …
67
+
68
+ from checkanysite.models import Scan
69
+
70
+ scans = client.scans.batch_and_wait(["stripe.com", "lovable.dev"], checks=["ai"])
71
+ for item in scans.data:
72
+ if not isinstance(item, Scan): # a ScanBatchFailure: this one couldn't be scanned
73
+ print(item.input, item.error.code)
74
+ elif item.status == "completed":
75
+ print(item.domain, item.result.ai.level)
76
+ else: # failed
77
+ print(item.domain, item.failure.code)
78
+ ```
79
+
80
+ A lookup batch takes 1 to 50 domains or URLs, a scan batch 1 to 10. `scans.batch` starts the scans and answers at once; `batch_and_wait` also waits until every one is complete, within one `timeout` for them all, and keeps the batch's `meta` (what it cost).
81
+
82
+ ## What each answer's headers said
83
+
84
+ Every object a method returns has `.meta`, read from the answer's headers:
85
+
86
+ | Attribute | Header |
87
+ | -------------------------------------------------------------- | ------------------------------------------ |
88
+ | `request_id` | `X-Request-Id`: quote it when you write us |
89
+ | `credits_cost`, `credits_remaining`, `credits_reset` | `X-Credits-*` (on calls that cost credits) |
90
+ | `rate_limit_limit`, `rate_limit_remaining`, `rate_limit_reset` | `X-RateLimit-*` |
91
+ | `retry_after_seconds` | `Retry-After` |
92
+ | `idempotent_replayed` | `Idempotent-Replayed` |
93
+ | `status_code`, `etag`, `headers` | the status, `ETag`, every header |
94
+
95
+ ## Errors
96
+
97
+ Every error is an `APIError`. When the API answers with an error, the class comes from the status, and `.code` says exactly what happened:
98
+
99
+ ```python
100
+ import checkanysite
101
+
102
+ domain = "example.com"
103
+ try:
104
+ site = client.sites.retrieve(domain, checks=["ai"])
105
+ except checkanysite.NotFoundError as error:
106
+ if error.code == "no_result": # nothing stored for this site yet: scan it
107
+ scan = client.scans.create_and_wait(domain, checks=["ai"])
108
+ except checkanysite.RateLimitError as error:
109
+ print(error.code) # rate_limited, or a limit used up, such as quota_exceeded
110
+ except checkanysite.APIStatusError as error:
111
+ print(error.status_code, error.code, error.message, error.param, error.request_id, error.docs)
112
+ except checkanysite.APIConnectionError:
113
+ print("No answer from the API")
114
+ ```
115
+
116
+ | Status | Class | Codes |
117
+ | ---------- | -------------------------- | --------------------------------------------------------------------------- |
118
+ | 400 | `BadRequestError` | `invalid_json`, `invalid_parameter`, `invalid_domain` |
119
+ | 401 | `AuthenticationError` | `missing_api_key`, `invalid_api_key`, `revoked_api_key`, `expired_api_key` |
120
+ | 403 | `PermissionDeniedError` | `read_only_key`, `browser_request` |
121
+ | 404 | `NotFoundError` | `not_found`, `no_result`, `scan_not_found` |
122
+ | 409 | `ConflictError` | `idempotency_key_in_use` |
123
+ | 422 | `UnprocessableEntityError` | `site_refused`, `idempotency_key_reused` |
124
+ | 429 | `RateLimitError` | `rate_limited`, `quota_exceeded`, `key_limit_reached`, `scan_limit_reached` |
125
+ | 500 and up | `InternalServerError` | `internal_error`, `capacity` |
126
+ | other | `APIStatusError` | `method_not_allowed` (405), `request_too_large` (413), … |
127
+ | no answer | `APIConnectionError` | `APITimeoutError` when the request timed out |
128
+
129
+ New codes may appear within v1; they land in the class for their status. Every code is explained on the [errors page](https://docs.checkanysite.com/errors).
130
+
131
+ ## Retries
132
+
133
+ A failure that may pass is retried twice by default, with exponential backoff (0.5, 1, 2 … up to 8 seconds) and jitter, waiting as long as `Retry-After` asks when the API sends it:
134
+
135
+ - no answer (a connection error or a timeout);
136
+ - 409 `idempotency_key_in_use`;
137
+ - 429 `rate_limited`, but never `quota_exceeded` or `key_limit_reached` (the month's credits are spent) or `scan_limit_reached` (the day's new scans are started);
138
+ - 500, 502, 503 and 504.
139
+
140
+ A `Retry-After` longer than 60 seconds isn't waited out: the error is raised so you can decide.
141
+
142
+ ```python
143
+ client = CheckAnySite(max_retries=5) # or 0 to turn retrying off
144
+ ```
145
+
146
+ ## Idempotency
147
+
148
+ Every POST carries an `Idempotency-Key`, a new UUID per call, kept across that call's retries: if an answer is lost and the call is retried, the API replays the first answer instead of scanning (and charging) twice. Pass your own key to make a call safe to repeat across runs:
149
+
150
+ ```python
151
+ scan = client.scans.create("example.com", idempotency_key="order-1234")
152
+ scan.meta.idempotent_replayed # True when this is the stored first answer
153
+ ```
154
+
155
+ A key is kept 24 hours. The same key with a different body raises `UnprocessableEntityError` (`idempotency_key_reused`).
156
+
157
+ ## Timeouts and configuration
158
+
159
+ ```python
160
+ import httpx
161
+ from checkanysite import CheckAnySite
162
+
163
+ client = CheckAnySite(
164
+ api_key="cas_live_…", # default: CHECKANYSITE_API_KEY
165
+ base_url="https://api.checkanysite.com/v1", # default: CHECKANYSITE_BASE_URL, else this
166
+ timeout=30.0, # seconds per request, or an httpx.Timeout
167
+ max_retries=2,
168
+ http_client=httpx.Client(proxy="http://proxy.internal:8080"), # optional; you close it
169
+ )
170
+ ```
171
+
172
+ Use the client as a context manager (`with CheckAnySite() as client:`) or call `client.close()` when done.
173
+
174
+ ## Async
175
+
176
+ `AsyncCheckAnySite` has the same methods, awaited:
177
+
178
+ ```python
179
+ import asyncio
180
+ from checkanysite import AsyncCheckAnySite
181
+
182
+
183
+ async def main() -> None:
184
+ async with AsyncCheckAnySite() as client:
185
+ sites = await asyncio.gather(
186
+ client.sites.retrieve("stripe.com", checks=["ai"]),
187
+ client.sites.retrieve("lovable.dev", checks=["ai"]),
188
+ )
189
+ scan = await client.scans.create_and_wait("example.com", checks=["dns"])
190
+
191
+
192
+ asyncio.run(main())
193
+ ```
194
+
195
+ ## Models
196
+
197
+ The answers are pydantic models generated from the API's OpenAPI document, in `checkanysite.models`. Attributes are snake_case (`site.scanned_at`); `model_dump(by_alias=True)` gives the API's camelCase JSON. Within v1 the API may add fields and values, never remove or rename them, so the models keep what they don't know: new fields are in `model.model_extra`, and new values of a field such as `level` arrive as strings.
198
+
199
+ ## Versioning
200
+
201
+ This library follows [semantic versioning](https://semver.org) and the API v1. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
202
+
203
+ ## Development
204
+
205
+ From `sdks/python/`, with [uv](https://docs.astral.sh/uv/):
206
+
207
+ ```sh
208
+ uv run ruff format --check . && uv run ruff check . && uv run mypy && uv run pytest
209
+ uv build # an sdist and a wheel in dist/
210
+ ```
211
+
212
+ The models come from the server's schemas. After an API change, from the repository root:
213
+
214
+ ```sh
215
+ pnpm sdk:python:models # exports openapi.json, then regenerates src/checkanysite/models/_generated.py
216
+ ```
@@ -0,0 +1,48 @@
1
+ """A first look at the CheckAnySite API: a stored result, a live scan and this month's credits.
2
+
3
+ Run it with your key in the environment (it spends a few of the month's free credits):
4
+
5
+ export CHECKANYSITE_API_KEY="cas_live_…"
6
+ python examples/quickstart.py stripe.com
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+
13
+ import checkanysite
14
+ from checkanysite import CheckAnySite
15
+
16
+
17
+ def main(domain: str) -> None:
18
+ with CheckAnySite() as client:
19
+ # The Lookup API: a stored result, at once. A miss is free.
20
+ try:
21
+ site = client.sites.retrieve(domain, checks=["ai", "tech"], max_age_days=30)
22
+ except checkanysite.NotFoundError as error:
23
+ print(f"No stored result for {domain} ({error.code}); scanning it now.")
24
+ else:
25
+ level = site.ai.level if site.ai else "not asked"
26
+ technologies = [item.name for item in site.tech.technologies] if site.tech else []
27
+ print(f"{site.domain}, scanned {site.scanned_at:%d %b %Y}")
28
+ print(f" AI builder traces: {level}")
29
+ print(f" Technologies: {', '.join(technologies) or 'none found'}")
30
+ print(f" Cost {site.meta.credits_cost} credits; {site.meta.credits_remaining} left")
31
+
32
+ # The Scan API: a live scan, polled until it is complete.
33
+ try:
34
+ scan = client.scans.create_and_wait(domain, checks=["readiness"], timeout=120)
35
+ except checkanysite.ScanTimeoutError as error:
36
+ print(f"Still scanning; read it later with scans.retrieve({error.scan_id!r}).")
37
+ else:
38
+ if scan.result and scan.result.readiness:
39
+ print(f" AI readiness: {scan.result.readiness.answer}")
40
+ elif scan.failure:
41
+ print(f" The scan failed: {scan.failure.message}")
42
+
43
+ usage = client.usage.retrieve()
44
+ print(f"{usage.remaining} of {usage.allowance} credits left this month.")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main(sys.argv[1] if len(sys.argv) > 1 else "stripe.com")