fetchpdf 0.1.3__tar.gz → 0.1.5__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 (123) hide show
  1. {fetchpdf-0.1.3/fetchpdf.egg-info → fetchpdf-0.1.5}/PKG-INFO +17 -11
  2. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/README.md +16 -10
  3. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/__init__.py +1 -1
  4. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/fetchpdf.py +76 -41
  5. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/get_cookies.py +6 -6
  6. fetchpdf-0.1.5/fetchpdf/retrieval/byline.py +257 -0
  7. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/figures.py +6 -0
  8. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/institutional.py +3 -3
  9. fetchpdf-0.1.5/fetchpdf/retrieval/references.py +280 -0
  10. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_html.py +163 -19
  11. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_markdown.py +10 -1
  12. {fetchpdf-0.1.3 → fetchpdf-0.1.5/fetchpdf.egg-info}/PKG-INFO +17 -11
  13. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf.egg-info/SOURCES.txt +4 -1
  14. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_cookie_offer.py +1 -1
  15. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_get_cookies.py +1 -1
  16. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_institutional.py +1 -1
  17. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_supplementary.py +24 -0
  18. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_to_html.py +140 -2
  19. fetchpdf-0.1.5/tests/test_xml_last.py +76 -0
  20. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/LICENSE +0 -0
  21. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/MANIFEST.in +0 -0
  22. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/__main__.py +0 -0
  23. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/_env.py +0 -0
  24. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/_http.py +0 -0
  25. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/fetch_abstract_from_doi.py +0 -0
  26. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/fetch_metadata_from_doi.py +0 -0
  27. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/__init__.py +0 -0
  28. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/_util.py +0 -0
  29. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/artifact.py +0 -0
  30. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/__init__.py +0 -0
  31. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
  32. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/openrouter.py +0 -0
  33. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/blocked.py +0 -0
  34. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/cache.py +0 -0
  35. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/chunk.py +0 -0
  36. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/classify.py +0 -0
  37. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/context.py +0 -0
  38. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/corresponding.py +0 -0
  39. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/engine.py +0 -0
  40. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/extract_images.py +0 -0
  41. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/fulltext_scan.py +0 -0
  42. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/goals.py +0 -0
  43. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/http.py +0 -0
  44. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/identifiers.py +0 -0
  45. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/ladder.json +0 -0
  46. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/linked_artifacts.py +0 -0
  47. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_adjudicate.py +0 -0
  48. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_agent_retrieval.py +0 -0
  49. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/normalize.py +0 -0
  50. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_identity.py +0 -0
  51. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_text.py +0 -0
  52. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/provenance.py +0 -0
  53. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/pubpeer.py +0 -0
  54. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/ratelimit.py +0 -0
  55. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/repository_waf.py +0 -0
  56. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/request_drafts.py +0 -0
  57. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/resolve.py +0 -0
  58. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/__init__.py +0 -0
  59. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/arxiv.py +0 -0
  60. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/core_text.py +0 -0
  61. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
  62. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc.py +0 -0
  63. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
  64. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/landing.py +0 -0
  65. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/legacy_pdf.py +0 -0
  66. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc.py +0 -0
  67. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
  68. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/preprint.py +0 -0
  69. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
  70. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/supplements.py +0 -0
  71. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_atypon.py +0 -0
  72. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_dspace.py +0 -0
  73. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_graph.py +0 -0
  74. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_index.py +0 -0
  75. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_jci.py +0 -0
  76. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_pmc.py +0 -0
  77. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_publishers.py +0 -0
  78. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplementary.py +0 -0
  79. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/tiers.py +0 -0
  80. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/validate.py +0 -0
  81. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf/retrieval/verify.py +0 -0
  82. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf.egg-info/dependency_links.txt +0 -0
  83. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf.egg-info/entry_points.txt +0 -0
  84. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf.egg-info/requires.txt +0 -0
  85. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/fetchpdf.egg-info/top_level.txt +0 -0
  86. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/pyproject.toml +0 -0
  87. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/requirements.txt +0 -0
  88. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/setup.cfg +0 -0
  89. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_agent_backends.py +0 -0
  90. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_blocked.py +0 -0
  91. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_corresponding.py +0 -0
  92. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_datacite_supplement_identity.py +0 -0
  93. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_dspace.py +0 -0
  94. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_entrypoints.py +0 -0
  95. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_epmc_repo.py +0 -0
  96. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_extract_images.py +0 -0
  97. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_figures.py +0 -0
  98. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_folder_encoding.py +0 -0
  99. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_fulltext_scan.py +0 -0
  100. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_jci_supplements.py +0 -0
  101. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_json_output.py +0 -0
  102. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_landing_candidates.py +0 -0
  103. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_landing_page_detection.py +0 -0
  104. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_last_resort_reachability.py +0 -0
  105. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_linked_artifacts.py +0 -0
  106. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_llm_adjudicate.py +0 -0
  107. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_llm_agent_retrieval.py +0 -0
  108. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_normalize_cells.py +0 -0
  109. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pdf_identity.py +0 -0
  110. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pdf_trailer.py +0 -0
  111. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pmc_page.py +0 -0
  112. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_polite_ua.py +0 -0
  113. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pubmed_identity.py +0 -0
  114. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pubpeer.py +0 -0
  115. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pull_for_record_call_sites.py +0 -0
  116. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_pure.py +0 -0
  117. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_request_drafts.py +0 -0
  118. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_retrieval.py +0 -0
  119. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_scan_record.py +0 -0
  120. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_screening_conversion.py +0 -0
  121. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_serpapi_scholar.py +0 -0
  122. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_short_conference_xml.py +0 -0
  123. {fetchpdf-0.1.3 → fetchpdf-0.1.5}/tests/test_supplement_asset_identity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchpdf
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
@@ -56,7 +56,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
56
56
  - [Supplementary Material](#supplementary-material)
57
57
  - [`--pull-everything`: the second layer](#--pull-everything-the-second-layer)
58
58
  - [Figure Images](#figure-images)
59
- - [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions) — papers your library subscribes to (`fetchpdf cookies setup`)
59
+ - [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions) — papers your library subscribes to (`fetchpdf fetchcookies`)
60
60
  - [Calling fetchpdf from a script or an agent](#calling-fetchpdf-from-a-script-or-an-agent) — `--json`
61
61
  - [API Reference](#api-reference)
62
62
  - [Download Sources](#download-sources-in-order-of-priority)
@@ -85,7 +85,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
85
85
  - 🖨️ **[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
86
  - 🔗 **[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
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
- - 🔐 **[Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions)** (opt-in): a one-time `fetchpdf cookies setup` lets fetchpdf download papers your library subscribes to
88
+ - 🔐 **[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
89
  - 🚀 **Batch Processing**: Process multiple DOIs/PMIDs from CSV files or lists
90
90
  - ⚡ **Parallel Execution**: Download multiple papers simultaneously with configurable workers
91
91
 
@@ -184,7 +184,7 @@ fetchpdf '10.1002/(SICI)1521-3773(19981116)37:21<2922::AID-ANIE2922>3.0.CO;2-1,
184
184
  # ...but single-quote any list holding ( ) < > ; -- the shell would read them as syntax
185
185
  ```
186
186
 
187
- **At a university?** Run `fetchpdf cookies setup` once and fetchpdf will also fetch
187
+ **At a university?** Run `fetchpdf fetchcookies` once and fetchpdf will also fetch
188
188
  papers your library subscribes to. See
189
189
  [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions).
190
190
 
@@ -1005,11 +1005,17 @@ fetchpdf-render ./out --html --pdf --fetch-figures # XML you already have
1005
1005
 
1006
1006
  **A rendering, not the publisher's PDF**, and never written as `{stem}.pdf`:
1007
1007
  that name means the typeset article to the skip check and the validators. The
1008
- page opens with a banner naming the source file, the DOI, and how many figures
1009
- were embedded. A figure whose image could not be had keeps a visible
1008
+ page opens with a banner naming the source file, the fetchpdf version and the
1009
+ DOI, plus a note for anything that could not be shown: figures without an image,
1010
+ tables published only as an image that is missing, or no reference list. A figure whose image could not be had keeps a visible
1010
1011
  placeholder, so an incomplete rendering looks incomplete. The prose and
1011
- tables come from the same walker as `--to-markdown`, so the two agree;
1012
- references are omitted and formulas stay as text.
1012
+ tables come from the same walker as `--to-markdown`, so the two agree, except
1013
+ that the page ends with the reference list (with DOI links), which the Markdown
1014
+ leaves out. Formulas stay as text. The XML never says how large a figure was
1015
+ typeset, so each is given about the same area (a square figure ~10.5 cm across),
1016
+ shaped by its aspect ratio and capped at the column width. Under the title come
1017
+ the authors, their affiliations and the journal citation (volume, issue, pages).
1018
+ Tables always fit the page width in the PDF; very wide ones print smaller.
1013
1019
 
1014
1020
  Only records whose XML was kept are rendered — add `--get-xml-or-html` to keep
1015
1021
  the XML beside the PDF.
@@ -1026,7 +1032,7 @@ library, for the papers that have no open-access copy.
1026
1032
  Browser cookie support is included in the standard fetchpdf installation.
1027
1033
 
1028
1034
  ```bash
1029
- fetchpdf cookies setup
1035
+ fetchpdf fetchcookies
1030
1036
  ```
1031
1037
 
1032
1038
  The wizard asks two questions, then opens some tabs:
@@ -1058,7 +1064,7 @@ session exists but the test article was refused: the session lapsed, or your
1058
1064
  library does not take that particular journal. Access is turned on only if at
1059
1065
  least one publisher works.
1060
1066
 
1061
- **Cookies often expire within hours**. You likely will have to rerun `fetchpdf cookies setup` periodically to restore access.
1067
+ **Cookies often expire within hours**. You likely will have to rerun `fetchpdf fetchcookies` periodically to restore access.
1062
1068
 
1063
1069
  ### Living with it
1064
1070
 
@@ -1092,7 +1098,7 @@ run. This works the same for a single DOI and for a batch:
1092
1098
  ```
1093
1099
 
1094
1100
  Runs that cannot be asked (pipes, cron, `--json`) print the command instead
1095
- (`fetchpdf cookies setup --publishers wiley,tandf`, or `refresh --publishers ...`).
1101
+ (`fetchpdf fetchcookies --publishers wiley,tandf`, or `refresh --publishers ...`).
1096
1102
  `--no-cookies` and `--cookies FILE` turn the offer off. Royal Society papers are
1097
1103
  not offered, because there is no test article to sign in on.
1098
1104
 
@@ -17,7 +17,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
17
17
  - [Supplementary Material](#supplementary-material)
18
18
  - [`--pull-everything`: the second layer](#--pull-everything-the-second-layer)
19
19
  - [Figure Images](#figure-images)
20
- - [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions) — papers your library subscribes to (`fetchpdf cookies setup`)
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`
22
22
  - [API Reference](#api-reference)
23
23
  - [Download Sources](#download-sources-in-order-of-priority)
@@ -46,7 +46,7 @@ A comprehensive Python package to download academic papers (PDFs) from DOIs (or
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
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
49
- - 🔐 **[Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions)** (opt-in): a one-time `fetchpdf cookies setup` lets fetchpdf download papers your library subscribes to
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
52
52
 
@@ -145,7 +145,7 @@ fetchpdf '10.1002/(SICI)1521-3773(19981116)37:21<2922::AID-ANIE2922>3.0.CO;2-1,
145
145
  # ...but single-quote any list holding ( ) < > ; -- the shell would read them as syntax
146
146
  ```
147
147
 
148
- **At a university?** Run `fetchpdf cookies setup` once and fetchpdf will also fetch
148
+ **At a university?** Run `fetchpdf fetchcookies` once and fetchpdf will also fetch
149
149
  papers your library subscribes to. See
150
150
  [Institutional access](#institutional-access-via-cookies-ie-using-your-librarys-subscriptions).
151
151
 
@@ -966,11 +966,17 @@ fetchpdf-render ./out --html --pdf --fetch-figures # XML you already have
966
966
 
967
967
  **A rendering, not the publisher's PDF**, and never written as `{stem}.pdf`:
968
968
  that name means the typeset article to the skip check and the validators. The
969
- page opens with a banner naming the source file, the DOI, and how many figures
970
- were embedded. A figure whose image could not be had keeps a visible
969
+ page opens with a banner naming the source file, the fetchpdf version and the
970
+ DOI, plus a note for anything that could not be shown: figures without an image,
971
+ tables published only as an image that is missing, or no reference list. A figure whose image could not be had keeps a visible
971
972
  placeholder, so an incomplete rendering looks incomplete. The prose and
972
- tables come from the same walker as `--to-markdown`, so the two agree;
973
- references are omitted and formulas stay as text.
973
+ tables come from the same walker as `--to-markdown`, so the two agree, except
974
+ that the page ends with the reference list (with DOI links), which the Markdown
975
+ leaves out. Formulas stay as text. The XML never says how large a figure was
976
+ typeset, so each is given about the same area (a square figure ~10.5 cm across),
977
+ shaped by its aspect ratio and capped at the column width. Under the title come
978
+ the authors, their affiliations and the journal citation (volume, issue, pages).
979
+ Tables always fit the page width in the PDF; very wide ones print smaller.
974
980
 
975
981
  Only records whose XML was kept are rendered — add `--get-xml-or-html` to keep
976
982
  the XML beside the PDF.
@@ -987,7 +993,7 @@ library, for the papers that have no open-access copy.
987
993
  Browser cookie support is included in the standard fetchpdf installation.
988
994
 
989
995
  ```bash
990
- fetchpdf cookies setup
996
+ fetchpdf fetchcookies
991
997
  ```
992
998
 
993
999
  The wizard asks two questions, then opens some tabs:
@@ -1019,7 +1025,7 @@ session exists but the test article was refused: the session lapsed, or your
1019
1025
  library does not take that particular journal. Access is turned on only if at
1020
1026
  least one publisher works.
1021
1027
 
1022
- **Cookies often expire within hours**. You likely will have to rerun `fetchpdf cookies setup` periodically to restore access.
1028
+ **Cookies often expire within hours**. You likely will have to rerun `fetchpdf fetchcookies` periodically to restore access.
1023
1029
 
1024
1030
  ### Living with it
1025
1031
 
@@ -1053,7 +1059,7 @@ run. This works the same for a single DOI and for a batch:
1053
1059
  ```
1054
1060
 
1055
1061
  Runs that cannot be asked (pipes, cron, `--json`) print the command instead
1056
- (`fetchpdf cookies setup --publishers wiley,tandf`, or `refresh --publishers ...`).
1062
+ (`fetchpdf fetchcookies --publishers wiley,tandf`, or `refresh --publishers ...`).
1057
1063
  `--no-cookies` and `--cookies FILE` turn the offer off. Royal Society papers are
1058
1064
  not offered, because there is no test article to sign in on.
1059
1065
 
@@ -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.3"
18
+ __version__ = "0.1.5"
19
19
  __all__ = [
20
20
  "fetch_pdf",
21
21
  "fetch_pdf_from_doi", # deprecated alias for fetch_pdf
@@ -2721,10 +2721,6 @@ def try_pmid_direct_pdf_fallback(pmid: str, save_path: str, verbose=False, allow
2721
2721
  if verbose:
2722
2722
  print(f"✅ PMID native PMCID route success for PMID {pmid}")
2723
2723
  return True
2724
- if allow_xml:
2725
- xml_got = try_pmc_xml_fallback(pmcid, save_path, verbose=verbose, doi=f"pmid:{pmid}")
2726
- if xml_got:
2727
- return xml_got
2728
2724
 
2729
2725
  # Europe PMC record full-text URLs.
2730
2726
  try:
@@ -2797,6 +2793,12 @@ def try_pmid_direct_pdf_fallback(pmid: str, save_path: str, verbose=False, allow
2797
2793
  if verbose:
2798
2794
  print(f" PMID native PubMed provider lookup failed for {pmid}: {str(e)[:100]}")
2799
2795
 
2796
+ # XML only after every PDF route above has failed.
2797
+ if allow_xml and pmcid:
2798
+ xml_got = try_pmc_xml_fallback(pmcid, save_path, verbose=verbose, doi=f"pmid:{pmid}")
2799
+ if xml_got:
2800
+ return xml_got
2801
+
2800
2802
  return False
2801
2803
 
2802
2804
 
@@ -4024,7 +4026,7 @@ def _try_institutional(save_path, resolved, cookies_file=None, verbose=False,
4024
4026
  _source_out=None):
4025
4027
  """The institutional-access route. Returns the saved path, or None.
4026
4028
 
4027
- Off unless the user opted in -- `fetchpdf cookies setup`, or --cookies FILE --
4029
+ Off unless the user opted in -- `fetchpdf fetchcookies`, or --cookies FILE --
4028
4030
  and silent for a publisher the route does not cover, so a run over APA or
4029
4031
  Elsevier DOIs carries no extra noise.
4030
4032
  """
@@ -4372,6 +4374,12 @@ def _fetch_pdf_chain(doi,
4372
4374
  # Defer Elsevier XML fallback until all PDF-oriented fallbacks have failed.
4373
4375
  elsevier_crossref_message = None
4374
4376
  _pmid_cache = [None] # Cached from PMC idconv when available; used for DOI->PMID fallback
4377
+ # XML copies found mid-chain (PMC, eLife, Crossref text-mining links) are
4378
+ # queued here, not returned: an open PDF later in the chain beats them.
4379
+ # PLOS ONE 10.1371/journal.pone.0000308 came back as XML because Europe
4380
+ # PMC's ?pdf=render 403'd and the PMC step took the JATS, while PLOS
4381
+ # itself serves the PDF. Each entry is (source, callable -> path or None).
4382
+ deferred_xml = []
4375
4383
 
4376
4384
  # Skip if PDF or XML already exists
4377
4385
  xml_path = os.path.splitext(save_path)[0] + ".xml"
@@ -5055,12 +5063,9 @@ def _fetch_pdf_chain(doi,
5055
5063
  # pdf=render 404s for OA records Europe PMC holds only as
5056
5064
  # JATS (eLife 34573, PLOS Currents). The XML is the paper.
5057
5065
  if allow_xml_fallback:
5058
- xml_got = try_pmc_xml_fallback(
5066
+ deferred_xml.append(("pmc", lambda pmcid=pmcid: try_pmc_xml_fallback(
5059
5067
  pmcid, save_path, verbose=verbose, doi=doi
5060
- )
5061
- if xml_got:
5062
- _record_source(_source_out, "pmc")
5063
- return xml_got
5068
+ )))
5064
5069
  # Europe PMC serves fullTextXML only for records inside
5065
5070
  # its OA subset. NIH author manuscripts are not in it --
5066
5071
  # free to read on the PMC website, not redistributable --
@@ -5075,26 +5080,17 @@ def _fetch_pdf_chain(doi,
5075
5080
  # A non-OA record's efetch reply is a well-formed
5076
5081
  # <article> with no <body> and the refusal in an XML
5077
5082
  # comment; _looks_like_jats_fulltext already rejects it.
5078
- xml_got = try_pmc_efetch_xml_fallback(
5083
+ deferred_xml.append(("pmc_efetch", lambda pmcid=pmcid: try_pmc_efetch_xml_fallback(
5079
5084
  pmcid, save_path, verbose=verbose, doi=doi
5080
- )
5081
- if xml_got:
5082
- _record_source(_source_out, "pmc_efetch")
5083
- return xml_got
5085
+ )))
5084
5086
  except Exception as e:
5085
5087
  if verbose: print(f"Error with PMC: {e}")
5086
5088
  pass
5087
5089
 
5088
5090
  # ---------------- eLife CDN / API XML (HTML site 406s) ----------------
5089
5091
  if allow_xml_fallback and _elife_article_id(doi):
5090
- try:
5091
- xml_got = try_elife_xml_fallback(doi, save_path, verbose=verbose)
5092
- if xml_got:
5093
- _record_source(_source_out, "elife")
5094
- return xml_got
5095
- except Exception as e:
5096
- if verbose:
5097
- print(f"Error with eLife XML: {e}")
5092
+ deferred_xml.append(("elife", lambda: try_elife_xml_fallback(
5093
+ doi, save_path, verbose=verbose)))
5098
5094
 
5099
5095
  # ---------------- eScholarship via PubMed LinkOut ----------------
5100
5096
  # UC and other universities deposit in eScholarship; PubMed abstract lists these
@@ -5131,7 +5127,7 @@ def _fetch_pdf_chain(doi,
5131
5127
  # tail (Crossref landing pages, CORE, DOAJ, DataCite, PMID, Elsevier) that
5132
5128
  # rarely finds what a subscription serves in a second or two. Grey sources
5133
5129
  # (fetchpdf_grey) still run after the whole walk. A no-op unless the user
5134
- # ran `fetchpdf cookies setup` or passed --cookies, and for publishers it does
5130
+ # ran `fetchpdf fetchcookies` or passed --cookies, and for publishers it does
5135
5131
  # not cover. Once per record: not on related-DOI re-entries.
5136
5132
  if _top_level and not (xml_only or xml_html_only):
5137
5133
  institutional_path = _try_institutional(
@@ -5165,12 +5161,9 @@ def _fetch_pdf_chain(doi,
5165
5161
  # is the CDN JATS file the HTML site 406s would have pointed at.
5166
5162
  if allow_xml_fallback:
5167
5163
  for xml_url in _crossref_tdm_xml_urls(m):
5168
- xml_got = _try_save_jats_xml(
5164
+ deferred_xml.append(("crossref", lambda xml_url=xml_url: _try_save_jats_xml(
5169
5165
  xml_url, save_path, verbose=verbose, doi=doi
5170
- )
5171
- if xml_got:
5172
- _record_source(_source_out, "crossref")
5173
- return xml_got
5166
+ )))
5174
5167
  # Landing page fallback
5175
5168
  landing = m.get("URL")
5176
5169
  if try_download(landing, save_path, verbose):
@@ -5199,13 +5192,14 @@ def _fetch_pdf_chain(doi,
5199
5192
  elsevier_crossref_message = m
5200
5193
  # A published paper's PsyArXiv/bioRxiv copy is a Crossref relation,
5201
5194
  # not Unpaywall's "best" location. Re-enter the chain on that DOI;
5202
- # _visited stops a cycle.
5195
+ # _visited stops a cycle. PDF only: the related chain returning its
5196
+ # XML here would beat every PDF source still ahead of this one.
5203
5197
  for related in _crossref_related_dois(m, doi):
5204
5198
  if verbose:
5205
5199
  print(f" Trying Crossref related DOI: {related}")
5206
5200
  related_path = fetch_pdf(
5207
5201
  related, save_path, email, verbose, delay=0,
5208
- allow_xml_fallback=allow_xml_fallback,
5202
+ allow_xml_fallback=False,
5209
5203
  use_playwright=use_playwright,
5210
5204
  _source_out=_source_out, _visited=_visited,
5211
5205
  )
@@ -5496,12 +5490,19 @@ def _fetch_pdf_chain(doi,
5496
5490
  if pmid:
5497
5491
  pmid_str = str(pmid).strip()
5498
5492
  pmid_got = try_pmid_direct_pdf_fallback(
5499
- pmid_str, save_path, verbose, allow_xml=allow_xml_fallback
5493
+ pmid_str, save_path, verbose, allow_xml=False
5500
5494
  )
5501
5495
  if pmid_got:
5502
5496
  _record_source(_source_out, "pmid_direct")
5503
5497
  return pmid_got if isinstance(pmid_got, str) else save_path
5504
-
5498
+ # The PMID route's own PMC XML, unless the PMC step above already
5499
+ # queued XML for this record. Re-running the route repeats its PDF
5500
+ # attempts before the XML, but only once every PDF source has failed.
5501
+ if allow_xml_fallback and not any(
5502
+ source in ("pmc", "pmc_efetch") for source, _ in deferred_xml
5503
+ ):
5504
+ deferred_xml.append(("pmid_direct", lambda: try_pmid_direct_pdf_fallback(
5505
+ pmid_str, save_path, verbose, allow_xml=True)))
5505
5506
 
5506
5507
  # ---------------- Google Scholar via SerpApi ---------------------------
5507
5508
  # Preserve search credits until the ordinary PDF sources are exhausted.
@@ -5530,6 +5531,20 @@ def _fetch_pdf_chain(doi,
5530
5531
  if verbose:
5531
5532
  print(f"Error with deferred Elsevier fallback: {e}")
5532
5533
 
5534
+ # ---------------- Deferred XML (every PDF source has failed) -----------
5535
+ # Last of all, after SerpApi too: XML is the last-ditch format, so a PDF
5536
+ # from any source -- even one that spends a search credit -- comes first.
5537
+ for xml_source, attempt in deferred_xml:
5538
+ try:
5539
+ xml_got = attempt()
5540
+ except Exception as e:
5541
+ if verbose:
5542
+ print(f"Error with deferred {xml_source} XML: {e}")
5543
+ continue
5544
+ if xml_got:
5545
+ _record_source(_source_out, xml_source)
5546
+ return xml_got if isinstance(xml_got, str) else save_path
5547
+
5533
5548
 
5534
5549
  def _append_missing_si_to_report(output_dir, si_statuses, run_timestamp, lock,
5535
5550
  drafts_by_doi=None, figure_statuses=None):
@@ -5809,6 +5824,14 @@ def _prune_empty_record_dir(record_dir):
5809
5824
  """
5810
5825
  if not record_dir:
5811
5826
  return
5827
+ # The resolution cache is written beside the artifact, so a record that got
5828
+ # nothing still holds one; alone, it is not something the record wrote.
5829
+ from .retrieval.cache import CACHE_FILENAME
5830
+ try:
5831
+ if os.listdir(record_dir) == [CACHE_FILENAME]:
5832
+ os.unlink(os.path.join(record_dir, CACHE_FILENAME))
5833
+ except OSError:
5834
+ pass
5812
5835
  try:
5813
5836
  os.rmdir(record_dir)
5814
5837
  except OSError:
@@ -6423,7 +6446,11 @@ def _batch_fetch_pdfs_inner(dois, output_dir, email=None, verbose=False, delay=0
6423
6446
  # path, or a record with four supplementary PDFs would report "pdf 5" and
6424
6447
  # corrupt the format composition tally.
6425
6448
  _pull_si(display_id, raw_identifier, canonical_doi, save_path)
6426
- _pull_figures(display_id, raw_identifier, canonical_doi, save_path)
6449
+ # Unlike the supplements, figures belong to an article we have: with no
6450
+ # artifact there is nothing for them to illustrate, and the pass would
6451
+ # leave an empty `_figures/` and manifest in a record that failed.
6452
+ if success:
6453
+ _pull_figures(display_id, raw_identifier, canonical_doi, save_path)
6427
6454
  _render_xml(display_id, save_path)
6428
6455
  _pull_images(result or save_path, display_id)
6429
6456
 
@@ -7000,7 +7027,7 @@ def _ask_yes(question: str) -> bool:
7000
7027
 
7001
7028
 
7002
7029
  def _import_cookies(cfg, publishers) -> bool:
7003
- """Run `fetchpdf cookies setup` (no config yet) or `refresh` in-process,
7030
+ """Run `fetchpdf fetchcookies` (no config yet) or `refresh` in-process,
7004
7031
  for *publishers* only -- the ones the failed papers are at.
7005
7032
 
7006
7033
  True when the retry is worth making: setup succeeded, or refresh found a
@@ -7064,7 +7091,7 @@ def _offer_cookie_import(failed, retry):
7064
7091
  if cfg is None:
7065
7092
  print(f"🔐 {n} failed {noun} ({where}) may be available through your "
7066
7093
  f"library's subscriptions, by importing your browser's cookies.")
7067
- command = f"fetchpdf cookies setup --publishers {','.join(publishers)}"
7094
+ command = f"fetchpdf fetchcookies --publishers {','.join(publishers)}"
7068
7095
  else:
7069
7096
  print(f"🔐 {n} failed {noun} ({where}) are at publishers your library access "
7070
7097
  f"covers. If your library sign-in there has lapsed, signing in again may get them.")
@@ -7319,6 +7346,9 @@ def main(argv=None):
7319
7346
  if argv and argv[0] == "cookies":
7320
7347
  from .get_cookies import main as cookies_main
7321
7348
  return cookies_main(argv[1:])
7349
+ if argv and argv[0] == "fetchcookies":
7350
+ from .get_cookies import main as cookies_main
7351
+ return cookies_main(["setup"] + argv[1:])
7322
7352
 
7323
7353
  pre = argparse.ArgumentParser(add_help=False)
7324
7354
  pre.add_argument("--json", action="store_true")
@@ -7410,7 +7440,7 @@ def _main(argv=None, json_out=None):
7410
7440
  description="Download PDFs from DOIs using multiple fallback sources.",
7411
7441
  epilog=(
7412
7442
  "Institutional access:\n"
7413
- " fetchpdf cookies setup # sign in through your library\n"
7443
+ " fetchpdf fetchcookies # sign in through your library\n"
7414
7444
  " fetchpdf cookies --help # all cookie commands\n\n"
7415
7445
  "Examples:\n"
7416
7446
  " fetchpdf papers.csv -o ./pdfs # batch from CSV\n"
@@ -7824,7 +7854,7 @@ def _main(argv=None, json_out=None):
7824
7854
  metavar="FILE",
7825
7855
  default=None,
7826
7856
  help="Institutional access from this cookie file instead of the one "
7827
- "`fetchpdf cookies setup` configured. Without it, access is used only "
7857
+ "`fetchpdf fetchcookies` configured. Without it, access is used only "
7828
7858
  "if setup has been run. Covers publishers whose PDF URL follows "
7829
7859
  "from the DOI (Wiley, T&F, SAGE, Springer, Royal Society, Hogrefe, "
7830
7860
  "INFORMS); tried after the fast open-access lookups. Only cookies "
@@ -7890,7 +7920,7 @@ def _main(argv=None, json_out=None):
7890
7920
  if args.cookies_only:
7891
7921
  if args.no_cookies or not _institutional.active_cookies_file():
7892
7922
  print("❌ --cookies-only: institutional access is not set up. Run "
7893
- "`fetchpdf cookies setup` (or pass --cookies FILE).")
7923
+ "`fetchpdf fetchcookies` (or pass --cookies FILE).")
7894
7924
  return 2
7895
7925
  _institutional.set_cookies_only(True)
7896
7926
  # "Alone" includes the grey last resorts an installed fetchpdf_grey adds.
@@ -8295,7 +8325,9 @@ def _main(argv=None, json_out=None):
8295
8325
  except Exception as e:
8296
8326
  print(f"\n📎 supplementary pass failed: {str(e)[:200]}")
8297
8327
 
8298
- if args.pull_figures:
8328
+ # Only for a record that was retrieved: figures with no article to go
8329
+ # with them are an empty `_figures/` and manifest in a failed record.
8330
+ if args.pull_figures and result:
8299
8331
  # A separate call above fetch_pdf, for the same reason the
8300
8332
  # supplementary pass is one: the tiered engine re-enters the chain
8301
8333
  # at T5 with a temporary save_path, and a flag threaded through
@@ -8337,6 +8369,8 @@ def _main(argv=None, json_out=None):
8337
8369
  print(f"\n🖨️ render: {outcome['error']}")
8338
8370
  except Exception as e:
8339
8371
  print(f"\n🖨️ render failed: {str(e)[:200]}")
8372
+ elif not result:
8373
+ print("\n🖨️ no XML could be retrieved for this record, so nothing to render")
8340
8374
  else:
8341
8375
  print("\n🖨️ no .xml was kept for this record, so nothing to render "
8342
8376
  "(add --get-xml-or-html)")
@@ -8391,7 +8425,8 @@ def _main(argv=None, json_out=None):
8391
8425
  return 0
8392
8426
  else:
8393
8427
  _prune_empty_record_dir(made_record_dir)
8394
- print(f"\n❌ Failed to download PDF for {args.doi}")
8428
+ wanted = "XML" if args.xml_only else ("XML or HTML" if args.xml_html_only else "PDF")
8429
+ print(f"\n❌ Failed to download {wanted} for {args.doi}")
8395
8430
  _emit_json(json_out, args.doi, resolved_identifier, None,
8396
8431
  JSON_STATUS_FAILED, identity=identity,
8397
8432
  reasons=reasons or ["no source produced a usable artifact"],
@@ -1,12 +1,12 @@
1
1
  """fetchpdf cookies: set up institutional access for fetchpdf, once.
2
2
 
3
- fetchpdf cookies setup # one-time wizard
3
+ fetchpdf fetchcookies # one-time wizard
4
4
  fetchpdf cookies check # which publishers your sessions currently open
5
5
  fetchpdf cookies refresh # re-open sign-in tabs for the ones that lapsed
6
6
  fetchpdf cookies disable # stop using institutional access
7
7
 
8
8
  Institutional access lets fetchpdf download a paper your library subscribes to
9
- when no open-access copy exists. It is OFF until you run ``fetchpdf cookies setup``.
9
+ when no open-access copy exists. It is OFF until you run ``fetchpdf fetchcookies``.
10
10
 
11
11
  How it works. Each publisher keeps your library sign-in as a cookie on its own
12
12
  site (wiley.com, tandfonline.com, ...). The wizard opens one subscription
@@ -378,7 +378,7 @@ def cmd_setup(args) -> int:
378
378
  import browser_cookie3 # noqa: F401
379
379
  except ImportError:
380
380
  print(" A required dependency is missing: browser_cookie3.\n"
381
- " Run pip install 'browser_cookie3>=0.19', then run fetchpdf cookies setup again.")
381
+ " Run pip install 'browser_cookie3>=0.19', then run fetchpdf fetchcookies again.")
382
382
  return 2
383
383
 
384
384
  # 1. Library
@@ -443,7 +443,7 @@ def cmd_setup(args) -> int:
443
443
 
444
444
  if not any(st == "ok" for st in status.values()):
445
445
  print(" No publisher served a PDF, so institutional access is NOT turned on.\n"
446
- " Check you can open a PDF in your browser, then run fetchpdf cookies setup again.")
446
+ " Check you can open a PDF in your browser, then run fetchpdf fetchcookies again.")
447
447
  return 1
448
448
 
449
449
  cfg["publishers"] = status
@@ -463,7 +463,7 @@ def cmd_setup(args) -> int:
463
463
  def _require_config() -> Optional[dict]:
464
464
  cfg = inst.load_access_config()
465
465
  if not cfg:
466
- print("Institutional access is not set up. Run: fetchpdf cookies setup")
466
+ print("Institutional access is not set up. Run: fetchpdf fetchcookies")
467
467
  return cfg
468
468
 
469
469
 
@@ -516,7 +516,7 @@ def cmd_disable(args) -> int:
516
516
  return 0
517
517
  cfg["enabled"] = False
518
518
  _write_private_json(ACCESS_CONFIG, cfg)
519
- print(f"Institutional access is OFF. Turn it back on with: fetchpdf cookies setup")
519
+ print(f"Institutional access is OFF. Turn it back on with: fetchpdf fetchcookies")
520
520
  return 0
521
521
 
522
522