litfetch
Resolve a scholarly article identifier to its retrievable artifacts — the full-text body and any supplementary material — and fetch their bytes.
litfetch is two cooperating seams:
- a fetch ladder — pluggable
Fetcherbackends (Europe PMC Bookshelf, PMC Open Access S3, Europe PMC, Elsevier OA, Springer OA) tried in priority order; the first to serve the body wins, returning aBlob(aFileplus its bytes); - an optional resolver layer — pluggable
Resolvers that enrich what you know about an article (pmid→pmcid/doi, etc.) so the ladder can act.
You hand it an ArticleIds bundle (any of pmid / pmcid / doi / bookid).
Resolution is demand-driven: a resolver only runs when the next fetcher needs
a pmid, pmcid, or doi you don't yet have, and runs at most once.
An article is modelled as a file-set: a collection of File references (the
body in its various media types, plus supplementary material, distinguished by
FileKind), each hosted upstream. litfetch fetches the raw artifacts and reports
their access terms; it does not render them. To turn a fetched JATS/Elsevier
body into markdown, run litdown
on the bytes yourself (see Render to markdown).
The examples below are a tour; docs/api.md is the full
reference for the public surface.
Install
pip install litfetch
bioRxiv / medRxiv preprint full text needs a browser-fingerprint HTTP client,
enabled by the biorxiv extra:
pip install 'litfetch[biorxiv]'
Usage
Fetch the body
Hand fetch_body an ArticleIds; the default ladder serves the first available
body as a Blob:
from litfetch import ArticleIds, fetch_body
blob = await fetch_body(ArticleIds(pmcid='PMC5334499'))
if blob:
print(blob.file.source, blob.file.media_type, len(blob.content))
Render to markdown
litfetch returns raw bytes, not markdown. Convert a JATS/Elsevier body with litdown — you pick and pin the converter:
import io
import litdown
from litfetch import ArticleIds, fetch_body
blob = await fetch_body(ArticleIds(pmcid='PMC5334499'))
if blob:
markdown = litdown.convert(io.BytesIO(blob.content))
Inject your own resolver
A resolver is an async (ArticleIds, Http) -> ArticleIds — the session running
it supplies the Http. Enrich from whatever you have — a corpus client, a local
cache, an API — and merge it in (this one ignores Http, hence _http):
from litfetch import ArticleIds, Http, fetch_body
async def my_resolver(ids: ArticleIds, _http: Http) -> ArticleIds:
if not ids.pmid:
return ids
pmcid, doi = await my_corpus.lookup(ids.pmid)
return ids.merge(ArticleIds(pmcid=pmcid, doi=doi))
blob = await fetch_body(ArticleIds(pmid='29622564'), resolver=my_resolver)
Use a bundled resolver
Bundled resolvers are constructed with their config, then passed in the same
slot. chain(...) composes several (yours first, fallbacks after); it stops
once pmid, pmcid, and doi are all known:
from litfetch import ArticleIds, fetch_body
from litfetch.resolvers import SemanticScholarResolver, NcbiIdConverterResolver, chain
resolver = chain(
my_resolver, # your own
SemanticScholarResolver(api_key=S2_KEY), # bundled
NcbiIdConverterResolver(tool='myapp'), # bundled
)
blob = await fetch_body(ArticleIds(pmid='29622564'), resolver=resolver)
Polite-pool identification (NCBI/Crossref email, Unpaywall's required email)
comes from a session contact, not a hardcoded default — set it on the session:
async with litfetch.Session(contact='you@example.org') as s: await s.fetch_body(...).
default_resolver() is a batteries-included, keyless chain
(Europe PMC search + NCBI ID Converter).
An API key that a host takes as an api_key query parameter (OpenAlex,
Springer) is masked in httpx2's request log line; importing litfetch installs
that filter on the httpx2 logger.
No resolver — you already hold the IDs
A non-PubMed paper you only have a DOI for, plus your own Elsevier key:
blob = await fetch_body(
ArticleIds(doi='10.1016/j.cell.2020.01.001'),
credentials={'elsevier_api_key': key},
)
A Bookshelf book part
An NCBI Bookshelf book part — a GeneReviews chapter, say — is reached by its
Bookshelf accession (NBK + digits), passed as bookid;
ArticleIds explains why the caller supplies it. The
ladder fetches the BITS XML (JATS's book tag set) from Europe PMC:
blob = await fetch_body(ArticleIds(bookid='NBK1247'))
Supplementary material
list_files enumerates the file-set (references, no bytes); fetch_file
materialises one:
from litfetch import ArticleIds, FileKind, list_files, fetch_file
files = await list_files(ArticleIds(pmcid='PMC5334499'), kind=FileKind.SUPPLEMENTARY)
for file in files:
blob = await fetch_file(file)
Access terms
Read the licence from the fetched bytes, falling back to an access authority (Unpaywall) when the bytes carry none:
from litfetch import extract_source_metadata, resolve_access
meta = extract_source_metadata(blob) # from the JATS/Elsevier bytes
if meta.licence is None:
meta = await resolve_access(ArticleIds(doi='10.1016/j.cell.2020.01.001'))
Resolvers stand alone
Each resolver is usable on its own as a cross-reference tool, independent of
fetching. A resolver is given the Http to use, so run it inside a session:
from litfetch import ArticleIds, Session
from litfetch.resolvers import SemanticScholarResolver
async with Session() as s:
ids = await SemanticScholarResolver()(ArticleIds(doi='10.1016/j.cell.2020.01.001'), s)
print(ids.pmid, ids.pmcid)
Batch: one session, a scope per paper
The one-shot functions above each open a throwaway session. For many papers,
hold one Session (pooled connection, shared pacing) and open a scope per
paper — the scope caches within itself, so a duplicate upstream call (e.g.
Unpaywall for both licence and PDF) is fetched once:
from litfetch import ArticleIds, Session
async with Session() as session:
for pmid in pmids:
async with session.scope() as s:
blob = await s.fetch_body(ArticleIds(pmid=pmid))
access = await s.resolve_access(ArticleIds(pmid=pmid))
Extending
- A new body fetcher: implement the
Fetcherprotocol — aname, arequires: frozenset[str]of theArticleIdsfields it needs, and an asyncfetch(ids, *, credentials, http)returning a bodyBloborNone. Add it to afetchers=list (or your owndefault_fetchers). - A new file source: implement the
FileSourceprotocol — aname, and asynclist_files(ids, ...)/fetch_file(file, ...)— to enumerate and materialise an article's file-set (body renditions and supplementary alike). - A new resolver: write an async
ArticleIds -> ArticleIdsthat fills gaps viaArticleIds.mergeand never overwrites a known id.
Development
uv sync
uv run ruff check . && uv run ruff format --check .
uv run pyright
uv run pytest
Metadata
Release files for litfetch 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| litfetch-0.5.0.tar.gz | 64.1 kB | With provenance | Details for litfetch-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| litfetch-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for litfetch-0.5.0-py3-none-any.whl |
Total release size: 111.8 kB
Release files / litfetch-0.5.0.tar.gz
| Download URL | litfetch-0.5.0.tar.gz |
|---|---|
| Size | 64.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
355be3aca322ba2d78f32c8d93e2c59d75fc819f03946026611a7945fb399445
|
|
BLAKE2b-256 checksum How to use checksums |
ec189208a96d0a1e35cc9f0bc7ad01b68675f46f4cf9eeaf1d49e01e533e305d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / litfetch-0.5.0-py3-none-any.whl
| Download URL | litfetch-0.5.0-py3-none-any.whl |
|---|---|
| Size | 47.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
584814eed022899de99255da24f6cf08862abdcf106ec7e82748812a0adbbd8d
|
|
BLAKE2b-256 checksum How to use checksums |
709ff675a48f344672158eadf39b91604dc8e9a94ce66e0ab61816d48a1904cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for litfetch 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for litfetch-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.