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.
Files changed (126) hide show
  1. {fetchpdf-0.1.4/fetchpdf.egg-info → fetchpdf-0.1.6}/PKG-INFO +74 -29
  2. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/README.md +72 -28
  3. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/__init__.py +1 -1
  4. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/_env.py +7 -0
  5. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/_http.py +24 -0
  6. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetch_abstract_from_doi.py +3 -2
  7. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetchpdf.py +540 -256
  8. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/get_cookies.py +17 -10
  9. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/_util.py +1 -4
  10. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/corresponding.py +1 -1
  11. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/engine.py +9 -7
  12. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/extract_images.py +2 -12
  13. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/fulltext_scan.py +9 -13
  14. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/goals.py +45 -15
  15. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/http.py +70 -10
  16. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/institutional.py +235 -40
  17. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/ladder.json +3 -3
  18. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/llm_adjudicate.py +1 -2
  19. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/llm_agent_retrieval.py +3 -2
  20. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/normalize.py +5 -1
  21. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pdf_identity.py +1 -3
  22. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/provenance.py +3 -2
  23. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pubpeer.py +1 -1
  24. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/request_drafts.py +13 -8
  25. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/resolve.py +0 -1
  26. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/legacy_pdf.py +4 -0
  27. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_atypon.py +0 -1
  28. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_graph.py +70 -52
  29. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_index.py +0 -1
  30. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplementary.py +72 -70
  31. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/to_html.py +9 -4
  32. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/to_markdown.py +13 -6
  33. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/validate.py +8 -6
  34. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/verify.py +2 -7
  35. {fetchpdf-0.1.4 → fetchpdf-0.1.6/fetchpdf.egg-info}/PKG-INFO +74 -29
  36. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/SOURCES.txt +5 -1
  37. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/requires.txt +1 -0
  38. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/pyproject.toml +3 -0
  39. fetchpdf-0.1.6/tests/test_elsevier_routing.py +315 -0
  40. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_institutional.py +55 -0
  41. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_polite_ua.py +4 -0
  42. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pure.py +8 -5
  43. fetchpdf-0.1.6/tests/test_review_fixes.py +100 -0
  44. fetchpdf-0.1.6/tests/test_xml_fallback_extras.py +61 -0
  45. fetchpdf-0.1.6/tests/test_xml_last.py +77 -0
  46. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/LICENSE +0 -0
  47. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/MANIFEST.in +0 -0
  48. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/__main__.py +0 -0
  49. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/fetch_metadata_from_doi.py +0 -0
  50. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/__init__.py +0 -0
  51. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/artifact.py +0 -0
  52. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/__init__.py +0 -0
  53. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
  54. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/backends/openrouter.py +0 -0
  55. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/blocked.py +0 -0
  56. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/byline.py +0 -0
  57. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/cache.py +0 -0
  58. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/chunk.py +0 -0
  59. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/classify.py +0 -0
  60. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/context.py +0 -0
  61. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/figures.py +0 -0
  62. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/identifiers.py +0 -0
  63. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/linked_artifacts.py +0 -0
  64. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/pdf_text.py +0 -0
  65. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/ratelimit.py +0 -0
  66. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/references.py +0 -0
  67. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/repository_waf.py +0 -0
  68. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/__init__.py +0 -0
  69. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/arxiv.py +0 -0
  70. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/core_text.py +0 -0
  71. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
  72. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/epmc.py +0 -0
  73. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
  74. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/landing.py +0 -0
  75. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/pmc.py +0 -0
  76. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
  77. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/preprint.py +0 -0
  78. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
  79. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/sources/supplements.py +0 -0
  80. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_dspace.py +0 -0
  81. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_jci.py +0 -0
  82. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_pmc.py +0 -0
  83. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/supplement_publishers.py +0 -0
  84. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf/retrieval/tiers.py +0 -0
  85. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/dependency_links.txt +0 -0
  86. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/entry_points.txt +0 -0
  87. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/fetchpdf.egg-info/top_level.txt +0 -0
  88. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/requirements.txt +0 -0
  89. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/setup.cfg +0 -0
  90. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_agent_backends.py +0 -0
  91. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_blocked.py +0 -0
  92. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_cookie_offer.py +0 -0
  93. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_corresponding.py +0 -0
  94. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_datacite_supplement_identity.py +0 -0
  95. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_dspace.py +0 -0
  96. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_entrypoints.py +0 -0
  97. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_epmc_repo.py +0 -0
  98. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_extract_images.py +0 -0
  99. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_figures.py +0 -0
  100. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_folder_encoding.py +0 -0
  101. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_fulltext_scan.py +0 -0
  102. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_get_cookies.py +0 -0
  103. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_jci_supplements.py +0 -0
  104. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_json_output.py +0 -0
  105. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_landing_candidates.py +0 -0
  106. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_landing_page_detection.py +0 -0
  107. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_last_resort_reachability.py +0 -0
  108. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_linked_artifacts.py +0 -0
  109. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_llm_adjudicate.py +0 -0
  110. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_llm_agent_retrieval.py +0 -0
  111. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_normalize_cells.py +0 -0
  112. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pdf_identity.py +0 -0
  113. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pdf_trailer.py +0 -0
  114. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pmc_page.py +0 -0
  115. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pubmed_identity.py +0 -0
  116. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pubpeer.py +0 -0
  117. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_pull_for_record_call_sites.py +0 -0
  118. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_request_drafts.py +0 -0
  119. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_retrieval.py +0 -0
  120. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_scan_record.py +0 -0
  121. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_screening_conversion.py +0 -0
  122. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_serpapi_scholar.py +0 -0
  123. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_short_conference_xml.py +0 -0
  124. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_supplement_asset_identity.py +0 -0
  125. {fetchpdf-0.1.4 → fetchpdf-0.1.6}/tests/test_supplementary.py +0 -0
  126. {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.4
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
- - [`--pull-everything`: the second layer](#--pull-everything-the-second-layer)
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](#--pull-everything-the-second-layer)**: `--pull-everything` adds a second layer — a model reads the paper itself and goes after the SI and datasets the APIs missed
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 only if you want to try additional scrapers enabled by `--add-playwright`. The scapers 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
+ 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 --pull-everything --llm-backend openrouter
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`, `--pull-everything`, `--llm-agent-retrieval`), an
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 [`--pull-everything`](#--pull-everything-the-second-layer) or `--llm-agent-retrieval` |
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
- [`--pull-everything`](#--pull-everything-the-second-layer) agent.
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
- ## `--pull-everything`: the second layer
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 --get-xml-or-html --make-subfolder --provenance \
823
- --pull-everything
832
+ fetchpdf papers.csv -o ./out --make-subfolder --provenance --fetch-everything
824
833
  ```
825
834
 
826
- `--pull-everything` implies `--pull-supplementary` and
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 `--pull-everything`, or with
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`) and **INFORMS** (`10.1287`). Other DOIs are untouched
1115
- and produce no extra output.
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 with `--add-playwright`
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
- - [`--pull-everything`: the second layer](#--pull-everything-the-second-layer)
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](#--pull-everything-the-second-layer)**: `--pull-everything` adds a second layer — a model reads the paper itself and goes after the SI and datasets the APIs missed
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 only if you want to try additional scrapers enabled by `--add-playwright`. The scapers 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).
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 --pull-everything --llm-backend openrouter
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`, `--pull-everything`, `--llm-agent-retrieval`), an
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 [`--pull-everything`](#--pull-everything-the-second-layer) or `--llm-agent-retrieval` |
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
- [`--pull-everything`](#--pull-everything-the-second-layer) agent.
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
- ## `--pull-everything`: the second layer
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 --get-xml-or-html --make-subfolder --provenance \
784
- --pull-everything
792
+ fetchpdf papers.csv -o ./out --make-subfolder --provenance --fetch-everything
785
793
  ```
786
794
 
787
- `--pull-everything` implies `--pull-supplementary` and
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 `--pull-everything`, or with
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`) and **INFORMS** (`10.1287`). Other DOIs are untouched
1076
- and produce no extra output.
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 with `--add-playwright`
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.4"
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", {}).get("display_name"),
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
- if verbose: print(f" Scopus error: {e}")
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