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.
Files changed (115) hide show
  1. {docpull-4.2.0/src/docpull.egg-info → docpull-4.3.0}/PKG-INFO +95 -8
  2. {docpull-4.2.0 → docpull-4.3.0}/README.md +94 -7
  3. {docpull-4.2.0 → docpull-4.3.0}/pyproject.toml +1 -1
  4. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/__init__.py +19 -2
  5. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/benchmark.py +666 -91
  6. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cli.py +14 -5
  7. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/markdown.py +50 -0
  8. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/special_cases.py +235 -19
  9. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/core/fetcher.py +84 -16
  10. docpull-4.3.0/src/docpull/judge.py +355 -0
  11. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/config.py +2 -1
  12. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/profiles.py +12 -0
  13. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/parallel_workflows.py +105 -17
  14. docpull-4.3.0/src/docpull/passk.py +200 -0
  15. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/manifest.py +4 -2
  16. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/__init__.py +5 -1
  17. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_ndjson.py +6 -2
  18. docpull-4.3.0/src/docpull/pipeline/steps/save_okf.py +372 -0
  19. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_sqlite.py +91 -0
  20. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/validate.py +10 -0
  21. docpull-4.3.0/src/docpull/scraper.py +351 -0
  22. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/source_scoring.py +10 -2
  23. {docpull-4.2.0 → docpull-4.3.0/src/docpull.egg-info}/PKG-INFO +95 -8
  24. {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/SOURCES.txt +7 -0
  25. {docpull-4.2.0 → docpull-4.3.0}/tests/test_benchmark.py +184 -0
  26. {docpull-4.2.0 → docpull-4.3.0}/tests/test_ci_policy.py +15 -0
  27. {docpull-4.2.0 → docpull-4.3.0}/tests/test_cli.py +29 -0
  28. {docpull-4.2.0 → docpull-4.3.0}/tests/test_conversion.py +23 -0
  29. docpull-4.3.0/tests/test_document_record.py +53 -0
  30. {docpull-4.2.0 → docpull-4.3.0}/tests/test_integration.py +16 -0
  31. docpull-4.3.0/tests/test_judge.py +128 -0
  32. {docpull-4.2.0 → docpull-4.3.0}/tests/test_naming.py +26 -1
  33. docpull-4.3.0/tests/test_outputs_e2e.py +357 -0
  34. docpull-4.3.0/tests/test_passk.py +83 -0
  35. {docpull-4.2.0 → docpull-4.3.0}/tests/test_save_ndjson.py +20 -0
  36. {docpull-4.2.0 → docpull-4.3.0}/tests/test_save_sqlite.py +33 -1
  37. {docpull-4.2.0 → docpull-4.3.0}/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.0}/LICENSE +0 -0
  40. {docpull-4.2.0 → docpull-4.3.0}/setup.cfg +0 -0
  41. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/__main__.py +0 -0
  42. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/__init__.py +0 -0
  43. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/frontier.py +0 -0
  44. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/manager.py +0 -0
  45. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/cache/streaming_dedup.py +0 -0
  46. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/__init__.py +0 -0
  47. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/chunking.py +0 -0
  48. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/extractor.py +0 -0
  49. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/protocols.py +0 -0
  50. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  51. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/core/__init__.py +0 -0
  52. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/__init__.py +0 -0
  53. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/_fetch.py +0 -0
  54. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/composite.py +0 -0
  55. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/crawler.py +0 -0
  56. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/filters.py +0 -0
  57. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  58. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  59. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  60. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  61. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/protocols.py +0 -0
  62. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/discovery/sitemap.py +0 -0
  63. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/doctor.py +0 -0
  64. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/fixtures/__init__.py +0 -0
  65. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  66. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/__init__.py +0 -0
  67. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/client.py +0 -0
  68. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/protocols.py +0 -0
  69. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/http/rate_limiter.py +0 -0
  70. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/__init__.py +0 -0
  71. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/server.py +0 -0
  72. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/sources.py +0 -0
  73. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/mcp/tools.py +0 -0
  74. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/metadata_extractor.py +0 -0
  75. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/__init__.py +0 -0
  76. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/document.py +0 -0
  77. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/events.py +0 -0
  78. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/models/run.py +0 -0
  79. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pack_tools.py +0 -0
  80. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/__init__.py +0 -0
  81. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/base.py +0 -0
  82. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  83. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/convert.py +0 -0
  84. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  85. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/fetch.py +0 -0
  86. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/metadata.py +0 -0
  87. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save.py +0 -0
  88. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  89. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/provider_cli.py +0 -0
  90. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/provider_keys.py +0 -0
  91. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/py.typed +0 -0
  92. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/__init__.py +0 -0
  93. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/download_policy.py +0 -0
  94. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/robots.py +0 -0
  95. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/security/url_validator.py +0 -0
  96. {docpull-4.2.0 → docpull-4.3.0}/src/docpull/time_utils.py +0 -0
  97. {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  98. {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/entry_points.txt +0 -0
  99. {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/requires.txt +0 -0
  100. {docpull-4.2.0 → docpull-4.3.0}/src/docpull.egg-info/top_level.txt +0 -0
  101. {docpull-4.2.0 → docpull-4.3.0}/tests/test_cache_conditional_get.py +0 -0
  102. {docpull-4.2.0 → docpull-4.3.0}/tests/test_chunking.py +0 -0
  103. {docpull-4.2.0 → docpull-4.3.0}/tests/test_convert_step_new.py +0 -0
  104. {docpull-4.2.0 → docpull-4.3.0}/tests/test_discovery.py +0 -0
  105. {docpull-4.2.0 → docpull-4.3.0}/tests/test_frontier_resume.py +0 -0
  106. {docpull-4.2.0 → docpull-4.3.0}/tests/test_link_extractors.py +0 -0
  107. {docpull-4.2.0 → docpull-4.3.0}/tests/test_mcp_server.py +0 -0
  108. {docpull-4.2.0 → docpull-4.3.0}/tests/test_mcp_tools.py +0 -0
  109. {docpull-4.2.0 → docpull-4.3.0}/tests/test_pack_tools.py +0 -0
  110. {docpull-4.2.0 → docpull-4.3.0}/tests/test_parallel_workflows.py +0 -0
  111. {docpull-4.2.0 → docpull-4.3.0}/tests/test_pipeline.py +0 -0
  112. {docpull-4.2.0 → docpull-4.3.0}/tests/test_provider_cli.py +0 -0
  113. {docpull-4.2.0 → docpull-4.3.0}/tests/test_real_site_regressions.py +0 -0
  114. {docpull-4.2.0 → docpull-4.3.0}/tests/test_security_hardening.py +0 -0
  115. {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.2.0
3
+ Version: 4.3.0
4
4
  Summary: Pull documentation from the web and convert to clean markdown
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -100,7 +100,8 @@ Dynamic: license-file
100
100
 
101
101
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
102
102
  [![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)
103
+ [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
104
+ [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
104
105
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](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| Detected and tagged; generic extractor produces Markdown |
164
- | Sphinx | Detected from generator metadata / Read the Docs hosts and tagged; generic extractor produces Markdown |
188
+ | Docusaurus| Extracts static article regions and tags framework metadata |
189
+ | Sphinx | Extracts static body/document regions and tags framework metadata |
190
+ | MkDocs / Material | Extracts static content regions and tags framework metadata |
191
+ | VitePress / VuePress | Extracts static doc regions and tags framework metadata |
192
+ | Astro Starlight | Extracts static markdown content regions and tags framework metadata |
193
+ | GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
194
+ | Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
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 paths, and
512
- chunk counts so regenerated corpora can be diffed and cited by agents.
584
+ `document_id` / `chunk_id` values, content hashes, file-backed relative output
585
+ paths (or the stdout marker for streamed NDJSON), and chunk counts so
586
+ regenerated corpora can be diffed and cited by agents. See
587
+ [`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
588
+ stability contract.
589
+
590
+ SQLite output includes an FTS5 index for local retrieval:
591
+
592
+ ```python
593
+ from pathlib import Path
594
+ from docpull import search_sqlite_documents
595
+
596
+ hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
597
+ for hit in hits:
598
+ print(hit.url, hit.snippet)
599
+ ```
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
  [![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,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| Detected and tagged; generic extractor produces Markdown |
68
- | Sphinx | Detected from generator metadata / Read the Docs hosts and tagged; generic extractor produces Markdown |
92
+ | Docusaurus| Extracts static article regions and tags framework metadata |
93
+ | Sphinx | Extracts static body/document regions and tags framework metadata |
94
+ | MkDocs / Material | Extracts static content regions and tags framework metadata |
95
+ | VitePress / VuePress | Extracts static doc regions and tags framework metadata |
96
+ | Astro Starlight | Extracts static markdown content regions and tags framework metadata |
97
+ | GitBook / ReadMe.io | Extracts static article/content regions and tags framework metadata |
98
+ | Redoc / Scalar | Extracts static API reference regions; OpenAPI JSON is still preferred when available |
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 paths, and
416
- chunk counts so regenerated corpora can be diffed and cited by agents.
488
+ `document_id` / `chunk_id` values, content hashes, file-backed relative output
489
+ paths (or the stdout marker for streamed NDJSON), and chunk counts so
490
+ regenerated corpora can be diffed and cited by agents. See
491
+ [`docs/corpus-manifest.md`](docs/corpus-manifest.md) for the schema and
492
+ stability contract.
493
+
494
+ SQLite output includes an FTS5 index for local retrieval:
495
+
496
+ ```python
497
+ from pathlib import Path
498
+ from docpull import search_sqlite_documents
499
+
500
+ hits = search_sqlite_documents(Path("docs/documents.db"), "rate limit")
501
+ for hit in hits:
502
+ print(hit.url, hit.snippet)
503
+ ```
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.2.0"
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 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.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",