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.
@@ -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