fetchpdf 0.1.6__tar.gz → 0.1.7__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.6/fetchpdf.egg-info → fetchpdf-0.1.7}/PKG-INFO +1 -1
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/__init__.py +1 -1
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/pdf_identity.py +92 -2
- {fetchpdf-0.1.6 → fetchpdf-0.1.7/fetchpdf.egg-info}/PKG-INFO +1 -1
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pdf_identity.py +104 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_serpapi_scholar.py +3 -1
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/LICENSE +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/MANIFEST.in +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/README.md +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/__main__.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/_env.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/_http.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/fetch_abstract_from_doi.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/fetch_metadata_from_doi.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/fetchpdf.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/get_cookies.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/__init__.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/_util.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/artifact.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/backends/__init__.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/backends/claude_cli.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/backends/openrouter.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/blocked.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/byline.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/cache.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/chunk.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/classify.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/context.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/corresponding.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/engine.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/extract_images.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/figures.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/fulltext_scan.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/goals.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/http.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/identifiers.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/institutional.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/ladder.json +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/linked_artifacts.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/llm_adjudicate.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/normalize.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/pdf_text.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/provenance.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/pubpeer.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/ratelimit.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/references.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/repository_waf.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/request_drafts.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/resolve.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/__init__.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/arxiv.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/core_text.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/crossref_tdm.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/epmc.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/epmc_repo.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/landing.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/legacy_pdf.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/pmc.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/pmc_html.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/preprint.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/publisher_html.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/sources/supplements.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_atypon.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_dspace.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_graph.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_index.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_jci.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_pmc.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplement_publishers.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/supplementary.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/tiers.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/to_html.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/to_markdown.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/validate.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf/retrieval/verify.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf.egg-info/SOURCES.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf.egg-info/dependency_links.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf.egg-info/entry_points.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf.egg-info/requires.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/fetchpdf.egg-info/top_level.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/pyproject.toml +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/requirements.txt +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/setup.cfg +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_agent_backends.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_blocked.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_cookie_offer.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_corresponding.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_datacite_supplement_identity.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_dspace.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_elsevier_routing.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_entrypoints.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_epmc_repo.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_extract_images.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_figures.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_folder_encoding.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_fulltext_scan.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_get_cookies.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_institutional.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_jci_supplements.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_json_output.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_landing_candidates.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_landing_page_detection.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_last_resort_reachability.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_linked_artifacts.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_llm_adjudicate.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_llm_agent_retrieval.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_normalize_cells.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pdf_trailer.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pmc_page.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_polite_ua.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pubmed_identity.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pubpeer.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pull_for_record_call_sites.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_pure.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_request_drafts.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_retrieval.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_review_fixes.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_scan_record.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_screening_conversion.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_short_conference_xml.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_supplement_asset_identity.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_supplementary.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_to_html.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_xml_fallback_extras.py +0 -0
- {fetchpdf-0.1.6 → fetchpdf-0.1.7}/tests/test_xml_last.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.7"
|
|
19
19
|
__all__ = [
|
|
20
20
|
"fetch_pdf",
|
|
21
21
|
"fetch_pdf_from_doi", # deprecated alias for fetch_pdf
|
|
@@ -160,6 +160,44 @@ PAGE_COUNT_MIN_SPAN = 2
|
|
|
160
160
|
|
|
161
161
|
_PAGE_RANGE_RE = re.compile(r"^\s*(?:[A-Za-z]*)(\d+)\s*(?:[-–—]\s*(?:[A-Za-z]*)(\d+))?\s*$")
|
|
162
162
|
|
|
163
|
+
#: S1 on its own is not enough when the front pages are legible and do not carry
|
|
164
|
+
#: the title (issue #5). The requested DOI is printed by the paper itself, but
|
|
165
|
+
#: also by a paper that CITES it and by the article's own supplementary file:
|
|
166
|
+
#: fetch_pdf("10.1037/0022-3514.74.6.1464") wrote 10.5964/meth.7155, whose
|
|
167
|
+
#: abstract reads "(IAT; Greenwald et al., 1998. DOI: 10.1037/0022-3514.74.6.1464)",
|
|
168
|
+
#: and the Supplementary Information PDF for 10.1038/nature08833 prints
|
|
169
|
+
#: "doi: 10.1038/nature08833" on page 1. Measured over 121 cached PDFs whose DOI
|
|
170
|
+
#: was found in the text, scoring the Crossref title against the front two pages:
|
|
171
|
+
#: the citing paper 0.52, the supplement 0.64, a line-numbered medRxiv preprint
|
|
172
|
+
#: (correct) 0.76, the other 118 correct papers >= 0.97. 0.70 sits in the gap; it
|
|
173
|
+
#: is narrow, so a page too short to judge (LEGIBLE_FRONT_CHARS) keeps the DOI
|
|
174
|
+
#: verdict rather than risk refusing a correct scan.
|
|
175
|
+
DOI_TITLE_PAGES = 2
|
|
176
|
+
DOI_TITLE_MIN = 0.70
|
|
177
|
+
|
|
178
|
+
#: The same DOI printed inside an author-year citation -- "Greenwald et al.,
|
|
179
|
+
#: 1998. DOI: 10.1037/...", "(Smith & Jones, 2020, https://doi.org/...)" -- is a
|
|
180
|
+
#: reference to the paper, not the paper's own masthead. Matched on the text
|
|
181
|
+
#: just before the DOI, so it also works when no title is known to compare.
|
|
182
|
+
_CITED_BEFORE_DOI_RE = re.compile(
|
|
183
|
+
r"(?:et\s+al\.?|&\s*[a-z][a-z'\-]+|\band\s+[a-z][a-z'\-]+)" # et al. / & Name / and Name
|
|
184
|
+
r"[\s,]*\(?\s*(?:1[89]|20)\d{2}[a-z]?\s*\)?" # , 1998 / (1998) / 2020a
|
|
185
|
+
r"\s*[.,;:]?\s*"
|
|
186
|
+
r"(?:doi\s*[:.]?\s*|(?:https?\s*:\s*//\s*)?(?:dx\s*\.\s*)?doi\s*\.\s*org\s*/\s*)?$",
|
|
187
|
+
re.IGNORECASE,
|
|
188
|
+
)
|
|
189
|
+
#: How far before the DOI the author-year pattern may end, and how far back a
|
|
190
|
+
#: "cite this article" line may start. A paper's own front matter often prints
|
|
191
|
+
#: a suggested citation in exactly the cited shape ("Cite this article as: Smith
|
|
192
|
+
#: et al., 2021. https://doi.org/..."), and that is the paper's own DOI.
|
|
193
|
+
CITED_CONTEXT_CHARS = 80
|
|
194
|
+
OWN_CITATION_LINE_CHARS = 400
|
|
195
|
+
_OWN_CITATION_RE = re.compile(
|
|
196
|
+
r"cite\s+(?:this|the)\s+(?:article|paper|chapter|work|preprint)|please\s+cite"
|
|
197
|
+
r"|how\s+to\s+cite|citation\s*:|recommended\s+citation|cite\s+as",
|
|
198
|
+
re.IGNORECASE,
|
|
199
|
+
)
|
|
200
|
+
|
|
163
201
|
#: Embedded titles that carry no identity: a converter's leftovers, a filename,
|
|
164
202
|
#: a template default. Present in a large minority of real PDFs, and treating
|
|
165
203
|
#: them as "declares a different article" would reject correct files.
|
|
@@ -279,11 +317,24 @@ def verify_pdf_identity(source, doi: Optional[str] = None,
|
|
|
279
317
|
|
|
280
318
|
# S1: the requested DOI, printed on the paper. True of essentially every
|
|
281
319
|
# version of record, and squashing means a line break inside it is harmless.
|
|
320
|
+
# Not on its own, though, when the DOI is only cited, or when the front pages
|
|
321
|
+
# are legible and do not carry the title (DOI_TITLE_MIN): then the later
|
|
322
|
+
# signals decide, and a refusal says the DOI was seen and why it did not count.
|
|
323
|
+
doi_discounted = ""
|
|
282
324
|
if doi:
|
|
283
325
|
wanted = squash(normalise_doi(doi))
|
|
284
326
|
if wanted and wanted in squashed_page:
|
|
285
|
-
|
|
286
|
-
|
|
327
|
+
if _doi_only_cited(text, doi):
|
|
328
|
+
doi_discounted = "only inside a citation"
|
|
329
|
+
elif title:
|
|
330
|
+
two_pages = squash(reader.text(DOI_TITLE_PAGES) or text)
|
|
331
|
+
fraction = matched_fraction(squash(title), two_pages)
|
|
332
|
+
if len(two_pages) >= LEGIBLE_FRONT_CHARS and fraction < DOI_TITLE_MIN:
|
|
333
|
+
doi_discounted = ("but its front pages carry only {:.0%} of the "
|
|
334
|
+
"title".format(fraction))
|
|
335
|
+
if not doi_discounted:
|
|
336
|
+
return truncated or IdentityVerdict(
|
|
337
|
+
VERIFIED, "requested DOI found in the PDF text", ["doi-in-text"])
|
|
287
338
|
|
|
288
339
|
# S1b: the arXiv id, stamped down the margin of every PDF arXiv serves. It
|
|
289
340
|
# is S1 for a class of record that S1 cannot reach and neither can S2-S4:
|
|
@@ -410,10 +461,23 @@ def verify_pdf_identity(source, doi: Optional[str] = None,
|
|
|
410
461
|
"so it cannot be checked against {}".format(
|
|
411
462
|
len(squashed_page), DOI_PAGES, doi or "the requested record"),
|
|
412
463
|
)
|
|
464
|
+
if doi_discounted:
|
|
465
|
+
return IdentityVerdict(
|
|
466
|
+
WRONG,
|
|
467
|
+
'PDF prints the requested DOI {}, so it is not taken as "{}" '
|
|
468
|
+
"(likely a paper citing it, or its supplement)".format(
|
|
469
|
+
doi_discounted, _clip(title)),
|
|
470
|
+
)
|
|
413
471
|
return IdentityVerdict(
|
|
414
472
|
WRONG,
|
|
415
473
|
'PDF does not contain the requested DOI or the title "{}"'.format(_clip(title)),
|
|
416
474
|
)
|
|
475
|
+
if doi_discounted:
|
|
476
|
+
return IdentityVerdict(
|
|
477
|
+
NO_REFERENCE,
|
|
478
|
+
"PDF prints {} {}, and no title is known to check it against".format(
|
|
479
|
+
doi or "the requested DOI", doi_discounted),
|
|
480
|
+
)
|
|
417
481
|
return IdentityVerdict(
|
|
418
482
|
NO_REFERENCE,
|
|
419
483
|
"no title known for {} and its DOI is not printed in the PDF, so there "
|
|
@@ -630,6 +694,32 @@ def _is_generic_title(embedded: str) -> bool:
|
|
|
630
694
|
return bool(_GENERIC_TITLE_RE.match(stripped) or _FILENAME_TITLE_RE.match(stripped))
|
|
631
695
|
|
|
632
696
|
|
|
697
|
+
def _doi_only_cited(text: str, doi: str) -> bool:
|
|
698
|
+
"""Whether every place `doi` is printed in `text` is inside a citation.
|
|
699
|
+
|
|
700
|
+
True only when the DOI appears at least once and each appearance follows an
|
|
701
|
+
author-year citation ("et al., 1998. DOI:") with no "cite this article" line
|
|
702
|
+
before it. A DOI printed once in a masthead and once in a citation is the
|
|
703
|
+
paper's own, so one non-citation appearance is enough to say False.
|
|
704
|
+
"""
|
|
705
|
+
wanted = normalise_doi(doi)
|
|
706
|
+
if not wanted:
|
|
707
|
+
return False
|
|
708
|
+
# Line wraps land anywhere inside a DOI, so allow whitespace between its characters.
|
|
709
|
+
pattern = re.compile(r"\s*".join(re.escape(c) for c in wanted), re.IGNORECASE)
|
|
710
|
+
hits = list(pattern.finditer(text))
|
|
711
|
+
if not hits:
|
|
712
|
+
return False
|
|
713
|
+
for hit in hits:
|
|
714
|
+
before = text[max(0, hit.start() - CITED_CONTEXT_CHARS):hit.start()]
|
|
715
|
+
if not _CITED_BEFORE_DOI_RE.search(before):
|
|
716
|
+
return False
|
|
717
|
+
line_window = text[max(0, hit.start() - OWN_CITATION_LINE_CHARS):hit.start()]
|
|
718
|
+
if _OWN_CITATION_RE.search(line_window):
|
|
719
|
+
return False
|
|
720
|
+
return True
|
|
721
|
+
|
|
722
|
+
|
|
633
723
|
def _clip(text: str, limit: int = 70) -> str:
|
|
634
724
|
text = " ".join((text or "").split())
|
|
635
725
|
return text if len(text) <= limit else text[:limit - 1] + "…"
|
|
@@ -14,6 +14,7 @@ from conftest import text_pdf
|
|
|
14
14
|
from fetchpdf.retrieval import pdf_identity
|
|
15
15
|
from fetchpdf.retrieval.pdf_identity import (
|
|
16
16
|
NO_ENGINE,
|
|
17
|
+
NO_REFERENCE,
|
|
17
18
|
TRUNCATED,
|
|
18
19
|
UNREADABLE,
|
|
19
20
|
VERIFIED,
|
|
@@ -583,3 +584,106 @@ def test_a_wall_is_discarded_even_when_metadata_is_unreachable(tmp_path, monkeyp
|
|
|
583
584
|
monkeypatch.setattr(fpd, "_title_for", lambda doi, verbose=False: None) # Crossref down
|
|
584
585
|
assert fpd._accept_downloaded_pdf(str(path), DOI) is False
|
|
585
586
|
assert not os.path.exists(path)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# --------------------------------------------------------------------------
|
|
590
|
+
# The requested DOI is printed, but not as this paper's own (issue #5)
|
|
591
|
+
# --------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
IAT_DOI = "10.1037/0022-3514.74.6.1464"
|
|
594
|
+
IAT_TITLE = ("Measuring individual differences in implicit cognition: "
|
|
595
|
+
"The implicit association test")
|
|
596
|
+
CITING_TITLE = ("Filling the gap between implicit associations and behavior: "
|
|
597
|
+
"A linear mixed-effects Rasch analysis of the Implicit Association Test")
|
|
598
|
+
CITING_PROSE = ["Participants completed the task in a single session and responses "
|
|
599
|
+
"were modelled with crossed random effects for items and persons."] * 8
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def test_paper_citing_the_doi_is_not_accepted_as_it(tmp_path):
|
|
603
|
+
"""10.5964/meth.7155, fetched for the 1998 IAT paper (issue #5).
|
|
604
|
+
|
|
605
|
+
Its abstract prints the requested DOI inside a citation. Before the fix the
|
|
606
|
+
DOI alone accepted it.
|
|
607
|
+
"""
|
|
608
|
+
path = _pdf(tmp_path, "citing.pdf", [
|
|
609
|
+
CITING_TITLE,
|
|
610
|
+
"The measure obtained from the Implicit Association Test (IAT; Greenwald",
|
|
611
|
+
f"et al., 1998. DOI: {IAT_DOI}) is often used to predict behaviour.",
|
|
612
|
+
] + CITING_PROSE)
|
|
613
|
+
verdict = verify_pdf_identity(path, IAT_DOI, IAT_TITLE)
|
|
614
|
+
assert verdict.state == WRONG
|
|
615
|
+
assert "only inside a citation" in verdict.reason
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def test_paper_citing_the_doi_is_not_accepted_when_no_title_is_known(tmp_path):
|
|
619
|
+
"""The citation shape alone is enough to refuse: there is no title to check."""
|
|
620
|
+
path = _pdf(tmp_path, "citing-untitled.pdf", [
|
|
621
|
+
CITING_TITLE,
|
|
622
|
+
f"(Greenwald, McGhee, & Schwartz, 1998, https://doi.org/{IAT_DOI}).",
|
|
623
|
+
] + CITING_PROSE)
|
|
624
|
+
verdict = verify_pdf_identity(path, IAT_DOI, None)
|
|
625
|
+
assert verdict.state == NO_REFERENCE
|
|
626
|
+
assert "only inside a citation" in verdict.reason
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def test_supplement_printing_the_doi_without_the_title_is_not_accepted(tmp_path):
|
|
630
|
+
"""The Supplementary Information PDF for 10.1038/nature08833 prints the
|
|
631
|
+
article DOI on page 1 but not its title (0.64 of it, measured)."""
|
|
632
|
+
path = _pdf(tmp_path, "si.pdf", [
|
|
633
|
+
"SUPPLEMENTARY INFORMATION",
|
|
634
|
+
"doi: 10.1038/nature08833",
|
|
635
|
+
"Supplementary Methods, Supplementary Figures S1-S12 and Supplementary Tables S1-S4",
|
|
636
|
+
] + ["Supplementary Figure legends and additional methods for the cohort "
|
|
637
|
+
"analyses, including sequencing depth and quality filters."] * 8)
|
|
638
|
+
verdict = verify_pdf_identity(path, "10.1038/nature08833", TITLE)
|
|
639
|
+
assert verdict.state == WRONG
|
|
640
|
+
assert "carry only" in verdict.reason
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_own_suggested_citation_still_counts_as_the_papers_doi(tmp_path):
|
|
644
|
+
"""The false-reject guard for the citation rule. Front matter often prints the
|
|
645
|
+
paper's own suggested citation in exactly the cited shape."""
|
|
646
|
+
path = _pdf(tmp_path, "own-cite.pdf", [
|
|
647
|
+
"Cite this article as: Smith et al., 2021.",
|
|
648
|
+
f"https://doi.org/{DOI}",
|
|
649
|
+
] + PROSE)
|
|
650
|
+
verdict = verify_pdf_identity(path, DOI, None)
|
|
651
|
+
assert verdict.state == VERIFIED
|
|
652
|
+
assert verdict.signals == ["doi-in-text"]
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_doi_in_masthead_and_in_a_citation_is_the_papers_own(tmp_path):
|
|
656
|
+
"""One appearance outside a citation is enough to keep S1."""
|
|
657
|
+
path = _pdf(tmp_path, "masthead.pdf", [
|
|
658
|
+
f"https://doi.org/{DOI}",
|
|
659
|
+
f"As reported earlier (Smith et al., 2021. doi: {DOI}), the index held.",
|
|
660
|
+
] + PROSE)
|
|
661
|
+
verdict = verify_pdf_identity(path, DOI, None)
|
|
662
|
+
assert verdict.state == VERIFIED
|
|
663
|
+
assert verdict.signals == ["doi-in-text"]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def test_line_numbered_preprint_with_a_partly_legible_title_keeps_the_doi(tmp_path):
|
|
667
|
+
"""A line-numbered medRxiv preprint (correct paper) scored 0.76: above the bar."""
|
|
668
|
+
words = TITLE.split()
|
|
669
|
+
half = len(words) // 2
|
|
670
|
+
numbered = [f"1 {' '.join(words[:half])}", "2 3 4 5 6", f"7 {' '.join(words[half:])}"]
|
|
671
|
+
path = _pdf(tmp_path, "medrxiv.pdf", numbered + [f"doi: {DOI}"] + PROSE)
|
|
672
|
+
from fetchpdf.retrieval._util import matched_fraction, squash
|
|
673
|
+
front = squash("\n".join(numbered + [f"doi: {DOI}"] + PROSE))
|
|
674
|
+
assert matched_fraction(squash(TITLE), front) >= pdf_identity.DOI_TITLE_MIN
|
|
675
|
+
verdict = verify_pdf_identity(path, DOI, TITLE)
|
|
676
|
+
assert verdict.state == VERIFIED
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def test_garbled_front_page_keeps_the_doi_verdict(tmp_path):
|
|
680
|
+
"""Too little text to judge the title: the DOI is still trusted."""
|
|
681
|
+
# Between MIN_FRONT_CHARS (read at all) and LEGIBLE_FRONT_CHARS (judged).
|
|
682
|
+
stamp = ["Downloaded from https://academic.example.org by guest on June 5, 2016"] * 4
|
|
683
|
+
path = _pdf(tmp_path, "scan.pdf", [f"doi:{DOI}"] + stamp)
|
|
684
|
+
from fetchpdf.retrieval._util import squash
|
|
685
|
+
chars = len(squash(chr(10).join([f"doi:{DOI}"] + stamp)))
|
|
686
|
+
assert pdf_identity.MIN_FRONT_CHARS <= chars < pdf_identity.LEGIBLE_FRONT_CHARS, chars
|
|
687
|
+
verdict = verify_pdf_identity(path, DOI, TITLE)
|
|
688
|
+
assert verdict.state == VERIFIED
|
|
689
|
+
assert verdict.signals == ["doi-in-text"]
|
|
@@ -193,7 +193,9 @@ def test_wrong_pdf_is_rejected_and_next_copy_is_tried(monkeypatch, tmp_path):
|
|
|
193
193
|
"doi:10.9999/wrong"] + ["Sedimentary rocks form in layers."] * 20))
|
|
194
194
|
else:
|
|
195
195
|
assert not path.exists(), "Rejected PDF must be removed"
|
|
196
|
-
|
|
196
|
+
# The correct copy prints its title, as a real article does: a
|
|
197
|
+
# DOI alone on legible pages is what a citing paper also prints.
|
|
198
|
+
path.write_bytes(text_pdf([TITLE, f"doi:{DOI}"] + ["Full text of the trial report."] * 250))
|
|
197
199
|
return True
|
|
198
200
|
|
|
199
201
|
monkeypatch.setattr(fpd, "try_download", download)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|