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.
Files changed (123) hide show
  1. {fetchpdf-0.1.4/fetchpdf.egg-info → fetchpdf-0.1.5}/PKG-INFO +1 -1
  2. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/__init__.py +1 -1
  3. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetchpdf.py +46 -31
  4. {fetchpdf-0.1.4 → fetchpdf-0.1.5/fetchpdf.egg-info}/PKG-INFO +1 -1
  5. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/SOURCES.txt +2 -1
  6. fetchpdf-0.1.5/tests/test_xml_last.py +76 -0
  7. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/LICENSE +0 -0
  8. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/MANIFEST.in +0 -0
  9. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/README.md +0 -0
  10. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/__main__.py +0 -0
  11. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/_env.py +0 -0
  12. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/_http.py +0 -0
  13. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetch_abstract_from_doi.py +0 -0
  14. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/fetch_metadata_from_doi.py +0 -0
  15. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/get_cookies.py +0 -0
  16. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/__init__.py +0 -0
  17. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/_util.py +0 -0
  18. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/artifact.py +0 -0
  19. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/__init__.py +0 -0
  20. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
  21. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/backends/openrouter.py +0 -0
  22. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/blocked.py +0 -0
  23. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/byline.py +0 -0
  24. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/cache.py +0 -0
  25. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/chunk.py +0 -0
  26. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/classify.py +0 -0
  27. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/context.py +0 -0
  28. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/corresponding.py +0 -0
  29. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/engine.py +0 -0
  30. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/extract_images.py +0 -0
  31. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/figures.py +0 -0
  32. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/fulltext_scan.py +0 -0
  33. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/goals.py +0 -0
  34. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/http.py +0 -0
  35. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/identifiers.py +0 -0
  36. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/institutional.py +0 -0
  37. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/ladder.json +0 -0
  38. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/linked_artifacts.py +0 -0
  39. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_adjudicate.py +0 -0
  40. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/llm_agent_retrieval.py +0 -0
  41. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/normalize.py +0 -0
  42. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_identity.py +0 -0
  43. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pdf_text.py +0 -0
  44. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/provenance.py +0 -0
  45. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/pubpeer.py +0 -0
  46. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/ratelimit.py +0 -0
  47. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/references.py +0 -0
  48. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/repository_waf.py +0 -0
  49. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/request_drafts.py +0 -0
  50. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/resolve.py +0 -0
  51. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/__init__.py +0 -0
  52. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/arxiv.py +0 -0
  53. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/core_text.py +0 -0
  54. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
  55. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc.py +0 -0
  56. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
  57. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/landing.py +0 -0
  58. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/legacy_pdf.py +0 -0
  59. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc.py +0 -0
  60. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
  61. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/preprint.py +0 -0
  62. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
  63. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/sources/supplements.py +0 -0
  64. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_atypon.py +0 -0
  65. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_dspace.py +0 -0
  66. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_graph.py +0 -0
  67. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_index.py +0 -0
  68. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_jci.py +0 -0
  69. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_pmc.py +0 -0
  70. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplement_publishers.py +0 -0
  71. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/supplementary.py +0 -0
  72. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/tiers.py +0 -0
  73. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_html.py +0 -0
  74. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/to_markdown.py +0 -0
  75. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/validate.py +0 -0
  76. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf/retrieval/verify.py +0 -0
  77. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/dependency_links.txt +0 -0
  78. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/entry_points.txt +0 -0
  79. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/requires.txt +0 -0
  80. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/fetchpdf.egg-info/top_level.txt +0 -0
  81. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/pyproject.toml +0 -0
  82. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/requirements.txt +0 -0
  83. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/setup.cfg +0 -0
  84. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_agent_backends.py +0 -0
  85. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_blocked.py +0 -0
  86. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_cookie_offer.py +0 -0
  87. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_corresponding.py +0 -0
  88. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_datacite_supplement_identity.py +0 -0
  89. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_dspace.py +0 -0
  90. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_entrypoints.py +0 -0
  91. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_epmc_repo.py +0 -0
  92. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_extract_images.py +0 -0
  93. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_figures.py +0 -0
  94. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_folder_encoding.py +0 -0
  95. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_fulltext_scan.py +0 -0
  96. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_get_cookies.py +0 -0
  97. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_institutional.py +0 -0
  98. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_jci_supplements.py +0 -0
  99. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_json_output.py +0 -0
  100. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_landing_candidates.py +0 -0
  101. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_landing_page_detection.py +0 -0
  102. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_last_resort_reachability.py +0 -0
  103. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_linked_artifacts.py +0 -0
  104. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_llm_adjudicate.py +0 -0
  105. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_llm_agent_retrieval.py +0 -0
  106. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_normalize_cells.py +0 -0
  107. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pdf_identity.py +0 -0
  108. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pdf_trailer.py +0 -0
  109. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pmc_page.py +0 -0
  110. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_polite_ua.py +0 -0
  111. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pubmed_identity.py +0 -0
  112. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pubpeer.py +0 -0
  113. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pull_for_record_call_sites.py +0 -0
  114. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_pure.py +0 -0
  115. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_request_drafts.py +0 -0
  116. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_retrieval.py +0 -0
  117. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_scan_record.py +0 -0
  118. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_screening_conversion.py +0 -0
  119. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_serpapi_scholar.py +0 -0
  120. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_short_conference_xml.py +0 -0
  121. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_supplement_asset_identity.py +0 -0
  122. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_supplementary.py +0 -0
  123. {fetchpdf-0.1.4 → fetchpdf-0.1.5}/tests/test_to_html.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchpdf
3
- Version: 0.1.4
3
+ Version: 0.1.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
@@ -15,7 +15,7 @@ from .fetch_metadata_from_doi import fetch_metadata_from_doi
15
15
  # behavioural difference, it is the same object.
16
16
  fetch_pdf_from_doi = fetch_pdf
17
17
 
18
- __version__ = "0.1.4"
18
+ __version__ = "0.1.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
- 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
@@ -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):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fetchpdf
3
- Version: 0.1.4
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
@@ -117,4 +117,5 @@ tests/test_serpapi_scholar.py
117
117
  tests/test_short_conference_xml.py
118
118
  tests/test_supplement_asset_identity.py
119
119
  tests/test_supplementary.py
120
- tests/test_to_html.py
120
+ tests/test_to_html.py
121
+ tests/test_xml_last.py
@@ -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