crawlforge 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.
- crawlforge-0.1.0/.gitignore +12 -0
- crawlforge-0.1.0/CHANGELOG.md +18 -0
- crawlforge-0.1.0/LICENSE +21 -0
- crawlforge-0.1.0/Makefile +21 -0
- crawlforge-0.1.0/PKG-INFO +269 -0
- crawlforge-0.1.0/README.md +232 -0
- crawlforge-0.1.0/crawlforge/__init__.py +38 -0
- crawlforge-0.1.0/crawlforge/_client.py +300 -0
- crawlforge-0.1.0/crawlforge/_generated/__init__.py +1 -0
- crawlforge-0.1.0/crawlforge/_generated/models.py +665 -0
- crawlforge-0.1.0/crawlforge/_generated/tools.py +2246 -0
- crawlforge-0.1.0/crawlforge/_version.py +10 -0
- crawlforge-0.1.0/crawlforge/errors.py +193 -0
- crawlforge-0.1.0/crawlforge/models.py +9 -0
- crawlforge-0.1.0/crawlforge/py.typed +0 -0
- crawlforge-0.1.0/crawlforge/types.py +34 -0
- crawlforge-0.1.0/openapi.json +6509 -0
- crawlforge-0.1.0/pyproject.toml +82 -0
- crawlforge-0.1.0/scripts/gen_models.py +535 -0
- crawlforge-0.1.0/scripts/generate.py +43 -0
- crawlforge-0.1.0/scripts/smoke.py +29 -0
- crawlforge-0.1.0/tests/__init__.py +0 -0
- crawlforge-0.1.0/tests/conftest.py +86 -0
- crawlforge-0.1.0/tests/fixtures/describe_fetch_url.json +72 -0
- crawlforge-0.1.0/tests/fixtures/error_400.json +22 -0
- crawlforge-0.1.0/tests/fixtures/error_401.json +15 -0
- crawlforge-0.1.0/tests/fixtures/error_402.json +22 -0
- crawlforge-0.1.0/tests/fixtures/error_402_no_recharge.json +22 -0
- crawlforge-0.1.0/tests/fixtures/error_429.json +20 -0
- crawlforge-0.1.0/tests/fixtures/error_429_host_backoff.json +16 -0
- crawlforge-0.1.0/tests/fixtures/error_429_no_header.json +19 -0
- crawlforge-0.1.0/tests/fixtures/error_500.json +15 -0
- crawlforge-0.1.0/tests/fixtures/error_502_blocked.json +21 -0
- crawlforge-0.1.0/tests/fixtures/error_502_html.json +10 -0
- crawlforge-0.1.0/tests/fixtures/extract_links.json +55 -0
- crawlforge-0.1.0/tests/fixtures/extract_metadata.json +88 -0
- crawlforge-0.1.0/tests/fixtures/extract_text.json +36 -0
- crawlforge-0.1.0/tests/fixtures/fetch_url.json +41 -0
- crawlforge-0.1.0/tests/fixtures/map_site.json +37 -0
- crawlforge-0.1.0/tests/fixtures/read_result.json +49 -0
- crawlforge-0.1.0/tests/fixtures/scrape_structured.json +38 -0
- crawlforge-0.1.0/tests/fixtures/search_web.json +60 -0
- crawlforge-0.1.0/tests/test_client.py +321 -0
- crawlforge-0.1.0/tests/test_errors.py +194 -0
- crawlforge-0.1.0/tests/test_package.py +36 -0
- crawlforge-0.1.0/tests/test_retries.py +149 -0
- crawlforge-0.1.0/tests/test_spec.py +134 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-07)
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `CrawlForge` (sync) and `AsyncCrawlForge` (async) clients on `httpx`.
|
|
8
|
+
- One typed method per tool, 30 in all, generated from `openapi.json`; every
|
|
9
|
+
keyword argument is validated locally by a pydantic v2 request model before
|
|
10
|
+
a request is sent.
|
|
11
|
+
- `call(tool, params)` for a raw body and `describe(tool)` for a tool's
|
|
12
|
+
self-description.
|
|
13
|
+
- Error classes by status: `ValidationError`, `AuthenticationError`,
|
|
14
|
+
`InsufficientCreditsError`, `RateLimitError`, `ToolError`; `NETWORK_ERROR`
|
|
15
|
+
and `UNEXPECTED_RESPONSE` on the base `CrawlForgeError`.
|
|
16
|
+
- Retries on 429 only, honouring `Retry-After` (seconds or HTTP date, capped
|
|
17
|
+
at 60 s) with jittered exponential backoff otherwise.
|
|
18
|
+
- Python 3.9 to 3.13. Runtime dependencies: `httpx` and `pydantic` only.
|
crawlforge-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CrawlForge
|
|
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,21 @@
|
|
|
1
|
+
PYTHON ?= python
|
|
2
|
+
|
|
3
|
+
.PHONY: generate lint typecheck test build clean
|
|
4
|
+
|
|
5
|
+
generate:
|
|
6
|
+
$(PYTHON) scripts/generate.py
|
|
7
|
+
|
|
8
|
+
lint:
|
|
9
|
+
ruff check .
|
|
10
|
+
|
|
11
|
+
typecheck:
|
|
12
|
+
mypy crawlforge scripts
|
|
13
|
+
|
|
14
|
+
test:
|
|
15
|
+
pytest
|
|
16
|
+
|
|
17
|
+
build:
|
|
18
|
+
$(PYTHON) -m build
|
|
19
|
+
|
|
20
|
+
clean:
|
|
21
|
+
rm -rf dist build *.egg-info .pytest_cache .mypy_cache .ruff_cache
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crawlforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the CrawlForge REST API: 30 metered web tools behind one API key.
|
|
5
|
+
Project-URL: Homepage, https://www.crawlforge.dev
|
|
6
|
+
Project-URL: Documentation, https://www.crawlforge.dev/docs/api-reference
|
|
7
|
+
Project-URL: Source, https://github.com/mysleekdesigns/crawlforge-sdk-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/mysleekdesigns/crawlforge-sdk-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: CrawlForge
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: crawler,crawlforge,extraction,mcp,sdk,search,web scraping
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx<1,>=0.27
|
|
29
|
+
Requires-Dist: pydantic<3,>=2
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# CrawlForge Python SDK
|
|
39
|
+
|
|
40
|
+
The official Python client for the [CrawlForge REST API](https://www.crawlforge.dev/docs/api-reference):
|
|
41
|
+
30 metered web tools (scrape, search, crawl, extract, research) behind one API key.
|
|
42
|
+
|
|
43
|
+
- Sync `CrawlForge` and async `AsyncCrawlForge` clients on `httpx`
|
|
44
|
+
- One typed method per tool, generated from the API's [OpenAPI spec](https://www.crawlforge.dev/openapi.json)
|
|
45
|
+
- Arguments validated locally by pydantic v2 before any request is sent
|
|
46
|
+
- Retries on rate limits only, honouring `Retry-After`
|
|
47
|
+
- Python 3.9+, fully typed (`py.typed`), runtime dependencies `httpx` and `pydantic` only
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install crawlforge
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Create an API key at https://www.crawlforge.dev/dashboard/keys and export it as
|
|
56
|
+
`CRAWLFORGE_API_KEY`, or pass `api_key=` to the client.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from crawlforge import CrawlForge
|
|
62
|
+
|
|
63
|
+
with CrawlForge() as client: # reads CRAWLFORGE_API_KEY
|
|
64
|
+
result = client.scrape(url="https://example.com", formats=["markdown", "links"])
|
|
65
|
+
|
|
66
|
+
print(result.data["formats"]["markdown"][:200])
|
|
67
|
+
print(result.credits_used, result.credits_remaining)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Async:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from crawlforge import AsyncCrawlForge
|
|
75
|
+
|
|
76
|
+
async def main() -> None:
|
|
77
|
+
async with AsyncCrawlForge() as client:
|
|
78
|
+
result = await client.search_web(query="web scraping", limit=5)
|
|
79
|
+
for hit in result.data["results"]:
|
|
80
|
+
print(hit["title"], hit["url"])
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Every tool method returns a `ToolResult`:
|
|
86
|
+
|
|
87
|
+
| Field | Meaning |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `data` | The tool's result. **Tool-specific and untyped (`dict`) in this release**; its shape is documented on the tool's docs page. |
|
|
90
|
+
| `credits_used` | Credits charged for this call. |
|
|
91
|
+
| `credits_remaining` | Your balance after the call. |
|
|
92
|
+
| `processing_time` | Server-side processing time in milliseconds. |
|
|
93
|
+
| `warnings` | Non-fatal notes, `[]` when there are none. |
|
|
94
|
+
|
|
95
|
+
## Calling a tool
|
|
96
|
+
|
|
97
|
+
Each tool is a method named exactly like the tool. Pass keyword arguments, a
|
|
98
|
+
dict, or the tool's request model; all three send the same body, and only the
|
|
99
|
+
fields you set are sent.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from crawlforge.models import FetchUrlRequest
|
|
103
|
+
|
|
104
|
+
client.fetch_url(url="https://example.com", timeout=15000)
|
|
105
|
+
client.fetch_url({"url": "https://example.com", "timeout": 15000})
|
|
106
|
+
client.fetch_url(FetchUrlRequest(url="https://example.com", timeout=15000))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Keyword names are the API's wire names, camelCase included (`onlyMainContent`,
|
|
110
|
+
`maxUrls`). Nested objects can be passed as dicts. A misspelled keyword is a
|
|
111
|
+
`TypeError` from the method signature; a wrong type, a value outside the spec's
|
|
112
|
+
range, or an unknown key in a dict is a `pydantic.ValidationError`. Either way
|
|
113
|
+
nothing is sent and nothing is charged.
|
|
114
|
+
|
|
115
|
+
Two escape hatches:
|
|
116
|
+
|
|
117
|
+
- `client.call(tool, params)` posts a raw body to any tool without local
|
|
118
|
+
validation, for a parameter the spec does not list or a tool newer than this
|
|
119
|
+
SDK.
|
|
120
|
+
- `client.describe(tool)` fetches the tool's self-description (`ToolInfo`: price,
|
|
121
|
+
JSON Schema, example) with no API key.
|
|
122
|
+
|
|
123
|
+
### Per-call timeout and retries
|
|
124
|
+
|
|
125
|
+
`with_options()` returns a copy of the client sharing the same connection pool:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
client.with_options(timeout=10.0, max_retries=0).fetch_url(url="https://example.com")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The tool parameter `timeout` (milliseconds, on `fetch_url` and ten other tools)
|
|
132
|
+
is unrelated to the HTTP timeout, which is why the HTTP one is not a keyword on
|
|
133
|
+
the tool methods.
|
|
134
|
+
|
|
135
|
+
## Tools and credits
|
|
136
|
+
|
|
137
|
+
Prices are the `x-credits` of each operation in the spec. An error response,
|
|
138
|
+
whatever its status, charges nothing: credits are deducted only after a tool
|
|
139
|
+
succeeded.
|
|
140
|
+
|
|
141
|
+
<!-- generated:tools:start -->
|
|
142
|
+
| Method | Credits | Note | Docs |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `agent` | 8 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/agent) |
|
|
145
|
+
| `analyze_content` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/analyze-content) |
|
|
146
|
+
| `batch_scrape` | 5 | 5 per URL attempted (skipped URLs are not charged) | [docs](https://www.crawlforge.dev/docs/api-reference/tools/batch-scrape) |
|
|
147
|
+
| `crawl_deep` | 4 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/crawl-deep) |
|
|
148
|
+
| `deep_research` | 10 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/deep-research) |
|
|
149
|
+
| `extract_content` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-content) |
|
|
150
|
+
| `extract_embedded_state` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-embedded-state) |
|
|
151
|
+
| `extract_links` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-links) |
|
|
152
|
+
| `extract_metadata` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-metadata) |
|
|
153
|
+
| `extract_structured` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-structured) |
|
|
154
|
+
| `extract_text` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-text) |
|
|
155
|
+
| `extract_with_llm` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-with-llm) |
|
|
156
|
+
| `fetch_url` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/fetch-url) |
|
|
157
|
+
| `generate_llms_txt` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/generate-llms-txt) |
|
|
158
|
+
| `get_batch_results` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/get-batch-results) |
|
|
159
|
+
| `list_ollama_models` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/list-ollama-models) |
|
|
160
|
+
| `localization` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/localization) |
|
|
161
|
+
| `map_site` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/map-site) |
|
|
162
|
+
| `process_document` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/process-document) |
|
|
163
|
+
| `read_result` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/read-result) |
|
|
164
|
+
| `reddit_search` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/reddit-search) |
|
|
165
|
+
| `scrape` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape) |
|
|
166
|
+
| `scrape_structured` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-structured) |
|
|
167
|
+
| `scrape_template` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-template) |
|
|
168
|
+
| `scrape_with_actions` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-with-actions) |
|
|
169
|
+
| `search_web` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/search-web) |
|
|
170
|
+
| `serp_rank` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/serp-rank) |
|
|
171
|
+
| `stealth_mode` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/stealth-mode) |
|
|
172
|
+
| `summarize_content` | 4 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/summarize-content) |
|
|
173
|
+
| `track_changes` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/track-changes) |
|
|
174
|
+
<!-- generated:tools:end -->
|
|
175
|
+
|
|
176
|
+
## Errors
|
|
177
|
+
|
|
178
|
+
All errors derive from `crawlforge.CrawlForgeError`, with `status`, `code`,
|
|
179
|
+
`message`, `details` and `request_id`. The class follows the HTTP status:
|
|
180
|
+
|
|
181
|
+
| Status | Class | Extra attributes |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| 400 | `ValidationError` | `details` is the list of validation issues |
|
|
184
|
+
| 401 | `AuthenticationError` | |
|
|
185
|
+
| 402 | `InsufficientCreditsError` | `auto_recharge_triggered` (from the `X-Auto-Recharge-Triggered` header) |
|
|
186
|
+
| 429 | `RateLimitError` | `retry_after` in seconds, `None` when absent; `code` is `RATE_LIMIT_EXCEEDED` or `HOST_BACKOFF` |
|
|
187
|
+
| other | `ToolError` | `blocked` (vendor and evidence when a bot wall was met) and `escalated` |
|
|
188
|
+
|
|
189
|
+
A response that is not JSON is a `CrawlForgeError` with code
|
|
190
|
+
`UNEXPECTED_RESPONSE` and the raw text in `details`; a connection or timeout
|
|
191
|
+
failure is code `NETWORK_ERROR` with `status` 0. No error message ever contains
|
|
192
|
+
your API key.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from crawlforge import CrawlForge, InsufficientCreditsError, RateLimitError, ToolError
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result = client.scrape(url="https://example.com", escalate=True)
|
|
199
|
+
except RateLimitError as e:
|
|
200
|
+
print("slow down", e.retry_after)
|
|
201
|
+
except InsufficientCreditsError as e:
|
|
202
|
+
print("top up", e.auto_recharge_triggered)
|
|
203
|
+
except ToolError as e:
|
|
204
|
+
print(e.code, e.blocked)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Retries
|
|
208
|
+
|
|
209
|
+
Only a 429 is retried, and only `max_retries` times (default 2, so up to three
|
|
210
|
+
requests). The wait is the `Retry-After` header when present (seconds or an HTTP
|
|
211
|
+
date, capped at 60 s), otherwise exponential backoff from 1 s with ±20 % jitter.
|
|
212
|
+
A 5xx is never retried, because a failed call is not charged but a repeated
|
|
213
|
+
one might be. After the last retry the `RateLimitError` is raised.
|
|
214
|
+
|
|
215
|
+
## Configuration
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
CrawlForge(
|
|
219
|
+
api_key=None, # default: CRAWLFORGE_API_KEY
|
|
220
|
+
base_url=None, # default: https://www.crawlforge.dev/api/v1
|
|
221
|
+
max_retries=2,
|
|
222
|
+
timeout=60.0, # seconds, per request
|
|
223
|
+
http_client=None, # your own httpx.Client (AsyncClient for AsyncCrawlForge)
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Passing `http_client` is also the test seam: an `httpx.Client` on an
|
|
228
|
+
`httpx.MockTransport` gives a fixture server with no sockets. The SDK stores
|
|
229
|
+
nothing: results live only in the `ToolResult` you hold.
|
|
230
|
+
|
|
231
|
+
## Python 3.9+
|
|
232
|
+
|
|
233
|
+
The generated models use `Optional[...]`, `List[...]` and `Dict[...]` rather
|
|
234
|
+
than `X | Y`, which pydantic cannot evaluate on 3.9. Tested on 3.9 through 3.13.
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
python -m venv .venv && . .venv/bin/activate
|
|
240
|
+
pip install -e ".[dev]"
|
|
241
|
+
python scripts/generate.py # or: make generate
|
|
242
|
+
ruff check . && mypy crawlforge scripts && pytest
|
|
243
|
+
python -m build
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`crawlforge/_generated/models.py`, `crawlforge/_generated/tools.py` and the
|
|
247
|
+
table above are generated from `openapi.json` by `scripts/generate.py`
|
|
248
|
+
(`scripts/gen_models.py` is the generator) and committed; CI regenerates and
|
|
249
|
+
fails on any diff. `openapi.json` is copied verbatim from
|
|
250
|
+
https://www.crawlforge.dev/openapi.json and never edited by hand.
|
|
251
|
+
|
|
252
|
+
The generator is in-repo and standard-library only. It replaced
|
|
253
|
+
`datamodel-code-generator` 0.76.2, which on this spec has no Python 3.9 target
|
|
254
|
+
(it emits `X | None`), splits the nine request schemas with a top-level
|
|
255
|
+
`anyOf`/`oneOf`/`allOf` cross-field rule into `RootModel` unions, maps
|
|
256
|
+
`format: uri` to `AnyUrl` (which rewrites URLs before they are sent) and
|
|
257
|
+
`format: email` to `EmailStr` (an extra dependency). The in-repo generator keeps
|
|
258
|
+
wire names, ignores the cross-field rules locally (the server enforces them),
|
|
259
|
+
skips `pattern` (the spec's lookaheads are not supported by pydantic's default
|
|
260
|
+
regex engine) and forbids unknown keys on request models.
|
|
261
|
+
|
|
262
|
+
## Links
|
|
263
|
+
|
|
264
|
+
- API reference: https://www.crawlforge.dev/docs/api-reference
|
|
265
|
+
- OpenAPI spec: https://www.crawlforge.dev/openapi.json
|
|
266
|
+
- API keys: https://www.crawlforge.dev/dashboard/keys
|
|
267
|
+
- Changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
268
|
+
|
|
269
|
+
MIT licensed.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# CrawlForge Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python client for the [CrawlForge REST API](https://www.crawlforge.dev/docs/api-reference):
|
|
4
|
+
30 metered web tools (scrape, search, crawl, extract, research) behind one API key.
|
|
5
|
+
|
|
6
|
+
- Sync `CrawlForge` and async `AsyncCrawlForge` clients on `httpx`
|
|
7
|
+
- One typed method per tool, generated from the API's [OpenAPI spec](https://www.crawlforge.dev/openapi.json)
|
|
8
|
+
- Arguments validated locally by pydantic v2 before any request is sent
|
|
9
|
+
- Retries on rate limits only, honouring `Retry-After`
|
|
10
|
+
- Python 3.9+, fully typed (`py.typed`), runtime dependencies `httpx` and `pydantic` only
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install crawlforge
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Create an API key at https://www.crawlforge.dev/dashboard/keys and export it as
|
|
19
|
+
`CRAWLFORGE_API_KEY`, or pass `api_key=` to the client.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from crawlforge import CrawlForge
|
|
25
|
+
|
|
26
|
+
with CrawlForge() as client: # reads CRAWLFORGE_API_KEY
|
|
27
|
+
result = client.scrape(url="https://example.com", formats=["markdown", "links"])
|
|
28
|
+
|
|
29
|
+
print(result.data["formats"]["markdown"][:200])
|
|
30
|
+
print(result.credits_used, result.credits_remaining)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Async:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from crawlforge import AsyncCrawlForge
|
|
38
|
+
|
|
39
|
+
async def main() -> None:
|
|
40
|
+
async with AsyncCrawlForge() as client:
|
|
41
|
+
result = await client.search_web(query="web scraping", limit=5)
|
|
42
|
+
for hit in result.data["results"]:
|
|
43
|
+
print(hit["title"], hit["url"])
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every tool method returns a `ToolResult`:
|
|
49
|
+
|
|
50
|
+
| Field | Meaning |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `data` | The tool's result. **Tool-specific and untyped (`dict`) in this release**; its shape is documented on the tool's docs page. |
|
|
53
|
+
| `credits_used` | Credits charged for this call. |
|
|
54
|
+
| `credits_remaining` | Your balance after the call. |
|
|
55
|
+
| `processing_time` | Server-side processing time in milliseconds. |
|
|
56
|
+
| `warnings` | Non-fatal notes, `[]` when there are none. |
|
|
57
|
+
|
|
58
|
+
## Calling a tool
|
|
59
|
+
|
|
60
|
+
Each tool is a method named exactly like the tool. Pass keyword arguments, a
|
|
61
|
+
dict, or the tool's request model; all three send the same body, and only the
|
|
62
|
+
fields you set are sent.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from crawlforge.models import FetchUrlRequest
|
|
66
|
+
|
|
67
|
+
client.fetch_url(url="https://example.com", timeout=15000)
|
|
68
|
+
client.fetch_url({"url": "https://example.com", "timeout": 15000})
|
|
69
|
+
client.fetch_url(FetchUrlRequest(url="https://example.com", timeout=15000))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Keyword names are the API's wire names, camelCase included (`onlyMainContent`,
|
|
73
|
+
`maxUrls`). Nested objects can be passed as dicts. A misspelled keyword is a
|
|
74
|
+
`TypeError` from the method signature; a wrong type, a value outside the spec's
|
|
75
|
+
range, or an unknown key in a dict is a `pydantic.ValidationError`. Either way
|
|
76
|
+
nothing is sent and nothing is charged.
|
|
77
|
+
|
|
78
|
+
Two escape hatches:
|
|
79
|
+
|
|
80
|
+
- `client.call(tool, params)` posts a raw body to any tool without local
|
|
81
|
+
validation, for a parameter the spec does not list or a tool newer than this
|
|
82
|
+
SDK.
|
|
83
|
+
- `client.describe(tool)` fetches the tool's self-description (`ToolInfo`: price,
|
|
84
|
+
JSON Schema, example) with no API key.
|
|
85
|
+
|
|
86
|
+
### Per-call timeout and retries
|
|
87
|
+
|
|
88
|
+
`with_options()` returns a copy of the client sharing the same connection pool:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
client.with_options(timeout=10.0, max_retries=0).fetch_url(url="https://example.com")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The tool parameter `timeout` (milliseconds, on `fetch_url` and ten other tools)
|
|
95
|
+
is unrelated to the HTTP timeout, which is why the HTTP one is not a keyword on
|
|
96
|
+
the tool methods.
|
|
97
|
+
|
|
98
|
+
## Tools and credits
|
|
99
|
+
|
|
100
|
+
Prices are the `x-credits` of each operation in the spec. An error response,
|
|
101
|
+
whatever its status, charges nothing: credits are deducted only after a tool
|
|
102
|
+
succeeded.
|
|
103
|
+
|
|
104
|
+
<!-- generated:tools:start -->
|
|
105
|
+
| Method | Credits | Note | Docs |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `agent` | 8 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/agent) |
|
|
108
|
+
| `analyze_content` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/analyze-content) |
|
|
109
|
+
| `batch_scrape` | 5 | 5 per URL attempted (skipped URLs are not charged) | [docs](https://www.crawlforge.dev/docs/api-reference/tools/batch-scrape) |
|
|
110
|
+
| `crawl_deep` | 4 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/crawl-deep) |
|
|
111
|
+
| `deep_research` | 10 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/deep-research) |
|
|
112
|
+
| `extract_content` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-content) |
|
|
113
|
+
| `extract_embedded_state` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-embedded-state) |
|
|
114
|
+
| `extract_links` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-links) |
|
|
115
|
+
| `extract_metadata` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-metadata) |
|
|
116
|
+
| `extract_structured` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-structured) |
|
|
117
|
+
| `extract_text` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-text) |
|
|
118
|
+
| `extract_with_llm` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/extract-with-llm) |
|
|
119
|
+
| `fetch_url` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/fetch-url) |
|
|
120
|
+
| `generate_llms_txt` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/generate-llms-txt) |
|
|
121
|
+
| `get_batch_results` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/get-batch-results) |
|
|
122
|
+
| `list_ollama_models` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/list-ollama-models) |
|
|
123
|
+
| `localization` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/localization) |
|
|
124
|
+
| `map_site` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/map-site) |
|
|
125
|
+
| `process_document` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/process-document) |
|
|
126
|
+
| `read_result` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/read-result) |
|
|
127
|
+
| `reddit_search` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/reddit-search) |
|
|
128
|
+
| `scrape` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape) |
|
|
129
|
+
| `scrape_structured` | 2 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-structured) |
|
|
130
|
+
| `scrape_template` | 1 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-template) |
|
|
131
|
+
| `scrape_with_actions` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/scrape-with-actions) |
|
|
132
|
+
| `search_web` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/search-web) |
|
|
133
|
+
| `serp_rank` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/serp-rank) |
|
|
134
|
+
| `stealth_mode` | 5 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/stealth-mode) |
|
|
135
|
+
| `summarize_content` | 4 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/summarize-content) |
|
|
136
|
+
| `track_changes` | 3 | | [docs](https://www.crawlforge.dev/docs/api-reference/tools/track-changes) |
|
|
137
|
+
<!-- generated:tools:end -->
|
|
138
|
+
|
|
139
|
+
## Errors
|
|
140
|
+
|
|
141
|
+
All errors derive from `crawlforge.CrawlForgeError`, with `status`, `code`,
|
|
142
|
+
`message`, `details` and `request_id`. The class follows the HTTP status:
|
|
143
|
+
|
|
144
|
+
| Status | Class | Extra attributes |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| 400 | `ValidationError` | `details` is the list of validation issues |
|
|
147
|
+
| 401 | `AuthenticationError` | |
|
|
148
|
+
| 402 | `InsufficientCreditsError` | `auto_recharge_triggered` (from the `X-Auto-Recharge-Triggered` header) |
|
|
149
|
+
| 429 | `RateLimitError` | `retry_after` in seconds, `None` when absent; `code` is `RATE_LIMIT_EXCEEDED` or `HOST_BACKOFF` |
|
|
150
|
+
| other | `ToolError` | `blocked` (vendor and evidence when a bot wall was met) and `escalated` |
|
|
151
|
+
|
|
152
|
+
A response that is not JSON is a `CrawlForgeError` with code
|
|
153
|
+
`UNEXPECTED_RESPONSE` and the raw text in `details`; a connection or timeout
|
|
154
|
+
failure is code `NETWORK_ERROR` with `status` 0. No error message ever contains
|
|
155
|
+
your API key.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from crawlforge import CrawlForge, InsufficientCreditsError, RateLimitError, ToolError
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
result = client.scrape(url="https://example.com", escalate=True)
|
|
162
|
+
except RateLimitError as e:
|
|
163
|
+
print("slow down", e.retry_after)
|
|
164
|
+
except InsufficientCreditsError as e:
|
|
165
|
+
print("top up", e.auto_recharge_triggered)
|
|
166
|
+
except ToolError as e:
|
|
167
|
+
print(e.code, e.blocked)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Retries
|
|
171
|
+
|
|
172
|
+
Only a 429 is retried, and only `max_retries` times (default 2, so up to three
|
|
173
|
+
requests). The wait is the `Retry-After` header when present (seconds or an HTTP
|
|
174
|
+
date, capped at 60 s), otherwise exponential backoff from 1 s with ±20 % jitter.
|
|
175
|
+
A 5xx is never retried, because a failed call is not charged but a repeated
|
|
176
|
+
one might be. After the last retry the `RateLimitError` is raised.
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
CrawlForge(
|
|
182
|
+
api_key=None, # default: CRAWLFORGE_API_KEY
|
|
183
|
+
base_url=None, # default: https://www.crawlforge.dev/api/v1
|
|
184
|
+
max_retries=2,
|
|
185
|
+
timeout=60.0, # seconds, per request
|
|
186
|
+
http_client=None, # your own httpx.Client (AsyncClient for AsyncCrawlForge)
|
|
187
|
+
)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Passing `http_client` is also the test seam: an `httpx.Client` on an
|
|
191
|
+
`httpx.MockTransport` gives a fixture server with no sockets. The SDK stores
|
|
192
|
+
nothing: results live only in the `ToolResult` you hold.
|
|
193
|
+
|
|
194
|
+
## Python 3.9+
|
|
195
|
+
|
|
196
|
+
The generated models use `Optional[...]`, `List[...]` and `Dict[...]` rather
|
|
197
|
+
than `X | Y`, which pydantic cannot evaluate on 3.9. Tested on 3.9 through 3.13.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
python -m venv .venv && . .venv/bin/activate
|
|
203
|
+
pip install -e ".[dev]"
|
|
204
|
+
python scripts/generate.py # or: make generate
|
|
205
|
+
ruff check . && mypy crawlforge scripts && pytest
|
|
206
|
+
python -m build
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`crawlforge/_generated/models.py`, `crawlforge/_generated/tools.py` and the
|
|
210
|
+
table above are generated from `openapi.json` by `scripts/generate.py`
|
|
211
|
+
(`scripts/gen_models.py` is the generator) and committed; CI regenerates and
|
|
212
|
+
fails on any diff. `openapi.json` is copied verbatim from
|
|
213
|
+
https://www.crawlforge.dev/openapi.json and never edited by hand.
|
|
214
|
+
|
|
215
|
+
The generator is in-repo and standard-library only. It replaced
|
|
216
|
+
`datamodel-code-generator` 0.76.2, which on this spec has no Python 3.9 target
|
|
217
|
+
(it emits `X | None`), splits the nine request schemas with a top-level
|
|
218
|
+
`anyOf`/`oneOf`/`allOf` cross-field rule into `RootModel` unions, maps
|
|
219
|
+
`format: uri` to `AnyUrl` (which rewrites URLs before they are sent) and
|
|
220
|
+
`format: email` to `EmailStr` (an extra dependency). The in-repo generator keeps
|
|
221
|
+
wire names, ignores the cross-field rules locally (the server enforces them),
|
|
222
|
+
skips `pattern` (the spec's lookaheads are not supported by pydantic's default
|
|
223
|
+
regex engine) and forbids unknown keys on request models.
|
|
224
|
+
|
|
225
|
+
## Links
|
|
226
|
+
|
|
227
|
+
- API reference: https://www.crawlforge.dev/docs/api-reference
|
|
228
|
+
- OpenAPI spec: https://www.crawlforge.dev/openapi.json
|
|
229
|
+
- API keys: https://www.crawlforge.dev/dashboard/keys
|
|
230
|
+
- Changelog: [CHANGELOG.md](CHANGELOG.md)
|
|
231
|
+
|
|
232
|
+
MIT licensed.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""CrawlForge Python SDK: 30 metered web tools behind one API key.
|
|
2
|
+
|
|
3
|
+
from crawlforge import CrawlForge
|
|
4
|
+
|
|
5
|
+
client = CrawlForge() # reads CRAWLFORGE_API_KEY
|
|
6
|
+
result = client.scrape(url="https://example.com", formats=["markdown"])
|
|
7
|
+
print(result.data["formats"]["markdown"], result.credits_remaining)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from crawlforge._client import AsyncCrawlForge, CrawlForge
|
|
11
|
+
from crawlforge._generated.models import ToolInfo
|
|
12
|
+
from crawlforge._generated.tools import TOOLS
|
|
13
|
+
from crawlforge._version import __version__
|
|
14
|
+
from crawlforge.errors import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
CrawlForgeError,
|
|
17
|
+
InsufficientCreditsError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
ToolError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
from crawlforge.types import ToolResult, ToolSpec
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AsyncCrawlForge",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"CrawlForge",
|
|
28
|
+
"CrawlForgeError",
|
|
29
|
+
"InsufficientCreditsError",
|
|
30
|
+
"RateLimitError",
|
|
31
|
+
"TOOLS",
|
|
32
|
+
"ToolError",
|
|
33
|
+
"ToolInfo",
|
|
34
|
+
"ToolResult",
|
|
35
|
+
"ToolSpec",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|