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