fetchpdf 0.1.4__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.
- {fetchpdf-0.1.4/fetchpdf.egg-info → fetchpdf-0.1.5}/PKG-INFO +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/__init__.py +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetchpdf.py +46 -31
- {fetchpdf-0.1.4 → fetchpdf-0.1.5/fetchpdf.egg-info}/PKG-INFO +1 -1
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/SOURCES.txt +2 -1
- fetchpdf-0.1.5/tests/test_xml_last.py +76 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/LICENSE +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/MANIFEST.in +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/README.md +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/__main__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/_env.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/_http.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetch_abstract_from_doi.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetch_metadata_from_doi.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/get_cookies.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/_util.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/artifact.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/openrouter.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/blocked.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/byline.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/cache.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/chunk.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/classify.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/context.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/corresponding.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/engine.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/extract_images.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/figures.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/fulltext_scan.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/goals.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/http.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/identifiers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/institutional.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/ladder.json +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/linked_artifacts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_adjudicate.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/normalize.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_text.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/provenance.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pubpeer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/ratelimit.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/references.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/repository_waf.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/request_drafts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/resolve.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/__init__.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/arxiv.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/core_text.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/landing.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/legacy_pdf.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/preprint.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/supplements.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_atypon.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_dspace.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_graph.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_index.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_jci.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_pmc.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_publishers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplementary.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/tiers.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_html.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_markdown.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/validate.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/verify.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/dependency_links.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/entry_points.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/requires.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/top_level.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/pyproject.toml +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/requirements.txt +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/setup.cfg +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_agent_backends.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_blocked.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_cookie_offer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_corresponding.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_datacite_supplement_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_dspace.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_entrypoints.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_epmc_repo.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_extract_images.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_figures.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_folder_encoding.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_fulltext_scan.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_get_cookies.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_institutional.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_jci_supplements.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_json_output.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_landing_candidates.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_landing_page_detection.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_last_resort_reachability.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_linked_artifacts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_llm_adjudicate.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_normalize_cells.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pdf_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pdf_trailer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pmc_page.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_polite_ua.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pubmed_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pubpeer.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pull_for_record_call_sites.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pure.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_request_drafts.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_retrieval.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_scan_record.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_screening_conversion.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_serpapi_scholar.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_short_conference_xml.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_supplement_asset_identity.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_supplementary.py +0 -0
- {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_to_html.py +0 -0
|
@@ -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.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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5091
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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=
|
|
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=
|
|
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):
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""The default chain settles for XML only after every PDF source has failed."""
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import fetchpdf.fetchpdf as fpd
|
|
8
|
+
|
|
9
|
+
DOI = "10.1371/journal.pone.0000308"
|
|
10
|
+
PMCID = "PMC1817752"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _miss(*a, **kw):
|
|
14
|
+
return SimpleNamespace(status_code=404, headers={}, text="", content=b"",
|
|
15
|
+
url="https://publisher.example/article", json=lambda: {})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def chain(monkeypatch, tmp_path):
|
|
20
|
+
"""A chain where PMC knows the paper but serves no PDF, and nothing else hits."""
|
|
21
|
+
def get(url, *a, **kw):
|
|
22
|
+
if "idconv" in url and DOI in url:
|
|
23
|
+
return SimpleNamespace(status_code=200,
|
|
24
|
+
json=lambda: {"records": [{"pmcid": PMCID}]})
|
|
25
|
+
return _miss()
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr(fpd.requests, "get", Mock(side_effect=_miss))
|
|
28
|
+
monkeypatch.setattr(fpd, "_get_with_retries", Mock(side_effect=get))
|
|
29
|
+
monkeypatch.setattr(fpd, "_crossref_raw_get", Mock(side_effect=_miss))
|
|
30
|
+
monkeypatch.setattr(fpd, "doi_to_pmid", lambda *a, **kw: None)
|
|
31
|
+
for name in ("try_core_fallback", "try_doaj_fallback", "try_datacite_fallback",
|
|
32
|
+
"try_apa_supplemental_fallback", "try_landing_page_pdf_fallback",
|
|
33
|
+
"try_download", "try_serpapi_scholar_fallback",
|
|
34
|
+
"try_pmc_efetch_xml_fallback"):
|
|
35
|
+
monkeypatch.setattr(fpd, name, Mock(return_value=False))
|
|
36
|
+
xml_path = str(tmp_path / "paper.xml")
|
|
37
|
+
pmc_xml = Mock(return_value=xml_path)
|
|
38
|
+
monkeypatch.setattr(fpd, "try_pmc_xml_fallback", pmc_xml)
|
|
39
|
+
return SimpleNamespace(pdf=str(tmp_path / "paper.pdf"), xml=xml_path, pmc_xml=pmc_xml)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_a_later_pdf_beats_xml_from_pmc(monkeypatch, chain):
|
|
43
|
+
monkeypatch.setattr(fpd, "try_core_fallback", Mock(return_value=True))
|
|
44
|
+
source = [None]
|
|
45
|
+
assert fpd._fetch_pdf_chain(DOI, chain.pdf, email="test@example.org", delay=0,
|
|
46
|
+
_source_out=source) == chain.pdf
|
|
47
|
+
chain.pmc_xml.assert_not_called()
|
|
48
|
+
assert source == ["core"]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_pmc_xml_is_the_last_resort(chain):
|
|
52
|
+
source = [None]
|
|
53
|
+
assert fpd._fetch_pdf_chain(DOI, chain.pdf, email="test@example.org", delay=0,
|
|
54
|
+
_source_out=source) == chain.xml
|
|
55
|
+
chain.pmc_xml.assert_called_once()
|
|
56
|
+
fpd.try_serpapi_scholar_fallback.assert_called_once()
|
|
57
|
+
assert source == ["pmc"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_pmid_route_tries_pdf_before_its_xml(monkeypatch, chain):
|
|
61
|
+
# No PMCID from the DOI, so only the PMID route can reach PMC XML.
|
|
62
|
+
monkeypatch.setattr(fpd, "_get_with_retries", Mock(side_effect=_miss))
|
|
63
|
+
monkeypatch.setattr(fpd, "doi_to_pmid", lambda *a, **kw: "17327915")
|
|
64
|
+
calls = []
|
|
65
|
+
|
|
66
|
+
def pmid_route(pmid, save_path, verbose=False, allow_xml=True):
|
|
67
|
+
calls.append(allow_xml)
|
|
68
|
+
return chain.xml if allow_xml else False
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr(fpd, "try_pmid_direct_pdf_fallback", pmid_route)
|
|
71
|
+
source = [None]
|
|
72
|
+
assert fpd._fetch_pdf_chain(DOI, chain.pdf, email="test@example.org", delay=0,
|
|
73
|
+
_source_out=source) == chain.xml
|
|
74
|
+
assert calls == [False, True]
|
|
75
|
+
fpd.try_serpapi_scholar_fallback.assert_called_once()
|
|
76
|
+
assert source == ["pmid_direct"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|