fetchpdf 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fetchpdf-0.1.3/fetchpdf.egg-info → fetchpdf-0.1.4}/PKG-INFO +17 -11
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/README.md +16 -10
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/__init__.py +1 -1
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/fetchpdf.py +30 -10
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/get_cookies.py +6 -6
- fetchpdf-0.1.4/fetchpdf/retrieval/byline.py +257 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/figures.py +6 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/institutional.py +3 -3
- fetchpdf-0.1.4/fetchpdf/retrieval/references.py +280 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/to_html.py +163 -19
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/to_markdown.py +10 -1
- {fetchpdf-0.1.3 → fetchpdf-0.1.4/fetchpdf.egg-info}/PKG-INFO +17 -11
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf.egg-info/SOURCES.txt +2 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_cookie_offer.py +1 -1
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_get_cookies.py +1 -1
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_institutional.py +1 -1
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_supplementary.py +24 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_to_html.py +140 -2
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/LICENSE +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/MANIFEST.in +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/__main__.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/_env.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/_http.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/fetch_abstract_from_doi.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/fetch_metadata_from_doi.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/__init__.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/_util.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/artifact.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/backends/__init__.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/backends/openrouter.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/blocked.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/cache.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/chunk.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/classify.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/context.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/corresponding.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/engine.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/extract_images.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/fulltext_scan.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/goals.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/http.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/identifiers.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/ladder.json +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/linked_artifacts.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/llm_adjudicate.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/normalize.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/pdf_identity.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/pdf_text.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/provenance.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/pubpeer.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/ratelimit.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/repository_waf.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/request_drafts.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/resolve.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/__init__.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/arxiv.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/core_text.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/epmc.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/landing.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/legacy_pdf.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/pmc.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/preprint.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/sources/supplements.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_atypon.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_dspace.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_graph.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_index.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_jci.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_pmc.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplement_publishers.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/supplementary.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/tiers.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/validate.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf/retrieval/verify.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf.egg-info/dependency_links.txt +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf.egg-info/entry_points.txt +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf.egg-info/requires.txt +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/fetchpdf.egg-info/top_level.txt +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/pyproject.toml +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/requirements.txt +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/setup.cfg +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_agent_backends.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_blocked.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_corresponding.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_datacite_supplement_identity.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_dspace.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_entrypoints.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_epmc_repo.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_extract_images.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_figures.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_folder_encoding.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_fulltext_scan.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_jci_supplements.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_json_output.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_landing_candidates.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_landing_page_detection.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_last_resort_reachability.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_linked_artifacts.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_llm_adjudicate.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_normalize_cells.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pdf_identity.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pdf_trailer.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pmc_page.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_polite_ua.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pubmed_identity.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pubpeer.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pull_for_record_call_sites.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_pure.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_request_drafts.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_retrieval.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_scan_record.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_screening_conversion.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_serpapi_scholar.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/tests/test_short_conference_xml.py +0 -0
- {fetchpdf-0.1.3 → fetchpdf-0.1.4}/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
|
+
Version: 0.1.4
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1009
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
970
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
18
|
+
__version__ = "0.1.4"
|
|
19
19
|
__all__ = [
|
|
20
20
|
"fetch_pdf",
|
|
21
21
|
"fetch_pdf_from_doi", # deprecated alias for fetch_pdf
|
|
@@ -4024,7 +4024,7 @@ def _try_institutional(save_path, resolved, cookies_file=None, verbose=False,
|
|
|
4024
4024
|
_source_out=None):
|
|
4025
4025
|
"""The institutional-access route. Returns the saved path, or None.
|
|
4026
4026
|
|
|
4027
|
-
Off unless the user opted in -- `fetchpdf
|
|
4027
|
+
Off unless the user opted in -- `fetchpdf fetchcookies`, or --cookies FILE --
|
|
4028
4028
|
and silent for a publisher the route does not cover, so a run over APA or
|
|
4029
4029
|
Elsevier DOIs carries no extra noise.
|
|
4030
4030
|
"""
|
|
@@ -5131,7 +5131,7 @@ def _fetch_pdf_chain(doi,
|
|
|
5131
5131
|
# tail (Crossref landing pages, CORE, DOAJ, DataCite, PMID, Elsevier) that
|
|
5132
5132
|
# rarely finds what a subscription serves in a second or two. Grey sources
|
|
5133
5133
|
# (fetchpdf_grey) still run after the whole walk. A no-op unless the user
|
|
5134
|
-
# ran `fetchpdf
|
|
5134
|
+
# ran `fetchpdf fetchcookies` or passed --cookies, and for publishers it does
|
|
5135
5135
|
# not cover. Once per record: not on related-DOI re-entries.
|
|
5136
5136
|
if _top_level and not (xml_only or xml_html_only):
|
|
5137
5137
|
institutional_path = _try_institutional(
|
|
@@ -5809,6 +5809,14 @@ def _prune_empty_record_dir(record_dir):
|
|
|
5809
5809
|
"""
|
|
5810
5810
|
if not record_dir:
|
|
5811
5811
|
return
|
|
5812
|
+
# The resolution cache is written beside the artifact, so a record that got
|
|
5813
|
+
# nothing still holds one; alone, it is not something the record wrote.
|
|
5814
|
+
from .retrieval.cache import CACHE_FILENAME
|
|
5815
|
+
try:
|
|
5816
|
+
if os.listdir(record_dir) == [CACHE_FILENAME]:
|
|
5817
|
+
os.unlink(os.path.join(record_dir, CACHE_FILENAME))
|
|
5818
|
+
except OSError:
|
|
5819
|
+
pass
|
|
5812
5820
|
try:
|
|
5813
5821
|
os.rmdir(record_dir)
|
|
5814
5822
|
except OSError:
|
|
@@ -6423,7 +6431,11 @@ def _batch_fetch_pdfs_inner(dois, output_dir, email=None, verbose=False, delay=0
|
|
|
6423
6431
|
# path, or a record with four supplementary PDFs would report "pdf 5" and
|
|
6424
6432
|
# corrupt the format composition tally.
|
|
6425
6433
|
_pull_si(display_id, raw_identifier, canonical_doi, save_path)
|
|
6426
|
-
|
|
6434
|
+
# Unlike the supplements, figures belong to an article we have: with no
|
|
6435
|
+
# artifact there is nothing for them to illustrate, and the pass would
|
|
6436
|
+
# leave an empty `_figures/` and manifest in a record that failed.
|
|
6437
|
+
if success:
|
|
6438
|
+
_pull_figures(display_id, raw_identifier, canonical_doi, save_path)
|
|
6427
6439
|
_render_xml(display_id, save_path)
|
|
6428
6440
|
_pull_images(result or save_path, display_id)
|
|
6429
6441
|
|
|
@@ -7000,7 +7012,7 @@ def _ask_yes(question: str) -> bool:
|
|
|
7000
7012
|
|
|
7001
7013
|
|
|
7002
7014
|
def _import_cookies(cfg, publishers) -> bool:
|
|
7003
|
-
"""Run `fetchpdf
|
|
7015
|
+
"""Run `fetchpdf fetchcookies` (no config yet) or `refresh` in-process,
|
|
7004
7016
|
for *publishers* only -- the ones the failed papers are at.
|
|
7005
7017
|
|
|
7006
7018
|
True when the retry is worth making: setup succeeded, or refresh found a
|
|
@@ -7064,7 +7076,7 @@ def _offer_cookie_import(failed, retry):
|
|
|
7064
7076
|
if cfg is None:
|
|
7065
7077
|
print(f"🔐 {n} failed {noun} ({where}) may be available through your "
|
|
7066
7078
|
f"library's subscriptions, by importing your browser's cookies.")
|
|
7067
|
-
command = f"fetchpdf
|
|
7079
|
+
command = f"fetchpdf fetchcookies --publishers {','.join(publishers)}"
|
|
7068
7080
|
else:
|
|
7069
7081
|
print(f"🔐 {n} failed {noun} ({where}) are at publishers your library access "
|
|
7070
7082
|
f"covers. If your library sign-in there has lapsed, signing in again may get them.")
|
|
@@ -7319,6 +7331,9 @@ def main(argv=None):
|
|
|
7319
7331
|
if argv and argv[0] == "cookies":
|
|
7320
7332
|
from .get_cookies import main as cookies_main
|
|
7321
7333
|
return cookies_main(argv[1:])
|
|
7334
|
+
if argv and argv[0] == "fetchcookies":
|
|
7335
|
+
from .get_cookies import main as cookies_main
|
|
7336
|
+
return cookies_main(["setup"] + argv[1:])
|
|
7322
7337
|
|
|
7323
7338
|
pre = argparse.ArgumentParser(add_help=False)
|
|
7324
7339
|
pre.add_argument("--json", action="store_true")
|
|
@@ -7410,7 +7425,7 @@ def _main(argv=None, json_out=None):
|
|
|
7410
7425
|
description="Download PDFs from DOIs using multiple fallback sources.",
|
|
7411
7426
|
epilog=(
|
|
7412
7427
|
"Institutional access:\n"
|
|
7413
|
-
" fetchpdf
|
|
7428
|
+
" fetchpdf fetchcookies # sign in through your library\n"
|
|
7414
7429
|
" fetchpdf cookies --help # all cookie commands\n\n"
|
|
7415
7430
|
"Examples:\n"
|
|
7416
7431
|
" fetchpdf papers.csv -o ./pdfs # batch from CSV\n"
|
|
@@ -7824,7 +7839,7 @@ def _main(argv=None, json_out=None):
|
|
|
7824
7839
|
metavar="FILE",
|
|
7825
7840
|
default=None,
|
|
7826
7841
|
help="Institutional access from this cookie file instead of the one "
|
|
7827
|
-
"`fetchpdf
|
|
7842
|
+
"`fetchpdf fetchcookies` configured. Without it, access is used only "
|
|
7828
7843
|
"if setup has been run. Covers publishers whose PDF URL follows "
|
|
7829
7844
|
"from the DOI (Wiley, T&F, SAGE, Springer, Royal Society, Hogrefe, "
|
|
7830
7845
|
"INFORMS); tried after the fast open-access lookups. Only cookies "
|
|
@@ -7890,7 +7905,7 @@ def _main(argv=None, json_out=None):
|
|
|
7890
7905
|
if args.cookies_only:
|
|
7891
7906
|
if args.no_cookies or not _institutional.active_cookies_file():
|
|
7892
7907
|
print("❌ --cookies-only: institutional access is not set up. Run "
|
|
7893
|
-
"`fetchpdf
|
|
7908
|
+
"`fetchpdf fetchcookies` (or pass --cookies FILE).")
|
|
7894
7909
|
return 2
|
|
7895
7910
|
_institutional.set_cookies_only(True)
|
|
7896
7911
|
# "Alone" includes the grey last resorts an installed fetchpdf_grey adds.
|
|
@@ -8295,7 +8310,9 @@ def _main(argv=None, json_out=None):
|
|
|
8295
8310
|
except Exception as e:
|
|
8296
8311
|
print(f"\n📎 supplementary pass failed: {str(e)[:200]}")
|
|
8297
8312
|
|
|
8298
|
-
|
|
8313
|
+
# Only for a record that was retrieved: figures with no article to go
|
|
8314
|
+
# with them are an empty `_figures/` and manifest in a failed record.
|
|
8315
|
+
if args.pull_figures and result:
|
|
8299
8316
|
# A separate call above fetch_pdf, for the same reason the
|
|
8300
8317
|
# supplementary pass is one: the tiered engine re-enters the chain
|
|
8301
8318
|
# at T5 with a temporary save_path, and a flag threaded through
|
|
@@ -8337,6 +8354,8 @@ def _main(argv=None, json_out=None):
|
|
|
8337
8354
|
print(f"\n🖨️ render: {outcome['error']}")
|
|
8338
8355
|
except Exception as e:
|
|
8339
8356
|
print(f"\n🖨️ render failed: {str(e)[:200]}")
|
|
8357
|
+
elif not result:
|
|
8358
|
+
print("\n🖨️ no XML could be retrieved for this record, so nothing to render")
|
|
8340
8359
|
else:
|
|
8341
8360
|
print("\n🖨️ no .xml was kept for this record, so nothing to render "
|
|
8342
8361
|
"(add --get-xml-or-html)")
|
|
@@ -8391,7 +8410,8 @@ def _main(argv=None, json_out=None):
|
|
|
8391
8410
|
return 0
|
|
8392
8411
|
else:
|
|
8393
8412
|
_prune_empty_record_dir(made_record_dir)
|
|
8394
|
-
|
|
8413
|
+
wanted = "XML" if args.xml_only else ("XML or HTML" if args.xml_html_only else "PDF")
|
|
8414
|
+
print(f"\n❌ Failed to download {wanted} for {args.doi}")
|
|
8395
8415
|
_emit_json(json_out, args.doi, resolved_identifier, None,
|
|
8396
8416
|
JSON_STATUS_FAILED, identity=identity,
|
|
8397
8417
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
519
|
+
print(f"Institutional access is OFF. Turn it back on with: fetchpdf fetchcookies")
|
|
520
520
|
return 0
|
|
521
521
|
|
|
522
522
|
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Authors, affiliations and the journal citation, for under the rendered title.
|
|
2
|
+
|
|
3
|
+
Like references.py, this is for the page a person reads (to_html.py) and is
|
|
4
|
+
kept out of the Markdown walker, whose readers do not need a byline.
|
|
5
|
+
|
|
6
|
+
JATS carries them in `<article-meta>`: `<contrib-group>` (each author's
|
|
7
|
+
`<xref ref-type="aff">` points at an `<aff>` by id), and the volume, issue and
|
|
8
|
+
pages as its direct children, with the journal title in `<journal-meta>`.
|
|
9
|
+
Elsevier's full-text XML carries `<ce:author-group>` (`<ce:cross-ref refid>`
|
|
10
|
+
to `<ce:affiliation>`) in the article head and the citation in `prism:`
|
|
11
|
+
elements of its `coredata`; when the head is absent, `dc:creator` still names
|
|
12
|
+
the authors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import html as html_lib
|
|
16
|
+
import xml.etree.ElementTree as ET
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import List, Optional
|
|
19
|
+
|
|
20
|
+
from ._util import localname
|
|
21
|
+
from .references import _compose
|
|
22
|
+
from .to_markdown import _clean, _first_text
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Byline:
|
|
27
|
+
authors: List[tuple] = field(default_factory=list) # (name, [markers])
|
|
28
|
+
affiliations: List[tuple] = field(default_factory=list) # (label, text)
|
|
29
|
+
journal: str = "" # HTML
|
|
30
|
+
|
|
31
|
+
def html(self) -> str:
|
|
32
|
+
if not (self.authors or self.affiliations or self.journal):
|
|
33
|
+
return ""
|
|
34
|
+
out = ['<div class="byline">']
|
|
35
|
+
if self.authors:
|
|
36
|
+
names = [_esc(name) + (f"<sup>{_esc(','.join(marks))}</sup>" if marks else "")
|
|
37
|
+
for name, marks in self.authors]
|
|
38
|
+
out.append(f'<p class="authors">{", ".join(names)}</p>')
|
|
39
|
+
if self.affiliations:
|
|
40
|
+
out.append('<ul class="affiliations">' + "".join(
|
|
41
|
+
"<li>" + (f"<sup>{_esc(label)}</sup> " if label else "") + f"{_esc(text)}</li>"
|
|
42
|
+
for label, text in self.affiliations) + "</ul>")
|
|
43
|
+
if self.journal:
|
|
44
|
+
out.append(f'<p class="journal">{self.journal}</p>')
|
|
45
|
+
out.append("</div>")
|
|
46
|
+
return "\n".join(out) + "\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def byline_from_xml(content: bytes) -> Byline:
|
|
50
|
+
try:
|
|
51
|
+
root = ET.fromstring(content)
|
|
52
|
+
except ET.ParseError:
|
|
53
|
+
return Byline()
|
|
54
|
+
article = next((e for e in root.iter() if localname(e.tag) == "article"), None)
|
|
55
|
+
front = _child(article, "front")
|
|
56
|
+
meta = _child(front, "article-meta")
|
|
57
|
+
if meta is not None:
|
|
58
|
+
return _jats(front, meta)
|
|
59
|
+
return _elsevier(root)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# -- JATS ---------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _jats(front, meta) -> Byline:
|
|
66
|
+
byline = Byline()
|
|
67
|
+
affs = [] # (id, label, text)
|
|
68
|
+
for aff in _jats_affs(meta):
|
|
69
|
+
label = _first_text(aff, "label", direct_only=True) or _first_text(aff, "sup", True)
|
|
70
|
+
text = _clean(" ".join(_text_without(aff, ("label", "sup", "institution-id"))))
|
|
71
|
+
if text:
|
|
72
|
+
affs.append((aff.get("id") or "", label, text.strip(" ,;")))
|
|
73
|
+
|
|
74
|
+
authors = [] # (name, [(rid, kind, text)])
|
|
75
|
+
for group in meta:
|
|
76
|
+
if localname(group.tag) != "contrib-group":
|
|
77
|
+
continue
|
|
78
|
+
for contrib in group:
|
|
79
|
+
if localname(contrib.tag) != "contrib":
|
|
80
|
+
continue
|
|
81
|
+
if (contrib.get("contrib-type") or "author").lower() != "author":
|
|
82
|
+
continue
|
|
83
|
+
name = _jats_name(contrib)
|
|
84
|
+
if name:
|
|
85
|
+
refs = [(x.get("rid") or "", (x.get("ref-type") or "").lower(),
|
|
86
|
+
_clean("".join(x.itertext())))
|
|
87
|
+
for x in contrib.iter() if localname(x.tag) == "xref"]
|
|
88
|
+
authors.append((name, refs))
|
|
89
|
+
|
|
90
|
+
# An editor's or reviewer's <aff> sits beside the authors'; when authors
|
|
91
|
+
# point at theirs, only those belong in the byline.
|
|
92
|
+
cited = {rid for _, refs in authors for rid, kind, _ in refs if kind == "aff"}
|
|
93
|
+
if cited & {aff_id for aff_id, _, _ in affs}:
|
|
94
|
+
affs = [a for a in affs if a[0] in cited]
|
|
95
|
+
if len(affs) > 1 and not any(label for _, label, _ in affs):
|
|
96
|
+
affs = [(aff_id, str(n), text) for n, (aff_id, _, text) in enumerate(affs, 1)]
|
|
97
|
+
labels = {aff_id: label for aff_id, label, _ in affs if aff_id}
|
|
98
|
+
byline.affiliations = [(label, text) for _, label, text in affs]
|
|
99
|
+
|
|
100
|
+
for name, refs in authors:
|
|
101
|
+
marks = []
|
|
102
|
+
for rid, kind, text in refs:
|
|
103
|
+
if kind not in ("aff", "corresp", "fn", "author-notes"):
|
|
104
|
+
continue
|
|
105
|
+
mark = labels.get(rid) if kind == "aff" and rid in labels else text
|
|
106
|
+
if mark and mark not in marks:
|
|
107
|
+
marks.append(mark)
|
|
108
|
+
byline.authors.append((name, marks))
|
|
109
|
+
|
|
110
|
+
journal_meta = _child(front, "journal-meta")
|
|
111
|
+
journal = ""
|
|
112
|
+
if journal_meta is not None:
|
|
113
|
+
journal = (_first_text(journal_meta, "journal-title")
|
|
114
|
+
or _first_text(journal_meta, "abbrev-journal-title"))
|
|
115
|
+
fpage, lpage = _direct(meta, "fpage"), _direct(meta, "lpage")
|
|
116
|
+
byline.journal = _citation(journal, _jats_year(meta), _direct(meta, "volume"),
|
|
117
|
+
_direct(meta, "issue"),
|
|
118
|
+
_pages(fpage, lpage) or _direct(meta, "elocation-id"))
|
|
119
|
+
return byline
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _jats_affs(meta):
|
|
123
|
+
"""Each `<aff>` once: under the contrib-groups or the meta, first of any alternatives."""
|
|
124
|
+
found = []
|
|
125
|
+
def visit(element):
|
|
126
|
+
for child in element:
|
|
127
|
+
tag = localname(child.tag)
|
|
128
|
+
if tag == "aff":
|
|
129
|
+
found.append(child)
|
|
130
|
+
elif tag == "aff-alternatives":
|
|
131
|
+
first = _child(child, "aff")
|
|
132
|
+
if first is not None:
|
|
133
|
+
found.append(first)
|
|
134
|
+
elif tag == "contrib-group":
|
|
135
|
+
visit(child)
|
|
136
|
+
visit(meta)
|
|
137
|
+
return found
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _jats_name(contrib) -> str:
|
|
141
|
+
for el in contrib:
|
|
142
|
+
tag = localname(el.tag)
|
|
143
|
+
if tag == "name-alternatives":
|
|
144
|
+
el = next((n for n in el if localname(n.tag) in ("name", "string-name")), None)
|
|
145
|
+
if el is None:
|
|
146
|
+
continue
|
|
147
|
+
tag = localname(el.tag)
|
|
148
|
+
if tag == "name":
|
|
149
|
+
return " ".join(filter(None, (_first_text(el, "prefix"), _first_text(el, "given-names"),
|
|
150
|
+
_first_text(el, "surname"), _first_text(el, "suffix"))))
|
|
151
|
+
if tag in ("string-name", "collab"):
|
|
152
|
+
return _clean(" ".join(el.itertext()))
|
|
153
|
+
return ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _jats_year(meta) -> str:
|
|
157
|
+
dates = [d for d in meta if localname(d.tag) == "pub-date"]
|
|
158
|
+
def kind(d):
|
|
159
|
+
return (d.get("pub-type") or d.get("date-type") or "").lower()
|
|
160
|
+
for wanted in ("ppub", "collection", "pub", "epub"):
|
|
161
|
+
for date in dates:
|
|
162
|
+
if kind(date) == wanted and _first_text(date, "year"):
|
|
163
|
+
return _first_text(date, "year")
|
|
164
|
+
return next((_first_text(d, "year") for d in dates if _first_text(d, "year")), "")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# -- Elsevier -----------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _elsevier(root) -> Byline:
|
|
171
|
+
byline = Byline()
|
|
172
|
+
labels = {}
|
|
173
|
+
groups = [e for e in root.iter() if localname(e.tag) == "author-group"]
|
|
174
|
+
for group in groups:
|
|
175
|
+
for aff in group:
|
|
176
|
+
if localname(aff.tag) != "affiliation":
|
|
177
|
+
continue
|
|
178
|
+
label = _first_text(aff, "label", direct_only=True)
|
|
179
|
+
text = _first_text(aff, "textfn") or _clean(
|
|
180
|
+
" ".join(_text_without(aff, ("label", "affiliation"))))
|
|
181
|
+
if text:
|
|
182
|
+
byline.affiliations.append((label, text))
|
|
183
|
+
if aff.get("id"):
|
|
184
|
+
labels[aff.get("id")] = label
|
|
185
|
+
for group in groups:
|
|
186
|
+
for person in group:
|
|
187
|
+
tag = localname(person.tag)
|
|
188
|
+
if tag == "author":
|
|
189
|
+
name = " ".join(filter(None, (_first_text(person, "given-name"),
|
|
190
|
+
_first_text(person, "surname"))))
|
|
191
|
+
elif tag == "collaboration":
|
|
192
|
+
name = _first_text(person, "text") or _clean(" ".join(person.itertext()))
|
|
193
|
+
else:
|
|
194
|
+
continue
|
|
195
|
+
marks = []
|
|
196
|
+
for ref in person.iter():
|
|
197
|
+
if localname(ref.tag) == "cross-ref":
|
|
198
|
+
# The affiliation's own label wins: some XML prints "1"
|
|
199
|
+
# at the author and "a" at the affiliation it points to.
|
|
200
|
+
mark = labels.get(ref.get("refid") or "") or _clean("".join(ref.itertext()))
|
|
201
|
+
if mark and mark not in marks:
|
|
202
|
+
marks.append(mark)
|
|
203
|
+
if name:
|
|
204
|
+
byline.authors.append((name, marks))
|
|
205
|
+
if not byline.authors:
|
|
206
|
+
for el in root.iter():
|
|
207
|
+
if localname(el.tag) == "creator":
|
|
208
|
+
surname, _, given = _clean("".join(el.itertext())).partition(", ")
|
|
209
|
+
byline.authors.append((" ".join(filter(None, (given, surname))), []))
|
|
210
|
+
|
|
211
|
+
coredata = next((e for e in root.iter() if localname(e.tag) == "coredata"), root)
|
|
212
|
+
first, last = _first_text(coredata, "startingpage"), _first_text(coredata, "endingpage")
|
|
213
|
+
byline.journal = _citation(
|
|
214
|
+
_first_text(coredata, "publicationname"), _first_text(coredata, "coverdate")[:4],
|
|
215
|
+
_first_text(coredata, "volume"), _first_text(coredata, "issueidentifier"),
|
|
216
|
+
_pages(first, last) or _first_text(coredata, "pagerange"))
|
|
217
|
+
return byline
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# -- shared -------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _citation(journal: str, year: str, volume: str, issue: str, pages: str) -> str:
|
|
224
|
+
"""`<i>Journal.</i> 2026;21(11):5256–5260.`, or "" when there is no journal."""
|
|
225
|
+
if not journal:
|
|
226
|
+
return ""
|
|
227
|
+
return _compose(authors=[], etal=False, title="", editors=[], source=journal,
|
|
228
|
+
publisher="", year=year, volume=volume, issue=issue, pages=pages,
|
|
229
|
+
doi="", url="", comment="")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _pages(first: str, last: str) -> str:
|
|
233
|
+
return f"{first}–{last}" if first and last and first != last else first
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _text_without(element, skip) -> List[str]:
|
|
237
|
+
"""Text of `element`, leaving out the subtrees of the named descendants (tails kept)."""
|
|
238
|
+
parts = [element.text or ""]
|
|
239
|
+
for child in element:
|
|
240
|
+
if localname(child.tag) not in skip:
|
|
241
|
+
parts.extend(_text_without(child, skip))
|
|
242
|
+
parts.append(child.tail or "")
|
|
243
|
+
return parts
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _direct(element, name: str) -> str:
|
|
247
|
+
return _first_text(element, name, direct_only=True)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _child(element, name: str):
|
|
251
|
+
if element is None:
|
|
252
|
+
return None
|
|
253
|
+
return next((c for c in element if localname(c.tag) == name), None)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _esc(text: Optional[str]) -> str:
|
|
257
|
+
return html_lib.escape(text or "", quote=True)
|
|
@@ -403,6 +403,12 @@ def _run(manifest, manifest_path, directory, stem, ids, ctx, http,
|
|
|
403
403
|
|
|
404
404
|
def finish() -> FiguresSummary:
|
|
405
405
|
manifest["completed_at"] = _iso(time.time())
|
|
406
|
+
# The directory is made before each download; when every one failed it
|
|
407
|
+
# is left empty, and an empty `_figures/` reads as "figures here".
|
|
408
|
+
try:
|
|
409
|
+
os.rmdir(directory)
|
|
410
|
+
except OSError:
|
|
411
|
+
pass # holds figures, or never made
|
|
406
412
|
path = write_manifest(manifest_path, manifest, ctx.verbose)
|
|
407
413
|
refusal = (manifest.get("refusal") or {}).get("kind", "")
|
|
408
414
|
rows = manifest["figures"]
|
|
@@ -48,7 +48,7 @@ SOURCE = "institutional_cookies"
|
|
|
48
48
|
# --------------------------------------------------------------------------
|
|
49
49
|
# Off until the user opts in. Two ways to opt in:
|
|
50
50
|
#
|
|
51
|
-
# * `fetchpdf
|
|
51
|
+
# * `fetchpdf fetchcookies` writes ACCESS_CONFIG (library + everyday browser). From
|
|
52
52
|
# then on every fetchpdf run re-reads the publisher cookies straight from that
|
|
53
53
|
# browser, once per process, so sessions the user keeps alive by ordinary
|
|
54
54
|
# browsing are picked up without re-exporting anything.
|
|
@@ -69,7 +69,7 @@ _live_resolved = False
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def load_access_config() -> Optional[dict]:
|
|
72
|
-
"""The `fetchpdf
|
|
72
|
+
"""The `fetchpdf fetchcookies` configuration, or None if access is not set up."""
|
|
73
73
|
try:
|
|
74
74
|
cfg = json.loads(ACCESS_CONFIG.read_text(encoding="utf-8"))
|
|
75
75
|
except (OSError, ValueError):
|
|
@@ -132,7 +132,7 @@ def cookies_user_managed() -> bool:
|
|
|
132
132
|
def active_cookies_file() -> Optional[str]:
|
|
133
133
|
"""The cookie file this process should use, or None when access is off.
|
|
134
134
|
|
|
135
|
-
An explicit ``--cookies`` wins. Otherwise, if `fetchpdf
|
|
135
|
+
An explicit ``--cookies`` wins. Otherwise, if `fetchpdf fetchcookies` has been
|
|
136
136
|
run, the configured browser's cookies are exported once per process to the
|
|
137
137
|
config dir and that file is used; if the browser cannot be read (locked
|
|
138
138
|
store, keyring prompt declined) the last export is used instead.
|