docpull 4.2.0__tar.gz → 4.3.1__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.2.0/src/docpull.egg-info → docpull-4.3.1}/PKG-INFO +141 -23
- {docpull-4.2.0 → docpull-4.3.1}/README.md +132 -18
- {docpull-4.2.0 → docpull-4.3.1}/pyproject.toml +22 -6
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/__init__.py +19 -2
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/benchmark.py +666 -91
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/cli.py +14 -5
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/markdown.py +50 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/special_cases.py +235 -19
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/core/fetcher.py +84 -16
- docpull-4.3.1/src/docpull/judge.py +355 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/config.py +2 -1
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/profiles.py +12 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/parallel_workflows.py +105 -17
- docpull-4.3.1/src/docpull/passk.py +200 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/manifest.py +4 -2
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/__init__.py +5 -1
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/save_ndjson.py +6 -2
- docpull-4.3.1/src/docpull/pipeline/steps/save_okf.py +372 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/save_sqlite.py +91 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/validate.py +10 -0
- docpull-4.3.1/src/docpull/scraper.py +351 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/source_scoring.py +10 -2
- {docpull-4.2.0 → docpull-4.3.1/src/docpull.egg-info}/PKG-INFO +141 -23
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull.egg-info/SOURCES.txt +7 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_benchmark.py +184 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_ci_policy.py +15 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_cli.py +29 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_conversion.py +23 -0
- docpull-4.3.1/tests/test_document_record.py +53 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_integration.py +16 -0
- docpull-4.3.1/tests/test_judge.py +128 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_naming.py +26 -1
- docpull-4.3.1/tests/test_outputs_e2e.py +357 -0
- docpull-4.3.1/tests/test_passk.py +83 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_save_ndjson.py +20 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_save_sqlite.py +33 -1
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_special_cases.py +134 -0
- docpull-4.2.0/tests/test_outputs_e2e.py +0 -120
- {docpull-4.2.0 → docpull-4.3.1}/LICENSE +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/setup.cfg +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/__main__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/cache/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/cache/frontier.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/cache/manager.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/chunking.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/core/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/composite.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/crawler.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/filters.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/doctor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/http/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/http/client.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/http/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/mcp/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/mcp/server.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/mcp/sources.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/mcp/tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/metadata_extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/document.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/events.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/models/run.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pack_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/base.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/convert.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/provider_cli.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/provider_keys.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/py.typed +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/security/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/security/download_policy.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/security/robots.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/security/url_validator.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull/time_utils.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull.egg-info/requires.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_cache_conditional_get.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_chunking.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_convert_step_new.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_discovery.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_frontier_resume.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_link_extractors.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_mcp_server.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_mcp_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_pack_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_parallel_workflows.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_pipeline.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_provider_cli.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_real_site_regressions.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_security_hardening.py +0 -0
- {docpull-4.2.0 → docpull-4.3.1}/tests/test_time_utils.py +0 -0
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docpull
|
|
3
|
-
Version: 4.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 4.3.1
|
|
4
|
+
Summary: Convert public web pages into clean Markdown for AI agents and RAG
|
|
5
5
|
Author-email: Zachary Roth <support@raintree.technology>
|
|
6
6
|
Maintainer-email: Raintree Technology <support@raintree.technology>
|
|
7
7
|
License-Expression: MIT
|
|
8
|
-
Project-URL: Homepage, https://
|
|
9
|
-
Project-URL: Documentation, https://
|
|
8
|
+
Project-URL: Homepage, https://docpull.raintree.technology
|
|
9
|
+
Project-URL: Documentation, https://docpull.raintree.technology
|
|
10
10
|
Project-URL: Repository, https://github.com/raintree-technology/docpull
|
|
11
11
|
Project-URL: Source Code, https://github.com/raintree-technology/docpull
|
|
12
12
|
Project-URL: Bug Tracker, https://github.com/raintree-technology/docpull/issues
|
|
13
13
|
Project-URL: Releases, https://github.com/raintree-technology/docpull/releases
|
|
14
|
-
|
|
14
|
+
Project-URL: MCP Plugin, https://github.com/raintree-technology/docpull/tree/main/plugin
|
|
15
|
+
Project-URL: Comparison Guide, https://github.com/raintree-technology/docpull/blob/main/docs/alternatives.md
|
|
16
|
+
Project-URL: Download Stats, https://pepy.tech/project/docpull
|
|
17
|
+
Project-URL: Changelog, https://github.com/raintree-technology/docpull/blob/main/docs/CHANGELOG.md
|
|
18
|
+
Keywords: python,markdown,documentation,web-scraping,web-crawler,crawler,developer-tools,cli,rag,llm,mcp,ai-agents,claude,ai-training-data
|
|
15
19
|
Classifier: Development Status :: 5 - Production/Stable
|
|
16
20
|
Classifier: Intended Audience :: Developers
|
|
17
21
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -96,11 +100,12 @@ Dynamic: license-file
|
|
|
96
100
|
|
|
97
101
|
# docpull
|
|
98
102
|
|
|
99
|
-
**
|
|
103
|
+
**Public web to agent-ready Markdown. Fast, local, browser-free.**
|
|
100
104
|
|
|
101
105
|
[](https://www.python.org/downloads/)
|
|
102
106
|
[](https://badge.fury.io/py/docpull)
|
|
103
|
-
[](https://pepy.tech/project/docpull)
|
|
108
|
+
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
104
109
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
105
110
|
|
|
106
111
|
<p align="center">
|
|
@@ -109,16 +114,35 @@ Dynamic: license-file
|
|
|
109
114
|
</a>
|
|
110
115
|
</p>
|
|
111
116
|
|
|
112
|
-
docpull is a
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
docpull is a Python CLI, SDK, and MCP server for pulling public static and
|
|
118
|
+
server-rendered web pages into clean Markdown, NDJSON, OKF bundles, SQLite, or
|
|
119
|
+
local archives. Documentation crawling is its sharpest default workflow, but
|
|
120
|
+
the product is broader than docs: blogs, API references, vendor pages, OpenAPI
|
|
121
|
+
specs, and other server-rendered web content all fit. It uses async HTTP instead
|
|
122
|
+
of Playwright, preserves source metadata, and is built for agent-selected URLs
|
|
123
|
+
with SSRF, XXE, DNS-rebinding, and CRLF-injection protections enabled by
|
|
124
|
+
default.
|
|
125
|
+
|
|
126
|
+
Use docpull when you need to:
|
|
127
|
+
|
|
128
|
+
- Load public web pages, docs, API references, or blogs into an LLM context.
|
|
129
|
+
- Build inspectable source corpora for RAG/search pipelines.
|
|
130
|
+
- Give a coding agent a local MCP tool for fetching, caching, grepping, and
|
|
131
|
+
reading docs.
|
|
132
|
+
- Mirror public web content for offline work while keeping source attribution.
|
|
133
|
+
|
|
134
|
+
docpull intentionally does not render JavaScript. For JS-only apps, browser
|
|
135
|
+
automation, paid extraction APIs, or general web crawling, see
|
|
136
|
+
[`docs/scraping-boundary.md`](docs/scraping-boundary.md) and
|
|
137
|
+
[`docs/alternatives.md`](docs/alternatives.md).
|
|
138
|
+
|
|
139
|
+
## Try it in 60 seconds
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install docpull
|
|
143
|
+
docpull https://docs.python.org/3/library/asyncio.html --single
|
|
144
|
+
docpull https://docs.python.org/3/library/asyncio.html --profile llm --stream | jq .
|
|
145
|
+
```
|
|
122
146
|
|
|
123
147
|
## Install
|
|
124
148
|
|
|
@@ -146,10 +170,27 @@ docpull https://docs.example.com/guide --single
|
|
|
146
170
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
147
171
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
148
172
|
|
|
173
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
174
|
+
docpull https://example.com --format okf -o ./site-okf
|
|
175
|
+
|
|
149
176
|
# Mirror scraped content for offline use
|
|
150
177
|
docpull https://docs.example.com --profile mirror --cache
|
|
151
178
|
```
|
|
152
179
|
|
|
180
|
+
## Project traction
|
|
181
|
+
|
|
182
|
+
<a href="https://pepy.tech/project/docpull">
|
|
183
|
+
<img alt="Cumulative PyPI download history chart for docpull" src="docs/cumulative-downloads-history.svg" />
|
|
184
|
+
</a>
|
|
185
|
+
|
|
186
|
+
<a href="https://star-history.com/#raintree-technology/docpull&Date">
|
|
187
|
+
<picture>
|
|
188
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date&theme=dark" />
|
|
189
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
190
|
+
<img alt="Star history chart for raintree-technology/docpull" src="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
191
|
+
</picture>
|
|
192
|
+
</a>
|
|
193
|
+
|
|
153
194
|
## Framework-aware extraction
|
|
154
195
|
|
|
155
196
|
docpull inspects each page before running the generic extractor and can pull
|
|
@@ -160,8 +201,13 @@ content directly from framework data feeds:
|
|
|
160
201
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
161
202
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
162
203
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
163
|
-
| Docusaurus|
|
|
164
|
-
| Sphinx |
|
|
204
|
+
| Docusaurus| Extracts static article regions and tags framework metadata |
|
|
205
|
+
| Sphinx | Extracts static body/document regions and tags framework metadata |
|
|
206
|
+
| MkDocs / Material | Extracts static content regions and tags framework metadata |
|
|
207
|
+
| VitePress / VuePress | Extracts static doc regions and tags framework metadata |
|
|
208
|
+
| Astro Starlight | Extracts static markdown content regions and tags framework metadata |
|
|
209
|
+
| GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
|
|
210
|
+
| Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
|
|
165
211
|
|
|
166
212
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
167
213
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -181,6 +227,30 @@ can route elsewhere).
|
|
|
181
227
|
|
|
182
228
|
## Python API
|
|
183
229
|
|
|
230
|
+
Scraper-facing API:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import asyncio
|
|
234
|
+
from docpull import scrape_one, Scraper
|
|
235
|
+
|
|
236
|
+
async def main():
|
|
237
|
+
page = await scrape_one("https://docs.python.org/3/library/asyncio.html")
|
|
238
|
+
print(page.title, page.source_type)
|
|
239
|
+
print(page.text[:500])
|
|
240
|
+
|
|
241
|
+
scraper = Scraper()
|
|
242
|
+
run = await scraper.scrape_site(
|
|
243
|
+
"https://docs.example.com",
|
|
244
|
+
output_format="ndjson",
|
|
245
|
+
max_pages=100,
|
|
246
|
+
)
|
|
247
|
+
print(run.stats.pages_fetched, run.manifest_path)
|
|
248
|
+
|
|
249
|
+
asyncio.run(main())
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Core fetcher API:
|
|
253
|
+
|
|
184
254
|
```python
|
|
185
255
|
from docpull import fetch_one
|
|
186
256
|
|
|
@@ -225,6 +295,7 @@ async def tool_call(url: str) -> str:
|
|
|
225
295
|
```bash
|
|
226
296
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
227
297
|
docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
|
|
298
|
+
docpull https://site.com --profile okf # OKF bundle with generated index.md files.
|
|
228
299
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
229
300
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
230
301
|
```
|
|
@@ -506,10 +577,57 @@ NDJSON (one record per page or chunk):
|
|
|
506
577
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
507
578
|
```
|
|
508
579
|
|
|
580
|
+
Open Knowledge Format:
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
docpull https://example.com --format okf -o ./site-okf
|
|
584
|
+
|
|
585
|
+
# Equivalent profile form
|
|
586
|
+
docpull https://example.com --profile okf -o ./site-okf
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
A generated OKF bundle has a normal Markdown tree:
|
|
590
|
+
|
|
591
|
+
```text
|
|
592
|
+
site-okf/
|
|
593
|
+
index.md
|
|
594
|
+
corpus.manifest.json
|
|
595
|
+
_root.md
|
|
596
|
+
docs/
|
|
597
|
+
index.md
|
|
598
|
+
getting-started.md
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
OKF output is an opt-in Markdown bundle. Scraped pages are written as concept
|
|
602
|
+
documents with OKF frontmatter (`type`, `title`, `description`, `resource`,
|
|
603
|
+
`tags`, `timestamp` when source metadata exists) and docpull keeps `source` as a
|
|
604
|
+
compatibility extension. Generated `index.md` files are reserved for directory
|
|
605
|
+
listings, so URL landing pages use safe concept filenames such as `_root.md` or
|
|
606
|
+
`_page.md` instead of occupying `index.md`. The root `index.md` declares
|
|
607
|
+
`okf_version: "0.1"`; nested indexes stay plain listing files.
|
|
608
|
+
|
|
609
|
+
Because OKF treats every non-reserved `.md` file in the tree as a concept, write
|
|
610
|
+
OKF output to a clean or dedicated directory rather than mixing it with unrelated
|
|
611
|
+
Markdown files.
|
|
612
|
+
|
|
509
613
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
510
614
|
documents. The manifest records the run identity, output format, stable
|
|
511
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
512
|
-
|
|
615
|
+
`document_id` / `chunk_id` values, content hashes, file-backed relative output
|
|
616
|
+
paths (or the stdout marker for streamed NDJSON), and chunk counts so
|
|
617
|
+
regenerated corpora can be diffed and cited by agents. See
|
|
618
|
+
[`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
|
|
619
|
+
stability contract.
|
|
620
|
+
|
|
621
|
+
SQLite output includes an FTS5 index for local retrieval:
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
from pathlib import Path
|
|
625
|
+
from docpull import search_sqlite_documents
|
|
626
|
+
|
|
627
|
+
hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
|
|
628
|
+
for hit in hits:
|
|
629
|
+
print(hit.url, hit.snippet)
|
|
630
|
+
```
|
|
513
631
|
|
|
514
632
|
## Security
|
|
515
633
|
|
|
@@ -530,9 +648,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
530
648
|
|
|
531
649
|
```
|
|
532
650
|
Core:
|
|
533
|
-
--profile {rag,mirror,quick,llm}
|
|
651
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
534
652
|
--single Fetch one URL (no crawl)
|
|
535
|
-
--format {markdown,json,ndjson,sqlite}
|
|
653
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
536
654
|
--stream Stream NDJSON to stdout
|
|
537
655
|
|
|
538
656
|
LLM / chunking:
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# docpull
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Public web to agent-ready Markdown. Fast, local, browser-free.**
|
|
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,16 +14,35 @@
|
|
|
13
14
|
</a>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
|
-
docpull is a
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
docpull is a Python CLI, SDK, and MCP server for pulling public static and
|
|
18
|
+
server-rendered web pages into clean Markdown, NDJSON, OKF bundles, SQLite, or
|
|
19
|
+
local archives. Documentation crawling is its sharpest default workflow, but
|
|
20
|
+
the product is broader than docs: blogs, API references, vendor pages, OpenAPI
|
|
21
|
+
specs, and other server-rendered web content all fit. It uses async HTTP instead
|
|
22
|
+
of Playwright, preserves source metadata, and is built for agent-selected URLs
|
|
23
|
+
with SSRF, XXE, DNS-rebinding, and CRLF-injection protections enabled by
|
|
24
|
+
default.
|
|
25
|
+
|
|
26
|
+
Use docpull when you need to:
|
|
27
|
+
|
|
28
|
+
- Load public web pages, docs, API references, or blogs into an LLM context.
|
|
29
|
+
- Build inspectable source corpora for RAG/search pipelines.
|
|
30
|
+
- Give a coding agent a local MCP tool for fetching, caching, grepping, and
|
|
31
|
+
reading docs.
|
|
32
|
+
- Mirror public web content for offline work while keeping source attribution.
|
|
33
|
+
|
|
34
|
+
docpull intentionally does not render JavaScript. For JS-only apps, browser
|
|
35
|
+
automation, paid extraction APIs, or general web crawling, see
|
|
36
|
+
[`docs/scraping-boundary.md`](docs/scraping-boundary.md) and
|
|
37
|
+
[`docs/alternatives.md`](docs/alternatives.md).
|
|
38
|
+
|
|
39
|
+
## Try it in 60 seconds
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install docpull
|
|
43
|
+
docpull https://docs.python.org/3/library/asyncio.html --single
|
|
44
|
+
docpull https://docs.python.org/3/library/asyncio.html --profile llm --stream | jq .
|
|
45
|
+
```
|
|
26
46
|
|
|
27
47
|
## Install
|
|
28
48
|
|
|
@@ -50,10 +70,27 @@ docpull https://docs.example.com/guide --single
|
|
|
50
70
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
51
71
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
52
72
|
|
|
73
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
74
|
+
docpull https://example.com --format okf -o ./site-okf
|
|
75
|
+
|
|
53
76
|
# Mirror scraped content for offline use
|
|
54
77
|
docpull https://docs.example.com --profile mirror --cache
|
|
55
78
|
```
|
|
56
79
|
|
|
80
|
+
## Project traction
|
|
81
|
+
|
|
82
|
+
<a href="https://pepy.tech/project/docpull">
|
|
83
|
+
<img alt="Cumulative PyPI download history chart for docpull" src="docs/cumulative-downloads-history.svg" />
|
|
84
|
+
</a>
|
|
85
|
+
|
|
86
|
+
<a href="https://star-history.com/#raintree-technology/docpull&Date">
|
|
87
|
+
<picture>
|
|
88
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date&theme=dark" />
|
|
89
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
90
|
+
<img alt="Star history chart for raintree-technology/docpull" src="https://api.star-history.com/svg?repos=raintree-technology/docpull&type=Date" />
|
|
91
|
+
</picture>
|
|
92
|
+
</a>
|
|
93
|
+
|
|
57
94
|
## Framework-aware extraction
|
|
58
95
|
|
|
59
96
|
docpull inspects each page before running the generic extractor and can pull
|
|
@@ -64,8 +101,13 @@ content directly from framework data feeds:
|
|
|
64
101
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
65
102
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
66
103
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
67
|
-
| Docusaurus|
|
|
68
|
-
| Sphinx |
|
|
104
|
+
| Docusaurus| Extracts static article regions and tags framework metadata |
|
|
105
|
+
| Sphinx | Extracts static body/document regions and tags framework metadata |
|
|
106
|
+
| MkDocs / Material | Extracts static content regions and tags framework metadata |
|
|
107
|
+
| VitePress / VuePress | Extracts static doc regions and tags framework metadata |
|
|
108
|
+
| Astro Starlight | Extracts static markdown content regions and tags framework metadata |
|
|
109
|
+
| GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
|
|
110
|
+
| Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
|
|
69
111
|
|
|
70
112
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
71
113
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -85,6 +127,30 @@ can route elsewhere).
|
|
|
85
127
|
|
|
86
128
|
## Python API
|
|
87
129
|
|
|
130
|
+
Scraper-facing API:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import asyncio
|
|
134
|
+
from docpull import scrape_one, Scraper
|
|
135
|
+
|
|
136
|
+
async def main():
|
|
137
|
+
page = await scrape_one("https://docs.python.org/3/library/asyncio.html")
|
|
138
|
+
print(page.title, page.source_type)
|
|
139
|
+
print(page.text[:500])
|
|
140
|
+
|
|
141
|
+
scraper = Scraper()
|
|
142
|
+
run = await scraper.scrape_site(
|
|
143
|
+
"https://docs.example.com",
|
|
144
|
+
output_format="ndjson",
|
|
145
|
+
max_pages=100,
|
|
146
|
+
)
|
|
147
|
+
print(run.stats.pages_fetched, run.manifest_path)
|
|
148
|
+
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Core fetcher API:
|
|
153
|
+
|
|
88
154
|
```python
|
|
89
155
|
from docpull import fetch_one
|
|
90
156
|
|
|
@@ -129,6 +195,7 @@ async def tool_call(url: str) -> str:
|
|
|
129
195
|
```bash
|
|
130
196
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
131
197
|
docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
|
|
198
|
+
docpull https://site.com --profile okf # OKF bundle with generated index.md files.
|
|
132
199
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
133
200
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
134
201
|
```
|
|
@@ -410,10 +477,57 @@ NDJSON (one record per page or chunk):
|
|
|
410
477
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
411
478
|
```
|
|
412
479
|
|
|
480
|
+
Open Knowledge Format:
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
docpull https://example.com --format okf -o ./site-okf
|
|
484
|
+
|
|
485
|
+
# Equivalent profile form
|
|
486
|
+
docpull https://example.com --profile okf -o ./site-okf
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
A generated OKF bundle has a normal Markdown tree:
|
|
490
|
+
|
|
491
|
+
```text
|
|
492
|
+
site-okf/
|
|
493
|
+
index.md
|
|
494
|
+
corpus.manifest.json
|
|
495
|
+
_root.md
|
|
496
|
+
docs/
|
|
497
|
+
index.md
|
|
498
|
+
getting-started.md
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
OKF output is an opt-in Markdown bundle. Scraped pages are written as concept
|
|
502
|
+
documents with OKF frontmatter (`type`, `title`, `description`, `resource`,
|
|
503
|
+
`tags`, `timestamp` when source metadata exists) and docpull keeps `source` as a
|
|
504
|
+
compatibility extension. Generated `index.md` files are reserved for directory
|
|
505
|
+
listings, so URL landing pages use safe concept filenames such as `_root.md` or
|
|
506
|
+
`_page.md` instead of occupying `index.md`. The root `index.md` declares
|
|
507
|
+
`okf_version: "0.1"`; nested indexes stay plain listing files.
|
|
508
|
+
|
|
509
|
+
Because OKF treats every non-reserved `.md` file in the tree as a concept, write
|
|
510
|
+
OKF output to a clean or dedicated directory rather than mixing it with unrelated
|
|
511
|
+
Markdown files.
|
|
512
|
+
|
|
413
513
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
414
514
|
documents. The manifest records the run identity, output format, stable
|
|
415
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
416
|
-
|
|
515
|
+
`document_id` / `chunk_id` values, content hashes, file-backed relative output
|
|
516
|
+
paths (or the stdout marker for streamed NDJSON), and chunk counts so
|
|
517
|
+
regenerated corpora can be diffed and cited by agents. See
|
|
518
|
+
[`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
|
|
519
|
+
stability contract.
|
|
520
|
+
|
|
521
|
+
SQLite output includes an FTS5 index for local retrieval:
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from pathlib import Path
|
|
525
|
+
from docpull import search_sqlite_documents
|
|
526
|
+
|
|
527
|
+
hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
|
|
528
|
+
for hit in hits:
|
|
529
|
+
print(hit.url, hit.snippet)
|
|
530
|
+
```
|
|
417
531
|
|
|
418
532
|
## Security
|
|
419
533
|
|
|
@@ -434,9 +548,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
434
548
|
|
|
435
549
|
```
|
|
436
550
|
Core:
|
|
437
|
-
--profile {rag,mirror,quick,llm}
|
|
551
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
438
552
|
--single Fetch one URL (no crawl)
|
|
439
|
-
--format {markdown,json,ndjson,sqlite}
|
|
553
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
440
554
|
--stream Stream NDJSON to stdout
|
|
441
555
|
|
|
442
556
|
LLM / chunking:
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docpull"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.1"
|
|
8
8
|
dynamic = []
|
|
9
|
-
description = "
|
|
9
|
+
description = "Convert public web pages into clean Markdown for AI agents and RAG"
|
|
10
10
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
license = "MIT"
|
|
@@ -18,8 +18,20 @@ maintainers = [
|
|
|
18
18
|
{name = "Raintree Technology", email = "support@raintree.technology"}
|
|
19
19
|
]
|
|
20
20
|
keywords = [
|
|
21
|
-
"python",
|
|
22
|
-
"
|
|
21
|
+
"python",
|
|
22
|
+
"markdown",
|
|
23
|
+
"documentation",
|
|
24
|
+
"web-scraping",
|
|
25
|
+
"web-crawler",
|
|
26
|
+
"crawler",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"cli",
|
|
29
|
+
"rag",
|
|
30
|
+
"llm",
|
|
31
|
+
"mcp",
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"claude",
|
|
34
|
+
"ai-training-data",
|
|
23
35
|
]
|
|
24
36
|
classifiers = [
|
|
25
37
|
# Development Status
|
|
@@ -134,12 +146,16 @@ dev = [
|
|
|
134
146
|
docpull = "docpull.cli:main"
|
|
135
147
|
|
|
136
148
|
[project.urls]
|
|
137
|
-
Homepage = "https://
|
|
138
|
-
Documentation = "https://
|
|
149
|
+
Homepage = "https://docpull.raintree.technology"
|
|
150
|
+
Documentation = "https://docpull.raintree.technology"
|
|
139
151
|
Repository = "https://github.com/raintree-technology/docpull"
|
|
140
152
|
"Source Code" = "https://github.com/raintree-technology/docpull"
|
|
141
153
|
"Bug Tracker" = "https://github.com/raintree-technology/docpull/issues"
|
|
142
154
|
"Releases" = "https://github.com/raintree-technology/docpull/releases"
|
|
155
|
+
"MCP Plugin" = "https://github.com/raintree-technology/docpull/tree/main/plugin"
|
|
156
|
+
"Comparison Guide" = "https://github.com/raintree-technology/docpull/blob/main/docs/alternatives.md"
|
|
157
|
+
"Download Stats" = "https://pepy.tech/project/docpull"
|
|
158
|
+
"Changelog" = "https://github.com/raintree-technology/docpull/blob/main/docs/CHANGELOG.md"
|
|
143
159
|
|
|
144
160
|
[tool.setuptools.packages.find]
|
|
145
161
|
where = ["src"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
docpull - Fetch and convert documentation
|
|
2
|
+
docpull - Fetch and convert static/server-rendered documentation to markdown.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
from docpull import Fetcher, DocpullConfig, ProfileName
|
|
@@ -14,7 +14,7 @@ Usage:
|
|
|
14
14
|
print(event)
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "4.
|
|
17
|
+
__version__ = "4.3.1"
|
|
18
18
|
|
|
19
19
|
from .cache import CacheManager, StreamingDeduplicator
|
|
20
20
|
from .conversion.chunking import Chunk, TokenCounter, chunk_markdown
|
|
@@ -31,12 +31,27 @@ from .models.config import (
|
|
|
31
31
|
)
|
|
32
32
|
from .models.events import EventType, FetchEvent, FetchStats
|
|
33
33
|
from .pipeline.base import PageContext
|
|
34
|
+
from .pipeline.steps import SqliteSearchResult, search_sqlite_documents
|
|
35
|
+
from .scraper import (
|
|
36
|
+
Scraper,
|
|
37
|
+
ScrapeResult,
|
|
38
|
+
ScrapeRunResult,
|
|
39
|
+
scrape_one,
|
|
40
|
+
scrape_one_blocking,
|
|
41
|
+
scrape_site,
|
|
42
|
+
)
|
|
34
43
|
|
|
35
44
|
__all__ = [
|
|
36
45
|
"__version__",
|
|
37
46
|
"Fetcher",
|
|
38
47
|
"fetch_blocking",
|
|
39
48
|
"fetch_one",
|
|
49
|
+
"ScrapeResult",
|
|
50
|
+
"ScrapeRunResult",
|
|
51
|
+
"Scraper",
|
|
52
|
+
"scrape_one",
|
|
53
|
+
"scrape_one_blocking",
|
|
54
|
+
"scrape_site",
|
|
40
55
|
"PageContext",
|
|
41
56
|
"DocpullConfig",
|
|
42
57
|
"ProfileName",
|
|
@@ -49,6 +64,8 @@ __all__ = [
|
|
|
49
64
|
"EventType",
|
|
50
65
|
"FetchEvent",
|
|
51
66
|
"FetchStats",
|
|
67
|
+
"SqliteSearchResult",
|
|
68
|
+
"search_sqlite_documents",
|
|
52
69
|
"CacheManager",
|
|
53
70
|
"StreamingDeduplicator",
|
|
54
71
|
"Chunk",
|