acadfetch 0.1.0__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 (49) hide show
  1. acadfetch-0.1.0/LICENSE +21 -0
  2. acadfetch-0.1.0/MANIFEST.in +4 -0
  3. acadfetch-0.1.0/PKG-INFO +204 -0
  4. acadfetch-0.1.0/README.md +176 -0
  5. acadfetch-0.1.0/acadfetch/__init__.py +5 -0
  6. acadfetch-0.1.0/acadfetch/__main__.py +6 -0
  7. acadfetch-0.1.0/acadfetch/auth.py +61 -0
  8. acadfetch-0.1.0/acadfetch/cli.py +280 -0
  9. acadfetch-0.1.0/acadfetch/client.py +209 -0
  10. acadfetch-0.1.0/acadfetch/consts.py +15 -0
  11. acadfetch-0.1.0/acadfetch/downloader.py +289 -0
  12. acadfetch-0.1.0/acadfetch/errors.py +43 -0
  13. acadfetch-0.1.0/acadfetch/manifest.py +89 -0
  14. acadfetch-0.1.0/acadfetch/naming.py +88 -0
  15. acadfetch-0.1.0/acadfetch/progress.py +161 -0
  16. acadfetch-0.1.0/acadfetch/py.typed +0 -0
  17. acadfetch-0.1.0/acadfetch/ratelimit.py +37 -0
  18. acadfetch-0.1.0/acadfetch/sources/__init__.py +6 -0
  19. acadfetch-0.1.0/acadfetch/sources/arxiv.py +70 -0
  20. acadfetch-0.1.0/acadfetch/sources/base.py +50 -0
  21. acadfetch-0.1.0/acadfetch/sources/direct.py +53 -0
  22. acadfetch-0.1.0/acadfetch/sources/doi.py +54 -0
  23. acadfetch-0.1.0/acadfetch/sources/figshare.py +86 -0
  24. acadfetch-0.1.0/acadfetch/sources/registry.py +60 -0
  25. acadfetch-0.1.0/acadfetch/sources/zenodo.py +92 -0
  26. acadfetch-0.1.0/acadfetch/units.py +25 -0
  27. acadfetch-0.1.0/acadfetch/verify.py +64 -0
  28. acadfetch-0.1.0/acadfetch.egg-info/PKG-INFO +204 -0
  29. acadfetch-0.1.0/acadfetch.egg-info/SOURCES.txt +47 -0
  30. acadfetch-0.1.0/acadfetch.egg-info/dependency_links.txt +1 -0
  31. acadfetch-0.1.0/acadfetch.egg-info/entry_points.txt +2 -0
  32. acadfetch-0.1.0/acadfetch.egg-info/top_level.txt +1 -0
  33. acadfetch-0.1.0/pyproject.toml +55 -0
  34. acadfetch-0.1.0/setup.cfg +4 -0
  35. acadfetch-0.1.0/tests/fixtures/README.md +41 -0
  36. acadfetch-0.1.0/tests/fixtures/figshare_403_block.html +6 -0
  37. acadfetch-0.1.0/tests/fixtures/figshare_403_forbidden.json +4 -0
  38. acadfetch-0.1.0/tests/fixtures/figshare_article.json +76 -0
  39. acadfetch-0.1.0/tests/fixtures/figshare_file.pdf +0 -0
  40. acadfetch-0.1.0/tests/test_auth.py +107 -0
  41. acadfetch-0.1.0/tests/test_cli.py +123 -0
  42. acadfetch-0.1.0/tests/test_client.py +173 -0
  43. acadfetch-0.1.0/tests/test_downloader.py +382 -0
  44. acadfetch-0.1.0/tests/test_manifest.py +85 -0
  45. acadfetch-0.1.0/tests/test_naming.py +57 -0
  46. acadfetch-0.1.0/tests/test_progress.py +219 -0
  47. acadfetch-0.1.0/tests/test_ratelimit.py +30 -0
  48. acadfetch-0.1.0/tests/test_sources.py +248 -0
  49. acadfetch-0.1.0/tests/test_verify.py +58 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 acadfetch contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ # Tests ship in the sdist (they are all offline), so their fixtures must too --
2
+ # otherwise `pytest` after unpacking fails on missing files.
3
+ recursive-include tests *.py
4
+ recursive-include tests/fixtures *
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: acadfetch
3
+ Version: 0.1.0
4
+ Summary: Resumable, rate-limited, checksum-verified bulk downloads from academic data repositories
5
+ Author: young-ski
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/young-ski/acadfetch
8
+ Project-URL: Source, https://github.com/young-ski/acadfetch
9
+ Project-URL: Issues, https://github.com/young-ski/acadfetch/issues
10
+ Keywords: zenodo,figshare,arxiv,doi,reproducibility,download
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Dynamic: license-file
28
+
29
+ # acadfetch
30
+
31
+ [![CI](https://github.com/young-ski/acadfetch/actions/workflows/ci.yml/badge.svg)](https://github.com/young-ski/acadfetch/actions/workflows/ci.yml)
32
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
33
+
34
+ Batch-download files from academic data repositories — resumably, rate-limited, and
35
+ checked against the checksums the repository published.
36
+
37
+ ```
38
+ $ acadfetch fetch 10.5281/zenodo.23612969 -o ./data
39
+ title : Dinler Tarihi Alanında Güncel Konular ve Akademik Değerlendirmeler
40
+ doi : 10.5281/zenodo.23612969
41
+ license : cc-by-4.0
42
+ files : 1, 1.6 MiB
43
+ landing : https://zenodo.org/records/23612969
44
+ Dinler Tarihi ... .pdf 1.2 MiB/1.6 MiB 312.4 KiB/s ETA 0:02
45
+ ok Dinler Tarihi ... .pdf 1.6 MiB (resumed at 512.0 KiB)
46
+
47
+ 1/1 file(s) in ./data
48
+ ```
49
+
50
+ **What it is not:** a search engine, a scraper for paywalled publishers, or a
51
+ citation manager. It is the boring part in the middle — the part that gets killed
52
+ by a flaky VPN at 80%, and the part that nobody writes down.
53
+
54
+ ## Why another downloader
55
+
56
+ Because `curl -O` in a loop loses three things that matter for research data:
57
+
58
+ | Problem | What happens with a plain loop | What acadfetch does |
59
+ | --- | --- | --- |
60
+ | Connection drops at 80% | restart from zero | resumes with `Range` + `If-Range`, keeps the partial |
61
+ | Silent corruption | nobody notices until analysis breaks | verifies the repository's `md5`/`sha256`, re-downloads on mismatch |
62
+ | "Which version did I actually use?" | untraceable | writes `manifest.json` with DOI, license, per-file SHA-256 |
63
+ | Getting rate-limited into a 403 | hammering, then blocked | one global token bucket, honours `Retry-After` |
64
+
65
+ The manifest is the point. A dataset you cannot re-verify is a dataset you cannot
66
+ cite.
67
+
68
+ ## Install
69
+
70
+ Python 3.10+. No runtime dependencies.
71
+
72
+ ```bash
73
+ pipx install acadfetch
74
+ # or, from a checkout
75
+ pipx install git+https://github.com/young-ski/acadfetch
76
+ python -m pip install -e .
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ ```bash
82
+ acadfetch fetch 10.5281/zenodo.123456 # a DOI
83
+ acadfetch fetch zenodo:123456 # a record id
84
+ acadfetch fetch figshare:987654
85
+ acadfetch fetch arxiv:2401.12345
86
+ acadfetch fetch https://example.org/dataset.tar.gz
87
+
88
+ acadfetch fetch zenodo:123456 -n # dry run: what would be fetched
89
+ acadfetch fetch zenodo:123456 --only raw.tar.gz
90
+ acadfetch fetch zenodo:123456 --max-size 2GB # refuse anything bigger
91
+ acadfetch fetch zenodo:123456 -j 1 --rate 0.5 # be a good citizen
92
+ acadfetch manifest zenodo:123456 -o m.json # metadata only, no bytes
93
+ acadfetch fetch zenodo:123456 --header "Authorization: Bearer $TOKEN" # restricted record
94
+
95
+ acadfetch verify ./data/manifest.json # re-hash a directory you downloaded
96
+ acadfetch sources # list adapters
97
+ ```
98
+
99
+ Progress is one aggregate line for the whole run (`3 file(s) 12.4/44.1 MiB 1.8 MiB/s ETA 0:17`),
100
+ redrawn in place on a TTY and written as occasional whole lines when stdout is a pipe.
101
+ `-q` turns it off.
102
+
103
+ Output defaults to `acadfetch-out/<source>-<doi-or-id>`. Files land in
104
+ `<name>.part` and are renamed only after the checksum matches, so an interrupted
105
+ run never leaves something that *looks* finished.
106
+
107
+ Exit codes: `0` success, `1` failure, `2` partial, `130` interrupted (rerun to resume).
108
+
109
+ ## Tokens
110
+
111
+ `ZENODO_TOKEN` and `FIGSHARE_TOKEN` are read from the environment and sent only
112
+ to that repository, so a token never travels to a host that has no business
113
+ seeing it. `--header` still overrides, `--no-token` ignores the environment, and
114
+ the manifest records the variable name plus the last four characters only:
115
+
116
+ ```bash
117
+ export ZENODO_TOKEN=... # restricted records, higher rate limits
118
+ acadfetch fetch zenodo:123456 # prints: auth : token from $ZENODO_TOKEN (…9f2c)
119
+ ```
120
+
121
+ ## Verification and the manifest
122
+
123
+ Every completed run writes `manifest.json`:
124
+
125
+ ```json
126
+ {
127
+ "schema": 1,
128
+ "doi": "10.5281/zenodo.123456",
129
+ "license": "cc-by-4.0",
130
+ "files": [
131
+ { "name": "raw.tar.gz", "size": 8123, "checksum": "md5:9f2c…",
132
+ "sha256": "b41d…", "state": "downloaded", "verified": true }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ `acadfetch verify` recomputes SHA-256 for each entry and exits non-zero on any
138
+ mismatch or missing file. Commit the manifest next to your analysis; it is what
139
+ makes the download reproducible a year later.
140
+
141
+ When a source publishes no checksum (arXiv), verification degrades to a byte
142
+ count and `verified` is `null` rather than a false sense of safety.
143
+
144
+ ## Supported sources
145
+
146
+ | Source | Target forms | Checksums |
147
+ | --- | --- | --- |
148
+ | Zenodo | DOI `10.5281/zenodo.N`, `zenodo:N`, record URL | md5 |
149
+ | Figshare | DOI `10.6084/m9.figshare.N`, `figshare:N`, article URL | md5 |
150
+ | arXiv | `arxiv:ID`, bare ID, abs/pdf URL | none (size only) |
151
+ | doi.org | any DOI, resolved then dispatched | depends |
152
+ | direct | any `http(s)` URL | headers, if present |
153
+
154
+ Figshare's API sits behind a WAF that intermittently answers 403 to non-browser
155
+ clients (an AWS ELB block page, some networks always, some never). Those 403s
156
+ are retried twice with backoff, then the legacy per-file endpoint is tried;
157
+ a 403 that is the API's own permission error is still terminal. Downloads
158
+ themselves go to `ndownloader`, which the WAF leaves alone. The payload shapes
159
+ are pinned in `tests/fixtures/`.
160
+
161
+ If you know your network gets through, `--retry-403 0` skips the retries
162
+ entirely and makes every 403 final on the first answer.
163
+
164
+ ## Politeness
165
+
166
+ Defaults: 3 parallel transfers, 2 requests/second, 4 retries with jittered
167
+ exponential backoff, `Retry-After` honoured up to 120 s. A WAF block page is
168
+ retried twice (`--retry-403 N`); Zenodo and arXiv are donation-funded —
169
+ `--rate 1 -j 1` costs you minutes and costs them less.
170
+
171
+ Respect the license in the manifest. `--dry-run` prints it before you spend
172
+ bandwidth; `unknown` means *check before redistributing*.
173
+
174
+ ## Adding a source
175
+
176
+ 1. Read `acadfetch/sources/zenodo.py` — an adapter is `match(target) -> id | None`
177
+ plus `resolve(client, target) -> Record`.
178
+ 2. Fill `File(checksum=...)` whenever the API publishes one.
179
+ 3. Never hardcode rate limits; take the shared `Client`.
180
+ 4. Add a `match` test and, if possible, a fixture of the real API payload.
181
+
182
+ `scripts/probe_resume.py` interrupts a live transfer and resumes it — the only
183
+ honest way to test this code path.
184
+
185
+ ## Known limits
186
+
187
+ - Tokens come from the environment only; no interactive `login` flow and no
188
+ per-file token refresh.
189
+ - No search/pagination; you bring the target.
190
+ - arXiv PDFs are fetched by constructed URL, so a withdrawn ID gives a 404 after
191
+ resolution.
192
+ - Windows ARM is untested.
193
+
194
+ ## 中文
195
+
196
+ 学术数据仓库的批量下载器:断点续传、全局限速、按仓库公布的校验和验证,并落
197
+ `manifest.json`(DOI、许可协议、每文件 SHA-256)供日后复现。
198
+
199
+ ```bash
200
+ acadfetch fetch 10.5281/zenodo.123456 -o ./data
201
+ acadfetch verify ./data/manifest.json
202
+ ```
203
+
204
+ MIT。
@@ -0,0 +1,176 @@
1
+ # acadfetch
2
+
3
+ [![CI](https://github.com/young-ski/acadfetch/actions/workflows/ci.yml/badge.svg)](https://github.com/young-ski/acadfetch/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ Batch-download files from academic data repositories — resumably, rate-limited, and
7
+ checked against the checksums the repository published.
8
+
9
+ ```
10
+ $ acadfetch fetch 10.5281/zenodo.23612969 -o ./data
11
+ title : Dinler Tarihi Alanında Güncel Konular ve Akademik Değerlendirmeler
12
+ doi : 10.5281/zenodo.23612969
13
+ license : cc-by-4.0
14
+ files : 1, 1.6 MiB
15
+ landing : https://zenodo.org/records/23612969
16
+ Dinler Tarihi ... .pdf 1.2 MiB/1.6 MiB 312.4 KiB/s ETA 0:02
17
+ ok Dinler Tarihi ... .pdf 1.6 MiB (resumed at 512.0 KiB)
18
+
19
+ 1/1 file(s) in ./data
20
+ ```
21
+
22
+ **What it is not:** a search engine, a scraper for paywalled publishers, or a
23
+ citation manager. It is the boring part in the middle — the part that gets killed
24
+ by a flaky VPN at 80%, and the part that nobody writes down.
25
+
26
+ ## Why another downloader
27
+
28
+ Because `curl -O` in a loop loses three things that matter for research data:
29
+
30
+ | Problem | What happens with a plain loop | What acadfetch does |
31
+ | --- | --- | --- |
32
+ | Connection drops at 80% | restart from zero | resumes with `Range` + `If-Range`, keeps the partial |
33
+ | Silent corruption | nobody notices until analysis breaks | verifies the repository's `md5`/`sha256`, re-downloads on mismatch |
34
+ | "Which version did I actually use?" | untraceable | writes `manifest.json` with DOI, license, per-file SHA-256 |
35
+ | Getting rate-limited into a 403 | hammering, then blocked | one global token bucket, honours `Retry-After` |
36
+
37
+ The manifest is the point. A dataset you cannot re-verify is a dataset you cannot
38
+ cite.
39
+
40
+ ## Install
41
+
42
+ Python 3.10+. No runtime dependencies.
43
+
44
+ ```bash
45
+ pipx install acadfetch
46
+ # or, from a checkout
47
+ pipx install git+https://github.com/young-ski/acadfetch
48
+ python -m pip install -e .
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ```bash
54
+ acadfetch fetch 10.5281/zenodo.123456 # a DOI
55
+ acadfetch fetch zenodo:123456 # a record id
56
+ acadfetch fetch figshare:987654
57
+ acadfetch fetch arxiv:2401.12345
58
+ acadfetch fetch https://example.org/dataset.tar.gz
59
+
60
+ acadfetch fetch zenodo:123456 -n # dry run: what would be fetched
61
+ acadfetch fetch zenodo:123456 --only raw.tar.gz
62
+ acadfetch fetch zenodo:123456 --max-size 2GB # refuse anything bigger
63
+ acadfetch fetch zenodo:123456 -j 1 --rate 0.5 # be a good citizen
64
+ acadfetch manifest zenodo:123456 -o m.json # metadata only, no bytes
65
+ acadfetch fetch zenodo:123456 --header "Authorization: Bearer $TOKEN" # restricted record
66
+
67
+ acadfetch verify ./data/manifest.json # re-hash a directory you downloaded
68
+ acadfetch sources # list adapters
69
+ ```
70
+
71
+ Progress is one aggregate line for the whole run (`3 file(s) 12.4/44.1 MiB 1.8 MiB/s ETA 0:17`),
72
+ redrawn in place on a TTY and written as occasional whole lines when stdout is a pipe.
73
+ `-q` turns it off.
74
+
75
+ Output defaults to `acadfetch-out/<source>-<doi-or-id>`. Files land in
76
+ `<name>.part` and are renamed only after the checksum matches, so an interrupted
77
+ run never leaves something that *looks* finished.
78
+
79
+ Exit codes: `0` success, `1` failure, `2` partial, `130` interrupted (rerun to resume).
80
+
81
+ ## Tokens
82
+
83
+ `ZENODO_TOKEN` and `FIGSHARE_TOKEN` are read from the environment and sent only
84
+ to that repository, so a token never travels to a host that has no business
85
+ seeing it. `--header` still overrides, `--no-token` ignores the environment, and
86
+ the manifest records the variable name plus the last four characters only:
87
+
88
+ ```bash
89
+ export ZENODO_TOKEN=... # restricted records, higher rate limits
90
+ acadfetch fetch zenodo:123456 # prints: auth : token from $ZENODO_TOKEN (…9f2c)
91
+ ```
92
+
93
+ ## Verification and the manifest
94
+
95
+ Every completed run writes `manifest.json`:
96
+
97
+ ```json
98
+ {
99
+ "schema": 1,
100
+ "doi": "10.5281/zenodo.123456",
101
+ "license": "cc-by-4.0",
102
+ "files": [
103
+ { "name": "raw.tar.gz", "size": 8123, "checksum": "md5:9f2c…",
104
+ "sha256": "b41d…", "state": "downloaded", "verified": true }
105
+ ]
106
+ }
107
+ ```
108
+
109
+ `acadfetch verify` recomputes SHA-256 for each entry and exits non-zero on any
110
+ mismatch or missing file. Commit the manifest next to your analysis; it is what
111
+ makes the download reproducible a year later.
112
+
113
+ When a source publishes no checksum (arXiv), verification degrades to a byte
114
+ count and `verified` is `null` rather than a false sense of safety.
115
+
116
+ ## Supported sources
117
+
118
+ | Source | Target forms | Checksums |
119
+ | --- | --- | --- |
120
+ | Zenodo | DOI `10.5281/zenodo.N`, `zenodo:N`, record URL | md5 |
121
+ | Figshare | DOI `10.6084/m9.figshare.N`, `figshare:N`, article URL | md5 |
122
+ | arXiv | `arxiv:ID`, bare ID, abs/pdf URL | none (size only) |
123
+ | doi.org | any DOI, resolved then dispatched | depends |
124
+ | direct | any `http(s)` URL | headers, if present |
125
+
126
+ Figshare's API sits behind a WAF that intermittently answers 403 to non-browser
127
+ clients (an AWS ELB block page, some networks always, some never). Those 403s
128
+ are retried twice with backoff, then the legacy per-file endpoint is tried;
129
+ a 403 that is the API's own permission error is still terminal. Downloads
130
+ themselves go to `ndownloader`, which the WAF leaves alone. The payload shapes
131
+ are pinned in `tests/fixtures/`.
132
+
133
+ If you know your network gets through, `--retry-403 0` skips the retries
134
+ entirely and makes every 403 final on the first answer.
135
+
136
+ ## Politeness
137
+
138
+ Defaults: 3 parallel transfers, 2 requests/second, 4 retries with jittered
139
+ exponential backoff, `Retry-After` honoured up to 120 s. A WAF block page is
140
+ retried twice (`--retry-403 N`); Zenodo and arXiv are donation-funded —
141
+ `--rate 1 -j 1` costs you minutes and costs them less.
142
+
143
+ Respect the license in the manifest. `--dry-run` prints it before you spend
144
+ bandwidth; `unknown` means *check before redistributing*.
145
+
146
+ ## Adding a source
147
+
148
+ 1. Read `acadfetch/sources/zenodo.py` — an adapter is `match(target) -> id | None`
149
+ plus `resolve(client, target) -> Record`.
150
+ 2. Fill `File(checksum=...)` whenever the API publishes one.
151
+ 3. Never hardcode rate limits; take the shared `Client`.
152
+ 4. Add a `match` test and, if possible, a fixture of the real API payload.
153
+
154
+ `scripts/probe_resume.py` interrupts a live transfer and resumes it — the only
155
+ honest way to test this code path.
156
+
157
+ ## Known limits
158
+
159
+ - Tokens come from the environment only; no interactive `login` flow and no
160
+ per-file token refresh.
161
+ - No search/pagination; you bring the target.
162
+ - arXiv PDFs are fetched by constructed URL, so a withdrawn ID gives a 404 after
163
+ resolution.
164
+ - Windows ARM is untested.
165
+
166
+ ## 中文
167
+
168
+ 学术数据仓库的批量下载器:断点续传、全局限速、按仓库公布的校验和验证,并落
169
+ `manifest.json`(DOI、许可协议、每文件 SHA-256)供日后复现。
170
+
171
+ ```bash
172
+ acadfetch fetch 10.5281/zenodo.123456 -o ./data
173
+ acadfetch verify ./data/manifest.json
174
+ ```
175
+
176
+ MIT。
@@ -0,0 +1,5 @@
1
+ """acadfetch: resumable, rate-limited, checksum-verified bulk downloads."""
2
+
3
+ from .consts import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,61 @@
1
+ """Repository tokens, read from the environment.
2
+
3
+ Putting a token in ``--header`` leaks it into shell history and into every
4
+ ``ps`` on the box. Each adapter instead declares the variable it reads, and the
5
+ token is attached per-source so it never travels to a host that has no business
6
+ seeing it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+
15
+ # source name -> variable that holds its token
16
+ TOKEN_ENV = {
17
+ "zenodo": "ZENODO_TOKEN",
18
+ "figshare": "FIGSHARE_TOKEN",
19
+ }
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Token:
24
+ source: str
25
+ env: str
26
+ value: str
27
+
28
+ @property
29
+ def headers(self) -> dict[str, str]:
30
+ return {"Authorization": f"Bearer {self.value}"}
31
+
32
+ @property
33
+ def hint(self) -> str:
34
+ """Enough to tell two tokens apart, not enough to use one."""
35
+ return f"…{self.value[-4:]}" if len(self.value) > 8 else "…"
36
+
37
+ def __str__(self) -> str:
38
+ return f"${self.env} ({self.hint})"
39
+
40
+
41
+ def load(environ: Mapping[str, str] | None = None) -> dict[str, Token]:
42
+ """Collect every token the environment provides, keyed by source."""
43
+ env = os.environ if environ is None else environ
44
+ found: dict[str, Token] = {}
45
+ for source, name in TOKEN_ENV.items():
46
+ value = (env.get(name) or "").strip()
47
+ if value:
48
+ found[source] = Token(source=source, env=name, value=value)
49
+ return found
50
+
51
+
52
+ def headers(tokens: Mapping[str, Token], source: str | None) -> dict[str, str]:
53
+ token = tokens.get(source or "")
54
+ return token.headers if token else {}
55
+
56
+
57
+ def describe(tokens: Mapping[str, Token], source: str | None) -> str:
58
+ token = tokens.get(source or "")
59
+ if token is None:
60
+ return ""
61
+ return f"token from {token}"