acadfetch
English · 简体中文
Batch-download files from academic data repositories — resumably, rate-limited, and checked against the checksums the repository published.
$ acadfetch fetch 10.5281/zenodo.23612969 -o ./data
title : Dinler Tarihi Alanında Güncel Konular ve Akademik Değerlendirmeler
doi : 10.5281/zenodo.23612969
license : cc-by-4.0
files : 1, 1.6 MiB
landing : https://zenodo.org/records/23612969
Dinler Tarihi ... .pdf 1.2 MiB/1.6 MiB 312.4 KiB/s ETA 0:02
ok Dinler Tarihi ... .pdf 1.6 MiB (resumed at 512.0 KiB)
1/1 file(s) in ./data
What it is not: a search engine, a scraper for paywalled publishers, or a citation manager. It is the boring part in the middle — the part that gets killed by a flaky VPN at 80%, and the part that nobody writes down.
Why another downloader
Because curl -O in a loop loses three things that matter for research data:
| Problem | What happens with a plain loop | What acadfetch does |
|---|---|---|
| Connection drops at 80% | restart from zero | resumes with Range + If-Range, keeps the partial |
| Silent corruption | nobody notices until analysis breaks | verifies the repository's md5/sha256, re-downloads on mismatch |
| "Which version did I actually use?" | untraceable | writes manifest.json with DOI, license, per-file SHA-256 |
| Getting rate-limited into a 403 | hammering, then blocked | one global token bucket, honours Retry-After |
The manifest is the point. A dataset you cannot re-verify is a dataset you cannot cite.
Install
Python 3.10–3.15. No runtime dependencies.
Try it without installing anything:
uvx acadfetch fetch 10.5281/zenodo.23612969 # uv
Or keep it on the PATH:
pipx install acadfetch
From a checkout, for development:
git clone https://github.com/young-ski/acadfetch && cd acadfetch
python -m pip install -e . pytest mypy && python -m pytest -q
Supported platforms
| Platform | Status |
|---|---|
| Linux | tested — CI, every supported Python |
| Windows | tested — CI on 3.10, 3.13 and 3.15 |
| macOS | best effort — CI runs it on 3.12 and 3.15, a failure is a bug to fix but does not hold a release |
| Windows ARM | untested |
| Python 3.15 | tested — CI, on Linux and Windows |
Usage
One target per run:
acadfetch fetch 10.5281/zenodo.23612969 # a DOI
acadfetch fetch arxiv:1706.03762 # an arXiv id
acadfetch fetch https://ndownloader.figshare.com/files/70600233/report.pdf # any URL
The first two are answered by Zenodo and arXiv and work from any network those
are reachable on. The third is a direct URL, so the host decides: figshare's CDN
answers 403 with a WAF block page from some networks — the same behaviour that
made its adapter not worth keeping in 0.2.0. acadfetch says so and keeps
nothing, rather than saving the block page as a PDF. If you hit that, the file
itself is fine; see the 403 section of
docs/troubleshooting.md.
figshare was removed in 0.2.0: pass the article's download URL instead, and
--checksum keeps the verification the API used to
give you.
The rest of the surface:
acadfetch fetch 10.5281/zenodo.23612969 -n # dry run: which dir, which files
acadfetch fetch 10.5281/zenodo.23612969 --only raw.tar.gz
acadfetch fetch 10.5281/zenodo.23612969 --max-size 2GB # refuse anything bigger
acadfetch fetch 10.5281/zenodo.23612969 -j 1 --rate 0.5 # be a good citizen
acadfetch fetch 10.5281/zenodo.23612969 --retries 2 --retry-403 0 # give up sooner
acadfetch fetch 10.5281/zenodo.23612969 --manifest-out build/m.json # a second copy
acadfetch manifest 10.5281/zenodo.23612969 -o m.json # metadata only, no bytes
acadfetch fetch zenodo:123456 --header "Authorization: Bearer $TOKEN" # restricted
acadfetch verify ./data/manifest.json # re-hash what you downloaded
acadfetch --version # what is installed
acadfetch sources # adapters, with an example each
10.5281/zenodo.123456, zenodo:123456 and arxiv:2401.12345 are placeholders;
use any real id.
In a pipeline
--json puts one report document on stdout and moves progress to stderr, so the
two never interleave:
acadfetch fetch zenodo:123456 --json | jq -r '.files[] | select(.state=="failed") | .name'
To hand the bytes to another downloader, -w/--urls writes an aria2-compatible
list (- is stdout) and transfers nothing:
acadfetch fetch zenodo:123456 -w - --glob '*.csv' | aria2c -i -
--glob takes shell patterns and combines with --only (both must pass).
Exit codes are unchanged, and a failure before any transfer still produces a
document with error set. report is the document's schema version (now 2, field reference in
docs/json-report.md):
fields are added, never removed or given a new meaning, and state may gain
values — treat an unknown one as non-terminal. verify --json reports per-file
MISMATCH rows the same way, and its summary counts skipped rows
separately. At the end of a failed transfer, part names the .part file that
was kept, so a retry can resume instead of starting over. A 403 that carries a
block page is retried; see docs/troubleshooting.md.
Progress is one aggregate line for the whole run
(3 file(s) 12.4/44.1 MiB 1.8 MiB/s ETA 0:17), redrawn in place on a TTY.
When stdout is a pipe it becomes a new line at most every 5 s (one extra when a
file starts or stops), so a CI log stays greppable without becoming a wall.
-q/--quiet turns it off.
-o/--out defaults to acadfetch-out/<source>-<id> (a URL adds its host and a short
hash), and -n prints it before
you spend bandwidth. Files land in <name>.part and are renamed only after the
checksum matches, so an interrupted or failed run never leaves something that
looks finished -- and never throws away the bytes it already paid for: rerun and
it resumes. The one case where a run must start over is a server that ignores
Range; the bytes already on disk are then moved aside and kept until a whole
new copy has arrived, so a restart that dies early costs nothing. acadfetch-out/
is worth adding to a project's .gitignore; this repository already has it.
The default directory names the target, not only its filename — data.csv from
two hosts is two datasets, and they do not share a directory. A file already
there is reused when its checksum matches; with no checksum, the size plus one
more tie (an explicit -o, or a manifest in that directory that recorded the
same URL) is required, and --force skips the question entirely.
A DOI that resolves to a web page instead of a file is refused, naming the page: fetching the landing page as if it were the dataset is how a repository's front door ends up being cited as a data file.
A run that cannot fit is refused before the first byte is transferred, naming the shortfall:
needs 41.2 GiB but 12.0 GiB is free. What is already on disk is subtracted, so
a resume is judged on the remainder. Each finished file is flushed before its
rename (--no-fsync skips that; it is faster and weaker against a power cut).
Exit codes: 0 success, 1 failure, 2 partial, 64 bad usage, 130 interrupted
(rerun to resume).
Tokens
ZENODO_TOKEN is read from the environment and sent only to Zenodo, so a token
never travels to a host that has no business seeing it -- including across a
redirect: a hop to another host (a storage CDN, a download mirror) goes out
without it, a hop from https to http drops it with a warning (--allow-http
keeps it), and a presigned URL gets no Authorization at all, because S3
rejects a request that carries both. Cookies and proxy credentials are dropped
on any cross-origin hop as well. --header still overrides, --no-token
ignores the environment, and the manifest records the variable name plus the
last four characters only:
export ZENODO_TOKEN=... # restricted records, higher rate limits
acadfetch fetch zenodo:123456 # prints: auth : token from $ZENODO_TOKEN (…9f2c)
Verification and the manifest
Every completed run writes manifest.json:
{
"schema": 3,
"doi": "10.5281/zenodo.123456",
"license": "cc-by-4.0",
"files": [
{ "name": "raw.tar.gz", "size": 8123, "checksum": "md5:9f2c…",
"sha256": "b41d…", "state": "downloaded", "selected": true,
"verified": true, "verified_reason": "matched-md5" },
{ "name": "notes.txt", "size": 12, "state": "not-selected", "selected": false,
"sha256": null, "verified": null, "verified_reason": "not-selected" }
]
}
The manifest describes the record, not the run: --only still writes a row
for every file, so verify can tell "not fetched" from "fetched and then lost",
and a successful --only run verifies green. Add --require-all when you want
the whole record accounted for. Each fetched row also records final_url,
etag, http_status and retrieved_at, and credential query parameters are
stripped from every URL (--record-full-url keeps them). Full field and state
reference: docs/manifest.md.
name is the file on disk, which can differ from the repository's name when two
files collide. verified_reason separates the two kinds of verified: null:
no-checksum-published (arXiv) from unparseable-checksum (a repository that
published something we cannot name an algorithm for). Schema 1 and 2 manifests
still read, and verify says so for schema 1, because their names were recorded
before duplicates were resolved.
acadfetch verify recomputes SHA-256 for each entry and exits non-zero on any
mismatch or missing file; rows the run never selected are reported skipped.
Add --against-remote to ask the source again and compare what it publishes
now: a manifest someone rewrote stops agreeing even though the bytes still match
it. --require-remote makes an unreachable source a failure.
Commit the manifest next to your analysis; it is what makes the download
reproducible a year later.
Names are resolved against the manifest's own directory, and a name that points
outside it — ../sibling/file, an absolute path, a symlink leading out — is
reported as escaped rather than quietly hashed, because a manifest is a
document someone can hand you. --allow-escape follows them; --root changes
what "inside" means.
When a source publishes no checksum (arXiv), verification degrades to a byte
count and verified is null rather than a false sense of safety.
Supported sources
| Source | Target forms | Checksums |
|---|---|---|
| Zenodo | DOI 10.5281/zenodo.N, zenodo:N, record URL |
md5 |
| arXiv | arxiv:ID, bare ID, abs/pdf URL |
none (size only) |
| doi.org | any DOI, resolved then dispatched | depends |
| direct | any http(s) URL |
headers, if present |
figshare (figshare:N, 10.6084/…, article URLs) was removed in 0.2.0; it
answers with the conversion instead of a generic "cannot tell what this is".
See docs/migration-0.2.md.
A CDN in front of a direct URL can answer 403 with a block page. Those are
retried twice with backoff, while a 403 carrying the application's own JSON is
terminal on the first answer; --retry-403 N changes the count.
Networking
Proxies, timeouts and a private CA are flags, not luck:
acadfetch fetch zenodo:123456 --proxy http://proxy.campus:3128 # overrides the environment
acadfetch fetch zenodo:123456 --no-proxy # ignore HTTP(S)_PROXY
acadfetch fetch zenodo:123456 --timeout 15 # idle/connect budget, seconds
acadfetch fetch zenodo:123456 --ca-bundle corp-tls.pem # trust this PEM instead of the store
--proxy takes http:// or https:// and wins over --no-proxy; without
either, HTTP_PROXY / HTTPS_PROXY / ALL_PROXY are honoured by urllib as
usual. A timeout is per request, connect to first byte.
Debugging
--verbose (or ACADFETCH_DEBUG=1) traces each request, each retry and the
traceback of a failure to stderr; stdout, --json documents and exit codes are
unchanged. Credential header values and presigned query parameters are masked,
so the trace can be pasted into an issue.
Politeness
Defaults: 3 parallel transfers (-j/--concurrency), 2 requests/second, 4 retries with jittered
exponential backoff, Retry-After honoured up to 120 s. A block page is retried twice
(--retry-403 N); Zenodo and arXiv are donation-funded —
--rate 1 -j 1 costs you minutes and costs them less.
Respect the license in the manifest. --dry-run prints it before you spend
bandwidth; unknown means check before redistributing.
Adding a source
- Read
acadfetch/sources/zenodo.py— an adapter ismatch(target) -> id | Noneplusresolve(client, target) -> Record. - Fill
File(checksum=...)whenever the API publishes one. - Never hardcode rate limits; take the shared
Client. - Add a
matchtest and, if possible, a fixture of the real API payload.
scripts/probe_resume.py interrupts a live transfer and resumes it — the only
honest way to test this code path.
Getting involved
A 30-second tour shows the whole loop, and docs/integrations.md has Make, CI, aria2, a Snakemake storage plugin and a pooch downloader.
A nightly job downloads three fixed public records and verifies them, so a rotted adapter opens an issue before a user reports one. It never blocks a merge; the next good night closes the issue.
- Stuck on something → docs/troubleshooting.md first; it is short and indexed by symptom
- Usage questions → Discussions
- Bugs → the bug template; say whether
verifyreportsMISMATCH - Security (bad bytes, path escapes, token leaks) →
SECURITY.md, privately - Adapters, tests, docs → PRs welcome, read
CONTRIBUTING.mdfirst
Known limits
- figshare was removed in 0.2.0; existing figshare manifests still verify, and
new targets convert with one
--checksumcommand (docs/migration-0.2.md). - Tokens come from the environment only; no interactive
loginflow and no per-file token refresh. - No search/pagination; you bring the target.
- arXiv PDFs are fetched by constructed URL, so a withdrawn ID gives a 404 after resolution.
- Windows ARM is untested.
- macOS is best effort: it is in CI but not release-blocking, and nobody has run it by hand yet.
- A source that publishes neither a size nor a checksum (bare
directURLs without aContent-LengthonHEAD) can still be truncated silently by a server that lies aboutContent-Length, because HTTP gives no in-band way to notice the extra bytes.
中文
学术数据仓库的批量下载器:断点续传、全局限速、按仓库公布的校验和验证,并落
manifest.json(DOI、许可协议、每文件 SHA-256)供日后复现。
它不是搜索器、不抓付费墙、也不是引文管理器 —— 它只管"取数据"这一步,并让这一步 事后可证明。
安装
Python 3.10–3.15,零运行时依赖。
uvx acadfetch fetch 10.5281/zenodo.23612969 # 不安装,直接跑
pipx install acadfetch # 或者装到 PATH 上
快速开始
10.5281/zenodo.23612969 是真实记录,可以直接复制;其余 id 都是占位符。
acadfetch fetch 10.5281/zenodo.23612969 -n # 先看:落到哪、传多少、许可是什么
acadfetch fetch 10.5281/zenodo.23612969 -o ./data
acadfetch verify ./data/manifest.json # 事后复验;对不上就非零退出
目标也可以是 arxiv:1706.03762、任意 DOI,或任意 https:// 直链。
直链成败由对方主机决定:figshare 的 CDN 在某些网络会回 403 拦截页(当年删掉 figshare 适配器正是因为这个)。acadfetch 会明说这是拦截页、而不是把 HTML 存成 PDF,并把下一条命令给你。
它替你保住的东西
| 情况 | 它怎么做 |
|---|---|
| 传到 80% 断了 | .part 保留,重跑用 Range/If-Range 接着传;服务器若无视 Range,旧字节先挪到一边,整份新文件到齐才替换 |
| 悄悄损坏 | 按仓库公布的 md5/sha256 校验,不符就重下;仍不符则 verified: false 并删掉坏字节 |
| "我到底下的是哪个版本" | manifest.json 记 DOI、许可、每文件 SHA-256、最终 URL、HTTP 状态与时间 |
| 被限流吃 403 | 全局令牌桶限速,遵守 Retry-After,拦截页按次数重试 |
默认 3 并发、2 请求/秒、4 次重试。Zenodo 和 arXiv 靠募捐运营,--rate 1 -j 1
多花你几分钟,对面少受点罪。
进流水线
acadfetch fetch 10.5281/zenodo.23612969 --json # 一个 JSON 文档给 stdout,进度走 stderr
acadfetch fetch 10.5281/zenodo.23612969 -w - # 输出 aria2 可用 URL 列表,不传字节
--json 的字段契约见 docs/json-report.md;report 是文档
版本号,只加字段、不改语义。
退出码
0 成功、1 失败、2 部分成功、64 用法错误、130 被中断(.part 已保留,
重跑即可续)。
令牌
ZENODO_TOKEN 从环境读取,只发给 Zenodo;跨主机跳转时剥掉,https 降级到
http 时也会剥掉并告警。manifest 只记变量名和后四位。
export ZENODO_TOKEN=...
acadfetch fetch zenodo:123456
支持的源
| 源 | 目标形式 | 校验和 |
|---|---|---|
| Zenodo | DOI 10.5281/zenodo.N、zenodo:N、记录 URL |
md5 |
| arXiv | arxiv:ID、裸 ID、abs/pdf URL |
无(只比字节数) |
| doi.org | 任意 DOI,解析后再分派 | 视源而定 |
| 直链 | 任意 http(s) URL |
响应头里有就用 |
figshare 适配器在 0.2.0 已移除;源不公布校验和时用 --checksum md5:... 补上,
旧 manifest 仍可 verify。见 docs/migration-0.2.md。
已知限制
manifest.json没有签名:verify证明磁盘上的字节与 manifest 一致,不能证明 manifest 本身没被改写。补这一层用verify --against-remote(会重新问源站), 细节见 SECURITY.md。- arXiv 不公布校验和,验证降级为字节数,
verified是null而不是假的安心。 - 没有搜索与分页,目标得你自己给。
- Windows ARM 未测;macOS 只做尽力而为(在 CI 里,但不阻塞发布)。
更多
- 30 秒演示 docs/demo.md · 按症状排障 docs/troubleshooting.md · 接进流水线 docs/integrations.md(Make、CI、aria2、Snakemake 插件、pooch 下载器)
- 加源、动传输、写测试的规矩见 CONTRIBUTING.md
许可
工具是 MIT。数据本身的许可见 manifest 的 license 字段;unknown 表示转发前先
确认,而 -n 会在花掉带宽之前就把它打出来。
Citing
acadfetch --cite prints how to cite the tool; the machine-readable form is
CITATION.cff. For the data, cite the repository record — the
manifest is the part that lets a reader re-check it.
Audit trail
How the 3.15 support claim was reached, including what it did not prove — the free-threaded build, Windows ARM, and every place the probe ran short: docs/py315-windows-report.md.
Metadata
Release files for acadfetch 0.7.2
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 |
|---|---|---|---|---|
| acadfetch-0.7.2.tar.gz | 284.3 kB | With provenance | Details for acadfetch-0.7.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| acadfetch-0.7.2-py3-none-any.whl | Python 3 | none | any | Details for acadfetch-0.7.2-py3-none-any.whl |
Total release size: 354.0 kB
Release files / acadfetch-0.7.2.tar.gz
| Download URL | acadfetch-0.7.2.tar.gz |
|---|---|
| Size | 284.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dff12d2ef8e25bafe16464144cc8c936a871c5a318453d088f9add2576329167
|
|
BLAKE2b-256 checksum How to use checksums |
d323ab3538a61159e3aa01f1cecfeace28c946c7322413f6c605ee81124d68be
|
| 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 Nov 13, 2026.
Transparency logRelease files / acadfetch-0.7.2-py3-none-any.whl
| Download URL | acadfetch-0.7.2-py3-none-any.whl |
|---|---|
| Size | 69.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f58fc70afb67a5db699498f5ba7856bf73b40d687dd9612fea061764e487a584
|
|
BLAKE2b-256 checksum How to use checksums |
6e1dff8541c0e52e88b58d9123f4f8fc22aee7b90557268f9db169051af3a4b5
|
| 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 Nov 13, 2026.
Transparency logSecurity for acadfetch 0.7.2
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
Both files in this release have been uploaded with provenance metadata.
Attestation for acadfetch-0.7.2.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 Nov 13, 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.