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.
Files changed (115) hide show
  1. {docpull-4.1.0/src/docpull.egg-info → docpull-4.3.0}/PKG-INFO +211 -22
  2. {docpull-4.1.0 → docpull-4.3.0}/README.md +207 -21
  3. {docpull-4.1.0 → docpull-4.3.0}/pyproject.toml +5 -1
  4. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/__init__.py +19 -2
  5. docpull-4.3.0/src/docpull/benchmark.py +3497 -0
  6. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cli.py +22 -5
  7. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/markdown.py +50 -0
  8. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/special_cases.py +235 -19
  9. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/core/fetcher.py +84 -16
  10. docpull-4.3.0/src/docpull/judge.py +355 -0
  11. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/config.py +2 -1
  12. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/profiles.py +12 -0
  13. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pack_tools.py +29 -12
  14. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/parallel_workflows.py +140 -121
  15. docpull-4.3.0/src/docpull/passk.py +200 -0
  16. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/manifest.py +4 -2
  17. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/__init__.py +5 -1
  18. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_ndjson.py +62 -2
  19. docpull-4.3.0/src/docpull/pipeline/steps/save_okf.py +372 -0
  20. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_sqlite.py +91 -0
  21. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/validate.py +10 -0
  22. docpull-4.3.0/src/docpull/provider_cli.py +426 -0
  23. docpull-4.3.0/src/docpull/provider_keys.py +217 -0
  24. docpull-4.3.0/src/docpull/scraper.py +351 -0
  25. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/source_scoring.py +10 -2
  26. {docpull-4.1.0 → docpull-4.3.0/src/docpull.egg-info}/PKG-INFO +211 -22
  27. {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/SOURCES.txt +12 -0
  28. {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/requires.txt +4 -0
  29. docpull-4.3.0/tests/test_benchmark.py +829 -0
  30. {docpull-4.1.0 → docpull-4.3.0}/tests/test_ci_policy.py +15 -0
  31. {docpull-4.1.0 → docpull-4.3.0}/tests/test_cli.py +29 -0
  32. {docpull-4.1.0 → docpull-4.3.0}/tests/test_conversion.py +23 -0
  33. docpull-4.3.0/tests/test_document_record.py +53 -0
  34. {docpull-4.1.0 → docpull-4.3.0}/tests/test_integration.py +16 -0
  35. docpull-4.3.0/tests/test_judge.py +128 -0
  36. {docpull-4.1.0 → docpull-4.3.0}/tests/test_naming.py +26 -1
  37. docpull-4.3.0/tests/test_outputs_e2e.py +357 -0
  38. {docpull-4.1.0 → docpull-4.3.0}/tests/test_pack_tools.py +42 -0
  39. {docpull-4.1.0 → docpull-4.3.0}/tests/test_parallel_workflows.py +41 -0
  40. docpull-4.3.0/tests/test_passk.py +83 -0
  41. docpull-4.3.0/tests/test_provider_cli.py +85 -0
  42. {docpull-4.1.0 → docpull-4.3.0}/tests/test_save_ndjson.py +28 -0
  43. {docpull-4.1.0 → docpull-4.3.0}/tests/test_save_sqlite.py +33 -1
  44. {docpull-4.1.0 → docpull-4.3.0}/tests/test_special_cases.py +134 -0
  45. docpull-4.1.0/tests/test_outputs_e2e.py +0 -120
  46. {docpull-4.1.0 → docpull-4.3.0}/LICENSE +0 -0
  47. {docpull-4.1.0 → docpull-4.3.0}/setup.cfg +0 -0
  48. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/__main__.py +0 -0
  49. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/__init__.py +0 -0
  50. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/frontier.py +0 -0
  51. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/manager.py +0 -0
  52. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/cache/streaming_dedup.py +0 -0
  53. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/__init__.py +0 -0
  54. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/chunking.py +0 -0
  55. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/extractor.py +0 -0
  56. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/protocols.py +0 -0
  57. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  58. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/core/__init__.py +0 -0
  59. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/__init__.py +0 -0
  60. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/_fetch.py +0 -0
  61. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/composite.py +0 -0
  62. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/crawler.py +0 -0
  63. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/filters.py +0 -0
  64. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  65. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  66. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  67. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  68. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/protocols.py +0 -0
  69. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/discovery/sitemap.py +0 -0
  70. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/doctor.py +0 -0
  71. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/fixtures/__init__.py +0 -0
  72. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  73. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/__init__.py +0 -0
  74. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/client.py +0 -0
  75. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/protocols.py +0 -0
  76. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/http/rate_limiter.py +0 -0
  77. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/__init__.py +0 -0
  78. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/server.py +0 -0
  79. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/sources.py +0 -0
  80. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/mcp/tools.py +0 -0
  81. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/metadata_extractor.py +0 -0
  82. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/__init__.py +0 -0
  83. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/document.py +0 -0
  84. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/events.py +0 -0
  85. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/models/run.py +0 -0
  86. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/__init__.py +0 -0
  87. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/base.py +0 -0
  88. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  89. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/convert.py +0 -0
  90. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  91. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/fetch.py +0 -0
  92. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/metadata.py +0 -0
  93. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save.py +0 -0
  94. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  95. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/py.typed +0 -0
  96. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/__init__.py +0 -0
  97. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/download_policy.py +0 -0
  98. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/robots.py +0 -0
  99. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/security/url_validator.py +0 -0
  100. {docpull-4.1.0 → docpull-4.3.0}/src/docpull/time_utils.py +0 -0
  101. {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  102. {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/entry_points.txt +0 -0
  103. {docpull-4.1.0 → docpull-4.3.0}/src/docpull.egg-info/top_level.txt +0 -0
  104. {docpull-4.1.0 → docpull-4.3.0}/tests/test_cache_conditional_get.py +0 -0
  105. {docpull-4.1.0 → docpull-4.3.0}/tests/test_chunking.py +0 -0
  106. {docpull-4.1.0 → docpull-4.3.0}/tests/test_convert_step_new.py +0 -0
  107. {docpull-4.1.0 → docpull-4.3.0}/tests/test_discovery.py +0 -0
  108. {docpull-4.1.0 → docpull-4.3.0}/tests/test_frontier_resume.py +0 -0
  109. {docpull-4.1.0 → docpull-4.3.0}/tests/test_link_extractors.py +0 -0
  110. {docpull-4.1.0 → docpull-4.3.0}/tests/test_mcp_server.py +0 -0
  111. {docpull-4.1.0 → docpull-4.3.0}/tests/test_mcp_tools.py +0 -0
  112. {docpull-4.1.0 → docpull-4.3.0}/tests/test_pipeline.py +0 -0
  113. {docpull-4.1.0 → docpull-4.3.0}/tests/test_real_site_regressions.py +0 -0
  114. {docpull-4.1.0 → docpull-4.3.0}/tests/test_security_hardening.py +0 -0
  115. {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.1.0
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 static documentation sites into clean, AI-ready Markdown — fast.**
99
+ **Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
97
100
 
98
101
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
99
102
  [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
100
- [![Downloads](https://pepy.tech/badge/docpull)](https://pepy.tech/project/docpull)
103
+ [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
104
+ [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
101
105
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](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
- docpull uses async HTTP (not Playwright) to fetch server-rendered pages,
110
- extracts main content, and writes clean Markdown with source-URL frontmatter —
111
- in seconds, with a small install footprint. It won't render JavaScript, but for
112
- the large class of docs that don't need it (API references, Python/Go stdlib,
113
- most dev-tool docs, OpenAPI specs, Next.js and Docusaurus builds), it is a
114
- fast, auditable, sandbox-friendly way to pipe documentation into an LLM context,
115
- a RAG index, or an offline archive. SSRF, XXE, DNS-rebinding, and
116
- CRLF-injection protections are on by default — a necessity when an AI agent
117
- is choosing the URLs.
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
- # Crawl and save Markdown
162
+ # Scrape a page graph and save Markdown
135
163
  docpull https://docs.example.com
136
164
 
137
- # One page, no crawl — the fast path for agents
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
- # Mirror a site for offline use
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| Detected and tagged; generic extractor produces Markdown |
158
- | Sphinx | Detected from generator metadata / Read the Docs hosts and tagged; generic extractor produces Markdown |
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
- The pack contains:
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 paths, and
481
- chunk counts so regenerated corpora can be diffed and cited by agents.
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 static documentation sites into clean, AI-ready Markdown — fast.**
3
+ **Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
4
4
 
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
7
- [![Downloads](https://pepy.tech/badge/docpull)](https://pepy.tech/project/docpull)
7
+ [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
8
+ [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
8
9
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](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
- docpull uses async HTTP (not Playwright) to fetch server-rendered pages,
17
- extracts main content, and writes clean Markdown with source-URL frontmatter —
18
- in seconds, with a small install footprint. It won't render JavaScript, but for
19
- the large class of docs that don't need it (API references, Python/Go stdlib,
20
- most dev-tool docs, OpenAPI specs, Next.js and Docusaurus builds), it is a
21
- fast, auditable, sandbox-friendly way to pipe documentation into an LLM context,
22
- a RAG index, or an offline archive. SSRF, XXE, DNS-rebinding, and
23
- CRLF-injection protections are on by default — a necessity when an AI agent
24
- is choosing the URLs.
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
- # Crawl and save Markdown
66
+ # Scrape a page graph and save Markdown
42
67
  docpull https://docs.example.com
43
68
 
44
- # One page, no crawl — the fast path for agents
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
- # Mirror a site for offline use
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| Detected and tagged; generic extractor produces Markdown |
65
- | Sphinx | Detected from generator metadata / Read the Docs hosts and tagged; generic extractor produces Markdown |
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
- The pack contains:
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 paths, and
388
- chunk counts so regenerated corpora can be diffed and cited by agents.
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