agent-ready-client 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.
- agent_ready_client-0.1.0/.gitignore +22 -0
- agent_ready_client-0.1.0/LICENSE +21 -0
- agent_ready_client-0.1.0/PKG-INFO +114 -0
- agent_ready_client-0.1.0/README.md +94 -0
- agent_ready_client-0.1.0/pyproject.toml +43 -0
- agent_ready_client-0.1.0/src/agent_ready/__init__.py +27 -0
- agent_ready_client-0.1.0/src/agent_ready/client.py +296 -0
- agent_ready_client-0.1.0/src/agent_ready/py.typed +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# JS
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
.npm/
|
|
6
|
+
|
|
7
|
+
# Python
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
dist-python/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
|
|
17
|
+
# Build outputs (python -m build writes to python/dist)
|
|
18
|
+
python/dist/
|
|
19
|
+
|
|
20
|
+
# OS / editor
|
|
21
|
+
.DS_Store
|
|
22
|
+
*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agent Ready
|
|
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,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-ready-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client SDK for the Agent Ready API — scan any URL for AI agent-readability (Vercel Agent Readability Spec, llmstxt.org, agent-protocol manifests).
|
|
5
|
+
Project-URL: Homepage, https://agent-ready.dev
|
|
6
|
+
Project-URL: Documentation, https://agent-ready.dev/docs/api
|
|
7
|
+
Project-URL: Repository, https://github.com/mlava/agent-ready-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/mlava/agent-ready-sdk/issues
|
|
9
|
+
Author: Agent Ready
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aeo,agent-readability,agent-ready,ai-agents,geo,llms.txt,mcp,sdk
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# agent-ready-client (Python SDK)
|
|
22
|
+
|
|
23
|
+
Official **Python** client SDK for the [Agent Ready](https://agent-ready.dev) API — scan any public URL for **AI agent-readability** against the Vercel Agent Readability Spec, the [llmstxt.org](https://llmstxt.org) standard, and agent-protocol manifests (MCP server cards, A2A, `agents.json`, `agent-permissions.json`, UCP, x402, NLWeb).
|
|
24
|
+
|
|
25
|
+
Zero runtime dependencies — pure standard library (`urllib`). Python 3.8+.
|
|
26
|
+
|
|
27
|
+
> Prefer the terminal? Use the [`agent-ready-scanner`](https://www.npmjs.com/package/agent-ready-scanner) CLI. Working in JS/TS? See [`agent-ready-client`](https://www.npmjs.com/package/agent-ready-client) on npm. This package is for calling the API **from your own Python code**.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install agent-ready-client
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from agent_ready import AgentReady
|
|
40
|
+
|
|
41
|
+
ar = AgentReady(api_key=os.environ["AGENT_READY_API_KEY"])
|
|
42
|
+
|
|
43
|
+
scan = ar.scan("https://example.com") # start + poll to completion
|
|
44
|
+
print(scan["vercelScore"], scan["vercelRating"]) # 96 "excellent"
|
|
45
|
+
|
|
46
|
+
for check in scan["siteChecks"]:
|
|
47
|
+
if check["status"] == "fail":
|
|
48
|
+
print(check["checkId"], check["name"], "->", check["howToFix"])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
`scan`, `start_scan`, `get_scan`, and `list_scans` require a **Pro API key**
|
|
54
|
+
(issue one at <https://agent-ready.dev/dashboard/api-keys>). Pass it explicitly
|
|
55
|
+
or set `AGENT_READY_API_KEY` in the environment:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
ar = AgentReady() # reads AGENT_READY_API_KEY
|
|
59
|
+
ar = AgentReady(api_key="ar_live_...") # or pass it in
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`ask` is **public** and needs no key.
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
AgentReady(api_key=None, base_url="https://agent-ready.dev", timeout=30.0)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
| Method | Returns | Notes |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `scan(url, page_limit=None, poll_interval=2.0, timeout=120.0)` | `Scan` | Start **and poll** to completion. |
|
|
73
|
+
| `start_scan(url, page_limit=None)` | `StartScanResponse` | Queue only; returns the id. |
|
|
74
|
+
| `get_scan(id)` | `Scan` | Fetch a scan (running or finished). |
|
|
75
|
+
| `list_scans(limit=None, cursor=None)` | `ScanListResponse` | Your scans, newest first. |
|
|
76
|
+
| `ask(query, item_type=None, mode=None)` | `dict` | NLWeb doc search. **No key required.** |
|
|
77
|
+
|
|
78
|
+
### Fire-and-forget + poll later
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
started = ar.start_scan("https://example.com", page_limit=25)
|
|
82
|
+
# ...later...
|
|
83
|
+
scan = ar.get_scan(started["id"])
|
|
84
|
+
if scan["status"] == "completed":
|
|
85
|
+
print(scan["vercelScore"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Errors
|
|
89
|
+
|
|
90
|
+
Every failure raises `ApiError` with a stable `code` and (when from a response)
|
|
91
|
+
an HTTP `status`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from agent_ready import ApiError
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
ar.scan("https://example.com")
|
|
98
|
+
except ApiError as err:
|
|
99
|
+
if err.code == "rate_limited":
|
|
100
|
+
... # back off and retry
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Common codes: `missing_api_key`, `unauthorized`, `subscription_required`,
|
|
104
|
+
`rate_limited`, `invalid_request`, `timeout`, `network_error`.
|
|
105
|
+
|
|
106
|
+
## Links
|
|
107
|
+
|
|
108
|
+
- API docs & OpenAPI 3.1 spec: <https://agent-ready.dev/docs/api> · <https://agent-ready.dev/api/v1/openapi.json>
|
|
109
|
+
- Methodology (all checks): <https://agent-ready.dev/methodology>
|
|
110
|
+
- JS/TS SDK: <https://www.npmjs.com/package/agent-ready-client> · CLI: <https://www.npmjs.com/package/agent-ready-scanner>
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT © Agent Ready
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# agent-ready-client (Python SDK)
|
|
2
|
+
|
|
3
|
+
Official **Python** client SDK for the [Agent Ready](https://agent-ready.dev) API — scan any public URL for **AI agent-readability** against the Vercel Agent Readability Spec, the [llmstxt.org](https://llmstxt.org) standard, and agent-protocol manifests (MCP server cards, A2A, `agents.json`, `agent-permissions.json`, UCP, x402, NLWeb).
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies — pure standard library (`urllib`). Python 3.8+.
|
|
6
|
+
|
|
7
|
+
> Prefer the terminal? Use the [`agent-ready-scanner`](https://www.npmjs.com/package/agent-ready-scanner) CLI. Working in JS/TS? See [`agent-ready-client`](https://www.npmjs.com/package/agent-ready-client) on npm. This package is for calling the API **from your own Python code**.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install agent-ready-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
from agent_ready import AgentReady
|
|
20
|
+
|
|
21
|
+
ar = AgentReady(api_key=os.environ["AGENT_READY_API_KEY"])
|
|
22
|
+
|
|
23
|
+
scan = ar.scan("https://example.com") # start + poll to completion
|
|
24
|
+
print(scan["vercelScore"], scan["vercelRating"]) # 96 "excellent"
|
|
25
|
+
|
|
26
|
+
for check in scan["siteChecks"]:
|
|
27
|
+
if check["status"] == "fail":
|
|
28
|
+
print(check["checkId"], check["name"], "->", check["howToFix"])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Authentication
|
|
32
|
+
|
|
33
|
+
`scan`, `start_scan`, `get_scan`, and `list_scans` require a **Pro API key**
|
|
34
|
+
(issue one at <https://agent-ready.dev/dashboard/api-keys>). Pass it explicitly
|
|
35
|
+
or set `AGENT_READY_API_KEY` in the environment:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
ar = AgentReady() # reads AGENT_READY_API_KEY
|
|
39
|
+
ar = AgentReady(api_key="ar_live_...") # or pass it in
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ask` is **public** and needs no key.
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
AgentReady(api_key=None, base_url="https://agent-ready.dev", timeout=30.0)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Method | Returns | Notes |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `scan(url, page_limit=None, poll_interval=2.0, timeout=120.0)` | `Scan` | Start **and poll** to completion. |
|
|
53
|
+
| `start_scan(url, page_limit=None)` | `StartScanResponse` | Queue only; returns the id. |
|
|
54
|
+
| `get_scan(id)` | `Scan` | Fetch a scan (running or finished). |
|
|
55
|
+
| `list_scans(limit=None, cursor=None)` | `ScanListResponse` | Your scans, newest first. |
|
|
56
|
+
| `ask(query, item_type=None, mode=None)` | `dict` | NLWeb doc search. **No key required.** |
|
|
57
|
+
|
|
58
|
+
### Fire-and-forget + poll later
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
started = ar.start_scan("https://example.com", page_limit=25)
|
|
62
|
+
# ...later...
|
|
63
|
+
scan = ar.get_scan(started["id"])
|
|
64
|
+
if scan["status"] == "completed":
|
|
65
|
+
print(scan["vercelScore"])
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Errors
|
|
69
|
+
|
|
70
|
+
Every failure raises `ApiError` with a stable `code` and (when from a response)
|
|
71
|
+
an HTTP `status`:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from agent_ready import ApiError
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
ar.scan("https://example.com")
|
|
78
|
+
except ApiError as err:
|
|
79
|
+
if err.code == "rate_limited":
|
|
80
|
+
... # back off and retry
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Common codes: `missing_api_key`, `unauthorized`, `subscription_required`,
|
|
84
|
+
`rate_limited`, `invalid_request`, `timeout`, `network_error`.
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- API docs & OpenAPI 3.1 spec: <https://agent-ready.dev/docs/api> · <https://agent-ready.dev/api/v1/openapi.json>
|
|
89
|
+
- Methodology (all checks): <https://agent-ready.dev/methodology>
|
|
90
|
+
- JS/TS SDK: <https://www.npmjs.com/package/agent-ready-client> · CLI: <https://www.npmjs.com/package/agent-ready-scanner>
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT © Agent Ready
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-ready-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client SDK for the Agent Ready API — scan any URL for AI agent-readability (Vercel Agent Readability Spec, llmstxt.org, agent-protocol manifests)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Agent Ready" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"agent-ready",
|
|
16
|
+
"agent-readability",
|
|
17
|
+
"llms.txt",
|
|
18
|
+
"mcp",
|
|
19
|
+
"ai-agents",
|
|
20
|
+
"sdk",
|
|
21
|
+
"aeo",
|
|
22
|
+
"geo",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
dependencies = []
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://agent-ready.dev"
|
|
35
|
+
Documentation = "https://agent-ready.dev/docs/api"
|
|
36
|
+
Repository = "https://github.com/mlava/agent-ready-sdk"
|
|
37
|
+
Issues = "https://github.com/mlava/agent-ready-sdk/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/agent_ready"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["src", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Official Python client SDK for the Agent Ready API.
|
|
2
|
+
|
|
3
|
+
See https://agent-ready.dev/docs/api for the full API reference.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import (
|
|
7
|
+
AgentReady,
|
|
8
|
+
ApiError,
|
|
9
|
+
CheckResult,
|
|
10
|
+
Scan,
|
|
11
|
+
ScanListResponse,
|
|
12
|
+
ScanSummary,
|
|
13
|
+
StartScanResponse,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AgentReady",
|
|
20
|
+
"ApiError",
|
|
21
|
+
"CheckResult",
|
|
22
|
+
"Scan",
|
|
23
|
+
"ScanListResponse",
|
|
24
|
+
"ScanSummary",
|
|
25
|
+
"StartScanResponse",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Official Python client SDK for the Agent Ready API.
|
|
2
|
+
|
|
3
|
+
Scan any public URL for AI agent-readability against the Vercel Agent
|
|
4
|
+
Readability Spec, the llmstxt.org standard, and agent-protocol manifests.
|
|
5
|
+
|
|
6
|
+
from agent_ready import AgentReady
|
|
7
|
+
ar = AgentReady(api_key="ar_live_...")
|
|
8
|
+
scan = ar.scan("https://example.com")
|
|
9
|
+
print(scan["vercelScore"], scan["vercelRating"])
|
|
10
|
+
|
|
11
|
+
Zero runtime dependencies — uses only the standard library (``urllib``).
|
|
12
|
+
Mirrors the transport in the ``agent-ready-client`` (JS) and ``agent-ready-cli``
|
|
13
|
+
packages so behaviour stays consistent across surfaces.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import socket
|
|
21
|
+
import time
|
|
22
|
+
import urllib.error
|
|
23
|
+
import urllib.parse
|
|
24
|
+
import urllib.request
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
try: # TypedDict is in typing on 3.8+, but keep the import defensive.
|
|
28
|
+
from typing import TypedDict
|
|
29
|
+
except ImportError: # pragma: no cover
|
|
30
|
+
TypedDict = None # type: ignore[assignment]
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AgentReady",
|
|
34
|
+
"ApiError",
|
|
35
|
+
"CheckResult",
|
|
36
|
+
"Scan",
|
|
37
|
+
"ScanSummary",
|
|
38
|
+
"ScanListResponse",
|
|
39
|
+
"StartScanResponse",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
DEFAULT_BASE_URL = "https://agent-ready.dev"
|
|
43
|
+
DEFAULT_TIMEOUT = 30.0
|
|
44
|
+
DEFAULT_SCAN_TIMEOUT = 120.0
|
|
45
|
+
DEFAULT_POLL_INTERVAL = 2.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ApiError(Exception):
|
|
49
|
+
"""Raised for every API, network, and timeout failure.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
code: Stable machine code (e.g. ``"unauthorized"``, ``"rate_limited"``,
|
|
53
|
+
``"timeout"``).
|
|
54
|
+
status: HTTP status when the failure came from a response, else ``None``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, code: str, message: str, status: Optional[int] = None) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.code = code
|
|
60
|
+
self.status = status
|
|
61
|
+
|
|
62
|
+
def __str__(self) -> str:
|
|
63
|
+
message = super().__str__()
|
|
64
|
+
return f"[{self.code}] {message}" if self.code else message
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if TypedDict is not None:
|
|
68
|
+
|
|
69
|
+
class CheckResult(TypedDict):
|
|
70
|
+
checkId: str
|
|
71
|
+
name: str
|
|
72
|
+
status: str # "pass" | "fail" | "warn" | "error"
|
|
73
|
+
message: str
|
|
74
|
+
howToFix: Optional[str]
|
|
75
|
+
details: Dict[str, Any]
|
|
76
|
+
|
|
77
|
+
class _PageResult(TypedDict):
|
|
78
|
+
url: str
|
|
79
|
+
checks: List[CheckResult]
|
|
80
|
+
|
|
81
|
+
class Scan(TypedDict, total=False):
|
|
82
|
+
id: str
|
|
83
|
+
rootUrl: str
|
|
84
|
+
status: str # "running" | "completed" | "failed"
|
|
85
|
+
createdAt: str
|
|
86
|
+
completedAt: Optional[str]
|
|
87
|
+
pagesDiscovered: int
|
|
88
|
+
pagesScanned: int
|
|
89
|
+
vercelScore: int
|
|
90
|
+
vercelRating: str
|
|
91
|
+
llmstxtScore: int
|
|
92
|
+
siteChecks: List[CheckResult]
|
|
93
|
+
llmstxtChecks: List[CheckResult]
|
|
94
|
+
pageResults: List[_PageResult]
|
|
95
|
+
shareToken: str
|
|
96
|
+
|
|
97
|
+
class StartScanResponse(TypedDict):
|
|
98
|
+
id: str
|
|
99
|
+
status: str
|
|
100
|
+
url: str
|
|
101
|
+
pollUrl: str
|
|
102
|
+
|
|
103
|
+
class ScanSummary(TypedDict):
|
|
104
|
+
id: str
|
|
105
|
+
shareToken: str
|
|
106
|
+
domain: str
|
|
107
|
+
rootUrl: str
|
|
108
|
+
vercelScore: Optional[int]
|
|
109
|
+
vercelRating: Optional[str]
|
|
110
|
+
llmstxtScore: Optional[int]
|
|
111
|
+
pagesScanned: Optional[int]
|
|
112
|
+
createdAt: str
|
|
113
|
+
|
|
114
|
+
class ScanListResponse(TypedDict, total=False):
|
|
115
|
+
data: List[ScanSummary]
|
|
116
|
+
nextCursor: str
|
|
117
|
+
else: # pragma: no cover - typing fallback for very old runtimes
|
|
118
|
+
CheckResult = Dict[str, Any] # type: ignore[misc,assignment]
|
|
119
|
+
Scan = Dict[str, Any] # type: ignore[misc,assignment]
|
|
120
|
+
StartScanResponse = Dict[str, Any] # type: ignore[misc,assignment]
|
|
121
|
+
ScanSummary = Dict[str, Any] # type: ignore[misc,assignment]
|
|
122
|
+
ScanListResponse = Dict[str, Any] # type: ignore[misc,assignment]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AgentReady:
|
|
126
|
+
"""Client for the Agent Ready REST API. Stateless and reusable."""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
api_key: Optional[str] = None,
|
|
131
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
132
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
133
|
+
) -> None:
|
|
134
|
+
key = api_key if api_key is not None else os.environ.get("AGENT_READY_API_KEY")
|
|
135
|
+
self.api_key: Optional[str] = (key or "").strip() or None
|
|
136
|
+
self.base_url: str = base_url.rstrip("/")
|
|
137
|
+
self.timeout: float = timeout
|
|
138
|
+
|
|
139
|
+
def start_scan(
|
|
140
|
+
self, url: str, page_limit: Optional[int] = None
|
|
141
|
+
) -> "StartScanResponse":
|
|
142
|
+
"""Start a scan and return immediately (does not wait for completion)."""
|
|
143
|
+
body: Dict[str, Any] = {"url": url}
|
|
144
|
+
if page_limit is not None:
|
|
145
|
+
body["pageLimit"] = page_limit
|
|
146
|
+
return self._request("POST", "/api/v1/scans", body=body)
|
|
147
|
+
|
|
148
|
+
def get_scan(self, scan_id: str) -> "Scan":
|
|
149
|
+
"""Fetch a scan (running or finished) by id."""
|
|
150
|
+
return self._request(
|
|
151
|
+
"GET", "/api/v1/scans/" + urllib.parse.quote(scan_id, safe="")
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def scan(
|
|
155
|
+
self,
|
|
156
|
+
url: str,
|
|
157
|
+
page_limit: Optional[int] = None,
|
|
158
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
159
|
+
timeout: float = DEFAULT_SCAN_TIMEOUT,
|
|
160
|
+
) -> "Scan":
|
|
161
|
+
"""Start a scan and poll until it completes, returning the full result.
|
|
162
|
+
|
|
163
|
+
Raises ``ApiError("timeout")`` if it is still running past ``timeout``
|
|
164
|
+
seconds — the scan keeps running server-side, so re-fetch it later with
|
|
165
|
+
:meth:`get_scan` using the id from the error message.
|
|
166
|
+
"""
|
|
167
|
+
started = self.start_scan(url, page_limit=page_limit)
|
|
168
|
+
deadline = time.monotonic() + timeout
|
|
169
|
+
while True:
|
|
170
|
+
result = self.get_scan(started["id"])
|
|
171
|
+
if result.get("status") != "running":
|
|
172
|
+
return result
|
|
173
|
+
if time.monotonic() >= deadline:
|
|
174
|
+
raise ApiError(
|
|
175
|
+
"timeout",
|
|
176
|
+
"Scan {0} still running past the wait budget. "
|
|
177
|
+
"Re-fetch it with get_scan({0!r}).".format(started["id"]),
|
|
178
|
+
)
|
|
179
|
+
time.sleep(poll_interval)
|
|
180
|
+
|
|
181
|
+
def list_scans(
|
|
182
|
+
self, limit: Optional[int] = None, cursor: Optional[str] = None
|
|
183
|
+
) -> "ScanListResponse":
|
|
184
|
+
"""List your scans, newest first."""
|
|
185
|
+
params: Dict[str, str] = {}
|
|
186
|
+
if limit is not None:
|
|
187
|
+
params["limit"] = str(limit)
|
|
188
|
+
if cursor:
|
|
189
|
+
params["cursor"] = cursor
|
|
190
|
+
path = "/api/v1/scans"
|
|
191
|
+
if params:
|
|
192
|
+
path += "?" + urllib.parse.urlencode(params)
|
|
193
|
+
return self._request("GET", path)
|
|
194
|
+
|
|
195
|
+
def ask(
|
|
196
|
+
self,
|
|
197
|
+
query: str,
|
|
198
|
+
item_type: Optional[str] = None,
|
|
199
|
+
mode: Optional[str] = None,
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
"""Natural-language search over Agent Ready's docs (NLWeb ``/ask``).
|
|
202
|
+
|
|
203
|
+
Public — no API key required. Returns the ``_meta`` envelope as-is,
|
|
204
|
+
including for no-results (404) and rate-limited (429) responses.
|
|
205
|
+
"""
|
|
206
|
+
body: Dict[str, Any] = {"query": {"q": query, "itemType": item_type}}
|
|
207
|
+
if mode:
|
|
208
|
+
body["prefer"] = {"mode": mode}
|
|
209
|
+
return self._request(
|
|
210
|
+
"POST",
|
|
211
|
+
"/api/v1/ask",
|
|
212
|
+
body=body,
|
|
213
|
+
require_key=False,
|
|
214
|
+
pass_envelope_on_error=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# ---- transport ----------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def _request(
|
|
220
|
+
self,
|
|
221
|
+
method: str,
|
|
222
|
+
path: str,
|
|
223
|
+
body: Optional[Dict[str, Any]] = None,
|
|
224
|
+
require_key: bool = True,
|
|
225
|
+
pass_envelope_on_error: bool = False,
|
|
226
|
+
) -> Any:
|
|
227
|
+
if require_key and not self.api_key:
|
|
228
|
+
raise ApiError(
|
|
229
|
+
"missing_api_key",
|
|
230
|
+
"No API key set. Issue a Pro key at "
|
|
231
|
+
"https://agent-ready.dev/dashboard/api-keys and pass api_key= "
|
|
232
|
+
"or set AGENT_READY_API_KEY.",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
headers: Dict[str, str] = {"Accept": "application/json"}
|
|
236
|
+
if self.api_key:
|
|
237
|
+
headers["Authorization"] = "Bearer " + self.api_key
|
|
238
|
+
data: Optional[bytes] = None
|
|
239
|
+
if body is not None:
|
|
240
|
+
data = json.dumps(body).encode("utf-8")
|
|
241
|
+
headers["Content-Type"] = "application/json"
|
|
242
|
+
|
|
243
|
+
req = urllib.request.Request(
|
|
244
|
+
self.base_url + path, data=data, headers=headers, method=method
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
249
|
+
status = resp.status
|
|
250
|
+
text = resp.read().decode("utf-8")
|
|
251
|
+
except urllib.error.HTTPError as err:
|
|
252
|
+
status = err.code
|
|
253
|
+
text = err.read().decode("utf-8", "replace")
|
|
254
|
+
except (socket.timeout, TimeoutError) as err:
|
|
255
|
+
raise ApiError(
|
|
256
|
+
"timeout",
|
|
257
|
+
"Request to {0} timed out after {1}s.".format(path, self.timeout),
|
|
258
|
+
) from err
|
|
259
|
+
except urllib.error.URLError as err:
|
|
260
|
+
reason = err.reason
|
|
261
|
+
if isinstance(reason, (socket.timeout, TimeoutError)):
|
|
262
|
+
raise ApiError(
|
|
263
|
+
"timeout",
|
|
264
|
+
"Request to {0} timed out after {1}s.".format(path, self.timeout),
|
|
265
|
+
) from err
|
|
266
|
+
raise ApiError(
|
|
267
|
+
"network_error",
|
|
268
|
+
"Network error calling {0}: {1}".format(path, reason),
|
|
269
|
+
) from err
|
|
270
|
+
|
|
271
|
+
payload: Any = None
|
|
272
|
+
if text:
|
|
273
|
+
try:
|
|
274
|
+
payload = json.loads(text)
|
|
275
|
+
except json.JSONDecodeError:
|
|
276
|
+
payload = None
|
|
277
|
+
|
|
278
|
+
# `/ask` answers and failures both carry a `_meta` envelope; pass through.
|
|
279
|
+
if (
|
|
280
|
+
pass_envelope_on_error
|
|
281
|
+
and isinstance(payload, dict)
|
|
282
|
+
and "_meta" in payload
|
|
283
|
+
):
|
|
284
|
+
return payload
|
|
285
|
+
|
|
286
|
+
if status < 200 or status >= 300:
|
|
287
|
+
detail = payload.get("error") if isinstance(payload, dict) else None
|
|
288
|
+
code = detail.get("code") if isinstance(detail, dict) else None
|
|
289
|
+
message = detail.get("message") if isinstance(detail, dict) else None
|
|
290
|
+
raise ApiError(
|
|
291
|
+
code or "http_{0}".format(status),
|
|
292
|
+
message or text or "HTTP {0} from {1}".format(status, path),
|
|
293
|
+
status,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return payload
|
|
File without changes
|