docpull 4.3.1__tar.gz → 4.4.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 (118) hide show
  1. {docpull-4.3.1 → docpull-4.4.0}/LICENSE +1 -1
  2. {docpull-4.3.1/src/docpull.egg-info → docpull-4.4.0}/PKG-INFO +41 -12
  3. {docpull-4.3.1 → docpull-4.4.0}/README.md +37 -6
  4. {docpull-4.3.1 → docpull-4.4.0}/pyproject.toml +7 -7
  5. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/__init__.py +2 -2
  6. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/benchmark.py +1 -1
  7. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cli.py +13 -5
  8. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/extractor.py +0 -9
  9. docpull-4.4.0/src/docpull/conversion/filings.py +57 -0
  10. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/markdown.py +1 -7
  11. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/core/fetcher.py +1 -5
  12. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -3
  13. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/static.py +0 -1
  14. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/sitemap.py +0 -4
  15. docpull-4.4.0/src/docpull/evidence_pack.py +1050 -0
  16. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/client.py +57 -9
  17. docpull-4.4.0/src/docpull/mcp/__init__.py +10 -0
  18. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/server.py +12 -12
  19. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/sources.py +3 -3
  20. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/tools.py +8 -8
  21. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/metadata_extractor.py +0 -10
  22. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/config.py +10 -5
  23. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/document.py +24 -1
  24. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/profiles.py +23 -0
  25. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/parallel_workflows.py +2 -2
  26. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/manifest.py +15 -0
  27. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/convert.py +9 -0
  28. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/metadata.py +0 -5
  29. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save.py +2 -8
  30. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_json.py +0 -2
  31. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_okf.py +2 -2
  32. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/download_policy.py +4 -4
  33. {docpull-4.3.1 → docpull-4.4.0/src/docpull.egg-info}/PKG-INFO +41 -12
  34. {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/SOURCES.txt +3 -0
  35. {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/requires.txt +2 -2
  36. {docpull-4.3.1 → docpull-4.4.0}/tests/test_cache_conditional_get.py +0 -9
  37. {docpull-4.3.1 → docpull-4.4.0}/tests/test_ci_policy.py +1 -1
  38. {docpull-4.3.1 → docpull-4.4.0}/tests/test_cli.py +28 -1
  39. {docpull-4.3.1 → docpull-4.4.0}/tests/test_conversion.py +1 -1
  40. {docpull-4.3.1 → docpull-4.4.0}/tests/test_discovery.py +2 -2
  41. docpull-4.4.0/tests/test_evidence_pack.py +236 -0
  42. {docpull-4.3.1 → docpull-4.4.0}/tests/test_integration.py +19 -0
  43. {docpull-4.3.1 → docpull-4.4.0}/tests/test_mcp_tools.py +11 -2
  44. {docpull-4.3.1 → docpull-4.4.0}/tests/test_outputs_e2e.py +1 -1
  45. {docpull-4.3.1 → docpull-4.4.0}/tests/test_parallel_workflows.py +10 -0
  46. {docpull-4.3.1 → docpull-4.4.0}/tests/test_security_hardening.py +98 -3
  47. docpull-4.3.1/src/docpull/mcp/__init__.py +0 -9
  48. {docpull-4.3.1 → docpull-4.4.0}/setup.cfg +0 -0
  49. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/__main__.py +0 -0
  50. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/__init__.py +0 -0
  51. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/frontier.py +0 -0
  52. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/manager.py +0 -0
  53. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/streaming_dedup.py +0 -0
  54. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/__init__.py +0 -0
  55. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/chunking.py +0 -0
  56. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/protocols.py +0 -0
  57. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/special_cases.py +0 -0
  58. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  59. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/core/__init__.py +0 -0
  60. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/__init__.py +0 -0
  61. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/_fetch.py +0 -0
  62. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/composite.py +0 -0
  63. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/crawler.py +0 -0
  64. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/filters.py +0 -0
  65. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  66. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  67. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/protocols.py +0 -0
  68. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/doctor.py +0 -0
  69. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/fixtures/__init__.py +0 -0
  70. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  71. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/__init__.py +0 -0
  72. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/protocols.py +0 -0
  73. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/rate_limiter.py +0 -0
  74. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/judge.py +0 -0
  75. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/__init__.py +0 -0
  76. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/events.py +0 -0
  77. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/run.py +0 -0
  78. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pack_tools.py +0 -0
  79. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/passk.py +0 -0
  80. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/__init__.py +0 -0
  81. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/base.py +0 -0
  82. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/__init__.py +0 -0
  83. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/chunk.py +0 -0
  84. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/dedup.py +0 -0
  85. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/fetch.py +0 -0
  86. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  87. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  88. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/validate.py +0 -0
  89. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/provider_cli.py +0 -0
  90. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/provider_keys.py +0 -0
  91. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/py.typed +0 -0
  92. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/scraper.py +0 -0
  93. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/__init__.py +0 -0
  94. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/robots.py +0 -0
  95. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/url_validator.py +0 -0
  96. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/source_scoring.py +0 -0
  97. {docpull-4.3.1 → docpull-4.4.0}/src/docpull/time_utils.py +0 -0
  98. {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/dependency_links.txt +0 -0
  99. {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/entry_points.txt +0 -0
  100. {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/top_level.txt +0 -0
  101. {docpull-4.3.1 → docpull-4.4.0}/tests/test_benchmark.py +0 -0
  102. {docpull-4.3.1 → docpull-4.4.0}/tests/test_chunking.py +0 -0
  103. {docpull-4.3.1 → docpull-4.4.0}/tests/test_convert_step_new.py +0 -0
  104. {docpull-4.3.1 → docpull-4.4.0}/tests/test_document_record.py +0 -0
  105. {docpull-4.3.1 → docpull-4.4.0}/tests/test_frontier_resume.py +0 -0
  106. {docpull-4.3.1 → docpull-4.4.0}/tests/test_judge.py +0 -0
  107. {docpull-4.3.1 → docpull-4.4.0}/tests/test_link_extractors.py +0 -0
  108. {docpull-4.3.1 → docpull-4.4.0}/tests/test_mcp_server.py +0 -0
  109. {docpull-4.3.1 → docpull-4.4.0}/tests/test_naming.py +0 -0
  110. {docpull-4.3.1 → docpull-4.4.0}/tests/test_pack_tools.py +0 -0
  111. {docpull-4.3.1 → docpull-4.4.0}/tests/test_passk.py +0 -0
  112. {docpull-4.3.1 → docpull-4.4.0}/tests/test_pipeline.py +0 -0
  113. {docpull-4.3.1 → docpull-4.4.0}/tests/test_provider_cli.py +0 -0
  114. {docpull-4.3.1 → docpull-4.4.0}/tests/test_real_site_regressions.py +0 -0
  115. {docpull-4.3.1 → docpull-4.4.0}/tests/test_save_ndjson.py +0 -0
  116. {docpull-4.3.1 → docpull-4.4.0}/tests/test_save_sqlite.py +0 -0
  117. {docpull-4.3.1 → docpull-4.4.0}/tests/test_special_cases.py +0 -0
  118. {docpull-4.3.1 → docpull-4.4.0}/tests/test_time_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Raintree Technology
3
+ Copyright (c) 2025-2026 Raintree Technology
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 4.3.1
3
+ Version: 4.4.0
4
4
  Summary: Convert public web pages into clean Markdown for AI agents and RAG
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -15,16 +15,14 @@ Project-URL: MCP Plugin, https://github.com/raintree-technology/docpull/tree/mai
15
15
  Project-URL: Comparison Guide, https://github.com/raintree-technology/docpull/blob/main/docs/alternatives.md
16
16
  Project-URL: Download Stats, https://pepy.tech/project/docpull
17
17
  Project-URL: Changelog, https://github.com/raintree-technology/docpull/blob/main/docs/CHANGELOG.md
18
- Keywords: python,markdown,documentation,web-scraping,web-crawler,crawler,developer-tools,cli,rag,llm,mcp,ai-agents,claude,ai-training-data
18
+ Keywords: python,markdown,documentation,web-extraction,web-scraping,web-crawler,crawler,developer-tools,cli,rag,llm,mcp,ai-agents,context-engineering,claude,ai-training-data
19
19
  Classifier: Development Status :: 5 - Production/Stable
20
20
  Classifier: Intended Audience :: Developers
21
21
  Classifier: Intended Audience :: Information Technology
22
22
  Classifier: Intended Audience :: Science/Research
23
23
  Classifier: Intended Audience :: Education
24
24
  Classifier: Environment :: Console
25
- Classifier: Topic :: Documentation
26
25
  Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
27
- Classifier: Topic :: Software Development :: Documentation
28
26
  Classifier: Topic :: Text Processing :: Markup :: HTML
29
27
  Classifier: Topic :: Text Processing :: Markup :: Markdown
30
28
  Classifier: Topic :: Utilities
@@ -54,7 +52,7 @@ Requires-Dist: pyyaml>=6.0
54
52
  Requires-Dist: pydantic>=2.0
55
53
  Requires-Dist: urllib3>=2.7.0
56
54
  Provides-Extra: proxy
57
- Requires-Dist: aiohttp-socks>=0.8.0; extra == "proxy"
55
+ Requires-Dist: aiohttp-socks>=0.11.0; extra == "proxy"
58
56
  Provides-Extra: normalize
59
57
  Requires-Dist: url-normalize>=1.4.0; extra == "normalize"
60
58
  Provides-Extra: trafilatura
@@ -73,7 +71,7 @@ Requires-Dist: raindrop-ai>=0.0.50; extra == "observability"
73
71
  Provides-Extra: llm
74
72
  Requires-Dist: tiktoken>=0.7.0; extra == "llm"
75
73
  Provides-Extra: all
76
- Requires-Dist: aiohttp-socks>=0.8.0; extra == "all"
74
+ Requires-Dist: aiohttp-socks>=0.11.0; extra == "all"
77
75
  Requires-Dist: url-normalize>=1.4.0; extra == "all"
78
76
  Requires-Dist: trafilatura>=1.12.0; extra == "all"
79
77
  Requires-Dist: tiktoken>=0.7.0; extra == "all"
@@ -116,9 +114,9 @@ Dynamic: license-file
116
114
 
117
115
  docpull is a Python CLI, SDK, and MCP server for pulling public static and
118
116
  server-rendered web pages into clean Markdown, NDJSON, OKF bundles, SQLite, or
119
- local archives. Documentation crawling is its sharpest default workflow, but
120
- the product is broader than docs: blogs, API references, vendor pages, OpenAPI
121
- specs, and other server-rendered web content all fit. It uses async HTTP instead
117
+ local archives. Web-source extraction is the core workflow; docs are one
118
+ high-value lane alongside blogs, API references, vendor pages, OpenAPI specs,
119
+ and other server-rendered web content. It uses async HTTP instead
122
120
  of Playwright, preserves source metadata, and is built for agent-selected URLs
123
121
  with SSRF, XXE, DNS-rebinding, and CRLF-injection protections enabled by
124
122
  default.
@@ -173,6 +171,13 @@ docpull https://docs.example.com --profile llm --stream | jq .
173
171
  # Open Knowledge Format bundle for agent/wiki interoperability
174
172
  docpull https://example.com --format okf -o ./site-okf
175
173
 
174
+ # SEC filing evidence pack with citation anchors
175
+ docpull evidence-pack filings.ndjson \
176
+ --profile sec-filing \
177
+ --rules docs/examples/vendor-dependency-rules.yml \
178
+ --sec-user-agent "YourOrg your-email@example.com" \
179
+ --output-dir packs/dla-vendors
180
+
176
181
  # Mirror scraped content for offline use
177
182
  docpull https://docs.example.com --profile mirror --cache
178
183
  ```
@@ -296,15 +301,37 @@ async def tool_call(url: str) -> str:
296
301
  docpull https://site.com --profile rag # Default. Dedup, rich metadata.
297
302
  docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
298
303
  docpull https://site.com --profile okf # OKF bundle with generated index.md files.
304
+ docpull https://site.com --profile sec-filing # EDGAR-friendly NDJSON chunks, trafilatura, Inline XBRL cleanup.
299
305
  docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
300
306
  docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
301
307
  ```
302
308
 
309
+ ## SEC filing evidence packs
310
+
311
+ `docpull evidence-pack` turns an NDJSON list of filings into a source pack for
312
+ government/vendor-dependency research. Each input row needs a filing URL under
313
+ `primary_document_url` or `url`; filing fields such as `cik`,
314
+ `accession_number`, `form`, `filing_date`, and `issuer_name` are carried into
315
+ the emitted records.
316
+
317
+ Use `docs/examples/vendor-dependency-rules.yml` as a starting rule profile for
318
+ government customer, customer concentration, segment revenue, and related-party
319
+ signals. Rules can use literal strings or regex entries with optional
320
+ confidence values.
321
+
322
+ The pack writes `evidence.pack.json`, `documents.ndjson`, `evidence.ndjson`,
323
+ `diagnostics.ndjson`, `sources.md`, `corpus.manifest.json`,
324
+ `EVIDENCE_CONTEXT.md`, and `AGENT_CONTEXT.md`. Evidence hits include source URL,
325
+ chunk id, section heading when available, quote, surrounding context, source
326
+ document hash, confidence, and extraction method. Diagnostics flag no readable
327
+ content, high XBRL noise, degraded tables, missing evidence categories, and
328
+ source hash changes since the last run.
329
+
303
330
  ## Parallel context packs
304
331
 
305
332
  `docpull[parallel]` adds an optional source-discovery and research layer on top
306
333
  of Parallel web intelligence APIs. Use the core crawler when you already know
307
- the docs URL. Use Parallel packs when an agent needs current web sources found,
334
+ the target URL. Use Parallel packs when an agent needs current web sources found,
308
335
  extracted, researched, organized, and loaded as durable local context.
309
336
 
310
337
  Parallel handles live Search, Extract, Task, Entity Search, FindAll, TaskGroup,
@@ -527,7 +554,7 @@ Write:
527
554
  - `parallel_context_pack(...)` — build or dry-run a Parallel Search + Extract pack without shelling out.
528
555
  - `parallel_api_pack(source, kind?, output_dir?)` — build a pack from `llms.txt` or OpenAPI.
529
556
  - `add_source(name, url, description?, category?, max_pages?, force?)` — register a user alias (HTTPS-only, atomic write to `sources.yaml`).
530
- - `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached docs.
557
+ - `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached Markdown.
531
558
 
532
559
  All schema-backed tools return `structuredContent` validated against an
533
560
  `outputSchema` for clients that prefer typed output. `fetch_url` intentionally
@@ -641,6 +668,8 @@ for hit in hits:
641
668
  When running with `--proxy`, DNS pinning is delegated to the proxy. Pass
642
669
  `--require-pinned-dns` to refuse this configuration and keep the connector-
643
670
  level SSRF guarantees in effect.
671
+ HTTP and HTTPS proxies work with the base install; SOCKS proxies require
672
+ `pip install "docpull[proxy]"`.
644
673
 
645
674
  ## Options
646
675
 
@@ -648,7 +677,7 @@ Run `docpull --help` for the full list. Highlights:
648
677
 
649
678
  ```
650
679
  Core:
651
- --profile {rag,mirror,quick,llm,okf}
680
+ --profile {rag,mirror,quick,llm,okf,sec-filing}
652
681
  --single Fetch one URL (no crawl)
653
682
  --format {markdown,json,ndjson,sqlite,okf}
654
683
  --stream Stream NDJSON to stdout
@@ -16,9 +16,9 @@
16
16
 
17
17
  docpull is a Python CLI, SDK, and MCP server for pulling public static and
18
18
  server-rendered web pages into clean Markdown, NDJSON, OKF bundles, SQLite, or
19
- local archives. Documentation crawling is its sharpest default workflow, but
20
- the product is broader than docs: blogs, API references, vendor pages, OpenAPI
21
- specs, and other server-rendered web content all fit. It uses async HTTP instead
19
+ local archives. Web-source extraction is the core workflow; docs are one
20
+ high-value lane alongside blogs, API references, vendor pages, OpenAPI specs,
21
+ and other server-rendered web content. It uses async HTTP instead
22
22
  of Playwright, preserves source metadata, and is built for agent-selected URLs
23
23
  with SSRF, XXE, DNS-rebinding, and CRLF-injection protections enabled by
24
24
  default.
@@ -73,6 +73,13 @@ docpull https://docs.example.com --profile llm --stream | jq .
73
73
  # Open Knowledge Format bundle for agent/wiki interoperability
74
74
  docpull https://example.com --format okf -o ./site-okf
75
75
 
76
+ # SEC filing evidence pack with citation anchors
77
+ docpull evidence-pack filings.ndjson \
78
+ --profile sec-filing \
79
+ --rules docs/examples/vendor-dependency-rules.yml \
80
+ --sec-user-agent "YourOrg your-email@example.com" \
81
+ --output-dir packs/dla-vendors
82
+
76
83
  # Mirror scraped content for offline use
77
84
  docpull https://docs.example.com --profile mirror --cache
78
85
  ```
@@ -196,15 +203,37 @@ async def tool_call(url: str) -> str:
196
203
  docpull https://site.com --profile rag # Default. Dedup, rich metadata.
197
204
  docpull https://site.com --profile llm # NDJSON + chunks + metadata; JS-only pages skip unless --strict-js-required is passed.
198
205
  docpull https://site.com --profile okf # OKF bundle with generated index.md files.
206
+ docpull https://site.com --profile sec-filing # EDGAR-friendly NDJSON chunks, trafilatura, Inline XBRL cleanup.
199
207
  docpull https://site.com --profile mirror # Full archive, polite, cached, hierarchical paths.
200
208
  docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
201
209
  ```
202
210
 
211
+ ## SEC filing evidence packs
212
+
213
+ `docpull evidence-pack` turns an NDJSON list of filings into a source pack for
214
+ government/vendor-dependency research. Each input row needs a filing URL under
215
+ `primary_document_url` or `url`; filing fields such as `cik`,
216
+ `accession_number`, `form`, `filing_date`, and `issuer_name` are carried into
217
+ the emitted records.
218
+
219
+ Use `docs/examples/vendor-dependency-rules.yml` as a starting rule profile for
220
+ government customer, customer concentration, segment revenue, and related-party
221
+ signals. Rules can use literal strings or regex entries with optional
222
+ confidence values.
223
+
224
+ The pack writes `evidence.pack.json`, `documents.ndjson`, `evidence.ndjson`,
225
+ `diagnostics.ndjson`, `sources.md`, `corpus.manifest.json`,
226
+ `EVIDENCE_CONTEXT.md`, and `AGENT_CONTEXT.md`. Evidence hits include source URL,
227
+ chunk id, section heading when available, quote, surrounding context, source
228
+ document hash, confidence, and extraction method. Diagnostics flag no readable
229
+ content, high XBRL noise, degraded tables, missing evidence categories, and
230
+ source hash changes since the last run.
231
+
203
232
  ## Parallel context packs
204
233
 
205
234
  `docpull[parallel]` adds an optional source-discovery and research layer on top
206
235
  of Parallel web intelligence APIs. Use the core crawler when you already know
207
- the docs URL. Use Parallel packs when an agent needs current web sources found,
236
+ the target URL. Use Parallel packs when an agent needs current web sources found,
208
237
  extracted, researched, organized, and loaded as durable local context.
209
238
 
210
239
  Parallel handles live Search, Extract, Task, Entity Search, FindAll, TaskGroup,
@@ -427,7 +456,7 @@ Write:
427
456
  - `parallel_context_pack(...)` — build or dry-run a Parallel Search + Extract pack without shelling out.
428
457
  - `parallel_api_pack(source, kind?, output_dir?)` — build a pack from `llms.txt` or OpenAPI.
429
458
  - `add_source(name, url, description?, category?, max_pages?, force?)` — register a user alias (HTTPS-only, atomic write to `sources.yaml`).
430
- - `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached docs.
459
+ - `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached Markdown.
431
460
 
432
461
  All schema-backed tools return `structuredContent` validated against an
433
462
  `outputSchema` for clients that prefer typed output. `fetch_url` intentionally
@@ -541,6 +570,8 @@ for hit in hits:
541
570
  When running with `--proxy`, DNS pinning is delegated to the proxy. Pass
542
571
  `--require-pinned-dns` to refuse this configuration and keep the connector-
543
572
  level SSRF guarantees in effect.
573
+ HTTP and HTTPS proxies work with the base install; SOCKS proxies require
574
+ `pip install "docpull[proxy]"`.
544
575
 
545
576
  ## Options
546
577
 
@@ -548,7 +579,7 @@ Run `docpull --help` for the full list. Highlights:
548
579
 
549
580
  ```
550
581
  Core:
551
- --profile {rag,mirror,quick,llm,okf}
582
+ --profile {rag,mirror,quick,llm,okf,sec-filing}
552
583
  --single Fetch one URL (no crawl)
553
584
  --format {markdown,json,ndjson,sqlite,okf}
554
585
  --stream Stream NDJSON to stdout
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "4.3.1"
7
+ version = "4.4.0"
8
8
  dynamic = []
9
9
  description = "Convert public web pages into clean Markdown for AI agents and RAG"
10
10
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -21,6 +21,7 @@ keywords = [
21
21
  "python",
22
22
  "markdown",
23
23
  "documentation",
24
+ "web-extraction",
24
25
  "web-scraping",
25
26
  "web-crawler",
26
27
  "crawler",
@@ -30,6 +31,7 @@ keywords = [
30
31
  "llm",
31
32
  "mcp",
32
33
  "ai-agents",
34
+ "context-engineering",
33
35
  "claude",
34
36
  "ai-training-data",
35
37
  ]
@@ -47,9 +49,7 @@ classifiers = [
47
49
  "Environment :: Console",
48
50
 
49
51
  # Topic
50
- "Topic :: Documentation",
51
52
  "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
52
- "Topic :: Software Development :: Documentation",
53
53
  "Topic :: Text Processing :: Markup :: HTML",
54
54
  "Topic :: Text Processing :: Markup :: Markdown",
55
55
  "Topic :: Utilities",
@@ -79,17 +79,17 @@ dependencies = [
79
79
  "defusedxml>=0.7.1",
80
80
  "extruct>=0.15.0",
81
81
  "aiohttp>=3.14.0", # 3.14.0 fixes CVE-2026-34993 and CVE-2026-47265
82
- "idna>=3.15",
82
+ "idna>=3.15", # Security floor for transitive URL/IDNA handling; Dependabot raises as needed
83
83
  "regex>=2024.11.6",
84
84
  "rich>=13.0.0",
85
85
  "pyyaml>=6.0",
86
86
  "pydantic>=2.0",
87
- "urllib3>=2.7.0",
87
+ "urllib3>=2.7.0", # Security floor for transitive HTTP clients; Dependabot raises as needed
88
88
  ]
89
89
 
90
90
  [project.optional-dependencies]
91
91
  proxy = [
92
- "aiohttp-socks>=0.8.0",
92
+ "aiohttp-socks>=0.11.0",
93
93
  ]
94
94
  normalize = [
95
95
  "url-normalize>=1.4.0",
@@ -116,7 +116,7 @@ llm = [
116
116
  "tiktoken>=0.7.0",
117
117
  ]
118
118
  all = [
119
- "aiohttp-socks>=0.8.0",
119
+ "aiohttp-socks>=0.11.0",
120
120
  "url-normalize>=1.4.0",
121
121
  "trafilatura>=1.12.0",
122
122
  "tiktoken>=0.7.0",
@@ -1,5 +1,5 @@
1
1
  """
2
- docpull - Fetch and convert static/server-rendered documentation to markdown.
2
+ docpull - Fetch and convert static/server-rendered web content 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.3.1"
17
+ __version__ = "4.4.0"
18
18
 
19
19
  from .cache import CacheManager, StreamingDeduplicator
20
20
  from .conversion.chunking import Chunk, TokenCounter, chunk_markdown
@@ -237,7 +237,7 @@ def create_benchmark_parser() -> argparse.ArgumentParser:
237
237
  "quick",
238
238
  help="Run a small real-site benchmark and write JSON/Markdown reports",
239
239
  )
240
- quick.add_argument("--target-url", default=DEFAULT_TARGET_URL, help="Docs URL to crawl")
240
+ quick.add_argument("--target-url", default=DEFAULT_TARGET_URL, help="Target URL to crawl")
241
241
  quick.add_argument(
242
242
  "--target-set",
243
243
  choices=TARGET_SET_CHOICES,
@@ -48,7 +48,7 @@ def create_parser() -> argparse.ArgumentParser:
48
48
  """Create argument parser for CLI."""
49
49
  parser = argparse.ArgumentParser(
50
50
  prog="docpull",
51
- description="Fetch and convert static/server-rendered documentation to markdown",
51
+ description="Fetch and convert static/server-rendered web content to markdown",
52
52
  formatter_class=argparse.RawDescriptionHelpFormatter,
53
53
  epilog="""
54
54
  Examples:
@@ -69,7 +69,7 @@ Examples:
69
69
  parser.add_argument(
70
70
  "url",
71
71
  nargs="?",
72
- help="URL to fetch documentation from",
72
+ help="URL to fetch content from",
73
73
  )
74
74
 
75
75
  parser.add_argument(
@@ -87,9 +87,12 @@ Examples:
87
87
  parser.add_argument(
88
88
  "--profile",
89
89
  "-p",
90
- choices=["rag", "mirror", "quick", "llm", "okf"],
90
+ choices=["rag", "mirror", "quick", "llm", "okf", "sec-filing"],
91
91
  default="rag",
92
- help="Preset profile (default: rag). 'llm' streams chunked NDJSON; 'okf' writes an OKF bundle.",
92
+ help=(
93
+ "Preset profile (default: rag). 'llm' streams chunked NDJSON; "
94
+ "'okf' writes an OKF bundle; 'sec-filing' tunes extraction for EDGAR filings."
95
+ ),
93
96
  )
94
97
 
95
98
  parser.add_argument(
@@ -257,7 +260,7 @@ Examples:
257
260
  "--proxy",
258
261
  type=str,
259
262
  metavar="URL",
260
- help="Proxy URL",
263
+ help="HTTP, HTTPS, or SOCKS proxy URL (SOCKS requires docpull[proxy])",
261
264
  )
262
265
  network_group.add_argument(
263
266
  "--user-agent",
@@ -384,6 +387,7 @@ def run_fetcher(args: argparse.Namespace) -> int:
384
387
  "quick": ProfileName.QUICK,
385
388
  "llm": ProfileName.LLM,
386
389
  "okf": ProfileName.OKF,
390
+ "sec-filing": ProfileName.SEC_FILING,
387
391
  }
388
392
  profile = profile_map.get(args.profile, ProfileName.RAG)
389
393
 
@@ -683,6 +687,10 @@ def main(argv: list[str] | None = None) -> int:
683
687
  from .pack_tools import run_pack_cli
684
688
 
685
689
  return run_pack_cli(raw_argv[1:])
690
+ if raw_argv and raw_argv[0] == "evidence-pack":
691
+ from .evidence_pack import run_evidence_pack_cli
692
+
693
+ return run_evidence_pack_cli(raw_argv[1:])
686
694
  if raw_argv and raw_argv[0] == "benchmark":
687
695
  from .benchmark import run_benchmark_cli
688
696
 
@@ -196,12 +196,10 @@ class MainContentExtractor:
196
196
  keep_attrs = {"href", "src", "alt", "title", "class", "id"}
197
197
 
198
198
  for tag in element.find_all(True):
199
- # Get list of attrs to remove (can't modify during iteration)
200
199
  attrs_to_remove = [attr for attr in tag.attrs if attr not in keep_attrs]
201
200
  for attr in attrs_to_remove:
202
201
  del tag[attr]
203
202
 
204
- # Remove empty class/id
205
203
  if tag.get("class") == []:
206
204
  del tag["class"]
207
205
  if tag.get("id") == "":
@@ -211,7 +209,6 @@ class MainContentExtractor:
211
209
  """Convert relative URLs to absolute URLs."""
212
210
  urlparse(base_url)
213
211
 
214
- # Resolve href attributes
215
212
  for tag in element.find_all("a", href=True):
216
213
  href = tag["href"]
217
214
  if href.startswith("#"):
@@ -219,7 +216,6 @@ class MainContentExtractor:
219
216
  if not href.startswith(("http://", "https://", "//")):
220
217
  tag["href"] = urljoin(base_url, href)
221
218
 
222
- # Resolve src attributes
223
219
  for tag in element.find_all(src=True):
224
220
  src = tag["src"]
225
221
  if not src.startswith(("http://", "https://", "//", "data:")):
@@ -227,11 +223,8 @@ class MainContentExtractor:
227
223
 
228
224
  def _clean_whitespace(self, text: str) -> str:
229
225
  """Clean up excessive whitespace."""
230
- # Normalize line endings
231
226
  text = text.replace("\r\n", "\n").replace("\r", "\n")
232
- # Remove excessive blank lines (more than 2)
233
227
  text = re.sub(r"\n{3,}", "\n\n", text)
234
- # Remove trailing whitespace on lines
235
228
  text = "\n".join(line.rstrip() for line in text.split("\n"))
236
229
  return text.strip()
237
230
 
@@ -248,7 +241,6 @@ class MainContentExtractor:
248
241
  """
249
242
  soup = self._parse_html(html)
250
243
 
251
- # Find main content
252
244
  main_content = self._find_main_content(soup)
253
245
  if main_content is None:
254
246
  # Try the entire document as fallback
@@ -261,7 +253,6 @@ class MainContentExtractor:
261
253
  # Make a copy to avoid modifying original
262
254
  content = BeautifulSoup(str(main_content), "html.parser")
263
255
 
264
- # Clean up
265
256
  # Math-rendering libraries often hide the source TeX in script,
266
257
  # MathML annotation, or aria-hidden wrappers that the generic cleanup
267
258
  # removes. Normalize those to Markdown text before deleting chrome.
@@ -0,0 +1,57 @@
1
+ """SEC filing-oriented HTML cleanup helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bs4 import BeautifulSoup, Tag
6
+
7
+ _HIDDEN_INLINE_XBRL_TAGS = {"ix:hidden", "ix:header", "ix:references", "ix:resources"}
8
+
9
+
10
+ def clean_inline_xbrl_html(html: bytes) -> tuple[bytes, dict[str, int]]:
11
+ """Remove hidden Inline XBRL scaffolding while preserving visible facts.
12
+
13
+ SEC filings often include machine-readable Inline XBRL tags around visible
14
+ text plus hidden header/resources blocks. The visible ``ix:*`` tags are
15
+ unwrapped so their text remains available, while hidden sections and
16
+ browser-hidden elements are removed before extraction.
17
+ """
18
+ text = html.decode("utf-8", errors="replace")
19
+ soup = BeautifulSoup(text, "html.parser")
20
+ stats = {
21
+ "hidden_inline_xbrl_removed": 0,
22
+ "hidden_elements_removed": 0,
23
+ "inline_xbrl_tags_unwrapped": 0,
24
+ }
25
+
26
+ for tag in list(soup.find_all(True)):
27
+ if not isinstance(tag, Tag):
28
+ continue
29
+ name = (tag.name or "").lower()
30
+ if not name:
31
+ continue
32
+ if name in _HIDDEN_INLINE_XBRL_TAGS:
33
+ tag.decompose()
34
+ stats["hidden_inline_xbrl_removed"] += 1
35
+ continue
36
+ if _is_hidden_element(tag):
37
+ tag.decompose()
38
+ stats["hidden_elements_removed"] += 1
39
+ continue
40
+ if name.startswith("ix:"):
41
+ tag.unwrap()
42
+ stats["inline_xbrl_tags_unwrapped"] += 1
43
+
44
+ return str(soup).encode("utf-8"), stats
45
+
46
+
47
+ def _is_hidden_element(tag: Tag) -> bool:
48
+ if tag.has_attr("hidden") or tag.get("aria-hidden") == "true":
49
+ return True
50
+ style = str(tag.get("style") or "").replace(" ", "").lower()
51
+ if "display:none" in style or "visibility:hidden" in style:
52
+ return True
53
+ classes = tag.get("class")
54
+ return isinstance(classes, list) and any(str(item).lower() == "hidden" for item in classes)
55
+
56
+
57
+ __all__ = ["clean_inline_xbrl_html"]
@@ -384,13 +384,10 @@ class HtmlToMarkdown:
384
384
  markdown = _restore_latex_math_delimiters(markdown)
385
385
  markdown = _rewrite_markdown_links(markdown, _normalize_protected_absolute_destination)
386
386
 
387
- # Remove excessive blank lines
388
387
  markdown = re.sub(r"\n{3,}", "\n\n", markdown)
389
388
 
390
- # Remove trailing whitespace on each line
391
389
  markdown = "\n".join(line.rstrip() for line in markdown.split("\n"))
392
390
 
393
- # Ensure single newline at end
394
391
  return markdown.strip() + "\n"
395
392
 
396
393
  def _fix_relative_links(self, markdown: str, base_url: str) -> str:
@@ -412,13 +409,10 @@ class HtmlToMarkdown:
412
409
  Markdown string
413
410
  """
414
411
  try:
415
- # Set base URL for link resolution
416
412
  self._converter.baseurl = url
417
413
 
418
- # Convert to markdown
419
414
  markdown = self._converter.handle(html)
420
415
 
421
- # Clean up output
422
416
  markdown = self._clean_output(markdown)
423
417
 
424
418
  # Fix any remaining relative links
@@ -518,7 +512,7 @@ class FrontmatterBuilder:
518
512
  def build_okf(
519
513
  self,
520
514
  *,
521
- concept_type: str = "Documentation Page",
515
+ concept_type: str = "Web Page",
522
516
  title: str | None = None,
523
517
  resource: str | None = None,
524
518
  description: str | None = None,
@@ -54,23 +54,18 @@ def _url_to_filename(url: str, base_url: str | None = None) -> str:
54
54
  parsed = urlparse(url)
55
55
  path = parsed.path.strip("/")
56
56
 
57
- # Remove base URL prefix if provided
58
57
  if base_url:
59
58
  base_path = urlparse(base_url).path.strip("/")
60
59
  if path.startswith(base_path):
61
60
  path = path[len(base_path) :].strip("/")
62
61
 
63
- # Convert path to filename
64
62
  if not path or path == "/":
65
63
  filename = "index"
66
64
  else:
67
- # Replace path separators with underscores
68
65
  filename = path.replace("/", "_")
69
- # Remove file extension if present
70
66
  if filename.endswith(".html") or filename.endswith(".htm"):
71
67
  filename = filename.rsplit(".", 1)[0]
72
68
 
73
- # Clean up filename
74
69
  filename = re.sub(r"[^\w\-]", "_", filename)
75
70
  filename = re.sub(r"_+", "_", filename)
76
71
  filename = filename.strip("_")
@@ -402,6 +397,7 @@ class Fetcher:
402
397
  enable_special_cases=self.config.content_filter.enable_special_cases,
403
398
  use_trafilatura=self.config.content_filter.extractor == "trafilatura",
404
399
  strict_js_required=self.config.content_filter.strict_js_required,
400
+ clean_inline_xbrl=self.config.content_filter.clean_inline_xbrl,
405
401
  )
406
402
  )
407
403
 
@@ -274,16 +274,13 @@ class EnhancedLinkExtractor:
274
274
  logger.debug("Could not resolve href %r against %s: %s", href, base_url, err)
275
275
  return None
276
276
 
277
- # Validate it's a proper URL
278
277
  parsed = urlparse(absolute_url)
279
278
  if not parsed.scheme or not parsed.netloc:
280
279
  return None
281
280
 
282
- # Only allow http/https
283
281
  if parsed.scheme not in ("http", "https"):
284
282
  return None
285
283
 
286
- # Remove fragment
287
284
  clean_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
288
285
  if parsed.query:
289
286
  clean_url += f"?{parsed.query}"
@@ -126,7 +126,6 @@ class StaticLinkExtractor:
126
126
  logger.debug("Could not resolve href %r against %s: %s", href, base_url, err)
127
127
  return None
128
128
 
129
- # Remove fragment
130
129
  parsed = urlparse(absolute_url)
131
130
  clean_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
132
131
  if parsed.query:
@@ -170,12 +170,10 @@ class SitemapDiscoverer:
170
170
  ns = SITEMAP_NS if use_ns else {}
171
171
  prefix = "ns:" if use_ns else ""
172
172
 
173
- # Find page URLs
174
173
  for url_elem in root.findall(f".//{prefix}url/{prefix}loc", ns):
175
174
  if url_elem.text:
176
175
  page_urls.append(url_elem.text.strip())
177
176
 
178
- # Find nested sitemap URLs
179
177
  for sitemap_elem in root.findall(f".//{prefix}sitemap/{prefix}loc", ns):
180
178
  if sitemap_elem.text:
181
179
  sitemap_urls.append(sitemap_elem.text.strip())
@@ -229,14 +227,12 @@ class SitemapDiscoverer:
229
227
  if self._filter and not self._filter.should_include(url):
230
228
  continue
231
229
 
232
- # Skip duplicates
233
230
  if not self._seen.add(url):
234
231
  continue
235
232
 
236
233
  yield url
237
234
  count += 1
238
235
 
239
- # Process nested sitemaps
240
236
  remaining = max_urls - count if max_urls is not None else None
241
237
  for nested_url in nested_sitemaps:
242
238
  if remaining is not None and remaining <= 0: