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.
Files changed (47) hide show
  1. crawlforge-0.1.0/.gitignore +12 -0
  2. crawlforge-0.1.0/CHANGELOG.md +18 -0
  3. crawlforge-0.1.0/LICENSE +21 -0
  4. crawlforge-0.1.0/Makefile +21 -0
  5. crawlforge-0.1.0/PKG-INFO +269 -0
  6. crawlforge-0.1.0/README.md +232 -0
  7. crawlforge-0.1.0/crawlforge/__init__.py +38 -0
  8. crawlforge-0.1.0/crawlforge/_client.py +300 -0
  9. crawlforge-0.1.0/crawlforge/_generated/__init__.py +1 -0
  10. crawlforge-0.1.0/crawlforge/_generated/models.py +665 -0
  11. crawlforge-0.1.0/crawlforge/_generated/tools.py +2246 -0
  12. crawlforge-0.1.0/crawlforge/_version.py +10 -0
  13. crawlforge-0.1.0/crawlforge/errors.py +193 -0
  14. crawlforge-0.1.0/crawlforge/models.py +9 -0
  15. crawlforge-0.1.0/crawlforge/py.typed +0 -0
  16. crawlforge-0.1.0/crawlforge/types.py +34 -0
  17. crawlforge-0.1.0/openapi.json +6509 -0
  18. crawlforge-0.1.0/pyproject.toml +82 -0
  19. crawlforge-0.1.0/scripts/gen_models.py +535 -0
  20. crawlforge-0.1.0/scripts/generate.py +43 -0
  21. crawlforge-0.1.0/scripts/smoke.py +29 -0
  22. crawlforge-0.1.0/tests/__init__.py +0 -0
  23. crawlforge-0.1.0/tests/conftest.py +86 -0
  24. crawlforge-0.1.0/tests/fixtures/describe_fetch_url.json +72 -0
  25. crawlforge-0.1.0/tests/fixtures/error_400.json +22 -0
  26. crawlforge-0.1.0/tests/fixtures/error_401.json +15 -0
  27. crawlforge-0.1.0/tests/fixtures/error_402.json +22 -0
  28. crawlforge-0.1.0/tests/fixtures/error_402_no_recharge.json +22 -0
  29. crawlforge-0.1.0/tests/fixtures/error_429.json +20 -0
  30. crawlforge-0.1.0/tests/fixtures/error_429_host_backoff.json +16 -0
  31. crawlforge-0.1.0/tests/fixtures/error_429_no_header.json +19 -0
  32. crawlforge-0.1.0/tests/fixtures/error_500.json +15 -0
  33. crawlforge-0.1.0/tests/fixtures/error_502_blocked.json +21 -0
  34. crawlforge-0.1.0/tests/fixtures/error_502_html.json +10 -0
  35. crawlforge-0.1.0/tests/fixtures/extract_links.json +55 -0
  36. crawlforge-0.1.0/tests/fixtures/extract_metadata.json +88 -0
  37. crawlforge-0.1.0/tests/fixtures/extract_text.json +36 -0
  38. crawlforge-0.1.0/tests/fixtures/fetch_url.json +41 -0
  39. crawlforge-0.1.0/tests/fixtures/map_site.json +37 -0
  40. crawlforge-0.1.0/tests/fixtures/read_result.json +49 -0
  41. crawlforge-0.1.0/tests/fixtures/scrape_structured.json +38 -0
  42. crawlforge-0.1.0/tests/fixtures/search_web.json +60 -0
  43. crawlforge-0.1.0/tests/test_client.py +321 -0
  44. crawlforge-0.1.0/tests/test_errors.py +194 -0
  45. crawlforge-0.1.0/tests/test_package.py +36 -0
  46. crawlforge-0.1.0/tests/test_retries.py +149 -0
  47. crawlforge-0.1.0/tests/test_spec.py +134 -0
@@ -0,0 +1,12 @@
1
+ .venv*/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.pyc
7
+ .env
8
+ .env.*
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ .DS_Store
@@ -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.
@@ -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
+ ]