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.
- {docpull-4.3.1 → docpull-4.4.0}/LICENSE +1 -1
- {docpull-4.3.1/src/docpull.egg-info → docpull-4.4.0}/PKG-INFO +41 -12
- {docpull-4.3.1 → docpull-4.4.0}/README.md +37 -6
- {docpull-4.3.1 → docpull-4.4.0}/pyproject.toml +7 -7
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/__init__.py +2 -2
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/benchmark.py +1 -1
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cli.py +13 -5
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/extractor.py +0 -9
- docpull-4.4.0/src/docpull/conversion/filings.py +57 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/markdown.py +1 -7
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/core/fetcher.py +1 -5
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -3
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/static.py +0 -1
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/sitemap.py +0 -4
- docpull-4.4.0/src/docpull/evidence_pack.py +1050 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/client.py +57 -9
- docpull-4.4.0/src/docpull/mcp/__init__.py +10 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/server.py +12 -12
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/sources.py +3 -3
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/mcp/tools.py +8 -8
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/metadata_extractor.py +0 -10
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/config.py +10 -5
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/document.py +24 -1
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/profiles.py +23 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/parallel_workflows.py +2 -2
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/manifest.py +15 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/convert.py +9 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/metadata.py +0 -5
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save.py +2 -8
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_json.py +0 -2
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_okf.py +2 -2
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/download_policy.py +4 -4
- {docpull-4.3.1 → docpull-4.4.0/src/docpull.egg-info}/PKG-INFO +41 -12
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/SOURCES.txt +3 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/requires.txt +2 -2
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_cache_conditional_get.py +0 -9
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_ci_policy.py +1 -1
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_cli.py +28 -1
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_conversion.py +1 -1
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_discovery.py +2 -2
- docpull-4.4.0/tests/test_evidence_pack.py +236 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_integration.py +19 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_mcp_tools.py +11 -2
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_outputs_e2e.py +1 -1
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_parallel_workflows.py +10 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_security_hardening.py +98 -3
- docpull-4.3.1/src/docpull/mcp/__init__.py +0 -9
- {docpull-4.3.1 → docpull-4.4.0}/setup.cfg +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/__main__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/frontier.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/manager.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/chunking.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/protocols.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/special_cases.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/core/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/composite.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/crawler.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/filters.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/discovery/protocols.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/doctor.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/fixtures/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/fixtures/parallel-search-extract.json +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/protocols.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/judge.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/events.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/models/run.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pack_tools.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/passk.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/base.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/fetch.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/pipeline/steps/validate.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/provider_cli.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/provider_keys.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/py.typed +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/scraper.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/__init__.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/robots.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/security/url_validator.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/source_scoring.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull/time_utils.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_benchmark.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_chunking.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_convert_step_new.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_document_record.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_frontier_resume.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_judge.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_link_extractors.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_mcp_server.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_naming.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_pack_tools.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_passk.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_pipeline.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_provider_cli.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_real_site_regressions.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_save_ndjson.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_save_sqlite.py +0 -0
- {docpull-4.3.1 → docpull-4.4.0}/tests/test_special_cases.py +0 -0
- {docpull-4.3.1 → docpull-4.4.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.
|
|
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.
|
|
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.
|
|
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.
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
|
|
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.
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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="
|
|
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
|
|
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
|
|
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=
|
|
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="
|
|
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 = "
|
|
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:
|