docpull 4.2.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.2.0/src/docpull.egg-info → docpull-4.3.0}/PKG-INFO +95 -8
- {docpull-4.2.0 → docpull-4.3.0}/README.md +94 -7
- {docpull-4.2.0 → docpull-4.3.0}/pyproject.toml +1 -1
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/__init__.py +19 -2
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/benchmark.py +666 -91
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cli.py +14 -5
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/markdown.py +50 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/special_cases.py +235 -19
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/core/fetcher.py +84 -16
- docpull-4.3.0/src/docpull/judge.py +355 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/config.py +2 -1
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/profiles.py +12 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/parallel_workflows.py +105 -17
- docpull-4.3.0/src/docpull/passk.py +200 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/manifest.py +4 -2
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/__init__.py +5 -1
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_ndjson.py +6 -2
- docpull-4.3.0/src/docpull/pipeline/steps/save_okf.py +372 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_sqlite.py +91 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/validate.py +10 -0
- docpull-4.3.0/src/docpull/scraper.py +351 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/source_scoring.py +10 -2
- {docpull-4.2.0 → docpull-4.3.0/src/docpull.egg-info}/PKG-INFO +95 -8
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/SOURCES.txt +7 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_benchmark.py +184 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_ci_policy.py +15 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_cli.py +29 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_conversion.py +23 -0
- docpull-4.3.0/tests/test_document_record.py +53 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_integration.py +16 -0
- docpull-4.3.0/tests/test_judge.py +128 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_naming.py +26 -1
- docpull-4.3.0/tests/test_outputs_e2e.py +357 -0
- docpull-4.3.0/tests/test_passk.py +83 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_save_ndjson.py +20 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_save_sqlite.py +33 -1
- {docpull-4.2.0 → docpull-4.3.0}/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.0}/LICENSE +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/setup.cfg +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/__main__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/frontier.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/manager.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/chunking.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/core/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/composite.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/crawler.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/filters.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/doctor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/client.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/protocols.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/server.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/sources.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/metadata_extractor.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/document.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/events.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/run.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pack_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/base.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/convert.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/provider_cli.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/provider_keys.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/py.typed +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/__init__.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/download_policy.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/robots.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/url_validator.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull/time_utils.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/requires.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_cache_conditional_get.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_chunking.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_convert_step_new.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_discovery.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_frontier_resume.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_link_extractors.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_mcp_server.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_mcp_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_pack_tools.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_parallel_workflows.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_pipeline.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_provider_cli.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_real_site_regressions.py +0 -0
- {docpull-4.2.0 → docpull-4.3.0}/tests/test_security_hardening.py +0 -0
- {docpull-4.2.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>
|
|
@@ -100,7 +100,8 @@ Dynamic: license-file
|
|
|
100
100
|
|
|
101
101
|
[](https://www.python.org/downloads/)
|
|
102
102
|
[](https://badge.fury.io/py/docpull)
|
|
103
|
-
[](https://pepy.tech/project/docpull)
|
|
104
|
+
[](https://github.com/raintree-technology/docpull/stargazers)
|
|
104
105
|
[](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
|
|
105
106
|
|
|
106
107
|
<p align="center">
|
|
@@ -109,6 +110,22 @@ Dynamic: license-file
|
|
|
109
110
|
</a>
|
|
110
111
|
</p>
|
|
111
112
|
|
|
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
|
+
|
|
112
129
|
docpull is a web scraper for static and server-rendered sites, with
|
|
113
130
|
documentation crawling as its sharpest default workflow. It uses async HTTP (not
|
|
114
131
|
Playwright) to fetch pages, discover links, extract main content, and write clean
|
|
@@ -120,6 +137,11 @@ sandbox-friendly way to pipe web content into an LLM context, a RAG index, or an
|
|
|
120
137
|
offline archive. SSRF, XXE, DNS-rebinding, and CRLF-injection protections are on
|
|
121
138
|
by default — a necessity when an AI agent is choosing the URLs.
|
|
122
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.
|
|
144
|
+
|
|
123
145
|
## Install
|
|
124
146
|
|
|
125
147
|
```bash
|
|
@@ -146,6 +168,9 @@ docpull https://docs.example.com/guide --single
|
|
|
146
168
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
147
169
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
148
170
|
|
|
171
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
172
|
+
docpull https://docs.example.com --format okf
|
|
173
|
+
|
|
149
174
|
# Mirror scraped content for offline use
|
|
150
175
|
docpull https://docs.example.com --profile mirror --cache
|
|
151
176
|
```
|
|
@@ -160,8 +185,13 @@ content directly from framework data feeds:
|
|
|
160
185
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
161
186
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
162
187
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
163
|
-
| Docusaurus|
|
|
164
|
-
| 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 |
|
|
165
195
|
|
|
166
196
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
167
197
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -181,6 +211,30 @@ can route elsewhere).
|
|
|
181
211
|
|
|
182
212
|
## Python API
|
|
183
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
|
+
|
|
184
238
|
```python
|
|
185
239
|
from docpull import fetch_one
|
|
186
240
|
|
|
@@ -225,6 +279,7 @@ async def tool_call(url: str) -> str:
|
|
|
225
279
|
```bash
|
|
226
280
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
227
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.
|
|
228
283
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
229
284
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
230
285
|
```
|
|
@@ -506,10 +561,42 @@ NDJSON (one record per page or chunk):
|
|
|
506
561
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
507
562
|
```
|
|
508
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
|
+
|
|
509
582
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
510
583
|
documents. The manifest records the run identity, output format, stable
|
|
511
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
512
|
-
|
|
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
|
+
```
|
|
513
600
|
|
|
514
601
|
## Security
|
|
515
602
|
|
|
@@ -530,9 +617,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
530
617
|
|
|
531
618
|
```
|
|
532
619
|
Core:
|
|
533
|
-
--profile {rag,mirror,quick,llm}
|
|
620
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
534
621
|
--single Fetch one URL (no crawl)
|
|
535
|
-
--format {markdown,json,ndjson,sqlite}
|
|
622
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
536
623
|
--stream Stream NDJSON to stdout
|
|
537
624
|
|
|
538
625
|
LLM / chunking:
|
|
@@ -4,7 +4,8 @@
|
|
|
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,6 +14,22 @@
|
|
|
13
14
|
</a>
|
|
14
15
|
</p>
|
|
15
16
|
|
|
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
|
+
|
|
16
33
|
docpull is a web scraper for static and server-rendered sites, with
|
|
17
34
|
documentation crawling as its sharpest default workflow. It uses async HTTP (not
|
|
18
35
|
Playwright) to fetch pages, discover links, extract main content, and write clean
|
|
@@ -24,6 +41,11 @@ sandbox-friendly way to pipe web content into an LLM context, a RAG index, or an
|
|
|
24
41
|
offline archive. SSRF, XXE, DNS-rebinding, and CRLF-injection protections are on
|
|
25
42
|
by default — a necessity when an AI agent is choosing the URLs.
|
|
26
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.
|
|
48
|
+
|
|
27
49
|
## Install
|
|
28
50
|
|
|
29
51
|
```bash
|
|
@@ -50,6 +72,9 @@ docpull https://docs.example.com/guide --single
|
|
|
50
72
|
# LLM-ready NDJSON with 4k-token chunks streamed to stdout
|
|
51
73
|
docpull https://docs.example.com --profile llm --stream | jq .
|
|
52
74
|
|
|
75
|
+
# Open Knowledge Format bundle for agent/wiki interoperability
|
|
76
|
+
docpull https://docs.example.com --format okf
|
|
77
|
+
|
|
53
78
|
# Mirror scraped content for offline use
|
|
54
79
|
docpull https://docs.example.com --profile mirror --cache
|
|
55
80
|
```
|
|
@@ -64,8 +89,13 @@ content directly from framework data feeds:
|
|
|
64
89
|
| Next.js | Parses `__NEXT_DATA__` JSON |
|
|
65
90
|
| Mintlify | `__NEXT_DATA__` with Mintlify tagging |
|
|
66
91
|
| OpenAPI | Renders `openapi.json` / `swagger.json` into Markdown |
|
|
67
|
-
| Docusaurus|
|
|
68
|
-
| 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 |
|
|
69
99
|
|
|
70
100
|
JS-only SPAs with no server-rendered content are detected and skipped with a
|
|
71
101
|
clear reason (or, with `--strict-js-required`, reported as an error so agents
|
|
@@ -85,6 +115,30 @@ can route elsewhere).
|
|
|
85
115
|
|
|
86
116
|
## Python API
|
|
87
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
|
+
|
|
88
142
|
```python
|
|
89
143
|
from docpull import fetch_one
|
|
90
144
|
|
|
@@ -129,6 +183,7 @@ async def tool_call(url: str) -> str:
|
|
|
129
183
|
```bash
|
|
130
184
|
docpull https://site.com --profile rag # Default. Dedup, rich metadata.
|
|
131
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.
|
|
132
187
|
docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
|
|
133
188
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
134
189
|
```
|
|
@@ -410,10 +465,42 @@ NDJSON (one record per page or chunk):
|
|
|
410
465
|
{"document_id": "doc_...", "chunk_id": "chunk_...", "url": "...", "title": "...", "content": "...", "hash": "...", "token_count": 842, "chunk_index": 0}
|
|
411
466
|
```
|
|
412
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
|
+
|
|
413
486
|
Every output format also writes `corpus.manifest.json` next to the generated
|
|
414
487
|
documents. The manifest records the run identity, output format, stable
|
|
415
|
-
`document_id` / `chunk_id` values, content hashes, relative output
|
|
416
|
-
|
|
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
|
+
```
|
|
417
504
|
|
|
418
505
|
## Security
|
|
419
506
|
|
|
@@ -434,9 +521,9 @@ Run `docpull --help` for the full list. Highlights:
|
|
|
434
521
|
|
|
435
522
|
```
|
|
436
523
|
Core:
|
|
437
|
-
--profile {rag,mirror,quick,llm}
|
|
524
|
+
--profile {rag,mirror,quick,llm,okf}
|
|
438
525
|
--single Fetch one URL (no crawl)
|
|
439
|
-
--format {markdown,json,ndjson,sqlite}
|
|
526
|
+
--format {markdown,json,ndjson,sqlite,okf}
|
|
440
527
|
--stream Stream NDJSON to stdout
|
|
441
528
|
|
|
442
529
|
LLM / chunking:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docpull"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.0"
|
|
8
8
|
dynamic = []
|
|
9
9
|
description = "Pull documentation from the web and convert to clean markdown"
|
|
10
10
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
@@ -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.0"
|
|
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",
|