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
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 243 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 280 281 282 | |
fetch(fname, offline=None)
Return a verified local path for fname, downloading if needed.
Source code in src/fwl_io/fetch.py
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | |
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
500 501 502 503 504 505 506 507 508 509 510 511 512 513 | |
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
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 | |
is_fetched()
Return whether a completed :meth:fetch_all left this dataset in place.
That is a current stamp and every registry file present, or for an archive dataset
every member the stamp records. Contents are not hashed again; fwl-io check
does that.
Source code in src/fwl_io/fetch.py
515 516 517 518 519 520 521 522 523 524 525 526 | |
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
824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 | |
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
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 | |
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; useful for large files. The bar needs
tqdm and a |
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
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 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 | |
read_stamp(directory)
The stamp record in directory, or None if there is no usable one.
Every reader of a stamp goes through here, so none of them has to
rediscover that the file may be absent, unreadable, not JSON, JSON that
is not an object, or an object written to a schema this version does
not know. The third is the one worth naming: a stamp is an ordinary
file that can be edited or truncated, and a reader that parsed []
and then asked it for a key would raise where it should have decided
the stamp says nothing.
The schema is what makes the rest of that safe over time. A stamp written by a future version can be well-formed JSON in a shape whose fields no longer mean what they did, and the fields this version reads would then be trusted while meaning something else; an unrecognised schema is treated as no stamp, so the tree is refetched and restamped rather than misread.
Source code in src/fwl_io/fetch.py
165 166 167 168 169 170 171 172 173 174 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 | |