docpull 4.1.0__tar.gz → 4.2.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 (107) hide show
  1. {docpull-4.1.0/src/docpull.egg-info → docpull-4.2.0}/PKG-INFO +117 -15
  2. {docpull-4.1.0 → docpull-4.2.0}/README.md +113 -14
  3. {docpull-4.1.0 → docpull-4.2.0}/pyproject.toml +5 -1
  4. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/__init__.py +1 -1
  5. docpull-4.2.0/src/docpull/benchmark.py +2922 -0
  6. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/cli.py +8 -0
  7. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pack_tools.py +29 -12
  8. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/parallel_workflows.py +35 -104
  9. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/save_ndjson.py +56 -0
  10. docpull-4.2.0/src/docpull/provider_cli.py +426 -0
  11. docpull-4.2.0/src/docpull/provider_keys.py +217 -0
  12. {docpull-4.1.0 → docpull-4.2.0/src/docpull.egg-info}/PKG-INFO +117 -15
  13. {docpull-4.1.0 → docpull-4.2.0}/src/docpull.egg-info/SOURCES.txt +5 -0
  14. {docpull-4.1.0 → docpull-4.2.0}/src/docpull.egg-info/requires.txt +4 -0
  15. docpull-4.2.0/tests/test_benchmark.py +645 -0
  16. {docpull-4.1.0 → docpull-4.2.0}/tests/test_pack_tools.py +42 -0
  17. {docpull-4.1.0 → docpull-4.2.0}/tests/test_parallel_workflows.py +41 -0
  18. docpull-4.2.0/tests/test_provider_cli.py +85 -0
  19. {docpull-4.1.0 → docpull-4.2.0}/tests/test_save_ndjson.py +8 -0
  20. {docpull-4.1.0 → docpull-4.2.0}/LICENSE +0 -0
  21. {docpull-4.1.0 → docpull-4.2.0}/setup.cfg +0 -0
  22. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/__main__.py +0 -0
  23. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/cache/__init__.py +0 -0
  24. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/cache/frontier.py +0 -0
  25. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/cache/manager.py +0 -0
  26. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/cache/streaming_dedup.py +0 -0
  27. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/__init__.py +0 -0
  28. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/chunking.py +0 -0
  29. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/extractor.py +0 -0
  30. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/markdown.py +0 -0
  31. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/protocols.py +0 -0
  32. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/special_cases.py +0 -0
  33. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  34. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/core/__init__.py +0 -0
  35. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/core/fetcher.py +0 -0
  36. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/__init__.py +0 -0
  37. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/_fetch.py +0 -0
  38. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/composite.py +0 -0
  39. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/crawler.py +0 -0
  40. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/filters.py +0 -0
  41. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  42. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  43. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  44. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  45. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/protocols.py +0 -0
  46. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/discovery/sitemap.py +0 -0
  47. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/doctor.py +0 -0
  48. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/fixtures/__init__.py +0 -0
  49. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  50. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/http/__init__.py +0 -0
  51. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/http/client.py +0 -0
  52. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/http/protocols.py +0 -0
  53. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/http/rate_limiter.py +0 -0
  54. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/mcp/__init__.py +0 -0
  55. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/mcp/server.py +0 -0
  56. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/mcp/sources.py +0 -0
  57. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/mcp/tools.py +0 -0
  58. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/metadata_extractor.py +0 -0
  59. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/__init__.py +0 -0
  60. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/config.py +0 -0
  61. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/document.py +0 -0
  62. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/events.py +0 -0
  63. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/profiles.py +0 -0
  64. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/models/run.py +0 -0
  65. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/__init__.py +0 -0
  66. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/base.py +0 -0
  67. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/manifest.py +0 -0
  68. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/__init__.py +0 -0
  69. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  70. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/convert.py +0 -0
  71. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  72. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/fetch.py +0 -0
  73. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/metadata.py +0 -0
  74. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/save.py +0 -0
  75. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  76. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  77. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/pipeline/steps/validate.py +0 -0
  78. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/py.typed +0 -0
  79. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/security/__init__.py +0 -0
  80. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/security/download_policy.py +0 -0
  81. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/security/robots.py +0 -0
  82. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/security/url_validator.py +0 -0
  83. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/source_scoring.py +0 -0
  84. {docpull-4.1.0 → docpull-4.2.0}/src/docpull/time_utils.py +0 -0
  85. {docpull-4.1.0 → docpull-4.2.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  86. {docpull-4.1.0 → docpull-4.2.0}/src/docpull.egg-info/entry_points.txt +0 -0
  87. {docpull-4.1.0 → docpull-4.2.0}/src/docpull.egg-info/top_level.txt +0 -0
  88. {docpull-4.1.0 → docpull-4.2.0}/tests/test_cache_conditional_get.py +0 -0
  89. {docpull-4.1.0 → docpull-4.2.0}/tests/test_chunking.py +0 -0
  90. {docpull-4.1.0 → docpull-4.2.0}/tests/test_ci_policy.py +0 -0
  91. {docpull-4.1.0 → docpull-4.2.0}/tests/test_cli.py +0 -0
  92. {docpull-4.1.0 → docpull-4.2.0}/tests/test_conversion.py +0 -0
  93. {docpull-4.1.0 → docpull-4.2.0}/tests/test_convert_step_new.py +0 -0
  94. {docpull-4.1.0 → docpull-4.2.0}/tests/test_discovery.py +0 -0
  95. {docpull-4.1.0 → docpull-4.2.0}/tests/test_frontier_resume.py +0 -0
  96. {docpull-4.1.0 → docpull-4.2.0}/tests/test_integration.py +0 -0
  97. {docpull-4.1.0 → docpull-4.2.0}/tests/test_link_extractors.py +0 -0
  98. {docpull-4.1.0 → docpull-4.2.0}/tests/test_mcp_server.py +0 -0
  99. {docpull-4.1.0 → docpull-4.2.0}/tests/test_mcp_tools.py +0 -0
  100. {docpull-4.1.0 → docpull-4.2.0}/tests/test_naming.py +0 -0
  101. {docpull-4.1.0 → docpull-4.2.0}/tests/test_outputs_e2e.py +0 -0
  102. {docpull-4.1.0 → docpull-4.2.0}/tests/test_pipeline.py +0 -0
  103. {docpull-4.1.0 → docpull-4.2.0}/tests/test_real_site_regressions.py +0 -0
  104. {docpull-4.1.0 → docpull-4.2.0}/tests/test_save_sqlite.py +0 -0
  105. {docpull-4.1.0 → docpull-4.2.0}/tests/test_security_hardening.py +0 -0
  106. {docpull-4.1.0 → docpull-4.2.0}/tests/test_special_cases.py +0 -0
  107. {docpull-4.1.0 → docpull-4.2.0}/tests/test_time_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 4.1.0
3
+ Version: 4.2.0
4
4
  Summary: Pull documentation from the web and convert to clean markdown
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -64,6 +64,8 @@ Requires-Dist: python-multipart>=0.0.27; extra == "mcp"
64
64
  Requires-Dist: starlette>=1.0.1; extra == "mcp"
65
65
  Provides-Extra: parallel
66
66
  Requires-Dist: parallel-web>=1.0.1; extra == "parallel"
67
+ Provides-Extra: observability
68
+ Requires-Dist: raindrop-ai>=0.0.50; extra == "observability"
67
69
  Provides-Extra: llm
68
70
  Requires-Dist: tiktoken>=0.7.0; extra == "llm"
69
71
  Provides-Extra: all
@@ -76,6 +78,7 @@ Requires-Dist: pyjwt>=2.13.0; extra == "all"
76
78
  Requires-Dist: python-multipart>=0.0.27; extra == "all"
77
79
  Requires-Dist: starlette>=1.0.1; extra == "all"
78
80
  Requires-Dist: parallel-web>=1.0.1; extra == "all"
81
+ Requires-Dist: raindrop-ai>=0.0.50; extra == "all"
79
82
  Provides-Extra: dev
80
83
  Requires-Dist: pytest>=7.0.0; extra == "dev"
81
84
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -93,7 +96,7 @@ Dynamic: license-file
93
96
 
94
97
  # docpull
95
98
 
96
- **Security-hardened, browser-free crawler that turns static documentation sites into clean, AI-ready Markdown — fast.**
99
+ **Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
97
100
 
98
101
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
99
102
  [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
@@ -106,15 +109,16 @@ Dynamic: license-file
106
109
  </a>
107
110
  </p>
108
111
 
109
- docpull uses async HTTP (not Playwright) to fetch server-rendered pages,
110
- extracts main content, and writes clean Markdown with source-URL frontmatter
111
- in seconds, with a small install footprint. It won't render JavaScript, but for
112
- the large class of docs that don't need it (API references, Python/Go stdlib,
113
- most dev-tool docs, OpenAPI specs, Next.js and Docusaurus builds), it is a
114
- fast, auditable, sandbox-friendly way to pipe documentation into an LLM context,
115
- a RAG index, or an offline archive. SSRF, XXE, DNS-rebinding, and
116
- CRLF-injection protections are on by default a necessity when an AI agent
117
- is choosing the URLs.
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.
118
122
 
119
123
  ## Install
120
124
 
@@ -125,22 +129,24 @@ pip install docpull
125
129
  pip install 'docpull[llm]' # tiktoken for token-accurate chunking
126
130
  pip install 'docpull[trafilatura]' # alternative extractor for noisy pages
127
131
  pip install 'docpull[mcp]' # run as an MCP server for AI agents
132
+ pip install 'docpull[parallel]' # Parallel API context packs
133
+ pip install 'docpull[observability]' # Raindrop benchmark tracing
128
134
  pip install 'docpull[all]' # everything above
129
135
  ```
130
136
 
131
137
  ## Quick start
132
138
 
133
139
  ```bash
134
- # Crawl and save Markdown
140
+ # Scrape a page graph and save Markdown
135
141
  docpull https://docs.example.com
136
142
 
137
- # One page, no crawl — the fast path for agents
143
+ # Scrape one page, no crawl — the fast path for agents
138
144
  docpull https://docs.example.com/guide --single
139
145
 
140
146
  # LLM-ready NDJSON with 4k-token chunks streamed to stdout
141
147
  docpull https://docs.example.com --profile llm --stream | jq .
142
148
 
143
- # Mirror a site for offline use
149
+ # Mirror scraped content for offline use
144
150
  docpull https://docs.example.com --profile mirror --cache
145
151
  ```
146
152
 
@@ -274,7 +280,32 @@ value and reports the key source (`env`, `project_env`, `user_config`, or
274
280
  `missing`). It does not make a live Parallel call or prove the key is valid. Use
275
281
  `--json` when an agent or CI job needs machine-readable configuration status.
276
282
 
277
- The pack contains:
283
+ ## Optional live providers
284
+
285
+ Parallel, Tavily, and Exa are equal optional live providers for benchmark and
286
+ provider context-pack workflows. You can configure zero, one, two, or all three
287
+ API keys. docpull looks for keys in this order: environment variables, project
288
+ `.env.local`, then `~/.config/docpull/secrets.env`. Missing providers are skipped
289
+ instead of failing provider-neutral runs.
290
+
291
+ ```bash
292
+ docpull providers auth --json
293
+ docpull providers init parallel
294
+ docpull providers init tavily
295
+ docpull providers init exa
296
+
297
+ docpull providers context-pack "Compare AI web-search APIs for agents" \
298
+ --provider auto \
299
+ --query "AI web search API" \
300
+ --include-domain docs.parallel.ai \
301
+ --output-dir ./packs/provider-comparison
302
+ ```
303
+
304
+ Use `--provider all` to request all three and record skipped-provider metadata
305
+ for keys or optional SDKs that are not configured. Use repeated `--provider`
306
+ values to run a specific subset.
307
+
308
+ A Parallel pack contains:
278
309
 
279
310
  - `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
280
311
  - `documents.ndjson` — chunked records for agents and RAG pipelines.
@@ -547,6 +578,77 @@ Reproduce with `make benchmark` (requires `aiohttp`; runs the gated
547
578
  benchmark in `tests/benchmarks/` and prints progress plus a JSON line you can
548
579
  pipe into trend tooling).
549
580
 
581
+ For real-site and live-provider context-pack benchmarks, use the first-class
582
+ benchmark harness:
583
+
584
+ ```bash
585
+ docpull benchmark quick
586
+ docpull benchmark quick --provider auto --max-estimated-cost 0.05
587
+ docpull benchmark quick --provider all --max-estimated-cost 0.10
588
+ docpull benchmark quick --target-set provider-matrix --provider all \
589
+ --max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
590
+ --max-estimated-cost 0.10
591
+ docpull benchmark article .bench/runs/<run>/benchmark.report.json
592
+ ```
593
+
594
+ `docpull benchmark quick` writes `benchmark.report.json` and
595
+ `benchmark.summary.md` under `.bench/runs/<timestamp>/` by default. The core
596
+ cases measure an LLM-profile crawl plus a cached rerun. Passing `--provider auto`
597
+ runs every locally ready provider. Passing `--provider all` requests Parallel,
598
+ Tavily, and Exa, then records missing keys or optional SDKs in
599
+ `skipped_providers` without failing the benchmark. Live Parallel calls still use
600
+ the local `--max-estimated-cost` guard before any work starts.
601
+
602
+ Use `--target-set tool-docs` to run a cross-pull matrix across the Parallel,
603
+ Exa, Tavily, Raindrop, and DocPull documentation sites. Use `--target-set provider-matrix`
604
+ to add three low-cap adversarial public targets for JS-heavy docs, noisy
605
+ archived navigation, and freshness-sensitive pricing. Matrix runs skip the
606
+ cached core pass by default so the headline grid stays provider x target; pass
607
+ `--cached-pass` to force cache measurement across the matrix.
608
+
609
+ The compatibility flags `--parallel`, `--tavily`, and `--exa` still work as
610
+ provider aliases. Tavily Search + Extract and Exa Search-with-contents are
611
+ normalized into the same local
612
+ `documents.ndjson`, `corpus.manifest.json`, `sources.md`, provider `*.pack.json`,
613
+ `pack.score.json`, and `source.scores.json` artifacts as the core and Parallel
614
+ cases. The report keeps the legacy pack score and adds weighted benchmark
615
+ sub-scores for coverage, cleanliness, source fidelity, freshness, and density.
616
+ Keys can be exported as `PARALLEL_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY`
617
+ or stored together in `~/.config/docpull/secrets.env`; docpull does not write
618
+ keys into benchmark artifacts.
619
+
620
+ Tavily usage is credit-based, so set `TAVILY_CREDIT_USD` or pass
621
+ `--tavily-credit-usd` to convert credits into estimated dollars for cost
622
+ comparisons. Without that value, Tavily credits are still recorded in
623
+ `cost_units` but excluded from normalized USD totals.
624
+
625
+ To trace the benchmark in Raindrop for a publishable observability/eval loop,
626
+ install the optional SDK and pass `--trace raindrop`:
627
+
628
+ ```bash
629
+ pip install 'docpull[parallel,observability]'
630
+ export PARALLEL_API_KEY="<your-parallel-api-key>"
631
+ export TAVILY_API_KEY="<your-tavily-api-key>"
632
+ export TAVILY_CREDIT_USD="<account-credit-value>"
633
+ export EXA_API_KEY="<your-exa-api-key>"
634
+ export RAINDROP_WRITE_KEY="<your-raindrop-write-key>"
635
+ docpull benchmark quick --target-set provider-matrix --provider all --trace raindrop \
636
+ --max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
637
+ --max-estimated-cost 0.10
638
+ docpull benchmark article .bench/runs/<run>/benchmark.report.json
639
+ ```
640
+
641
+ Raindrop traces are metadata-only by default: docpull records timings, counts,
642
+ scores, costs, selected URLs, provider, workflow, target, prompt, settings, and
643
+ artifact paths, but does not send scraped page content unless a future caller
644
+ explicitly adds that behavior. `RAINDROP_WRITE_KEY` can be exported or stored in
645
+ the same `~/.config/docpull/secrets.env` file as the provider keys.
646
+
647
+ When Raindrop tracing is enabled, DocPull also emits Raindrop signals for
648
+ attention-worthy benchmark cells: failed cases, low scores, slow cases,
649
+ high-cost cells, and score-dimension warnings. The benchmark report stores the
650
+ Raindrop event id and signal counts so traced runs can be audited later.
651
+
550
652
  ## Troubleshooting
551
653
 
552
654
  ```bash
@@ -1,6 +1,6 @@
1
1
  # docpull
2
2
 
3
- **Security-hardened, browser-free crawler that turns static documentation sites into clean, AI-ready Markdown — fast.**
3
+ **Security-hardened, browser-free web scraper and crawler that turns server-rendered pages into clean, AI-ready Markdown — fast.**
4
4
 
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
6
  [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
@@ -13,15 +13,16 @@
13
13
  </a>
14
14
  </p>
15
15
 
16
- docpull uses async HTTP (not Playwright) to fetch server-rendered pages,
17
- extracts main content, and writes clean Markdown with source-URL frontmatter
18
- in seconds, with a small install footprint. It won't render JavaScript, but for
19
- the large class of docs that don't need it (API references, Python/Go stdlib,
20
- most dev-tool docs, OpenAPI specs, Next.js and Docusaurus builds), it is a
21
- fast, auditable, sandbox-friendly way to pipe documentation into an LLM context,
22
- a RAG index, or an offline archive. SSRF, XXE, DNS-rebinding, and
23
- CRLF-injection protections are on by default a necessity when an AI agent
24
- is choosing the URLs.
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.
25
26
 
26
27
  ## Install
27
28
 
@@ -32,22 +33,24 @@ pip install docpull
32
33
  pip install 'docpull[llm]' # tiktoken for token-accurate chunking
33
34
  pip install 'docpull[trafilatura]' # alternative extractor for noisy pages
34
35
  pip install 'docpull[mcp]' # run as an MCP server for AI agents
36
+ pip install 'docpull[parallel]' # Parallel API context packs
37
+ pip install 'docpull[observability]' # Raindrop benchmark tracing
35
38
  pip install 'docpull[all]' # everything above
36
39
  ```
37
40
 
38
41
  ## Quick start
39
42
 
40
43
  ```bash
41
- # Crawl and save Markdown
44
+ # Scrape a page graph and save Markdown
42
45
  docpull https://docs.example.com
43
46
 
44
- # One page, no crawl — the fast path for agents
47
+ # Scrape one page, no crawl — the fast path for agents
45
48
  docpull https://docs.example.com/guide --single
46
49
 
47
50
  # LLM-ready NDJSON with 4k-token chunks streamed to stdout
48
51
  docpull https://docs.example.com --profile llm --stream | jq .
49
52
 
50
- # Mirror a site for offline use
53
+ # Mirror scraped content for offline use
51
54
  docpull https://docs.example.com --profile mirror --cache
52
55
  ```
53
56
 
@@ -181,7 +184,32 @@ value and reports the key source (`env`, `project_env`, `user_config`, or
181
184
  `missing`). It does not make a live Parallel call or prove the key is valid. Use
182
185
  `--json` when an agent or CI job needs machine-readable configuration status.
183
186
 
184
- The pack contains:
187
+ ## Optional live providers
188
+
189
+ Parallel, Tavily, and Exa are equal optional live providers for benchmark and
190
+ provider context-pack workflows. You can configure zero, one, two, or all three
191
+ API keys. docpull looks for keys in this order: environment variables, project
192
+ `.env.local`, then `~/.config/docpull/secrets.env`. Missing providers are skipped
193
+ instead of failing provider-neutral runs.
194
+
195
+ ```bash
196
+ docpull providers auth --json
197
+ docpull providers init parallel
198
+ docpull providers init tavily
199
+ docpull providers init exa
200
+
201
+ docpull providers context-pack "Compare AI web-search APIs for agents" \
202
+ --provider auto \
203
+ --query "AI web search API" \
204
+ --include-domain docs.parallel.ai \
205
+ --output-dir ./packs/provider-comparison
206
+ ```
207
+
208
+ Use `--provider all` to request all three and record skipped-provider metadata
209
+ for keys or optional SDKs that are not configured. Use repeated `--provider`
210
+ values to run a specific subset.
211
+
212
+ A Parallel pack contains:
185
213
 
186
214
  - `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
187
215
  - `documents.ndjson` — chunked records for agents and RAG pipelines.
@@ -454,6 +482,77 @@ Reproduce with `make benchmark` (requires `aiohttp`; runs the gated
454
482
  benchmark in `tests/benchmarks/` and prints progress plus a JSON line you can
455
483
  pipe into trend tooling).
456
484
 
485
+ For real-site and live-provider context-pack benchmarks, use the first-class
486
+ benchmark harness:
487
+
488
+ ```bash
489
+ docpull benchmark quick
490
+ docpull benchmark quick --provider auto --max-estimated-cost 0.05
491
+ docpull benchmark quick --provider all --max-estimated-cost 0.10
492
+ docpull benchmark quick --target-set provider-matrix --provider all \
493
+ --max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
494
+ --max-estimated-cost 0.10
495
+ docpull benchmark article .bench/runs/<run>/benchmark.report.json
496
+ ```
497
+
498
+ `docpull benchmark quick` writes `benchmark.report.json` and
499
+ `benchmark.summary.md` under `.bench/runs/<timestamp>/` by default. The core
500
+ cases measure an LLM-profile crawl plus a cached rerun. Passing `--provider auto`
501
+ runs every locally ready provider. Passing `--provider all` requests Parallel,
502
+ Tavily, and Exa, then records missing keys or optional SDKs in
503
+ `skipped_providers` without failing the benchmark. Live Parallel calls still use
504
+ the local `--max-estimated-cost` guard before any work starts.
505
+
506
+ Use `--target-set tool-docs` to run a cross-pull matrix across the Parallel,
507
+ Exa, Tavily, Raindrop, and DocPull documentation sites. Use `--target-set provider-matrix`
508
+ to add three low-cap adversarial public targets for JS-heavy docs, noisy
509
+ archived navigation, and freshness-sensitive pricing. Matrix runs skip the
510
+ cached core pass by default so the headline grid stays provider x target; pass
511
+ `--cached-pass` to force cache measurement across the matrix.
512
+
513
+ The compatibility flags `--parallel`, `--tavily`, and `--exa` still work as
514
+ provider aliases. Tavily Search + Extract and Exa Search-with-contents are
515
+ normalized into the same local
516
+ `documents.ndjson`, `corpus.manifest.json`, `sources.md`, provider `*.pack.json`,
517
+ `pack.score.json`, and `source.scores.json` artifacts as the core and Parallel
518
+ cases. The report keeps the legacy pack score and adds weighted benchmark
519
+ sub-scores for coverage, cleanliness, source fidelity, freshness, and density.
520
+ Keys can be exported as `PARALLEL_API_KEY`, `TAVILY_API_KEY`, and `EXA_API_KEY`
521
+ or stored together in `~/.config/docpull/secrets.env`; docpull does not write
522
+ keys into benchmark artifacts.
523
+
524
+ Tavily usage is credit-based, so set `TAVILY_CREDIT_USD` or pass
525
+ `--tavily-credit-usd` to convert credits into estimated dollars for cost
526
+ comparisons. Without that value, Tavily credits are still recorded in
527
+ `cost_units` but excluded from normalized USD totals.
528
+
529
+ To trace the benchmark in Raindrop for a publishable observability/eval loop,
530
+ install the optional SDK and pass `--trace raindrop`:
531
+
532
+ ```bash
533
+ pip install 'docpull[parallel,observability]'
534
+ export PARALLEL_API_KEY="<your-parallel-api-key>"
535
+ export TAVILY_API_KEY="<your-tavily-api-key>"
536
+ export TAVILY_CREDIT_USD="<account-credit-value>"
537
+ export EXA_API_KEY="<your-exa-api-key>"
538
+ export RAINDROP_WRITE_KEY="<your-raindrop-write-key>"
539
+ docpull benchmark quick --target-set provider-matrix --provider all --trace raindrop \
540
+ --max-pages 8 --max-depth 1 --max-search-results 5 --extract-limit 2 \
541
+ --max-estimated-cost 0.10
542
+ docpull benchmark article .bench/runs/<run>/benchmark.report.json
543
+ ```
544
+
545
+ Raindrop traces are metadata-only by default: docpull records timings, counts,
546
+ scores, costs, selected URLs, provider, workflow, target, prompt, settings, and
547
+ artifact paths, but does not send scraped page content unless a future caller
548
+ explicitly adds that behavior. `RAINDROP_WRITE_KEY` can be exported or stored in
549
+ the same `~/.config/docpull/secrets.env` file as the provider keys.
550
+
551
+ When Raindrop tracing is enabled, DocPull also emits Raindrop signals for
552
+ attention-worthy benchmark cells: failed cases, low scores, slow cases,
553
+ high-cost cells, and score-dimension warnings. The benchmark report stores the
554
+ Raindrop event id and signal counts so traced runs can be audited later.
555
+
457
556
  ## Troubleshooting
458
557
 
459
558
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "4.1.0"
7
+ version = "4.2.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"}
@@ -97,6 +97,9 @@ mcp = [
97
97
  parallel = [
98
98
  "parallel-web>=1.0.1",
99
99
  ]
100
+ observability = [
101
+ "raindrop-ai>=0.0.50",
102
+ ]
100
103
  llm = [
101
104
  "tiktoken>=0.7.0",
102
105
  ]
@@ -110,6 +113,7 @@ all = [
110
113
  "python-multipart>=0.0.27",
111
114
  "starlette>=1.0.1",
112
115
  "parallel-web>=1.0.1",
116
+ "raindrop-ai>=0.0.50",
113
117
  ]
114
118
  dev = [
115
119
  "pytest>=7.0.0",
@@ -14,7 +14,7 @@ Usage:
14
14
  print(event)
15
15
  """
16
16
 
17
- __version__ = "4.1.0"
17
+ __version__ = "4.2.0"
18
18
 
19
19
  from .cache import CacheManager, StreamingDeduplicator
20
20
  from .conversion.chunking import Chunk, TokenCounter, chunk_markdown