docpull 4.1.0__tar.gz → 4.3.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.
- {docpull-4.1.0/src/docpull.egg-info → docpull-4.3.0}/PKG-INFO +211 -22
- {docpull-4.1.0 → docpull-4.3.0}/README.md +207 -21
- {docpull-4.1.0 → docpull-4.3.0}/pyproject.toml +5 -1
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/__init__.py +19 -2
- docpull-4.3.0/src/docpull/benchmark.py +3497 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cli.py +22 -5
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/markdown.py +50 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/special_cases.py +235 -19
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/core/fetcher.py +84 -16
- docpull-4.3.0/src/docpull/judge.py +355 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/config.py +2 -1
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/profiles.py +12 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pack_tools.py +29 -12
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/parallel_workflows.py +140 -121
- docpull-4.3.0/src/docpull/passk.py +200 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/manifest.py +4 -2
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/__init__.py +5 -1
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_ndjson.py +62 -2
- docpull-4.3.0/src/docpull/pipeline/steps/save_okf.py +372 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_sqlite.py +91 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/validate.py +10 -0
- docpull-4.3.0/src/docpull/provider_cli.py +426 -0
- docpull-4.3.0/src/docpull/provider_keys.py +217 -0
- docpull-4.3.0/src/docpull/scraper.py +351 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/source_scoring.py +10 -2
- {docpull-4.1.0 → docpull-4.3.0/src/docpull.egg-info}/PKG-INFO +211 -22
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/SOURCES.txt +12 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/requires.txt +4 -0
- docpull-4.3.0/tests/test_benchmark.py +829 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_ci_policy.py +15 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_cli.py +29 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_conversion.py +23 -0
- docpull-4.3.0/tests/test_document_record.py +53 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_integration.py +16 -0
- docpull-4.3.0/tests/test_judge.py +128 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_naming.py +26 -1
- docpull-4.3.0/tests/test_outputs_e2e.py +357 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_pack_tools.py +42 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_parallel_workflows.py +41 -0
- docpull-4.3.0/tests/test_passk.py +83 -0
- docpull-4.3.0/tests/test_provider_cli.py +85 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_save_ndjson.py +28 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_save_sqlite.py +33 -1
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_special_cases.py +134 -0
- docpull-4.1.0/tests/test_outputs_e2e.py +0 -120
- {docpull-4.1.0 → docpull-4.3.0}/LICENSE +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/setup.cfg +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/__main__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/frontier.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/manager.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/chunking.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/extractor.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/protocols.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/core/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/composite.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/crawler.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/filters.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/protocols.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/doctor.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/client.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/protocols.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/server.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/sources.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/tools.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/metadata_extractor.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/document.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/events.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/run.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/base.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/convert.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/py.typed +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/__init__.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/download_policy.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/robots.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/url_validator.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull/time_utils.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_cache_conditional_get.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_chunking.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_convert_step_new.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_discovery.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_frontier_resume.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_link_extractors.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_mcp_server.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_mcp_tools.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_pipeline.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_real_site_regressions.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_security_hardening.py +0 -0
- {docpull-4.1.0 → docpull-4.3.0}/tests/test_time_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docpull
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: Pull documentation from the web and convert to clean markdown
|
|
5
5
|
Author-email: Zachary Roth <support@raintree.technology>
|
|
6
6
|
Maintainer-email: Raintree Technology <support@raintree.technology>
|
|
@@ -64,6 +64,8 @@ Requires-Dist: python-multipart>=0.0.27; extra == "mcp"
|
|
|
64
64
|
Requires-Dist: starlette>=1.0.1; extra == "mcp"
|
|
65
65
|
Provides-Extra: parallel
|
|
66
66
|
Requires-Dist: parallel-web>=1.0.1; extra == "parallel"
|
|
67
|
+
Provides-Extra: observability
|
|
68
|
+
Requires-Dist: raindrop-ai>=0.0.50; extra == "observability"
|
|
67
69
|
Provides-Extra: llm
|
|
68
70
|
Requires-Dist: tiktoken>=0.7.0; extra == "llm"
|
|
69
71
|
Provides-Extra: all
|
|
@@ -76,6 +78,7 @@ Requires-Dist: pyjwt>=2.13.0; extra == "all"
|
|
|
76
78
|
Requires-Dist: python-multipart>=0.0.27; extra == "all"
|
|
77
79
|
Requires-Dist: starlette>=1.0.1; extra == "all"
|
|
78
80
|
Requires-Dist: parallel-web>=1.0.1; extra == "all"
|
|
81
|
+
Requires-Dist: raindrop-ai>=0.0.50; extra == "all"
|
|
79
82
|
Provides-Extra: dev
|
|
80
83
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
81
84
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -93,11 +96,12 @@ Dynamic: license-file
|
|
|
93
96
|
|
|
94
97
|
# docpull
|
|
95
98
|
|
|
96
|
-
**Security-hardened, browser-free crawler that turns
|
|
99
|
+
**Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
|
|
97
100
|
|
|
98
101
|
[](https://www.python.org/downloads/)
|
|
99
102
|
[](https://badge.fury.io/py/docpull)
|
|
100
|
-
[](https://pepy.tech/project/docpull)
|
|
104
|
+
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
101
105
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
102
106
|
|
|
103
107
|
<p align="center">
|
|
@@ -106,15 +110,37 @@ Dynamic: license-file
|
|
|
106
110
|
</a>
|
|
107
111
|
</p>
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
## Download History
|
|
114
|
+
|
|
115
|
+
<a href="https://pepy.tech/project/docpull">
|
|
116
|
+
<img alt="Cumulative PyPI download history chart for docpull" src="docs/cumulative-downloads-history.svg" />
|
|
117
|
+
</a>
|
|
118
|
+
|
|
119
|
+
## Star History
|
|
120
|
+
|
|
121
|
+
<a href="https://star-history.com/#raintree-technology/docpull&Date">
|
|
122
|
+
<picture>
|
|
123
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date&theme=dark" />
|
|
124
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
125
|
+
<img alt="Star history chart for raintree-technology/docpull" src="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
126
|
+
</picture>
|
|
127
|
+
</a>
|
|
128
|
+
|
|
129
|
+
docpull is a web scraper for static and server-rendered sites, with
|
|
130
|
+
documentation crawling as its sharpest default workflow. It uses async HTTP (not
|
|
131
|
+
Playwright) to fetch pages, discover links, extract main content, and write clean
|
|
132
|
+
Markdown with source-URL frontmatter — in seconds, with a small install
|
|
133
|
+
footprint. It won't render JavaScript, but for the large class of sites that
|
|
134
|
+
don't need it (API references, vendor docs, Python/Go stdlib, blogs, OpenAPI
|
|
135
|
+
specs, Next.js and Docusaurus builds), it is a fast, auditable,
|
|
136
|
+
sandbox-friendly way to pipe web content into an LLM context, a RAG index, or an
|
|
137
|
+
offline archive. SSRF, XXE, DNS-rebinding, and CRLF-injection protections are on
|
|
138
|
+
by default — a necessity when an AI agent is choosing the URLs.
|
|
139
|
+
|
|
140
|
+
docpull is intentionally not a general browser-automation scraper. See
|
|
141
|
+
[`docs/scraping-boundary.md`](docs/scraping-boundary.md) for the exact product
|
|
142
|
+
boundary and when to use Scrapy, Crawlee, hosted extraction APIs, or trafilatura
|
|
143
|
+
directly.
|
|
118
144
|
|
|
119
145
|
## Install
|
|
120
146
|
|
|
@@ -125,22 +151,27 @@ pip install docpull
|
|
|
125
151
|
pip install 'docpull[llm]' # tiktoken for token-accurate chunking
|
|
126
152
|
pip install 'docpull[trafilatura]' # alternative extractor for noisy pages
|
|
127
153
|
pip install 'docpull[mcp]' # run as an MCP server for AI agents
|
|
154
|
+
pip install 'docpull[parallel]' # Parallel API context packs
|
|
155
|
+
pip install 'docpull[observability]' # Raindrop benchmark tracing
|
|
128
156
|
pip install 'docpull[all]' # everything above
|
|
129
157
|
```
|
|
130
158
|
|
|
131
159
|
## Quick start
|
|
132
160
|
|
|
133
161
|
```bash
|
|
134
|
-
#
|
|
162
|
+
# Scrape a page graph and save Markdown
|
|
135
163
|
docpull https://docs.example.com
|
|
136
164
|
|
|
137
|
-
#
|
|
165
|
+
# Scrape one page, no crawl — the fast path for agents
|
|
138
166
|
docpull https://docs.example.com/guide --single
|
|
139
167
|
|
|
140
168
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
141
169
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
142
170
|
|
|
143
|
-
#
|
|
171
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
172
|
+
docpull https://docs.example.com --format okf
|
|
173
|
+
|
|
174
|
+
# Mirror scraped content for offline use
|
|
144
175
|
docpull https://docs.example.com --profile mirror --cache
|
|
145
176
|
```
|
|
146
177
|
|
|
@@ -154,8 +185,13 @@ content directly from framework data feeds:
|
|
|
154
185
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
155
186
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
156
187
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
157
|
-
| Docusaurus|
|
|
158
|
-
| Sphinx |
|
|
188
|
+
| Docusaurus| Extracts static article regions and tags framework metadata |
|
|
189
|
+
| Sphinx | Extracts static body/document regions and tags framework metadata |
|
|
190
|
+
| MkDocs / Material | Extracts static content regions and tags framework metadata |
|
|
191
|
+
| VitePress / VuePress | Extracts static doc regions and tags framework metadata |
|
|
192
|
+
| Astro Starlight | Extracts static markdown content regions and tags framework metadata |
|
|
193
|
+
| GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
|
|
194
|
+
| Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
|
|
159
195
|
|
|
160
196
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
161
197
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -175,6 +211,30 @@ can route elsewhere).
|
|
|
175
211
|
|
|
176
212
|
## Python API
|
|
177
213
|
|
|
214
|
+
Scraper-facing API:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import asyncio
|
|
218
|
+
from docpull import scrape_one, Scraper
|
|
219
|
+
|
|
220
|
+
async def main():
|
|
221
|
+
page = await scrape_one("https://docs.python.org/3/library/asyncio.html")
|
|
222
|
+
print(page.title, page.source_type)
|
|
223
|
+
print(page.text[:500])
|
|
224
|
+
|
|
225
|
+
scraper = Scraper()
|
|
226
|
+
run = await scraper.scrape_site(
|
|
227
|
+
"https://docs.example.com",
|
|
228
|
+
output_format="ndjson",
|
|
229
|
+
max_pages=100,
|
|
230
|
+
)
|
|
231
|
+
print(run.stats.pages_fetched, run.manifest_path)
|
|
232
|
+
|
|
233
|
+
asyncio.run(main())
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Core fetcher API:
|
|
237
|
+
|
|
178
238
|
```python
|
|
179
239
|
from docpull import fetch_one
|
|
180
240
|
|
|
@@ -219,6 +279,7 @@ async def tool_call(url: str) -> str:
|
|
|
219
279
|
```bash
|
|
220
280
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
221
281
|
docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
|
|
282
|
+
docpull https://site.com --profile okf # OKF bundle with generated index.md files.
|
|
222
283
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
223
284
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
224
285
|
```
|
|
@@ -274,7 +335,32 @@ value and reports the key source (`env`, `project_env`, `user_config`, or
|
|
|
274
335
|
`missing`). It does not make a live Parallel call or prove the key is valid. Use
|
|
275
336
|
`--json` when an agent or CI job needs machine-readable configuration status.
|
|
276
337
|
|
|
277
|
-
|
|
338
|
+
## Optional live providers
|
|
339
|
+
|
|
340
|
+
Parallel, Tavily, and Exa are equal optional live providers for benchmark and
|
|
341
|
+
provider context-pack workflows. You can configure zero, one, two, or all three
|
|
342
|
+
API keys. docpull looks for keys in this order: environment variables, project
|
|
343
|
+
`.env.local`, then `~/.config/docpull/secrets.env`. Missing providers are skipped
|
|
344
|
+
instead of failing provider-neutral runs.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
docpull providers auth --json
|
|
348
|
+
docpull providers init parallel
|
|
349
|
+
docpull providers init tavily
|
|
350
|
+
docpull providers init exa
|
|
351
|
+
|
|
352
|
+
docpull providers context-pack "Compare AI web-search APIs for agents" \
|
|
353
|
+
--provider auto \
|
|
354
|
+
--query "AI web search API" \
|
|
355
|
+
--include-domain docs.parallel.ai \
|
|
356
|
+
--output-dir ./packs/provider-comparison
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Use `--provider all` to request all three and record skipped-provider metadata
|
|
360
|
+
for keys or optional SDKs that are not configured. Use repeated `--provider`
|
|
361
|
+
values to run a specific subset.
|
|
362
|
+
|
|
363
|
+
A Parallel pack contains:
|
|
278
364
|
|
|
279
365
|
- `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
|
|
280
366
|
- `documents.ndjson` — chunked records for agents and RAG pipelines.
|
|
@@ -475,10 +561,42 @@ NDJSON (one record per page or chunk):
|
|
|
475
561
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
476
562
|
```
|
|
477
563
|
|
|
564
|
+
Open Knowledge Format:
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
docpull https://docs.example.com --format okf
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
OKF output is an opt-in Markdown bundle. Scraped pages are written as concept
|
|
571
|
+
documents with OKF frontmatter (`type`, `title`, `description`, `resource`,
|
|
572
|
+
`tags`, `timestamp` when source metadata exists) and docpull keeps `source` as a
|
|
573
|
+
compatibility extension. Generated `index.md` files are reserved for directory
|
|
574
|
+
listings, so URL landing pages use safe concept filenames such as `_root.md` or
|
|
575
|
+
`_page.md` instead of occupying `index.md`. The root `index.md` declares
|
|
576
|
+
`okf_version: "0.1"`; nested indexes stay plain listing files.
|
|
577
|
+
|
|
578
|
+
Because OKF treats every non-reserved `.md` file in the tree as a concept, write
|
|
579
|
+
OKF output to a clean or dedicated directory rather than mixing it with unrelated
|
|
580
|
+
Markdown files.
|
|
581
|
+
|
|
478
582
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
479
583
|
documents. The manifest records the run identity, output format, stable
|
|
480
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
481
|
-
|
|
584
|
+
`document_id` / `chunk_id` values, content hashes, file-backed relative output
|
|
585
|
+
paths (or the stdout marker for streamed NDJSON), and chunk counts so
|
|
586
|
+
regenerated corpora can be diffed and cited by agents. See
|
|
587
|
+
[`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
|
|
588
|
+
stability contract.
|
|
589
|
+
|
|
590
|
+
SQLite output includes an FTS5 index for local retrieval:
|
|
591
|
+
|
|
592
|
+
```python
|
|
593
|
+
from pathlib import Path
|
|
594
|
+
from docpull import search_sqlite_documents
|
|
595
|
+
|
|
596
|
+
hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
|
|
597
|
+
for hit in hits:
|
|
598
|
+
print(hit.url, hit.snippet)
|
|
599
|
+
```
|
|
482
600
|
|
|
483
601
|
## Security
|
|
484
602
|
|
|
@@ -499,9 +617,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
499
617
|
|
|
500
618
|
```
|
|
501
619
|
Core:
|
|
502
|
-
--profile {rag,mirror,quick,llm}
|
|
620
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
503
621
|
--single Fetch one URL (no crawl)
|
|
504
|
-
--format {markdown,json,ndjson,sqlite}
|
|
622
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
505
623
|
--stream Stream NDJSON to stdout
|
|
506
624
|
|
|
507
625
|
LLM / chunking:
|
|
@@ -547,6 +665,77 @@ Reproduce with `make benchmark` (requires `aiohttp`; runs the gated
|
|
|
547
665
|
benchmark in `tests/benchmarks/` and prints progress plus a JSON line you can
|
|
548
666
|
pipe into trend tooling).
|
|
549
667
|
|
|
668
|
+
For real-site and live-provider context-pack benchmarks, use the first-class
|
|
669
|
+
benchmark harness:
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
docpull benchmark quick
|
|
673
|
+
docpull benchmark quick --provider auto --max-estimated-cost 0.05
|
|
674
|
+
docpull benchmark quick --provider all --max-estimated-cost 0.10
|
|
675
|
+
docpull benchmark quick --target-set provider-matrix --provider all \
|
|
676
|
+
--max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
|
|
677
|
+
--max-estimated-cost 0.10
|
|
678
|
+
docpull benchmark article .bench/runs/<run>/benchmark.report.json
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
`docpull benchmark quick` writes `benchmark.report.json` and
|
|
682
|
+
`benchmark.summary.md` under `.bench/runs/<timestamp>/` by default. The core
|
|
683
|
+
cases measure an LLM-profile crawl plus a cached rerun. Passing `--provider auto`
|
|
684
|
+
runs every locally ready provider. Passing `--provider all` requests Parallel,
|
|
685
|
+
Tavily, and Exa, then records missing keys or optional SDKs in
|
|
686
|
+
`skipped_providers` without failing the benchmark. Live Parallel calls still use
|
|
687
|
+
the local `--max-estimated-cost` guard before any work starts.
|
|
688
|
+
|
|
689
|
+
Use `--target-set tool-docs` to run a cross-pull matrix across the Parallel,
|
|
690
|
+
Exa, Tavily, Raindrop, and DocPull documentation sites. Use `--target-set provider-matrix`
|
|
691
|
+
to add three low-cap adversarial public targets for JS-heavy docs, noisy
|
|
692
|
+
archived navigation, and freshness-sensitive pricing. Matrix runs skip the
|
|
693
|
+
cached core pass by default so the headline grid stays provider x target; pass
|
|
694
|
+
`--cached-pass` to force cache measurement across the matrix.
|
|
695
|
+
|
|
696
|
+
The compatibility flags `--parallel`, `--tavily`, and `--exa` still work as
|
|
697
|
+
provider aliases. Tavily Search + Extract and Exa Search-with-contents are
|
|
698
|
+
normalized into the same local
|
|
699
|
+
`documents.ndjson`, `corpus.manifest.json`, `sources.md`, provider `*.pack.json`,
|
|
700
|
+
`pack.score.json`, and `source.scores.json` artifacts as the core and Parallel
|
|
701
|
+
cases. The report keeps the legacy pack score and adds weighted benchmark
|
|
702
|
+
sub-scores for coverage, cleanliness, source fidelity, freshness, and density.
|
|
703
|
+
Keys can be exported as `PARALLEL_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY`
|
|
704
|
+
or stored together in `~/.config/docpull/secrets.env`; docpull does not write
|
|
705
|
+
keys into benchmark artifacts.
|
|
706
|
+
|
|
707
|
+
Tavily usage is credit-based, so set `TAVILY_CREDIT_USD` or pass
|
|
708
|
+
`--tavily-credit-usd` to convert credits into estimated dollars for cost
|
|
709
|
+
comparisons. Without that value, Tavily credits are still recorded in
|
|
710
|
+
`cost_units` but excluded from normalized USD totals.
|
|
711
|
+
|
|
712
|
+
To trace the benchmark in Raindrop for a publishable observability/eval loop,
|
|
713
|
+
install the optional SDK and pass `--trace raindrop`:
|
|
714
|
+
|
|
715
|
+
```bash
|
|
716
|
+
pip install 'docpull[parallel,observability]'
|
|
717
|
+
export PARALLEL_API_KEY="<your-parallel-api-key>"
|
|
718
|
+
export TAVILY_API_KEY="<your-tavily-api-key>"
|
|
719
|
+
export TAVILY_CREDIT_USD="<account-credit-value>"
|
|
720
|
+
export EXA_API_KEY="<your-exa-api-key>"
|
|
721
|
+
export RAINDROP_WRITE_KEY="<your-raindrop-write-key>"
|
|
722
|
+
docpull benchmark quick --target-set provider-matrix --provider all --trace raindrop \
|
|
723
|
+
--max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
|
|
724
|
+
--max-estimated-cost 0.10
|
|
725
|
+
docpull benchmark article .bench/runs/<run>/benchmark.report.json
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Raindrop traces are metadata-only by default: docpull records timings, counts,
|
|
729
|
+
scores, costs, selected URLs, provider, workflow, target, prompt, settings, and
|
|
730
|
+
artifact paths, but does not send scraped page content unless a future caller
|
|
731
|
+
explicitly adds that behavior. `RAINDROP_WRITE_KEY` can be exported or stored in
|
|
732
|
+
the same `~/.config/docpull/secrets.env` file as the provider keys.
|
|
733
|
+
|
|
734
|
+
When Raindrop tracing is enabled, DocPull also emits Raindrop signals for
|
|
735
|
+
attention-worthy benchmark cells: failed cases, low scores, slow cases,
|
|
736
|
+
high-cost cells, and score-dimension warnings. The benchmark report stores the
|
|
737
|
+
Raindrop event id and signal counts so traced runs can be audited later.
|
|
738
|
+
|
|
550
739
|
## Troubleshooting
|
|
551
740
|
|
|
552
741
|
```bash
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# docpull
|
|
2
2
|
|
|
3
|
-
**Security-hardened, browser-free crawler that turns
|
|
3
|
+
**Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
|
|
4
4
|
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://badge.fury.io/py/docpull)
|
|
7
|
-
[](https://pepy.tech/project/docpull)
|
|
8
|
+
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
8
9
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
9
10
|
|
|
10
11
|
<p align="center">
|
|
@@ -13,15 +14,37 @@
|
|
|
13
14
|
</a>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
## Download History
|
|
18
|
+
|
|
19
|
+
<a href="https://pepy.tech/project/docpull">
|
|
20
|
+
<img alt="Cumulative PyPI download history chart for docpull" src="docs/cumulative-downloads-history.svg" />
|
|
21
|
+
</a>
|
|
22
|
+
|
|
23
|
+
## Star History
|
|
24
|
+
|
|
25
|
+
<a href="https://star-history.com/#raintree-technology/docpull&Date">
|
|
26
|
+
<picture>
|
|
27
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date&theme=dark" />
|
|
28
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
29
|
+
<img alt="Star history chart for raintree-technology/docpull" src="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
30
|
+
</picture>
|
|
31
|
+
</a>
|
|
32
|
+
|
|
33
|
+
docpull is a web scraper for static and server-rendered sites, with
|
|
34
|
+
documentation crawling as its sharpest default workflow. It uses async HTTP (not
|
|
35
|
+
Playwright) to fetch pages, discover links, extract main content, and write clean
|
|
36
|
+
Markdown with source-URL frontmatter — in seconds, with a small install
|
|
37
|
+
footprint. It won't render JavaScript, but for the large class of sites that
|
|
38
|
+
don't need it (API references, vendor docs, Python/Go stdlib, blogs, OpenAPI
|
|
39
|
+
specs, Next.js and Docusaurus builds), it is a fast, auditable,
|
|
40
|
+
sandbox-friendly way to pipe web content into an LLM context, a RAG index, or an
|
|
41
|
+
offline archive. SSRF, XXE, DNS-rebinding, and CRLF-injection protections are on
|
|
42
|
+
by default — a necessity when an AI agent is choosing the URLs.
|
|
43
|
+
|
|
44
|
+
docpull is intentionally not a general browser-automation scraper. See
|
|
45
|
+
[`docs/scraping-boundary.md`](docs/scraping-boundary.md) for the exact product
|
|
46
|
+
boundary and when to use Scrapy, Crawlee, hosted extraction APIs, or trafilatura
|
|
47
|
+
directly.
|
|
25
48
|
|
|
26
49
|
## Install
|
|
27
50
|
|
|
@@ -32,22 +55,27 @@ pip install docpull
|
|
|
32
55
|
pip install 'docpull[llm]' # tiktoken for token-accurate chunking
|
|
33
56
|
pip install 'docpull[trafilatura]' # alternative extractor for noisy pages
|
|
34
57
|
pip install 'docpull[mcp]' # run as an MCP server for AI agents
|
|
58
|
+
pip install 'docpull[parallel]' # Parallel API context packs
|
|
59
|
+
pip install 'docpull[observability]' # Raindrop benchmark tracing
|
|
35
60
|
pip install 'docpull[all]' # everything above
|
|
36
61
|
```
|
|
37
62
|
|
|
38
63
|
## Quick start
|
|
39
64
|
|
|
40
65
|
```bash
|
|
41
|
-
#
|
|
66
|
+
# Scrape a page graph and save Markdown
|
|
42
67
|
docpull https://docs.example.com
|
|
43
68
|
|
|
44
|
-
#
|
|
69
|
+
# Scrape one page, no crawl — the fast path for agents
|
|
45
70
|
docpull https://docs.example.com/guide --single
|
|
46
71
|
|
|
47
72
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
48
73
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
49
74
|
|
|
50
|
-
#
|
|
75
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
76
|
+
docpull https://docs.example.com --format okf
|
|
77
|
+
|
|
78
|
+
# Mirror scraped content for offline use
|
|
51
79
|
docpull https://docs.example.com --profile mirror --cache
|
|
52
80
|
```
|
|
53
81
|
|
|
@@ -61,8 +89,13 @@ content directly from framework data feeds:
|
|
|
61
89
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
62
90
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
63
91
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
64
|
-
| Docusaurus|
|
|
65
|
-
| Sphinx |
|
|
92
|
+
| Docusaurus| Extracts static article regions and tags framework metadata |
|
|
93
|
+
| Sphinx | Extracts static body/document regions and tags framework metadata |
|
|
94
|
+
| MkDocs / Material | Extracts static content regions and tags framework metadata |
|
|
95
|
+
| VitePress / VuePress | Extracts static doc regions and tags framework metadata |
|
|
96
|
+
| Astro Starlight | Extracts static markdown content regions and tags framework metadata |
|
|
97
|
+
| GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
|
|
98
|
+
| Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
|
|
66
99
|
|
|
67
100
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
68
101
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -82,6 +115,30 @@ can route elsewhere).
|
|
|
82
115
|
|
|
83
116
|
## Python API
|
|
84
117
|
|
|
118
|
+
Scraper-facing API:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import asyncio
|
|
122
|
+
from docpull import scrape_one, Scraper
|
|
123
|
+
|
|
124
|
+
async def main():
|
|
125
|
+
page = await scrape_one("https://docs.python.org/3/library/asyncio.html")
|
|
126
|
+
print(page.title, page.source_type)
|
|
127
|
+
print(page.text[:500])
|
|
128
|
+
|
|
129
|
+
scraper = Scraper()
|
|
130
|
+
run = await scraper.scrape_site(
|
|
131
|
+
"https://docs.example.com",
|
|
132
|
+
output_format="ndjson",
|
|
133
|
+
max_pages=100,
|
|
134
|
+
)
|
|
135
|
+
print(run.stats.pages_fetched, run.manifest_path)
|
|
136
|
+
|
|
137
|
+
asyncio.run(main())
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Core fetcher API:
|
|
141
|
+
|
|
85
142
|
```python
|
|
86
143
|
from docpull import fetch_one
|
|
87
144
|
|
|
@@ -126,6 +183,7 @@ async def tool_call(url: str) -> str:
|
|
|
126
183
|
```bash
|
|
127
184
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
128
185
|
docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
|
|
186
|
+
docpull https://site.com --profile okf # OKF bundle with generated index.md files.
|
|
129
187
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
130
188
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
131
189
|
```
|
|
@@ -181,7 +239,32 @@ value and reports the key source (`env`, `project_env`, `user_config`, or
|
|
|
181
239
|
`missing`). It does not make a live Parallel call or prove the key is valid. Use
|
|
182
240
|
`--json` when an agent or CI job needs machine-readable configuration status.
|
|
183
241
|
|
|
184
|
-
|
|
242
|
+
## Optional live providers
|
|
243
|
+
|
|
244
|
+
Parallel, Tavily, and Exa are equal optional live providers for benchmark and
|
|
245
|
+
provider context-pack workflows. You can configure zero, one, two, or all three
|
|
246
|
+
API keys. docpull looks for keys in this order: environment variables, project
|
|
247
|
+
`.env.local`, then `~/.config/docpull/secrets.env`. Missing providers are skipped
|
|
248
|
+
instead of failing provider-neutral runs.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
docpull providers auth --json
|
|
252
|
+
docpull providers init parallel
|
|
253
|
+
docpull providers init tavily
|
|
254
|
+
docpull providers init exa
|
|
255
|
+
|
|
256
|
+
docpull providers context-pack "Compare AI web-search APIs for agents" \
|
|
257
|
+
--provider auto \
|
|
258
|
+
--query "AI web search API" \
|
|
259
|
+
--include-domain docs.parallel.ai \
|
|
260
|
+
--output-dir ./packs/provider-comparison
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Use `--provider all` to request all three and record skipped-provider metadata
|
|
264
|
+
for keys or optional SDKs that are not configured. Use repeated `--provider`
|
|
265
|
+
values to run a specific subset.
|
|
266
|
+
|
|
267
|
+
A Parallel pack contains:
|
|
185
268
|
|
|
186
269
|
- `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
|
|
187
270
|
- `documents.ndjson` — chunked records for agents and RAG pipelines.
|
|
@@ -382,10 +465,42 @@ NDJSON (one record per page or chunk):
|
|
|
382
465
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
383
466
|
```
|
|
384
467
|
|
|
468
|
+
Open Knowledge Format:
|
|
469
|
+
|
|
470
|
+
```bash
|
|
471
|
+
docpull https://docs.example.com --format okf
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
OKF output is an opt-in Markdown bundle. Scraped pages are written as concept
|
|
475
|
+
documents with OKF frontmatter (`type`, `title`, `description`, `resource`,
|
|
476
|
+
`tags`, `timestamp` when source metadata exists) and docpull keeps `source` as a
|
|
477
|
+
compatibility extension. Generated `index.md` files are reserved for directory
|
|
478
|
+
listings, so URL landing pages use safe concept filenames such as `_root.md` or
|
|
479
|
+
`_page.md` instead of occupying `index.md`. The root `index.md` declares
|
|
480
|
+
`okf_version: "0.1"`; nested indexes stay plain listing files.
|
|
481
|
+
|
|
482
|
+
Because OKF treats every non-reserved `.md` file in the tree as a concept, write
|
|
483
|
+
OKF output to a clean or dedicated directory rather than mixing it with unrelated
|
|
484
|
+
Markdown files.
|
|
485
|
+
|
|
385
486
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
386
487
|
documents. The manifest records the run identity, output format, stable
|
|
387
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
388
|
-
|
|
488
|
+
`document_id` / `chunk_id` values, content hashes, file-backed relative output
|
|
489
|
+
paths (or the stdout marker for streamed NDJSON), and chunk counts so
|
|
490
|
+
regenerated corpora can be diffed and cited by agents. See
|
|
491
|
+
[`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
|
|
492
|
+
stability contract.
|
|
493
|
+
|
|
494
|
+
SQLite output includes an FTS5 index for local retrieval:
|
|
495
|
+
|
|
496
|
+
```python
|
|
497
|
+
from pathlib import Path
|
|
498
|
+
from docpull import search_sqlite_documents
|
|
499
|
+
|
|
500
|
+
hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
|
|
501
|
+
for hit in hits:
|
|
502
|
+
print(hit.url, hit.snippet)
|
|
503
|
+
```
|
|
389
504
|
|
|
390
505
|
## Security
|
|
391
506
|
|
|
@@ -406,9 +521,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
406
521
|
|
|
407
522
|
```
|
|
408
523
|
Core:
|
|
409
|
-
--profile {rag,mirror,quick,llm}
|
|
524
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
410
525
|
--single Fetch one URL (no crawl)
|
|
411
|
-
--format {markdown,json,ndjson,sqlite}
|
|
526
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
412
527
|
--stream Stream NDJSON to stdout
|
|
413
528
|
|
|
414
529
|
LLM / chunking:
|
|
@@ -454,6 +569,77 @@ Reproduce with `make benchmark` (requires `aiohttp`; runs the gated
|
|
|
454
569
|
benchmark in `tests/benchmarks/` and prints progress plus a JSON line you can
|
|
455
570
|
pipe into trend tooling).
|
|
456
571
|
|
|
572
|
+
For real-site and live-provider context-pack benchmarks, use the first-class
|
|
573
|
+
benchmark harness:
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
docpull benchmark quick
|
|
577
|
+
docpull benchmark quick --provider auto --max-estimated-cost 0.05
|
|
578
|
+
docpull benchmark quick --provider all --max-estimated-cost 0.10
|
|
579
|
+
docpull benchmark quick --target-set provider-matrix --provider all \
|
|
580
|
+
--max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
|
|
581
|
+
--max-estimated-cost 0.10
|
|
582
|
+
docpull benchmark article .bench/runs/<run>/benchmark.report.json
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
`docpull benchmark quick` writes `benchmark.report.json` and
|
|
586
|
+
`benchmark.summary.md` under `.bench/runs/<timestamp>/` by default. The core
|
|
587
|
+
cases measure an LLM-profile crawl plus a cached rerun. Passing `--provider auto`
|
|
588
|
+
runs every locally ready provider. Passing `--provider all` requests Parallel,
|
|
589
|
+
Tavily, and Exa, then records missing keys or optional SDKs in
|
|
590
|
+
`skipped_providers` without failing the benchmark. Live Parallel calls still use
|
|
591
|
+
the local `--max-estimated-cost` guard before any work starts.
|
|
592
|
+
|
|
593
|
+
Use `--target-set tool-docs` to run a cross-pull matrix across the Parallel,
|
|
594
|
+
Exa, Tavily, Raindrop, and DocPull documentation sites. Use `--target-set provider-matrix`
|
|
595
|
+
to add three low-cap adversarial public targets for JS-heavy docs, noisy
|
|
596
|
+
archived navigation, and freshness-sensitive pricing. Matrix runs skip the
|
|
597
|
+
cached core pass by default so the headline grid stays provider x target; pass
|
|
598
|
+
`--cached-pass` to force cache measurement across the matrix.
|
|
599
|
+
|
|
600
|
+
The compatibility flags `--parallel`, `--tavily`, and `--exa` still work as
|
|
601
|
+
provider aliases. Tavily Search + Extract and Exa Search-with-contents are
|
|
602
|
+
normalized into the same local
|
|
603
|
+
`documents.ndjson`, `corpus.manifest.json`, `sources.md`, provider `*.pack.json`,
|
|
604
|
+
`pack.score.json`, and `source.scores.json` artifacts as the core and Parallel
|
|
605
|
+
cases. The report keeps the legacy pack score and adds weighted benchmark
|
|
606
|
+
sub-scores for coverage, cleanliness, source fidelity, freshness, and density.
|
|
607
|
+
Keys can be exported as `PARALLEL_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY`
|
|
608
|
+
or stored together in `~/.config/docpull/secrets.env`; docpull does not write
|
|
609
|
+
keys into benchmark artifacts.
|
|
610
|
+
|
|
611
|
+
Tavily usage is credit-based, so set `TAVILY_CREDIT_USD` or pass
|
|
612
|
+
`--tavily-credit-usd` to convert credits into estimated dollars for cost
|
|
613
|
+
comparisons. Without that value, Tavily credits are still recorded in
|
|
614
|
+
`cost_units` but excluded from normalized USD totals.
|
|
615
|
+
|
|
616
|
+
To trace the benchmark in Raindrop for a publishable observability/eval loop,
|
|
617
|
+
install the optional SDK and pass `--trace raindrop`:
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
pip install 'docpull[parallel,observability]'
|
|
621
|
+
export PARALLEL_API_KEY="<your-parallel-api-key>"
|
|
622
|
+
export TAVILY_API_KEY="<your-tavily-api-key>"
|
|
623
|
+
export TAVILY_CREDIT_USD="<account-credit-value>"
|
|
624
|
+
export EXA_API_KEY="<your-exa-api-key>"
|
|
625
|
+
export RAINDROP_WRITE_KEY="<your-raindrop-write-key>"
|
|
626
|
+
docpull benchmark quick --target-set provider-matrix --provider all --trace raindrop \
|
|
627
|
+
--max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
|
|
628
|
+
--max-estimated-cost 0.10
|
|
629
|
+
docpull benchmark article .bench/runs/<run>/benchmark.report.json
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Raindrop traces are metadata-only by default: docpull records timings, counts,
|
|
633
|
+
scores, costs, selected URLs, provider, workflow, target, prompt, settings, and
|
|
634
|
+
artifact paths, but does not send scraped page content unless a future caller
|
|
635
|
+
explicitly adds that behavior. `RAINDROP_WRITE_KEY` can be exported or stored in
|
|
636
|
+
the same `~/.config/docpull/secrets.env` file as the provider keys.
|
|
637
|
+
|
|
638
|
+
When Raindrop tracing is enabled, DocPull also emits Raindrop signals for
|
|
639
|
+
attention-worthy benchmark cells: failed cases, low scores, slow cases,
|
|
640
|
+
high-cost cells, and score-dimension warnings. The benchmark report stores the
|
|
641
|
+
Raindrop event id and signal counts so traced runs can be audited later.
|
|
642
|
+
|
|
457
643
|
## Troubleshooting
|
|
458
644
|
|
|
459
645
|
```bash
|