fetch
fetch
Hash-verified, mirrored, offline-first file fetching.
The fetch order for every file is:
- The file already exists under the data root with a matching checksum.
- The file exists in the read-only shared cache (
FWL_DATA_CACHE) with a matching checksum and is copied into the data root atomically. - Offline mode is active: raise
OfflineDataError. - Download from each mirror in turn (Zenodo first, then Dataverse), verify the checksum, and move the file into place atomically.
Downloads are performed by pooch, which resolves doi: URLs for Zenodo
and Dataverse records natively. Files are never written directly to their
final location: every write lands in a staging directory on the same
filesystem (<data_root>/.fwl-io-staging) and is moved into place with
os.replace, so a crashed download can never leave a corrupt file that
later runs would trust. Stale staging entries are pruned opportunistically.
Mirror failures (network, checksum) and local placement failures (read-only tree, full disk) are reported as distinct errors: a permission problem on the data root is never disguised as a download problem.
A transient transport failure (a read or connect timeout, a dropped or reset connection, or a 429 or transient 5xx server response) is retried: every mirror is tried once per round, and the whole set is retried on a short backoff schedule when a round ends with no success and at least one transient failure. A round whose failures are all permanent (a 404 or a checksum mismatch) stops the retries at once. Every request carries an explicit connect and read timeout, so a stalled mirror socket fails in bounded time instead of hanging the fetch.
Concurrency: a burst of processes (for example many PROTEUS instances started
together) that all miss the same file would otherwise each download it,
hammering the mirrors and risking rate-limiting for the whole collaboration.
:meth:Fetcher.fetch serialises fetchers of the same file with a per-target
inter-process lock and re-checks the target under it, so only the first process
downloads and the rest reuse the completed file; distinct files still fetch in
parallel. The lock lives on the shared data root and is coherent across nodes
where the filesystem supports it (verified on Kapteyn NFS and Hรกbrรณk Lustre).
It is best-effort: if the filesystem has no usable lock manager or a holder
stalls past lock_timeout, waiters log a warning and fetch unguarded rather
than failing or blocking the batch.
A dataset pinned to a Zenodo version DOI resolves into a per-version
directory <subdir>/r<record-id> so successive deposits coexist, and
fetch_all writes a .fwl-io.json stamp (DOI, checksums, fetch date)
there so a completed dataset directory or shared cache is self-describing.
A dataset given extract="tar" or "zip" ships as a single archive: the
archive is downloaded and checksum-verified like any file, then extracted
(safely, rejecting members that escape the directory) into the version
directory, with the tree moved into place atomically. The archive is not kept.
DownloadError
Bases: RuntimeError
A file could not be obtained from any configured mirror.
Fetcher(subdir, registry, zenodo=None, dataverse=None, base_urls=None, data_root=None, progress=False, extract=None, lock_timeout=_LOCK_TIMEOUT_S)
Fetches the files of one dataset below the data root.
A dataset pinned to a Zenodo version DOI lands in a per-version directory
<data_root>/<subdir>/r<record-id>, so an updated deposit is placed
beside its predecessor rather than overwriting it, and :meth:fetch_all
writes a .fwl-io.json provenance stamp there so the completed
directory is self-describing. A source given only as base_urls (no
Zenodo pin) keeps the bare <data_root>/<subdir>.
Source code in src/fwl_io/fetch.py
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | |
fetch(fname, offline=None)
Return a verified local path for fname, downloading if needed.
Source code in src/fwl_io/fetch.py
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
fetch_all(offline=None)
Fetch every registry file, then stamp the version directory.
The stamp is written here (the whole-dataset operation), not by an
individual :meth:fetch, so a version directory populated one file at
a time is not stamped until a fetch_all completes it. A stamp-write
failure never fails the fetch: the data is already in place. An archive
dataset is downloaded, verified, and extracted as one operation.
Source code in src/fwl_io/fetch.py
457 458 459 460 461 462 463 464 465 466 467 468 469 470 | |
file_matches(fname)
True when the local file for fname matches its registry digest.
Reads the file; it does not fetch, and it does not check that the file
exists first, so a caller wanting to tell an absent file from a corrupt
one tests for presence itself. An unreadable file raises OSError
rather than reporting a mismatch, since a permission problem on the
tree is a different fault from wrong contents.
Raises:
| Type | Description |
|---|---|
KeyError
|
|
OSError
|
The file could not be read, whether because it is absent or because the tree denies access to it. |
Source code in src/fwl_io/fetch.py
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 | |
provenance()
Return (file, source, checksum) records for run-provenance manifests.
source is the actual origin of files resolved by this Fetcher
instance (local, cache:<path>, or the mirror that served the
download). Files not yet fetched in this session are marked with a
declared: prefix on the primary mirror, since their true origin
is unknown.
Source code in src/fwl_io/fetch.py
801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 | |
recorded_members()
Return the extracted members this dataset's stamp records.
None when there is no stamp describing this dataset's tree, which
says the tree is not there to be examined rather than that it is empty.
Callers must keep those apart: an empty list would read as a complete
tree of no files.
A stamp qualifies only when it describes this deposit and this archive kind. One left by a different fetch of the same subdirectory, a plain fetch or another version, does not describe this tree.
Members that would resolve outside the dataset directory are dropped. A stamp is a file on disk like any other and can be edited or replaced, so a name in it is treated with the same suspicion as a name inside an archive rather than joined onto the tree unchecked.
Source code in src/fwl_io/fetch.py
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 | |
OfflineDataError
Bases: RuntimeError
A required file is unavailable locally while offline mode is active.
create_fetcher(subdir, zenodo=None, dataverse=None, registry=None, base_urls=None, data_root=None, progress=False, extract=None, lock_timeout=_LOCK_TIMEOUT_S)
Create a :class:Fetcher for one dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subdir
|
str
|
Location of the dataset below the data root. When a Zenodo pin is
given the files land in a version directory |
required |
zenodo
|
str | None
|
Version DOIs of the primary record and its mirror. |
None
|
dataverse
|
str | None
|
Version DOIs of the primary record and its mirror. |
None
|
registry
|
dict | str | Path | None
|
Name-to-hash mapping, or the path of a committed registry file. |
None
|
base_urls
|
list[str] | None
|
Direct base URLs, tried before the DOI mirrors (used in tests and for non-DOI sources). |
None
|
data_root
|
str | Path | None
|
Override for the data root; defaults to the resolved FWL_DATA tree. |
None
|
progress
|
bool
|
Show a download progress bar (requires tqdm; useful for large files). |
False
|
extract
|
str | None
|
When set ( |
None
|
lock_timeout
|
float
|
Seconds a fetcher waits for the per-target download lock before giving up and fetching unguarded (default five minutes). The lock only suppresses duplicate concurrent downloads; a waiter that times out (or a filesystem without a working lock manager) falls back to its own fetch rather than blocking or failing. |
_LOCK_TIMEOUT_S
|
Source code in src/fwl_io/fetch.py
820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 | |