fetchpdf 0.1.4__tar.gz → 0.1.6__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.
- {fetchpdf-0.1.4/fetchpdf.egg-info → fetchpdf-0.1.6}/PKG-INFO +74 -29
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/README.md +72 -28
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/__init__.py +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/_env.py +7 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/_http.py +24 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetch_abstract_from_doi.py +3 -2
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetchpdf.py +540 -256
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/get_cookies.py +17 -10
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/_util.py +1 -4
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/corresponding.py +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/engine.py +9 -7
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/extract_images.py +2 -12
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/fulltext_scan.py +9 -13
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/goals.py +45 -15
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/http.py +70 -10
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/institutional.py +235 -40
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/ladder.json +3 -3
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/llm_adjudicate.py +1 -2
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/llm_agent_retrieval.py +3 -2
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/normalize.py +5 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pdf_identity.py +1 -3
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/provenance.py +3 -2
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pubpeer.py +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/request_drafts.py +13 -8
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/resolve.py +0 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/legacy_pdf.py +4 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_atypon.py +0 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_graph.py +70 -52
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_index.py +0 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplementary.py +72 -70
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/to_html.py +9 -4
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/to_markdown.py +13 -6
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/validate.py +8 -6
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/verify.py +2 -7
- {fetchpdf-0.1.4 → fetchpdf-0.1.6/fetchpdf.egg-info}/PKG-INFO +74 -29
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/SOURCES.txt +5 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/requires.txt +1 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/pyproject.toml +3 -0
- fetchpdf-0.1.6/tests/test_elsevier_routing.py +315 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_institutional.py +55 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_polite_ua.py +4 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pure.py +8 -5
- fetchpdf-0.1.6/tests/test_review_fixes.py +100 -0
- fetchpdf-0.1.6/tests/test_xml_fallback_extras.py +61 -0
- fetchpdf-0.1.6/tests/test_xml_last.py +77 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/LICENSE +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/MANIFEST.in +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/__main__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetch_metadata_from_doi.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/artifact.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/openrouter.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/blocked.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/byline.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/cache.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/chunk.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/classify.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/context.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/figures.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/identifiers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/linked_artifacts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pdf_text.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/ratelimit.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/references.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/repository_waf.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/arxiv.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/core_text.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/epmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/landing.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/pmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/preprint.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/supplements.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_dspace.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_jci.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_pmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_publishers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/tiers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/dependency_links.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/entry_points.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/top_level.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/requirements.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/setup.cfg +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_agent_backends.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_blocked.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_cookie_offer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_corresponding.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_datacite_supplement_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_dspace.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_entrypoints.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_epmc_repo.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_extract_images.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_figures.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_folder_encoding.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_fulltext_scan.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_get_cookies.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_jci_supplements.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_json_output.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_landing_candidates.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_landing_page_detection.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_last_resort_reachability.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_linked_artifacts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_llm_adjudicate.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_normalize_cells.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pdf_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pdf_trailer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pmc_page.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pubmed_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pubpeer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pull_for_record_call_sites.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_request_drafts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_retrieval.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_scan_record.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_screening_conversion.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_serpapi_scholar.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_short_conference_xml.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_supplement_asset_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_supplementary.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_to_html.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fetchpdf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A comprehensive tool to download PDFs from DOIs using multiple sources
|
|
5
5
|
Author-email: Dan Elton <delton17@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,6 +30,7 @@ Provides-Extra: better-text-images
|
|
|
30
30
|
Requires-Dist: pymupdf>=1.24; extra == "better-text-images"
|
|
31
31
|
Provides-Extra: render
|
|
32
32
|
Requires-Dist: markdown-it-py>=3.0; extra == "render"
|
|
33
|
+
Requires-Dist: Pillow>=9.0; extra == "render"
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
35
36
|
Requires-Dist: pytest-xdist>=3.0; extra == "dev"
|
|
@@ -54,7 +55,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
|
|
|
54
55
|
- [Batch Processing & Parallel Execution](#batch-processing--parallel-execution)
|
|
55
56
|
- [Format-Prioritized Retrieval](#format-prioritized-retrieval)
|
|
56
57
|
- [Supplementary Material](#supplementary-material)
|
|
57
|
-
- [`--
|
|
58
|
+
- [`--fetch-everything`: the second layer](#--fetch-everything-the-second-layer)
|
|
58
59
|
- [Figure Images](#figure-images)
|
|
59
60
|
- [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions) — papers your library subscribes to (`fetchpdf fetchcookies`)
|
|
60
61
|
- [Calling fetchpdf from a script or an agent](#calling-fetchpdf-from-a-script-or-an-agent) — `--json`
|
|
@@ -84,7 +85,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
|
|
|
84
85
|
- 🖼️ **[Figure Images](#figure-images)**: `--pull-figures` fetches the article's published figures from PMC (or the Elsevier object API) with a manifest of labels, captions and hashes
|
|
85
86
|
- 🖨️ **[XML to HTML / PDF](#rendering-the-xml-as-html-or-pdf)**: `--generate-html-from-xml` / `--generate-pdf-from-xml` render a retrieved XML as one self-contained page, figures and tables embedded
|
|
86
87
|
- 🔗 **[Linked Datasets & Code](#linked-datasets-and-code-stem_linked_artifactsjson)**: the same pass discovers each paper's external datasets/software via ScholeXplorer, Europe PMC and DataCite, downloads ownership-confirmed deposits from figshare/Zenodo/OSF/Dryad/Dataverse, and records every link in a sidecar
|
|
87
|
-
- 🤖 **[LLM-assisted retrieval](#--
|
|
88
|
+
- 🤖 **[LLM-assisted retrieval](#--fetch-everything-the-second-layer)**: `--fetch-everything` adds a second layer — a model reads the paper itself and goes after the SI and datasets the APIs missed
|
|
88
89
|
- 🔐 **[Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions)** (opt-in): a one-time `fetchpdf fetchcookies` lets fetchpdf download papers your library subscribes to
|
|
89
90
|
- 🚀 **Batch Processing**: Process multiple DOIs/PMIDs from CSV files or lists
|
|
90
91
|
- ⚡ **Parallel Execution**: Download multiple papers simultaneously with configurable workers
|
|
@@ -105,7 +106,7 @@ pip install -e .
|
|
|
105
106
|
|
|
106
107
|
### Optional: Install Playwright and Chromium for Browser Automation
|
|
107
108
|
|
|
108
|
-
Install Playwright and Chromium
|
|
109
|
+
Install Playwright and Chromium to enable additional browser-based scrapers. They are used automatically once Playwright is installed (pass `--no-playwright` to skip them). The scrapers are for SSRN, OSF (if direct download fails), Wiley 10.1111/ DOIs, HTML table retrieval (second route), and Atypon supplementary material (PNAS, Science, SAGE, T&F, Annual Reviews).
|
|
109
110
|
|
|
110
111
|
```bash
|
|
111
112
|
pip install -e ".[browser]" # or: pip install playwright
|
|
@@ -140,9 +141,10 @@ SERPAPI_API_KEY=your_serpapi_key # Optional Google Scholar PDF fallbac
|
|
|
140
141
|
|
|
141
142
|
# Optional — publisher-specific
|
|
142
143
|
ELSEVIER_TDM_API_KEY=your_elsevier_key # Elsevier text/data-mining access
|
|
144
|
+
ELSEVIER_INSTTOKEN=your_inst_token # Elsevier institutional token (needs the key above)
|
|
143
145
|
SCOPUS_API_KEY=your_scopus_key # Scopus abstract lookup during identifier resolution
|
|
144
146
|
|
|
145
|
-
# Optional — only for --
|
|
147
|
+
# Optional — only for --fetch-everything --llm-backend openrouter
|
|
146
148
|
OPENROUTER_API_KEY=your_openrouter_key # the retrieval agent's second backend
|
|
147
149
|
|
|
148
150
|
# Required by fetchpdf-pubpeer — PubPeer rejects keyless requests
|
|
@@ -193,7 +195,7 @@ papers your library subscribes to. See
|
|
|
193
195
|
By default a record yields one file: the best PDF the source chain finds. Three
|
|
194
196
|
flags widen that.
|
|
195
197
|
|
|
196
|
-
1. **Structured full text** — `--get-xml-or-html` keeps a JATS/TEI XML (else
|
|
198
|
+
1. **Structured full text** — `--get-xml-or-html-and-pdf` keeps a JATS/TEI XML (else
|
|
197
199
|
publisher HTML) copy alongside the PDF; `--to-markdown` also renders it to
|
|
198
200
|
`{stem}_from_xml.md` (or `{stem}_from_html.md`). XML and HTML are great
|
|
199
201
|
formats for using with AI (LLMs): complex tables usually survive better than
|
|
@@ -213,7 +215,7 @@ flags widen that.
|
|
|
213
215
|
|
|
214
216
|
```bash
|
|
215
217
|
fetchpdf papers.csv -o ./out -w 4 \
|
|
216
|
-
--get-xml-or-html --to-markdown \
|
|
218
|
+
--get-xml-or-html-and-pdf --to-markdown \
|
|
217
219
|
--download-data-artifacts \
|
|
218
220
|
--make-subfolder --provenance
|
|
219
221
|
```
|
|
@@ -442,10 +444,10 @@ fetchpdf papers.csv -o ./out --xml-html-only # XML or publisher HTML
|
|
|
442
444
|
fetchpdf papers.csv -o ./out --xml-only # XML, nothing else
|
|
443
445
|
|
|
444
446
|
# A structured copy AND the PDF for every record
|
|
445
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html
|
|
447
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf
|
|
446
448
|
|
|
447
449
|
# ...and a {stem}_from_xml.md / _from_html.md for each XML/HTML artifact
|
|
448
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html --to-markdown
|
|
450
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf --to-markdown
|
|
449
451
|
|
|
450
452
|
# Convert artifacts you already have, without re-downloading
|
|
451
453
|
fetchpdf-md ./out
|
|
@@ -463,7 +465,7 @@ supplement keeps its own extension, e.g. `{stem}.xlsx`), `{stem}.pdf`,
|
|
|
463
465
|
`{stem}.txt` and `{stem}.landing.html` — plus `{stem}_from_xml.md` /
|
|
464
466
|
`{stem}_from_html.md` under `--to-markdown`.
|
|
465
467
|
|
|
466
|
-
`--get-xml-or-html` doubles as a backfill: pointed at a directory of existing
|
|
468
|
+
`--get-xml-or-html-and-pdf` doubles as a backfill: pointed at a directory of existing
|
|
467
469
|
PDFs it fetches only the missing structured half and re-downloads nothing.
|
|
468
470
|
|
|
469
471
|
### Choosing a flag
|
|
@@ -473,7 +475,9 @@ PDFs it fetches only the missing structured half and re-downloads nothing.
|
|
|
473
475
|
| `--prioritize-xml` | everything, best format first | yes |
|
|
474
476
|
| `--xml-html-only` | T1 XML, T2 HTML | **no** — record fails |
|
|
475
477
|
| `--xml-only` | T1 XML | **no** — record fails |
|
|
476
|
-
| `--get-xml-or-html` | structured **and** PDF, both kept | yes, and keeps it |
|
|
478
|
+
| `--get-xml-or-html-and-pdf` | structured **and** PDF, both kept | yes, and keeps it |
|
|
479
|
+
| `--get-xml-and-pdf` | XML where available **and** PDF where available, both kept | yes, and keeps it |
|
|
480
|
+
| `--get-xml-and-html-and-pdf` | XML, publisher HTML and PDF, each to the extent available | yes, and keeps it |
|
|
477
481
|
|
|
478
482
|
`--xml-only` and `--xml-html-only` both imply `--prioritize-xml`. Given both,
|
|
479
483
|
`--xml-only` is stricter and wins (with a warning).
|
|
@@ -572,9 +576,9 @@ unless stdin and stdout are both terminals** — pipes, cron and CI get a one-li
|
|
|
572
576
|
count and today's behaviour, so no unattended run can block on it. It is also
|
|
573
577
|
suppressed when the answer is already known: any flag that turns the
|
|
574
578
|
supplementary pass on (`--pull-supplementary`, `--refresh-supplementary`,
|
|
575
|
-
`--download-data-artifacts`, `--
|
|
579
|
+
`--download-data-artifacts`, `--fetch-everything`, `--llm-agent-retrieval`), an
|
|
576
580
|
explicit `--on-existing skip` or `supplement`, `--abstract-only`, or the
|
|
577
|
-
goal-aware flags (`--get-xml-or-html`, `--upgrade-existing` on the tiered path),
|
|
581
|
+
goal-aware flags (`--get-xml-or-html-and-pdf`, `--upgrade-existing` on the tiered path),
|
|
578
582
|
which do not skip records at all. Single-identifier runs never ask.
|
|
579
583
|
|
|
580
584
|
What is *not* remembered between runs: failures. `failed_dois.csv` is a report,
|
|
@@ -590,6 +594,12 @@ retrieved, the PDF is kept and reported as unverified.
|
|
|
590
594
|
When a PDF is unavailable, FetchPDF tries XML or HTML full text automatically.
|
|
591
595
|
Use `--no-xml-fallback` to disable this fallback.
|
|
592
596
|
|
|
597
|
+
A record that comes back as XML only is filed in its own folder,
|
|
598
|
+
`{output_dir}/{stem}/`, with its figure images in `{stem}_figures/` and a
|
|
599
|
+
rendering at `{stem}_from_xml.pdf` (figures embedded; a rendering of the XML,
|
|
600
|
+
not the publisher's PDF). A later flat run finds it there. Use
|
|
601
|
+
`--no-xml-fallback-extras` to leave the bare `.xml` instead.
|
|
602
|
+
|
|
593
603
|
Publisher HTML (T2) uses lxml, which is installed with fetchpdf.
|
|
594
604
|
|
|
595
605
|
Advanced users can customize retrieval order, source capabilities, and host rate
|
|
@@ -673,7 +683,7 @@ Twenty routes, tried in a fixed order so the numbering is reproducible:
|
|
|
673
683
|
| Europe PMC datalinks | any PMID — text-mined accessions and data citations |
|
|
674
684
|
| Full-text scan | deposits the paper names in its own prose — only under `--download-data-artifacts` |
|
|
675
685
|
| Atypon publisher SI | PNAS, Science, Annual Reviews, SAGE, T&F — only when Europe PMC withholds the SI or the record has no PMCID; drives a headed browser, so it needs Playwright, Chromium and a display (or Xvfb) |
|
|
676
|
-
| LLM retrieval agent | only under [`--
|
|
686
|
+
| LLM retrieval agent | only under [`--fetch-everything`](#--fetch-everything-the-second-layer) or `--llm-agent-retrieval` |
|
|
677
687
|
| JCI | `10.1172/jci…` |
|
|
678
688
|
|
|
679
689
|
Wiley, ACS and MDPI serve HTTP 403 to plain requests and are not yet covered;
|
|
@@ -723,7 +733,7 @@ Supplementary files never appear in `failed_dois.csv`, in `source_tracking.csv`,
|
|
|
723
733
|
or in the format-composition tally; the missing-PDFs report lists gaps in them
|
|
724
734
|
only in its own separate section. The same holds for linked datasets,
|
|
725
735
|
[figures](#figure-images) and the
|
|
726
|
-
[`--
|
|
736
|
+
[`--fetch-everything`](#--fetch-everything-the-second-layer) agent.
|
|
727
737
|
|
|
728
738
|
### Linked datasets and code: `{stem}_linked_artifacts.json`
|
|
729
739
|
|
|
@@ -812,18 +822,19 @@ GitLab repositories, and the institutional-repository long tail.
|
|
|
812
822
|
Adding an enumerator is the same small pattern as `enumerate_dryad` /
|
|
813
823
|
`enumerate_dataverse` in `fetchpdf/retrieval/supplement_index.py`.
|
|
814
824
|
|
|
815
|
-
## `--
|
|
825
|
+
## `--fetch-everything`: the second layer
|
|
816
826
|
|
|
817
827
|
Everything above is layer 1 — APIs, indexes and enumerators. It is very good at
|
|
818
828
|
what publishers and repositories *declare*, and it cannot read. Layer 2 is a
|
|
819
829
|
model that reads the paper itself and goes after what layer 1 did not get.
|
|
820
830
|
|
|
821
831
|
```bash
|
|
822
|
-
fetchpdf papers.csv -o ./out --
|
|
823
|
-
--pull-everything
|
|
832
|
+
fetchpdf papers.csv -o ./out --make-subfolder --provenance --fetch-everything
|
|
824
833
|
```
|
|
825
834
|
|
|
826
|
-
`--
|
|
835
|
+
`--fetch-everything` implies `--get-xml-or-html-and-pdf` (XML, else HTML, beside the PDF
|
|
836
|
+
where one exists), `--pull-figures` (the publisher's figure images beside the
|
|
837
|
+
XML), `--pull-supplementary` and
|
|
827
838
|
`--download-data-artifacts`, turns on adjudication of the deterministic scan's
|
|
828
839
|
uncertain candidates, and adds the retrieval agent. Files land exactly where
|
|
829
840
|
they always did: supplements as numbered siblings of the PDF, linked deposits
|
|
@@ -895,7 +906,7 @@ backend has no general-purpose harness to lock down, because its two tools are
|
|
|
895
906
|
ours, so it downloads directly.
|
|
896
907
|
|
|
897
908
|
`--llm-backend` selects the retrieval agent's backend only. The adjudication of
|
|
898
|
-
uncertain full-text-scan candidates (on under `--
|
|
909
|
+
uncertain full-text-scan candidates (on under `--fetch-everything`, or with
|
|
899
910
|
`--llm-adjudicate-artifacts`) always calls the Claude Code CLI, as a single-turn
|
|
900
911
|
call with `--allowed-tools ""`; it does not get the `--disallowed-tools` /
|
|
901
912
|
`--strict-mcp-config` lockdown described above.
|
|
@@ -999,7 +1010,7 @@ from the network. `--generate-pdf-from-xml` prints the same page to
|
|
|
999
1010
|
|
|
1000
1011
|
```bash
|
|
1001
1012
|
pip install 'fetchpdf[render,browser]' && playwright install chromium
|
|
1002
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html --generate-pdf-from-xml
|
|
1013
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf --generate-pdf-from-xml
|
|
1003
1014
|
fetchpdf-render ./out --html --pdf --fetch-figures # XML you already have
|
|
1004
1015
|
```
|
|
1005
1016
|
|
|
@@ -1017,7 +1028,7 @@ shaped by its aspect ratio and capped at the column width. Under the title come
|
|
|
1017
1028
|
the authors, their affiliations and the journal citation (volume, issue, pages).
|
|
1018
1029
|
Tables always fit the page width in the PDF; very wide ones print smaller.
|
|
1019
1030
|
|
|
1020
|
-
Only records whose XML was kept are rendered — add `--get-xml-or-html` to keep
|
|
1031
|
+
Only records whose XML was kept are rendered — add `--get-xml-or-html-and-pdf` to keep
|
|
1021
1032
|
the XML beside the PDF.
|
|
1022
1033
|
|
|
1023
1034
|
## Institutional access via cookies (ie using your library's subscriptions)
|
|
@@ -1111,8 +1122,22 @@ your browser's cookies signs fetchpdf out as well.
|
|
|
1111
1122
|
Publishers whose PDF address follows from the DOI: **Wiley** (`10.1002`,
|
|
1112
1123
|
`10.1111`), **Taylor & Francis** (`10.1080`), **SAGE** (`10.1177`),
|
|
1113
1124
|
**Springer** (`10.1007`, `10.1057`), the **Royal Society** (`10.1098`),
|
|
1114
|
-
**Hogrefe** (`10.1027`)
|
|
1115
|
-
|
|
1125
|
+
**Hogrefe** (`10.1027`), **INFORMS** (`10.1287`), **PNAS** (`10.1073`),
|
|
1126
|
+
**Science** (`10.1126`), **ASCO** (`10.1200`), **AHA** (`10.1161`), **APS**
|
|
1127
|
+
(`10.1152`), **ASM** (`10.1128`), **ATS** (`10.1164`), **ACS** (`10.1021`) and
|
|
1128
|
+
**FASEB** (`10.1096`).
|
|
1129
|
+
|
|
1130
|
+
Publishers whose PDF address is not derivable from the DOI are reached through
|
|
1131
|
+
the DOI link, and the PDF the landing page declares is fetched: **AACR**
|
|
1132
|
+
(`10.1158`), **Cold Spring Harbor** (`10.1101`), **Oncotarget** (`10.18632`),
|
|
1133
|
+
**Oxford** (`10.1093`, `10.1210`, `10.4049`), **JAMA** (`10.1001`), **ASH**
|
|
1134
|
+
(`10.1182`), **Company of Biologists** (`10.1242`), **ERS** (`10.1183`),
|
|
1135
|
+
**BMJ** (`10.1136`), **Bioscientifica** (`10.1530`) and **LWW** (`10.1097`).
|
|
1136
|
+
|
|
1137
|
+
Several of these are open access but sit behind a Cloudflare bot check that a
|
|
1138
|
+
plain download cannot pass. Opening any article on the site once in your
|
|
1139
|
+
everyday browser earns the clearance cookie, and the next run reuses it.
|
|
1140
|
+
Other DOIs are untouched and produce no extra output.
|
|
1116
1141
|
|
|
1117
1142
|
It runs right after the fast open-access lookups (repositories, PMC,
|
|
1118
1143
|
Unpaywall), so a paper that is free anywhere is fetched from there and never
|
|
@@ -1120,7 +1145,7 @@ spends a subscription download. It runs before the slow long tail (Crossref
|
|
|
1120
1145
|
landing pages, CORE, DOAJ, DataCite, ...), where a subscription answers in a
|
|
1121
1146
|
second or two. The PDF meets the same identity check as every other source and
|
|
1122
1147
|
is reported as `institutional_cookies` in source tracking and `--json`. It also
|
|
1123
|
-
fills the PDF half of `--get-xml-or-html` runs.
|
|
1148
|
+
fills the PDF half of `--get-xml-or-html-and-pdf` runs.
|
|
1124
1149
|
|
|
1125
1150
|
### Sign-in, the VPN, and Cloudflare
|
|
1126
1151
|
|
|
@@ -1204,9 +1229,9 @@ refusal, not a bare failure:
|
|
|
1204
1229
|
| `path` | str \| null | Absolute path to the artifact, or null. |
|
|
1205
1230
|
| `format` | str | From the artifact actually written: `pdf`, `xml`, `html`, `latex`, `landing`, `suppl`, `abstract`, `text`, `other`. **A successful record is not necessarily a PDF** — with the default XML fallback a record can succeed as `xml`. |
|
|
1206
1231
|
| `source` | str \| null | The route that produced it (`unpaywall`, `core`, `osf`, …), `existing` for a skip, `structured_fallback` when no PDF was available and structured full text was saved instead. |
|
|
1207
|
-
| `identity` | str \| null | The verdict on the PDF **fetched in this run and reported in `path`**: `verified`, or a refusal — `wrong_article`, `truncated`, `unreadable`, `no_reference`, `no_engine`. Null when nothing was judged: an XML artifact, a file already on disk, or any run under the tiered flags (`--prioritize-xml`, `--get-xml-or-html`, `--xml-only`, `--xml-html-only`), where the engine validates per tier instead. A success with a non-`verified` identity is a PDF that was kept rather than deleted because the verdict could not be trusted — see [the identity check](#format-prioritized-retrieval) — not one that passed. |
|
|
1232
|
+
| `identity` | str \| null | The verdict on the PDF **fetched in this run and reported in `path`**: `verified`, or a refusal — `wrong_article`, `truncated`, `unreadable`, `no_reference`, `no_engine`. Null when nothing was judged: an XML artifact, a file already on disk, or any run under the tiered flags (`--prioritize-xml`, `--get-xml-or-html-and-pdf`, `--xml-only`, `--xml-html-only`), where the engine validates per tier instead. A success with a non-`verified` identity is a PDF that was kept rather than deleted because the verdict could not be trusted — see [the identity check](#format-prioritized-retrieval) — not one that passed. |
|
|
1208
1233
|
| `reasons` | list[str] | Why a record failed, or why a kept PDF could not be verified. Empty on a verified success. |
|
|
1209
|
-
| `paths` | list[str] | The other artifacts a tiered run wrote (`--get-xml-or-html`), empty otherwise. `path` is not repeated here. |
|
|
1234
|
+
| `paths` | list[str] | The other artifacts a tiered run wrote (`--get-xml-or-html-and-pdf`), empty otherwise. `path` is not repeated here. |
|
|
1210
1235
|
| `supplementary` | object | Only with `--pull-supplementary` (or a flag that implies it): `status` (`ok`, `none_found`, `partial`, `incomplete`, `error`, `skipped`), `written`, `skipped`, `manifest` (absolute path or null), `missing_declared` (files the article declares that were not obtained) and `blocked_urls` (refused to this client; open them in a browser). |
|
|
1211
1236
|
| `figures` | object | Only with `--pull-figures`: `status`, `written`, `failed`, `refusal` and `manifest`. |
|
|
1212
1237
|
| `images` | object | Only with `--extract-images`: `status`, `written` and `manifest`. |
|
|
@@ -1402,7 +1427,7 @@ These run first when the DOI matches (by prefix, or by the name appearing anywhe
|
|
|
1402
1427
|
9. **CORE** (core.ac.uk) — 40M+ repository papers; needs `COREAPIKEY`
|
|
1403
1428
|
10. **DOAJ** — open-access full-text links
|
|
1404
1429
|
11. **DataCite content URLs** — direct content and related resource links
|
|
1405
|
-
12. **Wiley rendered PDF** — only
|
|
1430
|
+
12. **Wiley rendered PDF** — only when Playwright is installed
|
|
1406
1431
|
13. **APA supplemental** — APA supplemental files, converting `.doc`/`.docx` to PDF when available
|
|
1407
1432
|
14. **Direct DOI resolver**
|
|
1408
1433
|
- Follows `https://doi.org/{doi}` to the landing page
|
|
@@ -1430,8 +1455,28 @@ These run first when the DOI matches (by prefix, or by the name appearing anywhe
|
|
|
1430
1455
|
|
|
1431
1456
|
18. **Elsevier full-text API**
|
|
1432
1457
|
- Elsevier records only (`10.1016/*`, or a Crossref landing page on sciencedirect.com / elsevier.com)
|
|
1433
|
-
- Tries the PDF first, then full-text XML (the XML half is skipped with `--no-xml-fallback`)
|
|
1434
1458
|
- Requires `ELSEVIER_TDM_API_KEY`
|
|
1459
|
+
- **`10.1016/*` DOIs are asked for their PDF first** — before PMC, Unpaywall,
|
|
1460
|
+
institutional access and the long tail of landing-page and repository
|
|
1461
|
+
probes (a dead repository host in Unpaywall's list alone can cost over a
|
|
1462
|
+
minute). An entitled record is one request instead of a walk through every
|
|
1463
|
+
other route. Only a *full* PDF ends
|
|
1464
|
+
the search: Elsevier's first-page-only preview (served with HTTP 200 and a
|
|
1465
|
+
real PDF when the key is not entitled) is discarded, so asking first cannot
|
|
1466
|
+
pre-empt a source that holds the whole paper. When the PDF is obtained, the
|
|
1467
|
+
full-text XML is fetched alongside it, so both are on disk.
|
|
1468
|
+
- The deferred step at the end of the chain remains: it takes the XML for
|
|
1469
|
+
records whose key is entitled to the XML but not the PDF, and covers
|
|
1470
|
+
ScienceDirect DOIs outside `10.1016/*`. It does not repeat a PDF request the
|
|
1471
|
+
early step already made. The XML half is skipped with `--no-xml-fallback`.
|
|
1472
|
+
- With `--prioritize-xml` / `--xml-only` / `--get-xml-or-html-and-pdf` the tiered
|
|
1473
|
+
ladder asks the same endpoint itself (`elsevier_tdm` at T1, `elsevier_pdf`
|
|
1474
|
+
at T5, ahead of the legacy chain).
|
|
1475
|
+
- **Off campus?** Entitlement is decided by your IP unless you also set
|
|
1476
|
+
`ELSEVIER_INSTTOKEN`. Request an institutional token from Elsevier (the
|
|
1477
|
+
developer portal at dev.elsevier.com, or your library); it is sent as
|
|
1478
|
+
`X-ELS-Insttoken` on every `api.elsevier.com` request, never to any other
|
|
1479
|
+
host, and is redacted from logs and sidecars.
|
|
1435
1480
|
|
|
1436
1481
|
If the whole chain yields no PDF, `fetch_pdf` tries structured full text
|
|
1437
1482
|
(T1 XML / T2 HTML) from the tier ladder before giving up; `--no-xml-fallback`
|
|
@@ -15,7 +15,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
|
|
|
15
15
|
- [Batch Processing & Parallel Execution](#batch-processing--parallel-execution)
|
|
16
16
|
- [Format-Prioritized Retrieval](#format-prioritized-retrieval)
|
|
17
17
|
- [Supplementary Material](#supplementary-material)
|
|
18
|
-
- [`--
|
|
18
|
+
- [`--fetch-everything`: the second layer](#--fetch-everything-the-second-layer)
|
|
19
19
|
- [Figure Images](#figure-images)
|
|
20
20
|
- [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions) — papers your library subscribes to (`fetchpdf fetchcookies`)
|
|
21
21
|
- [Calling fetchpdf from a script or an agent](#calling-fetchpdf-from-a-script-or-an-agent) — `--json`
|
|
@@ -45,7 +45,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
|
|
|
45
45
|
- 🖼️ **[Figure Images](#figure-images)**: `--pull-figures` fetches the article's published figures from PMC (or the Elsevier object API) with a manifest of labels, captions and hashes
|
|
46
46
|
- 🖨️ **[XML to HTML / PDF](#rendering-the-xml-as-html-or-pdf)**: `--generate-html-from-xml` / `--generate-pdf-from-xml` render a retrieved XML as one self-contained page, figures and tables embedded
|
|
47
47
|
- 🔗 **[Linked Datasets & Code](#linked-datasets-and-code-stem_linked_artifactsjson)**: the same pass discovers each paper's external datasets/software via ScholeXplorer, Europe PMC and DataCite, downloads ownership-confirmed deposits from figshare/Zenodo/OSF/Dryad/Dataverse, and records every link in a sidecar
|
|
48
|
-
- 🤖 **[LLM-assisted retrieval](#--
|
|
48
|
+
- 🤖 **[LLM-assisted retrieval](#--fetch-everything-the-second-layer)**: `--fetch-everything` adds a second layer — a model reads the paper itself and goes after the SI and datasets the APIs missed
|
|
49
49
|
- 🔐 **[Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions)** (opt-in): a one-time `fetchpdf fetchcookies` lets fetchpdf download papers your library subscribes to
|
|
50
50
|
- 🚀 **Batch Processing**: Process multiple DOIs/PMIDs from CSV files or lists
|
|
51
51
|
- ⚡ **Parallel Execution**: Download multiple papers simultaneously with configurable workers
|
|
@@ -66,7 +66,7 @@ pip install -e .
|
|
|
66
66
|
|
|
67
67
|
### Optional: Install Playwright and Chromium for Browser Automation
|
|
68
68
|
|
|
69
|
-
Install Playwright and Chromium
|
|
69
|
+
Install Playwright and Chromium to enable additional browser-based scrapers. They are used automatically once Playwright is installed (pass `--no-playwright` to skip them). The scrapers are for SSRN, OSF (if direct download fails), Wiley 10.1111/ DOIs, HTML table retrieval (second route), and Atypon supplementary material (PNAS, Science, SAGE, T&F, Annual Reviews).
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
pip install -e ".[browser]" # or: pip install playwright
|
|
@@ -101,9 +101,10 @@ SERPAPI_API_KEY=your_serpapi_key # Optional Google Scholar PDF fallbac
|
|
|
101
101
|
|
|
102
102
|
# Optional — publisher-specific
|
|
103
103
|
ELSEVIER_TDM_API_KEY=your_elsevier_key # Elsevier text/data-mining access
|
|
104
|
+
ELSEVIER_INSTTOKEN=your_inst_token # Elsevier institutional token (needs the key above)
|
|
104
105
|
SCOPUS_API_KEY=your_scopus_key # Scopus abstract lookup during identifier resolution
|
|
105
106
|
|
|
106
|
-
# Optional — only for --
|
|
107
|
+
# Optional — only for --fetch-everything --llm-backend openrouter
|
|
107
108
|
OPENROUTER_API_KEY=your_openrouter_key # the retrieval agent's second backend
|
|
108
109
|
|
|
109
110
|
# Required by fetchpdf-pubpeer — PubPeer rejects keyless requests
|
|
@@ -154,7 +155,7 @@ papers your library subscribes to. See
|
|
|
154
155
|
By default a record yields one file: the best PDF the source chain finds. Three
|
|
155
156
|
flags widen that.
|
|
156
157
|
|
|
157
|
-
1. **Structured full text** — `--get-xml-or-html` keeps a JATS/TEI XML (else
|
|
158
|
+
1. **Structured full text** — `--get-xml-or-html-and-pdf` keeps a JATS/TEI XML (else
|
|
158
159
|
publisher HTML) copy alongside the PDF; `--to-markdown` also renders it to
|
|
159
160
|
`{stem}_from_xml.md` (or `{stem}_from_html.md`). XML and HTML are great
|
|
160
161
|
formats for using with AI (LLMs): complex tables usually survive better than
|
|
@@ -174,7 +175,7 @@ flags widen that.
|
|
|
174
175
|
|
|
175
176
|
```bash
|
|
176
177
|
fetchpdf papers.csv -o ./out -w 4 \
|
|
177
|
-
--get-xml-or-html --to-markdown \
|
|
178
|
+
--get-xml-or-html-and-pdf --to-markdown \
|
|
178
179
|
--download-data-artifacts \
|
|
179
180
|
--make-subfolder --provenance
|
|
180
181
|
```
|
|
@@ -403,10 +404,10 @@ fetchpdf papers.csv -o ./out --xml-html-only # XML or publisher HTML
|
|
|
403
404
|
fetchpdf papers.csv -o ./out --xml-only # XML, nothing else
|
|
404
405
|
|
|
405
406
|
# A structured copy AND the PDF for every record
|
|
406
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html
|
|
407
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf
|
|
407
408
|
|
|
408
409
|
# ...and a {stem}_from_xml.md / _from_html.md for each XML/HTML artifact
|
|
409
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html --to-markdown
|
|
410
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf --to-markdown
|
|
410
411
|
|
|
411
412
|
# Convert artifacts you already have, without re-downloading
|
|
412
413
|
fetchpdf-md ./out
|
|
@@ -424,7 +425,7 @@ supplement keeps its own extension, e.g. `{stem}.xlsx`), `{stem}.pdf`,
|
|
|
424
425
|
`{stem}.txt` and `{stem}.landing.html` — plus `{stem}_from_xml.md` /
|
|
425
426
|
`{stem}_from_html.md` under `--to-markdown`.
|
|
426
427
|
|
|
427
|
-
`--get-xml-or-html` doubles as a backfill: pointed at a directory of existing
|
|
428
|
+
`--get-xml-or-html-and-pdf` doubles as a backfill: pointed at a directory of existing
|
|
428
429
|
PDFs it fetches only the missing structured half and re-downloads nothing.
|
|
429
430
|
|
|
430
431
|
### Choosing a flag
|
|
@@ -434,7 +435,9 @@ PDFs it fetches only the missing structured half and re-downloads nothing.
|
|
|
434
435
|
| `--prioritize-xml` | everything, best format first | yes |
|
|
435
436
|
| `--xml-html-only` | T1 XML, T2 HTML | **no** — record fails |
|
|
436
437
|
| `--xml-only` | T1 XML | **no** — record fails |
|
|
437
|
-
| `--get-xml-or-html` | structured **and** PDF, both kept | yes, and keeps it |
|
|
438
|
+
| `--get-xml-or-html-and-pdf` | structured **and** PDF, both kept | yes, and keeps it |
|
|
439
|
+
| `--get-xml-and-pdf` | XML where available **and** PDF where available, both kept | yes, and keeps it |
|
|
440
|
+
| `--get-xml-and-html-and-pdf` | XML, publisher HTML and PDF, each to the extent available | yes, and keeps it |
|
|
438
441
|
|
|
439
442
|
`--xml-only` and `--xml-html-only` both imply `--prioritize-xml`. Given both,
|
|
440
443
|
`--xml-only` is stricter and wins (with a warning).
|
|
@@ -533,9 +536,9 @@ unless stdin and stdout are both terminals** — pipes, cron and CI get a one-li
|
|
|
533
536
|
count and today's behaviour, so no unattended run can block on it. It is also
|
|
534
537
|
suppressed when the answer is already known: any flag that turns the
|
|
535
538
|
supplementary pass on (`--pull-supplementary`, `--refresh-supplementary`,
|
|
536
|
-
`--download-data-artifacts`, `--
|
|
539
|
+
`--download-data-artifacts`, `--fetch-everything`, `--llm-agent-retrieval`), an
|
|
537
540
|
explicit `--on-existing skip` or `supplement`, `--abstract-only`, or the
|
|
538
|
-
goal-aware flags (`--get-xml-or-html`, `--upgrade-existing` on the tiered path),
|
|
541
|
+
goal-aware flags (`--get-xml-or-html-and-pdf`, `--upgrade-existing` on the tiered path),
|
|
539
542
|
which do not skip records at all. Single-identifier runs never ask.
|
|
540
543
|
|
|
541
544
|
What is *not* remembered between runs: failures. `failed_dois.csv` is a report,
|
|
@@ -551,6 +554,12 @@ retrieved, the PDF is kept and reported as unverified.
|
|
|
551
554
|
When a PDF is unavailable, FetchPDF tries XML or HTML full text automatically.
|
|
552
555
|
Use `--no-xml-fallback` to disable this fallback.
|
|
553
556
|
|
|
557
|
+
A record that comes back as XML only is filed in its own folder,
|
|
558
|
+
`{output_dir}/{stem}/`, with its figure images in `{stem}_figures/` and a
|
|
559
|
+
rendering at `{stem}_from_xml.pdf` (figures embedded; a rendering of the XML,
|
|
560
|
+
not the publisher's PDF). A later flat run finds it there. Use
|
|
561
|
+
`--no-xml-fallback-extras` to leave the bare `.xml` instead.
|
|
562
|
+
|
|
554
563
|
Publisher HTML (T2) uses lxml, which is installed with fetchpdf.
|
|
555
564
|
|
|
556
565
|
Advanced users can customize retrieval order, source capabilities, and host rate
|
|
@@ -634,7 +643,7 @@ Twenty routes, tried in a fixed order so the numbering is reproducible:
|
|
|
634
643
|
| Europe PMC datalinks | any PMID — text-mined accessions and data citations |
|
|
635
644
|
| Full-text scan | deposits the paper names in its own prose — only under `--download-data-artifacts` |
|
|
636
645
|
| Atypon publisher SI | PNAS, Science, Annual Reviews, SAGE, T&F — only when Europe PMC withholds the SI or the record has no PMCID; drives a headed browser, so it needs Playwright, Chromium and a display (or Xvfb) |
|
|
637
|
-
| LLM retrieval agent | only under [`--
|
|
646
|
+
| LLM retrieval agent | only under [`--fetch-everything`](#--fetch-everything-the-second-layer) or `--llm-agent-retrieval` |
|
|
638
647
|
| JCI | `10.1172/jci…` |
|
|
639
648
|
|
|
640
649
|
Wiley, ACS and MDPI serve HTTP 403 to plain requests and are not yet covered;
|
|
@@ -684,7 +693,7 @@ Supplementary files never appear in `failed_dois.csv`, in `source_tracking.csv`,
|
|
|
684
693
|
or in the format-composition tally; the missing-PDFs report lists gaps in them
|
|
685
694
|
only in its own separate section. The same holds for linked datasets,
|
|
686
695
|
[figures](#figure-images) and the
|
|
687
|
-
[`--
|
|
696
|
+
[`--fetch-everything`](#--fetch-everything-the-second-layer) agent.
|
|
688
697
|
|
|
689
698
|
### Linked datasets and code: `{stem}_linked_artifacts.json`
|
|
690
699
|
|
|
@@ -773,18 +782,19 @@ GitLab repositories, and the institutional-repository long tail.
|
|
|
773
782
|
Adding an enumerator is the same small pattern as `enumerate_dryad` /
|
|
774
783
|
`enumerate_dataverse` in `fetchpdf/retrieval/supplement_index.py`.
|
|
775
784
|
|
|
776
|
-
## `--
|
|
785
|
+
## `--fetch-everything`: the second layer
|
|
777
786
|
|
|
778
787
|
Everything above is layer 1 — APIs, indexes and enumerators. It is very good at
|
|
779
788
|
what publishers and repositories *declare*, and it cannot read. Layer 2 is a
|
|
780
789
|
model that reads the paper itself and goes after what layer 1 did not get.
|
|
781
790
|
|
|
782
791
|
```bash
|
|
783
|
-
fetchpdf papers.csv -o ./out --
|
|
784
|
-
--pull-everything
|
|
792
|
+
fetchpdf papers.csv -o ./out --make-subfolder --provenance --fetch-everything
|
|
785
793
|
```
|
|
786
794
|
|
|
787
|
-
`--
|
|
795
|
+
`--fetch-everything` implies `--get-xml-or-html-and-pdf` (XML, else HTML, beside the PDF
|
|
796
|
+
where one exists), `--pull-figures` (the publisher's figure images beside the
|
|
797
|
+
XML), `--pull-supplementary` and
|
|
788
798
|
`--download-data-artifacts`, turns on adjudication of the deterministic scan's
|
|
789
799
|
uncertain candidates, and adds the retrieval agent. Files land exactly where
|
|
790
800
|
they always did: supplements as numbered siblings of the PDF, linked deposits
|
|
@@ -856,7 +866,7 @@ backend has no general-purpose harness to lock down, because its two tools are
|
|
|
856
866
|
ours, so it downloads directly.
|
|
857
867
|
|
|
858
868
|
`--llm-backend` selects the retrieval agent's backend only. The adjudication of
|
|
859
|
-
uncertain full-text-scan candidates (on under `--
|
|
869
|
+
uncertain full-text-scan candidates (on under `--fetch-everything`, or with
|
|
860
870
|
`--llm-adjudicate-artifacts`) always calls the Claude Code CLI, as a single-turn
|
|
861
871
|
call with `--allowed-tools ""`; it does not get the `--disallowed-tools` /
|
|
862
872
|
`--strict-mcp-config` lockdown described above.
|
|
@@ -960,7 +970,7 @@ from the network. `--generate-pdf-from-xml` prints the same page to
|
|
|
960
970
|
|
|
961
971
|
```bash
|
|
962
972
|
pip install 'fetchpdf[render,browser]' && playwright install chromium
|
|
963
|
-
fetchpdf papers.csv -o ./out --get-xml-or-html --generate-pdf-from-xml
|
|
973
|
+
fetchpdf papers.csv -o ./out --get-xml-or-html-and-pdf --generate-pdf-from-xml
|
|
964
974
|
fetchpdf-render ./out --html --pdf --fetch-figures # XML you already have
|
|
965
975
|
```
|
|
966
976
|
|
|
@@ -978,7 +988,7 @@ shaped by its aspect ratio and capped at the column width. Under the title come
|
|
|
978
988
|
the authors, their affiliations and the journal citation (volume, issue, pages).
|
|
979
989
|
Tables always fit the page width in the PDF; very wide ones print smaller.
|
|
980
990
|
|
|
981
|
-
Only records whose XML was kept are rendered — add `--get-xml-or-html` to keep
|
|
991
|
+
Only records whose XML was kept are rendered — add `--get-xml-or-html-and-pdf` to keep
|
|
982
992
|
the XML beside the PDF.
|
|
983
993
|
|
|
984
994
|
## Institutional access via cookies (ie using your library's subscriptions)
|
|
@@ -1072,8 +1082,22 @@ your browser's cookies signs fetchpdf out as well.
|
|
|
1072
1082
|
Publishers whose PDF address follows from the DOI: **Wiley** (`10.1002`,
|
|
1073
1083
|
`10.1111`), **Taylor & Francis** (`10.1080`), **SAGE** (`10.1177`),
|
|
1074
1084
|
**Springer** (`10.1007`, `10.1057`), the **Royal Society** (`10.1098`),
|
|
1075
|
-
**Hogrefe** (`10.1027`)
|
|
1076
|
-
|
|
1085
|
+
**Hogrefe** (`10.1027`), **INFORMS** (`10.1287`), **PNAS** (`10.1073`),
|
|
1086
|
+
**Science** (`10.1126`), **ASCO** (`10.1200`), **AHA** (`10.1161`), **APS**
|
|
1087
|
+
(`10.1152`), **ASM** (`10.1128`), **ATS** (`10.1164`), **ACS** (`10.1021`) and
|
|
1088
|
+
**FASEB** (`10.1096`).
|
|
1089
|
+
|
|
1090
|
+
Publishers whose PDF address is not derivable from the DOI are reached through
|
|
1091
|
+
the DOI link, and the PDF the landing page declares is fetched: **AACR**
|
|
1092
|
+
(`10.1158`), **Cold Spring Harbor** (`10.1101`), **Oncotarget** (`10.18632`),
|
|
1093
|
+
**Oxford** (`10.1093`, `10.1210`, `10.4049`), **JAMA** (`10.1001`), **ASH**
|
|
1094
|
+
(`10.1182`), **Company of Biologists** (`10.1242`), **ERS** (`10.1183`),
|
|
1095
|
+
**BMJ** (`10.1136`), **Bioscientifica** (`10.1530`) and **LWW** (`10.1097`).
|
|
1096
|
+
|
|
1097
|
+
Several of these are open access but sit behind a Cloudflare bot check that a
|
|
1098
|
+
plain download cannot pass. Opening any article on the site once in your
|
|
1099
|
+
everyday browser earns the clearance cookie, and the next run reuses it.
|
|
1100
|
+
Other DOIs are untouched and produce no extra output.
|
|
1077
1101
|
|
|
1078
1102
|
It runs right after the fast open-access lookups (repositories, PMC,
|
|
1079
1103
|
Unpaywall), so a paper that is free anywhere is fetched from there and never
|
|
@@ -1081,7 +1105,7 @@ spends a subscription download. It runs before the slow long tail (Crossref
|
|
|
1081
1105
|
landing pages, CORE, DOAJ, DataCite, ...), where a subscription answers in a
|
|
1082
1106
|
second or two. The PDF meets the same identity check as every other source and
|
|
1083
1107
|
is reported as `institutional_cookies` in source tracking and `--json`. It also
|
|
1084
|
-
fills the PDF half of `--get-xml-or-html` runs.
|
|
1108
|
+
fills the PDF half of `--get-xml-or-html-and-pdf` runs.
|
|
1085
1109
|
|
|
1086
1110
|
### Sign-in, the VPN, and Cloudflare
|
|
1087
1111
|
|
|
@@ -1165,9 +1189,9 @@ refusal, not a bare failure:
|
|
|
1165
1189
|
| `path` | str \| null | Absolute path to the artifact, or null. |
|
|
1166
1190
|
| `format` | str | From the artifact actually written: `pdf`, `xml`, `html`, `latex`, `landing`, `suppl`, `abstract`, `text`, `other`. **A successful record is not necessarily a PDF** — with the default XML fallback a record can succeed as `xml`. |
|
|
1167
1191
|
| `source` | str \| null | The route that produced it (`unpaywall`, `core`, `osf`, …), `existing` for a skip, `structured_fallback` when no PDF was available and structured full text was saved instead. |
|
|
1168
|
-
| `identity` | str \| null | The verdict on the PDF **fetched in this run and reported in `path`**: `verified`, or a refusal — `wrong_article`, `truncated`, `unreadable`, `no_reference`, `no_engine`. Null when nothing was judged: an XML artifact, a file already on disk, or any run under the tiered flags (`--prioritize-xml`, `--get-xml-or-html`, `--xml-only`, `--xml-html-only`), where the engine validates per tier instead. A success with a non-`verified` identity is a PDF that was kept rather than deleted because the verdict could not be trusted — see [the identity check](#format-prioritized-retrieval) — not one that passed. |
|
|
1192
|
+
| `identity` | str \| null | The verdict on the PDF **fetched in this run and reported in `path`**: `verified`, or a refusal — `wrong_article`, `truncated`, `unreadable`, `no_reference`, `no_engine`. Null when nothing was judged: an XML artifact, a file already on disk, or any run under the tiered flags (`--prioritize-xml`, `--get-xml-or-html-and-pdf`, `--xml-only`, `--xml-html-only`), where the engine validates per tier instead. A success with a non-`verified` identity is a PDF that was kept rather than deleted because the verdict could not be trusted — see [the identity check](#format-prioritized-retrieval) — not one that passed. |
|
|
1169
1193
|
| `reasons` | list[str] | Why a record failed, or why a kept PDF could not be verified. Empty on a verified success. |
|
|
1170
|
-
| `paths` | list[str] | The other artifacts a tiered run wrote (`--get-xml-or-html`), empty otherwise. `path` is not repeated here. |
|
|
1194
|
+
| `paths` | list[str] | The other artifacts a tiered run wrote (`--get-xml-or-html-and-pdf`), empty otherwise. `path` is not repeated here. |
|
|
1171
1195
|
| `supplementary` | object | Only with `--pull-supplementary` (or a flag that implies it): `status` (`ok`, `none_found`, `partial`, `incomplete`, `error`, `skipped`), `written`, `skipped`, `manifest` (absolute path or null), `missing_declared` (files the article declares that were not obtained) and `blocked_urls` (refused to this client; open them in a browser). |
|
|
1172
1196
|
| `figures` | object | Only with `--pull-figures`: `status`, `written`, `failed`, `refusal` and `manifest`. |
|
|
1173
1197
|
| `images` | object | Only with `--extract-images`: `status`, `written` and `manifest`. |
|
|
@@ -1363,7 +1387,7 @@ These run first when the DOI matches (by prefix, or by the name appearing anywhe
|
|
|
1363
1387
|
9. **CORE** (core.ac.uk) — 40M+ repository papers; needs `COREAPIKEY`
|
|
1364
1388
|
10. **DOAJ** — open-access full-text links
|
|
1365
1389
|
11. **DataCite content URLs** — direct content and related resource links
|
|
1366
|
-
12. **Wiley rendered PDF** — only
|
|
1390
|
+
12. **Wiley rendered PDF** — only when Playwright is installed
|
|
1367
1391
|
13. **APA supplemental** — APA supplemental files, converting `.doc`/`.docx` to PDF when available
|
|
1368
1392
|
14. **Direct DOI resolver**
|
|
1369
1393
|
- Follows `https://doi.org/{doi}` to the landing page
|
|
@@ -1391,8 +1415,28 @@ These run first when the DOI matches (by prefix, or by the name appearing anywhe
|
|
|
1391
1415
|
|
|
1392
1416
|
18. **Elsevier full-text API**
|
|
1393
1417
|
- Elsevier records only (`10.1016/*`, or a Crossref landing page on sciencedirect.com / elsevier.com)
|
|
1394
|
-
- Tries the PDF first, then full-text XML (the XML half is skipped with `--no-xml-fallback`)
|
|
1395
1418
|
- Requires `ELSEVIER_TDM_API_KEY`
|
|
1419
|
+
- **`10.1016/*` DOIs are asked for their PDF first** — before PMC, Unpaywall,
|
|
1420
|
+
institutional access and the long tail of landing-page and repository
|
|
1421
|
+
probes (a dead repository host in Unpaywall's list alone can cost over a
|
|
1422
|
+
minute). An entitled record is one request instead of a walk through every
|
|
1423
|
+
other route. Only a *full* PDF ends
|
|
1424
|
+
the search: Elsevier's first-page-only preview (served with HTTP 200 and a
|
|
1425
|
+
real PDF when the key is not entitled) is discarded, so asking first cannot
|
|
1426
|
+
pre-empt a source that holds the whole paper. When the PDF is obtained, the
|
|
1427
|
+
full-text XML is fetched alongside it, so both are on disk.
|
|
1428
|
+
- The deferred step at the end of the chain remains: it takes the XML for
|
|
1429
|
+
records whose key is entitled to the XML but not the PDF, and covers
|
|
1430
|
+
ScienceDirect DOIs outside `10.1016/*`. It does not repeat a PDF request the
|
|
1431
|
+
early step already made. The XML half is skipped with `--no-xml-fallback`.
|
|
1432
|
+
- With `--prioritize-xml` / `--xml-only` / `--get-xml-or-html-and-pdf` the tiered
|
|
1433
|
+
ladder asks the same endpoint itself (`elsevier_tdm` at T1, `elsevier_pdf`
|
|
1434
|
+
at T5, ahead of the legacy chain).
|
|
1435
|
+
- **Off campus?** Entitlement is decided by your IP unless you also set
|
|
1436
|
+
`ELSEVIER_INSTTOKEN`. Request an institutional token from Elsevier (the
|
|
1437
|
+
developer portal at dev.elsevier.com, or your library); it is sent as
|
|
1438
|
+
`X-ELS-Insttoken` on every `api.elsevier.com` request, never to any other
|
|
1439
|
+
host, and is redacted from logs and sidecars.
|
|
1396
1440
|
|
|
1397
1441
|
If the whole chain yields no PDF, `fetch_pdf` tries structured full text
|
|
1398
1442
|
(T1 XML / T2 HTML) from the tier ladder before giving up; `--no-xml-fallback`
|
|
@@ -15,7 +15,7 @@ from .fetch_metadata_from_doi import fetch_metadata_from_doi
|
|
|
15
15
|
# behavioural difference, it is the same object.
|
|
16
16
|
fetch_pdf_from_doi = fetch_pdf
|
|
17
17
|
|
|
18
|
-
__version__ = "0.1.
|
|
18
|
+
__version__ = "0.1.6"
|
|
19
19
|
__all__ = [
|
|
20
20
|
"fetch_pdf",
|
|
21
21
|
"fetch_pdf_from_doi", # deprecated alias for fetch_pdf
|
|
@@ -59,6 +59,13 @@ PUBPEER_DEVKEY = os.getenv("PUBPEER_DEVKEY")
|
|
|
59
59
|
|
|
60
60
|
# Elsevier full-text (TDM) retrieval.
|
|
61
61
|
ELSEVIER_TDM_API_KEY = os.getenv("ELSEVIER_TDM_API_KEY")
|
|
62
|
+
# Elsevier institutional token. Optional, and only meaningful alongside the key:
|
|
63
|
+
# it is issued by Elsevier (developer portal / your library) and tells the API
|
|
64
|
+
# to grant the key your institution's subscription entitlements even when the
|
|
65
|
+
# request does not come from an institutional IP -- off campus, behind a
|
|
66
|
+
# non-subscribing VPN, on a cloud host. Without it entitlement is decided by the
|
|
67
|
+
# requesting IP alone.
|
|
68
|
+
ELSEVIER_INSTTOKEN = os.getenv("ELSEVIER_INSTTOKEN")
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
if not EMAIL:
|
|
@@ -79,6 +79,30 @@ def looks_like_pdf(content: bytes, content_type: str = "") -> bool:
|
|
|
79
79
|
return ctype in ("application/pdf", "application/octet-stream") and bool(content)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def elsevier_headers(api_key: str, extra: dict = None) -> dict:
|
|
83
|
+
"""Request headers for any api.elsevier.com call that carries the TDM key.
|
|
84
|
+
|
|
85
|
+
The one place the credentials are spelled, so a request can never carry the
|
|
86
|
+
key and forget the institutional token. ``X-ELS-Insttoken`` is added when
|
|
87
|
+
ELSEVIER_INSTTOKEN is set and omitted otherwise -- an empty header would be
|
|
88
|
+
read as a malformed token and refused. It is looked up at call time, not
|
|
89
|
+
bound at import, so a process that loads its .env late still gets it.
|
|
90
|
+
|
|
91
|
+
Both go in headers, never the URL: a URL surfaces in exception text, logs
|
|
92
|
+
and provenance, and a credential there is a credential leaked.
|
|
93
|
+
|
|
94
|
+
The key is a parameter, not read here, because each caller holds its own
|
|
95
|
+
binding of it (and its tests pin that binding).
|
|
96
|
+
"""
|
|
97
|
+
from . import _env
|
|
98
|
+
|
|
99
|
+
out = {"X-ELS-APIKey": api_key}
|
|
100
|
+
if _env.ELSEVIER_INSTTOKEN:
|
|
101
|
+
out["X-ELS-Insttoken"] = _env.ELSEVIER_INSTTOKEN
|
|
102
|
+
out.update(extra or {})
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
82
106
|
def elsevier_first_page_only(headers) -> bool:
|
|
83
107
|
"""True when Elsevier's X-ELS-Status header marks a first-page-only PDF.
|
|
84
108
|
|
|
@@ -261,7 +261,7 @@ def fetch_abstract_from_doi(doi, email=None, delay=0.1, verbose=False):
|
|
|
261
261
|
"title": d.get("title"),
|
|
262
262
|
"abstract": abstract,
|
|
263
263
|
"authors": authors,
|
|
264
|
-
"journal": (d.get("primary_location") or {}).get("source"
|
|
264
|
+
"journal": ((d.get("primary_location") or {}).get("source") or {}).get("display_name"),
|
|
265
265
|
"year": d.get("publication_year"),
|
|
266
266
|
})
|
|
267
267
|
if verbose and abstract:
|
|
@@ -435,7 +435,8 @@ def fetch_abstract_from_doi(doi, email=None, delay=0.1, verbose=False):
|
|
|
435
435
|
if verbose and abstract:
|
|
436
436
|
print(" ✅ Abstract from Scopus")
|
|
437
437
|
except Exception as e:
|
|
438
|
-
|
|
438
|
+
# The exception text can carry the request URL, and with it the key.
|
|
439
|
+
if verbose: print(f" Scopus error: {type(e).__name__}")
|
|
439
440
|
time.sleep(delay)
|
|
440
441
|
if _done(): return result
|
|
441
442
|
|