Getting started
Fetch a file into the shared cache, read it back offline, and decode it into native-grid arrays.
EarthSciIO resolves a URL, downloads it once into a content-addressed cache shared by the Python, Julia and Rust implementations, and decodes the cached file into native-grid arrays. This guide follows one small file through both steps. Each snippet is cut from a program that runs, and checks its own result, in CI.
The examples read stations.csv, a four-row table of air-quality readings from
two stations, through a file:// URL so they need no network.
Fetch into the cache
The cache key is the SHA-256 of the resolved URL, so the same URL always lands on the same blob, whichever language fetched it. The first fetch downloads; the second is a hit.
Python
cache = Cache(root=cache_root, offline=False)
entry = cache.fetch(URL, source_loader="stations")
assert entry.status == "downloaded"
assert entry.path.read_bytes() == (FIXTURES / "stations.csv").read_bytes()
# The blob is addressed by the SHA-256 of the resolved URL.
assert entry.key == earthsciio.cache_key(URL)
# Asking again is a cache hit: no transfer happens.
again = cache.fetch(URL, source_loader="stations")
assert again.status == "hit" and again.path == entry.path
Julia
cache = Cache(; root = CACHE_ROOT, offline = false)
entry = fetch_blob(cache, URL; source_loader = "stations")
@assert entry isa CacheEntry
@assert entry.status == :downloaded
@assert read(entry.path) == read(joinpath(FIXTURES, "stations.csv"))
# The blob is addressed by the SHA-256 of the resolved URL.
@assert entry.key == cache_key(URL)
# Asking again is a cache hit: no transfer happens.
again = fetch_blob(cache, URL; source_loader = "stations")
@assert again.status == :hit && again.path == entry.path
Rust
let cache = Cache::builder()
.data_dir(cache_root.path())
.offline(false)
.build()?;
let blob = cache.fetch(&FetchRequest::new(&url).loader("stations"))?;
assert_eq!(std::fs::read(&blob.path)?, std::fs::read(&source)?);
// The blob is addressed by the SHA-256 of the resolved URL.
assert_eq!(blob.key, cache_key(&url));
// Asking again returns the same blob without a transfer.
let again = cache.fetch(&FetchRequest::new(&url))?;
assert_eq!(again.path, blob.path);
Work offline
Offline mode serves only what is already cached. A request that is not cached fails with a cache miss rather than opening a connection, which makes test runs hermetic.
Python
offline = Cache(root=cache_root, offline=True)
assert offline.fetch(URL).path == entry.path # served from disk only
try:
offline.fetch((FIXTURES / "not-cached.csv").as_uri())
raise AssertionError("an offline miss must raise")
except CacheMiss:
pass # offline mode never touches the network
Julia
offline = Cache(; root = CACHE_ROOT, offline = true)
@assert is_offline(offline)
@assert fetch_blob(offline, URL).path == entry.path # served from disk only
missed = try
fetch_blob(offline, "file://" * abspath(joinpath(FIXTURES, "not-cached.csv")))
false
catch err
err isa CacheMiss # offline mode never touches the network
end
@assert missed
Rust
let offline = Cache::builder()
.data_dir(cache_root.path())
.offline(true)
.build()?;
assert!(offline.is_offline());
assert_eq!(offline.fetch(&FetchRequest::new(&url))?.path, blob.path); // served from disk only
// An offline miss is an error, never a network request.
let miss = offline
.fetch(&FetchRequest::new("file:///not/cached.csv"))
.unwrap_err();
assert!(miss.is_cache_miss());
Choose the cache location
The cache root defaults to $EARTHSCIDATADIR; the constructors take an explicit
root.
Python
root = earthsciio.resolve_cache_root(cache_root)
assert Path(root) == cache_root
Read native-grid arrays
A provider ties a data source to the cache and to a format reader, and returns the file’s variables as raw native-grid arrays. Variable renaming, unit conversion and regridding are left to the rest of the stack.
Python
source = DataSource(
"stations",
"csv", # the format-registry key that selects the reader
(FIXTURES / "stations.csv").as_uri(),
variables=["value", "location_id"],
reader_kwargs={"numeric_columns": ["latitude", "longitude", "value"]},
)
data = Provider(source, cache).materialize()
assert isinstance(data, NativeDataset)
assert data.variable_names() == ["location_id", "value"]
assert data["value"].data.dtype == np.float64
assert list(data["value"].data) == [152.3, 168.7, 98.1, 110.4]
# Digit-only text columns stay strings: nothing is inferred.
assert data["location_id"].data == ["1", "1", "2", "2"]
Julia
url = "file://" * abspath(joinpath(FIXTURES, "stations.csv"))
p = const_provider(cache, url; format = "csv", source_loader = "stations",
variables = ["value", "location_id"],
reader_kwargs = (numeric_columns = ["latitude", "longitude", "value"],))
@assert is_const(p) && refresh_times(p) == Float64[] # a CONST provider never refreshes
data = materialize(p)
@assert data isa NativeDataset
@assert Set(variable_names(data)) == Set(["location_id", "value"])
@assert data["value"].data == [152.3, 168.7, 98.1, 110.4]
# Digit-only text columns stay strings: nothing is inferred.
@assert data["location_id"].data == ["1", "1", "2", "2"]
The full operation list is in the API reference , and the contract all three implementations follow is in the provider spec .