docpull 6.0.0__tar.gz → 6.1.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 (218) hide show
  1. {docpull-6.0.0/src/docpull.egg-info → docpull-6.1.0}/PKG-INFO +43 -18
  2. {docpull-6.0.0 → docpull-6.1.0}/README.md +31 -12
  3. {docpull-6.0.0 → docpull-6.1.0}/pyproject.toml +13 -6
  4. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/__init__.py +2 -2
  5. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/benchmark.py +42 -4
  6. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/cli.py +35 -1
  7. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/special_cases.py +148 -8
  8. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/trafilatura_extractor.py +1 -1
  9. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/core/fetcher.py +12 -0
  10. docpull-6.1.0/src/docpull/document_parse.py +1204 -0
  11. docpull-6.1.0/src/docpull/document_worker.py +137 -0
  12. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/mcp/server.py +59 -4
  13. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/mcp/tools.py +17 -3
  14. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/config.py +22 -0
  15. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/convert.py +76 -26
  16. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/fetch.py +6 -1
  17. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/metadata.py +4 -0
  18. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/security/download_policy.py +24 -3
  19. {docpull-6.0.0 → docpull-6.1.0/src/docpull.egg-info}/PKG-INFO +43 -18
  20. {docpull-6.0.0 → docpull-6.1.0}/src/docpull.egg-info/SOURCES.txt +2 -0
  21. {docpull-6.0.0 → docpull-6.1.0}/src/docpull.egg-info/requires.txt +10 -3
  22. {docpull-6.0.0 → docpull-6.1.0}/tests/test_benchmark.py +2 -0
  23. {docpull-6.0.0 → docpull-6.1.0}/tests/test_ci_policy.py +46 -2
  24. {docpull-6.0.0 → docpull-6.1.0}/tests/test_cli.py +12 -0
  25. {docpull-6.0.0 → docpull-6.1.0}/tests/test_convert_step_new.py +75 -0
  26. docpull-6.1.0/tests/test_document_parse.py +634 -0
  27. {docpull-6.0.0 → docpull-6.1.0}/tests/test_mcp_server.py +9 -0
  28. {docpull-6.0.0 → docpull-6.1.0}/tests/test_release_a_plus_check_script.py +18 -0
  29. docpull-6.1.0/tests/test_reproducible_release_build.py +179 -0
  30. {docpull-6.0.0 → docpull-6.1.0}/tests/test_security_hardening.py +40 -0
  31. {docpull-6.0.0 → docpull-6.1.0}/tests/test_special_cases.py +142 -0
  32. {docpull-6.0.0 → docpull-6.1.0}/tests/test_surface_contract.py +3 -0
  33. docpull-6.0.0/src/docpull/document_parse.py +0 -643
  34. docpull-6.0.0/tests/test_document_parse.py +0 -133
  35. {docpull-6.0.0 → docpull-6.1.0}/LICENSE +0 -0
  36. {docpull-6.0.0 → docpull-6.1.0}/setup.cfg +0 -0
  37. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/__main__.py +0 -0
  38. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/accounting.py +0 -0
  39. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/agent_publish.py +0 -0
  40. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/auth_cli.py +0 -0
  41. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/basis.py +0 -0
  42. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/cache/__init__.py +0 -0
  43. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/cache/frontier.py +0 -0
  44. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/cache/manager.py +0 -0
  45. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/cache/streaming_dedup.py +0 -0
  46. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_aliases.py +0 -0
  47. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_ci.py +0 -0
  48. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/__init__.py +0 -0
  49. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/_legacy_cli.py +0 -0
  50. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/brand.py +0 -0
  51. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/cli.py +0 -0
  52. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/common.py +0 -0
  53. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/dataset.py +0 -0
  54. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/feed.py +0 -0
  55. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/openapi.py +0 -0
  56. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/package.py +0 -0
  57. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/paper.py +0 -0
  58. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/product.py +0 -0
  59. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/repo.py +0 -0
  60. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/schema_extract.py +0 -0
  61. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/search.py +0 -0
  62. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/standards.py +0 -0
  63. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/styleguide.py +0 -0
  64. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/transcript.py +0 -0
  65. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/typed.py +0 -0
  66. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/typed_models.py +0 -0
  67. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/visuals.py +0 -0
  68. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/context_packs/wiki.py +0 -0
  69. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/__init__.py +0 -0
  70. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/article_cleanup.py +0 -0
  71. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/chunking.py +0 -0
  72. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/ensemble.py +0 -0
  73. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/extractor.py +0 -0
  74. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/filings.py +0 -0
  75. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/markdown.py +0 -0
  76. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/conversion/protocols.py +0 -0
  77. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/core/__init__.py +0 -0
  78. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/__init__.py +0 -0
  79. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/_fetch.py +0 -0
  80. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/composite.py +0 -0
  81. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/contracts.py +0 -0
  82. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/crawler.py +0 -0
  83. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/filters.py +0 -0
  84. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  85. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  86. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  87. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/link_extractors/static.py +0 -0
  88. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/protocols.py +0 -0
  89. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery/sitemap.py +0 -0
  90. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/discovery_cli.py +0 -0
  91. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/doctor.py +0 -0
  92. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/eval_grade.py +0 -0
  93. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/evidence_pack.py +0 -0
  94. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/exports.py +0 -0
  95. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/fixtures/__init__.py +0 -0
  96. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  97. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/free_core.py +0 -0
  98. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/free_core_smoke.py +0 -0
  99. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/graph.py +0 -0
  100. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/hosted.py +0 -0
  101. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/http/__init__.py +0 -0
  102. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/http/client.py +0 -0
  103. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/http/protocols.py +0 -0
  104. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/http/rate_limiter.py +0 -0
  105. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/judge.py +0 -0
  106. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/local_workflows.py +0 -0
  107. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/mcp/__init__.py +0 -0
  108. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/mcp/sources.py +0 -0
  109. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/metadata_extractor.py +0 -0
  110. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/__init__.py +0 -0
  111. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/document.py +0 -0
  112. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/events.py +0 -0
  113. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/profiles.py +0 -0
  114. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/models/run.py +0 -0
  115. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/monitor.py +0 -0
  116. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/output_contract.py +0 -0
  117. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pack_reader.py +0 -0
  118. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pack_tools.py +0 -0
  119. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/parallel_workflows.py +0 -0
  120. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/parity.py +0 -0
  121. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/parity_cli.py +0 -0
  122. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/passk.py +0 -0
  123. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/__init__.py +0 -0
  124. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/base.py +0 -0
  125. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/manifest.py +0 -0
  126. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/__init__.py +0 -0
  127. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  128. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  129. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/render.py +0 -0
  130. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/save.py +0 -0
  131. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/save_json.py +0 -0
  132. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  133. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/save_okf.py +0 -0
  134. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  135. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/pipeline/steps/validate.py +0 -0
  136. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/policy.py +0 -0
  137. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/policy_cli.py +0 -0
  138. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/project.py +0 -0
  139. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/provider_adapters.py +0 -0
  140. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/provider_capabilities.py +0 -0
  141. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/provider_cli.py +0 -0
  142. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/provider_keys.py +0 -0
  143. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/provider_probes.py +0 -0
  144. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/py.typed +0 -0
  145. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/redaction.py +0 -0
  146. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/rendering.py +0 -0
  147. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/scraper.py +0 -0
  148. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/security/__init__.py +0 -0
  149. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/security/robots.py +0 -0
  150. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/security/url_validator.py +0 -0
  151. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/server.py +0 -0
  152. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/share.py +0 -0
  153. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/skill_export.py +0 -0
  154. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/source_scoring.py +0 -0
  155. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/surface.py +0 -0
  156. {docpull-6.0.0 → docpull-6.1.0}/src/docpull/time_utils.py +0 -0
  157. {docpull-6.0.0 → docpull-6.1.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  158. {docpull-6.0.0 → docpull-6.1.0}/src/docpull.egg-info/entry_points.txt +0 -0
  159. {docpull-6.0.0 → docpull-6.1.0}/src/docpull.egg-info/top_level.txt +0 -0
  160. {docpull-6.0.0 → docpull-6.1.0}/tests/test_accounting.py +0 -0
  161. {docpull-6.0.0 → docpull-6.1.0}/tests/test_auth_cli.py +0 -0
  162. {docpull-6.0.0 → docpull-6.1.0}/tests/test_basis.py +0 -0
  163. {docpull-6.0.0 → docpull-6.1.0}/tests/test_cache_conditional_get.py +0 -0
  164. {docpull-6.0.0 → docpull-6.1.0}/tests/test_chunking.py +0 -0
  165. {docpull-6.0.0 → docpull-6.1.0}/tests/test_claim_audit_script.py +0 -0
  166. {docpull-6.0.0 → docpull-6.1.0}/tests/test_context_ci.py +0 -0
  167. {docpull-6.0.0 → docpull-6.1.0}/tests/test_context_packs.py +0 -0
  168. {docpull-6.0.0 → docpull-6.1.0}/tests/test_conversion.py +0 -0
  169. {docpull-6.0.0 → docpull-6.1.0}/tests/test_discovery.py +0 -0
  170. {docpull-6.0.0 → docpull-6.1.0}/tests/test_discovery_contracts.py +0 -0
  171. {docpull-6.0.0 → docpull-6.1.0}/tests/test_doctor.py +0 -0
  172. {docpull-6.0.0 → docpull-6.1.0}/tests/test_document_record.py +0 -0
  173. {docpull-6.0.0 → docpull-6.1.0}/tests/test_evidence_pack.py +0 -0
  174. {docpull-6.0.0 → docpull-6.1.0}/tests/test_exports.py +0 -0
  175. {docpull-6.0.0 → docpull-6.1.0}/tests/test_extractor_ensemble.py +0 -0
  176. {docpull-6.0.0 → docpull-6.1.0}/tests/test_feed_pack.py +0 -0
  177. {docpull-6.0.0 → docpull-6.1.0}/tests/test_free_core.py +0 -0
  178. {docpull-6.0.0 → docpull-6.1.0}/tests/test_frontier_resume.py +0 -0
  179. {docpull-6.0.0 → docpull-6.1.0}/tests/test_graph.py +0 -0
  180. {docpull-6.0.0 → docpull-6.1.0}/tests/test_hosted.py +0 -0
  181. {docpull-6.0.0 → docpull-6.1.0}/tests/test_integration.py +0 -0
  182. {docpull-6.0.0 → docpull-6.1.0}/tests/test_judge.py +0 -0
  183. {docpull-6.0.0 → docpull-6.1.0}/tests/test_link_extractors.py +0 -0
  184. {docpull-6.0.0 → docpull-6.1.0}/tests/test_live_web_smoke.py +0 -0
  185. {docpull-6.0.0 → docpull-6.1.0}/tests/test_local_workflows.py +0 -0
  186. {docpull-6.0.0 → docpull-6.1.0}/tests/test_mcp_tools.py +0 -0
  187. {docpull-6.0.0 → docpull-6.1.0}/tests/test_metadata_extractor.py +0 -0
  188. {docpull-6.0.0 → docpull-6.1.0}/tests/test_monitor.py +0 -0
  189. {docpull-6.0.0 → docpull-6.1.0}/tests/test_naming.py +0 -0
  190. {docpull-6.0.0 → docpull-6.1.0}/tests/test_openapi_pack.py +0 -0
  191. {docpull-6.0.0 → docpull-6.1.0}/tests/test_optional_extras_installed.py +0 -0
  192. {docpull-6.0.0 → docpull-6.1.0}/tests/test_output_contract.py +0 -0
  193. {docpull-6.0.0 → docpull-6.1.0}/tests/test_outputs_e2e.py +0 -0
  194. {docpull-6.0.0 → docpull-6.1.0}/tests/test_pack_server.py +0 -0
  195. {docpull-6.0.0 → docpull-6.1.0}/tests/test_pack_tools.py +0 -0
  196. {docpull-6.0.0 → docpull-6.1.0}/tests/test_parallel_workflows.py +0 -0
  197. {docpull-6.0.0 → docpull-6.1.0}/tests/test_parity_workflows.py +0 -0
  198. {docpull-6.0.0 → docpull-6.1.0}/tests/test_passk.py +0 -0
  199. {docpull-6.0.0 → docpull-6.1.0}/tests/test_pipeline.py +0 -0
  200. {docpull-6.0.0 → docpull-6.1.0}/tests/test_policy.py +0 -0
  201. {docpull-6.0.0 → docpull-6.1.0}/tests/test_policy_cli.py +0 -0
  202. {docpull-6.0.0 → docpull-6.1.0}/tests/test_project.py +0 -0
  203. {docpull-6.0.0 → docpull-6.1.0}/tests/test_provider_adapters.py +0 -0
  204. {docpull-6.0.0 → docpull-6.1.0}/tests/test_provider_cli.py +0 -0
  205. {docpull-6.0.0 → docpull-6.1.0}/tests/test_provider_keys.py +0 -0
  206. {docpull-6.0.0 → docpull-6.1.0}/tests/test_provider_probes.py +0 -0
  207. {docpull-6.0.0 → docpull-6.1.0}/tests/test_public_feature_smoke.py +0 -0
  208. {docpull-6.0.0 → docpull-6.1.0}/tests/test_rate_limiter.py +0 -0
  209. {docpull-6.0.0 → docpull-6.1.0}/tests/test_real_feature_smoke_script.py +0 -0
  210. {docpull-6.0.0 → docpull-6.1.0}/tests/test_real_site_regressions.py +0 -0
  211. {docpull-6.0.0 → docpull-6.1.0}/tests/test_rendering.py +0 -0
  212. {docpull-6.0.0 → docpull-6.1.0}/tests/test_save_ndjson.py +0 -0
  213. {docpull-6.0.0 → docpull-6.1.0}/tests/test_save_sqlite.py +0 -0
  214. {docpull-6.0.0 → docpull-6.1.0}/tests/test_share.py +0 -0
  215. {docpull-6.0.0 → docpull-6.1.0}/tests/test_source_scoring.py +0 -0
  216. {docpull-6.0.0 → docpull-6.1.0}/tests/test_time_utils.py +0 -0
  217. {docpull-6.0.0 → docpull-6.1.0}/tests/test_trafilatura_extractor.py +0 -0
  218. {docpull-6.0.0 → docpull-6.1.0}/tests/test_typed_knowledge_packs.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 6.0.0
3
+ Version: 6.1.0
4
4
  Summary: Declare, sync, diff, and lock context dependencies for AI agents
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://docpull.raintree.technology
9
- Project-URL: Documentation, https://docpull.raintree.technology
8
+ Project-URL: Homepage, https://github.com/raintree-technology/docpull
9
+ Project-URL: Documentation, https://github.com/raintree-technology/docpull#readme
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
@@ -71,11 +71,16 @@ Requires-Dist: uvicorn>=0.30.0; extra == "serve"
71
71
  Provides-Extra: parquet
72
72
  Requires-Dist: pyarrow>=16.0.0; extra == "parquet"
73
73
  Provides-Extra: markitdown
74
- Requires-Dist: markitdown>=0.1.0; extra == "markitdown"
74
+ Requires-Dist: pypdf>=6.14.2; extra == "markitdown"
75
+ Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "markitdown"
75
76
  Provides-Extra: unstructured
77
+ Requires-Dist: pypdf>=6.14.2; extra == "unstructured"
76
78
  Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "unstructured"
79
+ Provides-Extra: pdf
80
+ Requires-Dist: pypdf>=6.14.2; extra == "pdf"
77
81
  Provides-Extra: parse
78
- Requires-Dist: markitdown>=0.1.0; extra == "parse"
82
+ Requires-Dist: pypdf>=6.14.2; extra == "parse"
83
+ Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "parse"
79
84
  Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "parse"
80
85
  Provides-Extra: presidio
81
86
  Requires-Dist: presidio-analyzer>=2.2.0; extra == "presidio"
@@ -96,7 +101,8 @@ Requires-Dist: python-multipart>=0.0.27; extra == "all"
96
101
  Requires-Dist: starlette>=1.3.1; extra == "all"
97
102
  Requires-Dist: uvicorn>=0.30.0; extra == "all"
98
103
  Requires-Dist: pyarrow>=16.0.0; extra == "all"
99
- Requires-Dist: markitdown>=0.1.0; extra == "all"
104
+ Requires-Dist: pypdf>=6.14.2; extra == "all"
105
+ Requires-Dist: markitdown[docx,pdf]>=0.1.0; extra == "all"
100
106
  Requires-Dist: unstructured[all-docs]>=0.16.0; extra == "all"
101
107
  Requires-Dist: presidio-analyzer>=2.2.0; extra == "all"
102
108
  Requires-Dist: e2b>=2.0.0; extra == "all"
@@ -126,10 +132,12 @@ Dynamic: license-file
126
132
  [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
127
133
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
128
134
 
135
+ <!-- mcp-name: io.github.raintree-technology/docpull -->
136
+
129
137
  DocPull is a local-first dependency manager for AI context. Define the public
130
- docs and web sources an agent depends on, sync them into cited context packs,
131
- diff what changed, and export reproducible context for Cursor, Claude, Codex,
132
- OpenAI, LlamaIndex, LangChain, MCP clients, and RAG pipelines.
138
+ web sources an agent depends on, sync them into cited context packs, diff what
139
+ changed, and export reproducible context for Cursor, Claude, OpenAI,
140
+ LlamaIndex, LangChain, MCP clients, and RAG pipelines.
133
141
 
134
142
  The core workflow is a `docpull.yaml` plus a `.docpull/context.lock.json`,
135
143
  similar in spirit to code dependency manifests and lockfiles:
@@ -141,7 +149,7 @@ docpull install
141
149
  docpull deps
142
150
  docpull sync
143
151
  docpull diff
144
- docpull export context-pack --target codex
152
+ docpull export context-pack --target openai
145
153
  ```
146
154
 
147
155
  Bundled aliases such as `stripe`, `react`, `postgres`, `openai`, and
@@ -179,6 +187,12 @@ pages such as blogs, API references, OpenAPI specs, changelogs, vendor pages,
179
187
  product pages, filings, docs sites, and other pages where the useful content is
180
188
  available in HTML or embedded page data.
181
189
 
190
+ Direct fetches also recognize standards-style plain text and common textual
191
+ formats from their response media type, including extensionless URLs. Remote
192
+ documents remain denied by default; explicitly opt into local PDF parsing with
193
+ `docpull URL --remote-documents pdf` after installing `docpull[markitdown]` or
194
+ `docpull[parse]`. This does not enable a browser, cloud parser, or paid route.
195
+
182
196
  DocPull is browser-free by default. JS-only pages are skipped with a clear
183
197
  reason unless you explicitly opt into a local renderer. See
184
198
  [Web Source Boundary](docs/scraping-boundary.md) and
@@ -504,12 +518,11 @@ More examples live in [CLI Recipes](docs/examples/README.md).
504
518
  With an explicit `--skill-agent`, docpull stores the fetched corpus under
505
519
  `.docpull/skills/<name>/references` and creates agent-specific wrappers that
506
520
  point at that corpus. `--skill-agent claude` writes a Claude Code skill under
507
- `.claude/skills/<name>/`, `--skill-agent codex` writes a Codex skill under
508
- `.agents/skills/<name>/` with `agents/openai.yaml`, and `--skill-agent cursor`
509
- writes a Cursor project rule at `.cursor/rules/<name>.mdc`. Use
510
- `--skill-agent all` to create all three. If you pass `--output-dir`, docpull
511
- stages the generated corpus there; explicit `--skill-agent` targets still write
512
- their active agent wrappers.
521
+ `.claude/skills/<name>/`, and `--skill-agent cursor` writes a Cursor project
522
+ rule at `.cursor/rules/<name>.mdc`. Use `--skill-agent all` to create every
523
+ supported wrapper. If you pass `--output-dir`, docpull stages the generated
524
+ corpus there; explicit `--skill-agent` targets still write their active agent
525
+ wrappers.
513
526
 
514
527
  Use docpull when you need to:
515
528
 
@@ -591,6 +604,18 @@ MarkItDown or Unstructured parsers for complex office/PDF files when installed.
591
604
  Install `docpull[markitdown]`, `docpull[unstructured]`, or `docpull[parse]`
592
605
  for those backends.
593
606
 
607
+ An HTTPS PDF can enter the ordinary fetch contract only with explicit consent:
608
+
609
+ ```bash
610
+ docpull https://example.com/handbook.pdf --remote-documents pdf \
611
+ --remote-document-backend auto -o ./packs/handbook
612
+ ```
613
+
614
+ The response must identify as PDF and contain a PDF signature. DocPull parses
615
+ it locally through a mode-`0600` temporary file, records the source hash and
616
+ parser provenance, and removes the temporary source. Other remote download
617
+ types remain blocked.
618
+
594
619
  Every file-backed run writes `corpus.manifest.json` with stable document IDs,
595
620
  chunk IDs, hashes, output paths, and chunk counts. See
596
621
  [Corpus Manifest](docs/corpus-manifest.md).
@@ -719,7 +744,7 @@ part of the package release contract.
719
744
  - `docpull export` writes local files for OpenAI vector JSONL, LangChain,
720
745
  LlamaIndex, DSPy, Sheets CSV/TSV, n8n workflow JSON, Vercel AI SDK JSON,
721
746
  CrewAI JSON, warehouse NDJSON, optional Parquet via `docpull[parquet]`, and
722
- Codex/Claude/Cursor agent references.
747
+ agent reference bundles.
723
748
  ## Security Defaults
724
749
 
725
750
  - HTTPS-only fetching with robots.txt compliance.
@@ -753,7 +778,7 @@ docpull URL --preview-urls
753
778
 
754
779
  ## Links
755
780
 
756
- - [Website](https://docpull.raintree.technology)
781
+ - [Documentation](https://github.com/raintree-technology/docpull#readme)
757
782
  - [PyPI](https://pypi.org/project/docpull/)
758
783
  - [GitHub](https://github.com/raintree-technology/docpull)
759
784
  - [Metrics](METRICS.md)
@@ -8,10 +8,12 @@
8
8
  [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
9
9
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
10
10
 
11
+ <!-- mcp-name: io.github.raintree-technology/docpull -->
12
+
11
13
  DocPull is a local-first dependency manager for AI context. Define the public
12
- docs and web sources an agent depends on, sync them into cited context packs,
13
- diff what changed, and export reproducible context for Cursor, Claude, Codex,
14
- OpenAI, LlamaIndex, LangChain, MCP clients, and RAG pipelines.
14
+ web sources an agent depends on, sync them into cited context packs, diff what
15
+ changed, and export reproducible context for Cursor, Claude, OpenAI,
16
+ LlamaIndex, LangChain, MCP clients, and RAG pipelines.
15
17
 
16
18
  The core workflow is a `docpull.yaml` plus a `.docpull/context.lock.json`,
17
19
  similar in spirit to code dependency manifests and lockfiles:
@@ -23,7 +25,7 @@ docpull install
23
25
  docpull deps
24
26
  docpull sync
25
27
  docpull diff
26
- docpull export context-pack --target codex
28
+ docpull export context-pack --target openai
27
29
  ```
28
30
 
29
31
  Bundled aliases such as `stripe`, `react`, `postgres`, `openai`, and
@@ -61,6 +63,12 @@ pages such as blogs, API references, OpenAPI specs, changelogs, vendor pages,
61
63
  product pages, filings, docs sites, and other pages where the useful content is
62
64
  available in HTML or embedded page data.
63
65
 
66
+ Direct fetches also recognize standards-style plain text and common textual
67
+ formats from their response media type, including extensionless URLs. Remote
68
+ documents remain denied by default; explicitly opt into local PDF parsing with
69
+ `docpull URL --remote-documents pdf` after installing `docpull[markitdown]` or
70
+ `docpull[parse]`. This does not enable a browser, cloud parser, or paid route.
71
+
64
72
  DocPull is browser-free by default. JS-only pages are skipped with a clear
65
73
  reason unless you explicitly opt into a local renderer. See
66
74
  [Web Source Boundary](docs/scraping-boundary.md) and
@@ -386,12 +394,11 @@ More examples live in [CLI Recipes](docs/examples/README.md).
386
394
  With an explicit `--skill-agent`, docpull stores the fetched corpus under
387
395
  `.docpull/skills/<name>/references` and creates agent-specific wrappers that
388
396
  point at that corpus. `--skill-agent claude` writes a Claude Code skill under
389
- `.claude/skills/<name>/`, `--skill-agent codex` writes a Codex skill under
390
- `.agents/skills/<name>/` with `agents/openai.yaml`, and `--skill-agent cursor`
391
- writes a Cursor project rule at `.cursor/rules/<name>.mdc`. Use
392
- `--skill-agent all` to create all three. If you pass `--output-dir`, docpull
393
- stages the generated corpus there; explicit `--skill-agent` targets still write
394
- their active agent wrappers.
397
+ `.claude/skills/<name>/`, and `--skill-agent cursor` writes a Cursor project
398
+ rule at `.cursor/rules/<name>.mdc`. Use `--skill-agent all` to create every
399
+ supported wrapper. If you pass `--output-dir`, docpull stages the generated
400
+ corpus there; explicit `--skill-agent` targets still write their active agent
401
+ wrappers.
395
402
 
396
403
  Use docpull when you need to:
397
404
 
@@ -473,6 +480,18 @@ MarkItDown or Unstructured parsers for complex office/PDF files when installed.
473
480
  Install `docpull[markitdown]`, `docpull[unstructured]`, or `docpull[parse]`
474
481
  for those backends.
475
482
 
483
+ An HTTPS PDF can enter the ordinary fetch contract only with explicit consent:
484
+
485
+ ```bash
486
+ docpull https://example.com/handbook.pdf --remote-documents pdf \
487
+ --remote-document-backend auto -o ./packs/handbook
488
+ ```
489
+
490
+ The response must identify as PDF and contain a PDF signature. DocPull parses
491
+ it locally through a mode-`0600` temporary file, records the source hash and
492
+ parser provenance, and removes the temporary source. Other remote download
493
+ types remain blocked.
494
+
476
495
  Every file-backed run writes `corpus.manifest.json` with stable document IDs,
477
496
  chunk IDs, hashes, output paths, and chunk counts. See
478
497
  [Corpus Manifest](docs/corpus-manifest.md).
@@ -601,7 +620,7 @@ part of the package release contract.
601
620
  - `docpull export` writes local files for OpenAI vector JSONL, LangChain,
602
621
  LlamaIndex, DSPy, Sheets CSV/TSV, n8n workflow JSON, Vercel AI SDK JSON,
603
622
  CrewAI JSON, warehouse NDJSON, optional Parquet via `docpull[parquet]`, and
604
- Codex/Claude/Cursor agent references.
623
+ agent reference bundles.
605
624
  ## Security Defaults
606
625
 
607
626
  - HTTPS-only fetching with robots.txt compliance.
@@ -635,7 +654,7 @@ docpull URL --preview-urls
635
654
 
636
655
  ## Links
637
656
 
638
- - [Website](https://docpull.raintree.technology)
657
+ - [Documentation](https://github.com/raintree-technology/docpull#readme)
639
658
  - [PyPI](https://pypi.org/project/docpull/)
640
659
  - [GitHub](https://github.com/raintree-technology/docpull)
641
660
  - [Metrics](METRICS.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "6.0.0"
7
+ version = "6.1.0"
8
8
  dynamic = []
9
9
  description = "Declare, sync, diff, and lock context dependencies for AI agents"
10
10
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -123,13 +123,19 @@ parquet = [
123
123
  "pyarrow>=16.0.0",
124
124
  ]
125
125
  markitdown = [
126
- "markitdown>=0.1.0",
126
+ "pypdf>=6.14.2",
127
+ "markitdown[docx,pdf]>=0.1.0",
127
128
  ]
128
129
  unstructured = [
130
+ "pypdf>=6.14.2",
129
131
  "unstructured[all-docs]>=0.16.0",
130
132
  ]
133
+ pdf = [
134
+ "pypdf>=6.14.2",
135
+ ]
131
136
  parse = [
132
- "markitdown>=0.1.0",
137
+ "pypdf>=6.14.2",
138
+ "markitdown[docx,pdf]>=0.1.0",
133
139
  "unstructured[all-docs]>=0.16.0",
134
140
  ]
135
141
  presidio = [
@@ -154,7 +160,8 @@ all = [
154
160
  "starlette>=1.3.1",
155
161
  "uvicorn>=0.30.0",
156
162
  "pyarrow>=16.0.0",
157
- "markitdown>=0.1.0",
163
+ "pypdf>=6.14.2",
164
+ "markitdown[docx,pdf]>=0.1.0",
158
165
  "unstructured[all-docs]>=0.16.0",
159
166
  "presidio-analyzer>=2.2.0",
160
167
  "e2b>=2.0.0",
@@ -179,8 +186,8 @@ dev = [
179
186
  docpull = "docpull.cli:main"
180
187
 
181
188
  [project.urls]
182
- Homepage = "https://docpull.raintree.technology"
183
- Documentation = "https://docpull.raintree.technology"
189
+ Homepage = "https://github.com/raintree-technology/docpull"
190
+ Documentation = "https://github.com/raintree-technology/docpull#readme"
184
191
  Repository = "https://github.com/raintree-technology/docpull"
185
192
  "Source Code" = "https://github.com/raintree-technology/docpull"
186
193
  "Bug Tracker" = "https://github.com/raintree-technology/docpull/issues"
@@ -1,5 +1,5 @@
1
1
  """
2
- docpull - Fetch and convert static/server-rendered web content to markdown.
2
+ docpull - Context dependencies for AI agents.
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__ = "6.0.0"
17
+ __version__ = "6.1.0"
18
18
 
19
19
  from .cache import CacheManager, StreamingDeduplicator
20
20
  from .context_ci import CIThresholds, ContextCIError, run_context_ci
@@ -200,8 +200,8 @@ TOOL_DOC_TARGETS: tuple[_BenchmarkTarget, ...] = (
200
200
  _BenchmarkTarget(
201
201
  id="docpull_docs",
202
202
  label="DocPull docs",
203
- url="https://docpull.raintree.technology",
204
- include_domains=("docpull.raintree.technology",),
203
+ url="https://github.com/raintree-technology/docpull",
204
+ include_domains=("github.com",),
205
205
  objective="Build an agent context pack for DocPull documentation",
206
206
  queries=("DocPull documentation CLI provider benchmark docs",),
207
207
  freshness_terms=("changelog", "release", "benchmark"),
@@ -963,13 +963,45 @@ def run_quick_benchmark(
963
963
 
964
964
  report_path = run_dir / "benchmark.report.json"
965
965
  markdown_path = run_dir / "benchmark.summary.md"
966
+ config_path = run_dir / "benchmark.config.json"
967
+ generated_at = utc_now_iso()
968
+ safe_provider_status = _benchmark_provider_statuses(provider_status)
966
969
  artifacts: dict[str, str] = {
970
+ "config": str(config_path),
967
971
  "json": str(report_path),
968
972
  "markdown": str(markdown_path),
969
973
  }
974
+ benchmark_config = {
975
+ "schema_version": BENCHMARK_SCHEMA_VERSION,
976
+ "generated_at": generated_at,
977
+ "run_dir": str(run_dir),
978
+ "target_url": targets[0].url,
979
+ "target_set": target_set,
980
+ "targets": [target.report_dict() for target in targets],
981
+ "requested_providers": requested_providers,
982
+ "enabled_providers": ["core", *providers],
983
+ "skipped_providers": skipped_providers,
984
+ "provider_status": safe_provider_status,
985
+ "runs_per_case": runs,
986
+ "budget_limit_usd": budget_limit,
987
+ "zero_dollar": zero_dollar,
988
+ "settings": {
989
+ "max_pages": max_pages,
990
+ "max_depth": max_depth,
991
+ "max_concurrent": max_concurrent,
992
+ "per_host_concurrent": per_host_concurrent,
993
+ "cache_enabled": cache_enabled,
994
+ "cached_pass": cached_pass,
995
+ "mode": mode,
996
+ "max_search_results": max_search_results,
997
+ "extract_limit": extract_limit,
998
+ "max_estimated_cost": max_estimated_cost,
999
+ },
1000
+ "cost_normalization": _cost_normalization_metadata(tavily_credit_usd),
1001
+ }
970
1002
  report = {
971
1003
  "schema_version": BENCHMARK_SCHEMA_VERSION,
972
- "generated_at": utc_now_iso(),
1004
+ "generated_at": generated_at,
973
1005
  "run_dir": str(run_dir),
974
1006
  "target_url": targets[0].url,
975
1007
  "target_set": target_set,
@@ -981,7 +1013,7 @@ def run_quick_benchmark(
981
1013
  "skipped_providers": skipped_providers,
982
1014
  "budget_limit_usd": budget_limit,
983
1015
  "zero_dollar": zero_dollar,
984
- "provider_status": _benchmark_provider_statuses(provider_status),
1016
+ "provider_status": safe_provider_status,
985
1017
  "cost_normalization": _cost_normalization_metadata(tavily_credit_usd),
986
1018
  "runs_per_case": runs,
987
1019
  "trace": trace.metadata(),
@@ -1011,6 +1043,10 @@ def run_quick_benchmark(
1011
1043
  report["zero_dollar_completion"] = _zero_dollar_completion(cases)
1012
1044
  trace.finish(report)
1013
1045
  report["trace"] = trace.metadata()
1046
+ config_path.write_text(
1047
+ json.dumps(benchmark_config, indent=2, ensure_ascii=False) + "\n",
1048
+ encoding="utf-8",
1049
+ )
1014
1050
  report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
1015
1051
  markdown_path.write_text(_markdown_report(report), encoding="utf-8")
1016
1052
  return report
@@ -3848,6 +3884,7 @@ def _article_markdown(report: dict[str, Any], *, title: str) -> str:
3848
3884
  "",
3849
3885
  "## Artifacts",
3850
3886
  "",
3887
+ f"- Config: `{artifacts.get('config')}`",
3851
3888
  f"- JSON report: `{artifacts.get('json')}`",
3852
3889
  f"- Summary: `{artifacts.get('markdown')}`",
3853
3890
  "",
@@ -3977,6 +4014,7 @@ def _legacy_article_markdown(report: dict[str, Any], *, title: str) -> str:
3977
4014
  "",
3978
4015
  "## Artifacts",
3979
4016
  "",
4017
+ f"- Config: `{artifacts.get('config')}`",
3980
4018
  f"- JSON report: `{artifacts.get('json')}`",
3981
4019
  f"- Summary: `{artifacts.get('markdown')}`",
3982
4020
  "",
@@ -335,6 +335,32 @@ Examples:
335
335
  action="store_true",
336
336
  help="Stream NDJSON records to stdout as each page completes (implies --format ndjson)",
337
337
  )
338
+ parser.add_argument(
339
+ "--remote-documents",
340
+ choices=["off", "pdf"],
341
+ default=None,
342
+ help="Explicitly download and locally parse selected remote document types (default: off)",
343
+ )
344
+ parser.add_argument(
345
+ "--remote-document-backend",
346
+ choices=["auto", "pypdf", "markitdown", "unstructured"],
347
+ default=None,
348
+ help="Local parser backend for --remote-documents (default: auto)",
349
+ )
350
+ parser.add_argument(
351
+ "--remote-document-timeout-seconds",
352
+ type=int,
353
+ default=None,
354
+ metavar="SECONDS",
355
+ help="Wall-time limit for each isolated remote-document parser (default: 60)",
356
+ )
357
+ parser.add_argument(
358
+ "--remote-document-memory-mib",
359
+ type=int,
360
+ default=None,
361
+ metavar="MIB",
362
+ help="Address-space limit for each isolated remote-document parser (default: 1024)",
363
+ )
338
364
 
339
365
  # Crawl settings
340
366
  crawl_group = parser.add_argument_group("crawl settings")
@@ -677,6 +703,14 @@ def run_fetcher(args: argparse.Namespace) -> int:
677
703
  filter_kwargs["enable_special_cases"] = False
678
704
  if args.strict_js_required:
679
705
  filter_kwargs["strict_js_required"] = True
706
+ if args.remote_documents:
707
+ filter_kwargs["remote_documents"] = args.remote_documents
708
+ if args.remote_document_backend:
709
+ filter_kwargs["remote_document_backend"] = args.remote_document_backend
710
+ if args.remote_document_timeout_seconds is not None:
711
+ filter_kwargs["remote_document_timeout_seconds"] = args.remote_document_timeout_seconds
712
+ if args.remote_document_memory_mib is not None:
713
+ filter_kwargs["remote_document_memory_mib"] = args.remote_document_memory_mib
680
714
  if filter_kwargs:
681
715
  config_kwargs["content_filter"] = filter_kwargs
682
716
 
@@ -1164,7 +1198,7 @@ def run_render_cli(argv: list[str]) -> int:
1164
1198
  args.cloud_max_estimated_cost if backend in {"vercel-sandbox", "e2b-sandbox"} else None,
1165
1199
  )
1166
1200
  cloud_estimated_cost = (
1167
- estimate_cloud_render_cost_usd(cast(Literal["vercel-sandbox", "e2b-sandbox"], backend), config)
1201
+ estimate_cloud_render_cost_usd(backend, config)
1168
1202
  if backend in {"vercel-sandbox", "e2b-sandbox"}
1169
1203
  else 0.0
1170
1204
  )
@@ -16,6 +16,7 @@ import json
16
16
  import logging
17
17
  import re
18
18
  from dataclasses import dataclass
19
+ from email.message import Message
19
20
  from typing import Any, Protocol
20
21
  from urllib.parse import urlparse
21
22
 
@@ -710,33 +711,143 @@ class RawTextExtractor:
710
711
 
711
712
  name = "raw_text"
712
713
 
713
- _TEXT_SUFFIXES = (".md", ".markdown", ".mdx", ".txt")
714
+ _TEXT_SUFFIXES = (
715
+ ".csv",
716
+ ".json",
717
+ ".jsonl",
718
+ ".markdown",
719
+ ".md",
720
+ ".mdx",
721
+ ".ndjson",
722
+ ".rst",
723
+ ".text",
724
+ ".tsv",
725
+ ".txt",
726
+ ".xml",
727
+ ".yaml",
728
+ ".yml",
729
+ )
730
+ _TEXT_CONTENT_TYPES = {
731
+ "application/json",
732
+ "application/ld+json",
733
+ "application/x-ndjson",
734
+ "application/xml",
735
+ "application/yaml",
736
+ "application/x-yaml",
737
+ "text/csv",
738
+ "text/markdown",
739
+ "text/plain",
740
+ "text/tab-separated-values",
741
+ "text/x-markdown",
742
+ "text/x-rst",
743
+ "text/xml",
744
+ "text/yaml",
745
+ }
714
746
  _HTML_MARKERS = ("<html", "<body", "<!doctype")
715
-
716
- def try_extract(self, html: bytes, url: str) -> SpecialCaseResult | None:
747
+ _STRUCTURED_LANGUAGES = {
748
+ ".csv": "csv",
749
+ ".json": "json",
750
+ ".jsonl": "ndjson",
751
+ ".ndjson": "ndjson",
752
+ ".rst": "rst",
753
+ ".tsv": "tsv",
754
+ ".xml": "xml",
755
+ ".yaml": "yaml",
756
+ ".yml": "yaml",
757
+ }
758
+ _MEDIA_LANGUAGES = {
759
+ "application/json": "json",
760
+ "application/ld+json": "json",
761
+ "application/x-ndjson": "ndjson",
762
+ "application/xml": "xml",
763
+ "application/yaml": "yaml",
764
+ "application/x-yaml": "yaml",
765
+ "text/csv": "csv",
766
+ "text/tab-separated-values": "tsv",
767
+ "text/x-rst": "rst",
768
+ "text/xml": "xml",
769
+ "text/yaml": "yaml",
770
+ }
771
+
772
+ def try_extract(
773
+ self,
774
+ html: bytes,
775
+ url: str,
776
+ *,
777
+ content_type: str | None = None,
778
+ ) -> SpecialCaseResult | None:
717
779
  parsed = urlparse(url)
718
780
  path = parsed.path.lower()
719
- if not path.endswith(self._TEXT_SUFFIXES):
781
+ media_type = self._media_type(content_type)
782
+ identified_by_type = media_type in self._TEXT_CONTENT_TYPES
783
+ if not identified_by_type and not path.endswith(self._TEXT_SUFFIXES):
720
784
  return None
721
785
 
722
- text = _decode_html(html).strip()
786
+ text = self._decode_text(html, content_type).strip()
723
787
  if not text:
724
788
  return None
789
+ language = self._structured_language(path, media_type)
725
790
  lower_head = text[:500].lower()
726
- if any(marker in lower_head for marker in self._HTML_MARKERS):
791
+ if language is None and any(marker in lower_head for marker in self._HTML_MARKERS):
727
792
  return None
728
- if path.endswith(".txt") and not self._looks_like_docs_text(text, path):
793
+ if (
794
+ path.endswith((".txt", ".text"))
795
+ and not identified_by_type
796
+ and not self._looks_like_docs_text(text, path)
797
+ ):
729
798
  return None
730
799
 
731
800
  title = self._extract_title(text) or parsed.path.rsplit("/", 1)[-1] or "Document"
732
801
  source_type = "llms_txt" if path.endswith(("/llms.txt", "/llms-full.txt")) else self.name
802
+ markdown = self._fenced(text, language) if language else self._safe_direct_markdown(text)
733
803
  return SpecialCaseResult(
734
- markdown=text.rstrip() + "\n",
804
+ markdown=markdown,
735
805
  title=title,
736
806
  source_type=source_type,
737
807
  extra={"framework": source_type},
738
808
  )
739
809
 
810
+ @classmethod
811
+ def _structured_language(cls, path: str, media_type: str) -> str | None:
812
+ if media_type in {"text/markdown", "text/x-markdown"} or path.endswith((".markdown", ".md", ".mdx")):
813
+ return None
814
+ media_language = cls._MEDIA_LANGUAGES.get(media_type)
815
+ if media_language:
816
+ return media_language
817
+ return next(
818
+ (language for suffix, language in cls._STRUCTURED_LANGUAGES.items() if path.endswith(suffix)),
819
+ None,
820
+ )
821
+
822
+ @staticmethod
823
+ def _fenced(text: str, language: str) -> str:
824
+ longest_run = max((len(match.group(0)) for match in re.finditer(r"`+", text)), default=0)
825
+ fence = "`" * max(3, longest_run + 1)
826
+ return f"{fence}{language}\n{text.rstrip()}\n{fence}\n"
827
+
828
+ @staticmethod
829
+ def _safe_direct_markdown(text: str) -> str:
830
+ inert = re.sub(r"<(/?)script\b", r"&lt;\1script", text, flags=re.IGNORECASE)
831
+ return inert.rstrip() + "\n"
832
+
833
+ @staticmethod
834
+ def _media_type(content_type: str | None) -> str:
835
+ return (content_type or "").split(";", 1)[0].strip().casefold()
836
+
837
+ @staticmethod
838
+ def _decode_text(body: bytes, content_type: str | None) -> str:
839
+ if body.startswith(b"\xef\xbb\xbf"):
840
+ return body.decode("utf-8-sig", errors="replace")
841
+ encoding = "utf-8"
842
+ if content_type:
843
+ message = Message()
844
+ message["content-type"] = content_type
845
+ encoding = message.get_content_charset() or encoding
846
+ try:
847
+ return body.decode(encoding)
848
+ except (LookupError, UnicodeDecodeError):
849
+ return body.decode("utf-8", errors="replace")
850
+
740
851
  @staticmethod
741
852
  def _looks_like_docs_text(text: str, path: str) -> bool:
742
853
  if path.endswith(("/llms.txt", "/llms-full.txt")):
@@ -802,6 +913,33 @@ class SphinxObjectsInvExtractor:
802
913
  )
803
914
 
804
915
 
916
+ class RfcEditorExtractor:
917
+ """Preserve complete RFC Editor HTML documents instead of one scored section."""
918
+
919
+ name = "rfc_editor"
920
+
921
+ def try_extract(self, html: bytes, url: str) -> SpecialCaseResult | None:
922
+ parsed = urlparse(url)
923
+ if not _hostname_matches_domain(parsed.hostname or "", "rfc-editor.org"):
924
+ return None
925
+ if re.fullmatch(r"/rfc/rfc\d+\.html", parsed.path.casefold()) is None:
926
+ return None
927
+
928
+ from .extractor import MainContentExtractor
929
+ from .markdown import HtmlToMarkdown
930
+
931
+ extracted_html = MainContentExtractor(content_selectors=["body"]).extract(html, url)
932
+ markdown = HtmlToMarkdown().convert(extracted_html, url).strip()
933
+ if len(markdown) < 80:
934
+ return None
935
+ return SpecialCaseResult(
936
+ markdown=markdown + "\n",
937
+ title=_title_from_soup(_soup(html)),
938
+ source_type=self.name,
939
+ extra={"framework": "rfc_editor"},
940
+ )
941
+
942
+
805
943
  class MdxSourceExtractor:
806
944
  """Rewrite ``edit-this-page`` GitHub links to raw MDX source URLs.
807
945
 
@@ -842,6 +980,7 @@ class MdxSourceExtractor:
842
980
  DEFAULT_CHAIN: list[SpecialCaseExtractor] = [
843
981
  OpenApiExtractor(),
844
982
  RawTextExtractor(),
983
+ RfcEditorExtractor(),
845
984
  MintlifyExtractor(),
846
985
  NextDataExtractor(),
847
986
  MkDocsMaterialExtractor(),
@@ -969,6 +1108,7 @@ __all__ = [
969
1108
  "RawTextExtractor",
970
1109
  "ReadMeExtractor",
971
1110
  "RedocScalarExtractor",
1111
+ "RfcEditorExtractor",
972
1112
  "SpecialCaseExtractor",
973
1113
  "SpecialCaseResult",
974
1114
  "SphinxObjectsInvExtractor",
@@ -63,7 +63,7 @@ class TrafilaturaExtractor:
63
63
  if result is None:
64
64
  logger.debug("trafilatura returned no content for %s", url)
65
65
  return ""
66
- return result.strip() + "\n"
66
+ return str(result).strip() + "\n"
67
67
 
68
68
 
69
69
  __all__ = ["TrafilaturaExtractor"]