User Guide¶
vosfs is an asynchronous fsspec
filesystem for the OpenCADC Cavern VOSpace service. It exposes the vos
protocol so fsspec-aware Python tools can read, write, inspect, and mutate
OpenCADC VOSpace paths.
OpenCADC profile only
vosfs targets the OpenCADC VOSpace profile. It does not claim generic
IVOA VOSpace 2.1 conformance or compatibility with unrelated VOSpace
implementations.
Constructing a filesystem¶
The service is selected only by endpoint_url, an absolute OpenCADC service
base URL. No registry or shortname lookup is performed.
Install vosfs first:
import fsspec
fs = fsspec.filesystem(
"vos",
endpoint_url="https://staging.canfar.net/arc",
max_connections=100,
)
max_connections (default 100) is the number of HTTP requests, including
streamed bodies, that one filesystem will run at once. A caller that omits it
gets that default. The value is part of the filesystem's stored settings.
The apparent authority after the vos:// protocol marker is part of the
filesystem path, not a service selector: vos://a/b, vos:///a/b, /a/b, and
a/b all refer to the same path /a/b.
Credentials¶
At most one credential source may be configured, and each is reread before
every authenticated request. vosfs consumes access tokens but never acquires
or refreshes them.
| Option | Meaning |
|---|---|
token |
A literal bearer token (including an OIDC access token). |
tokenfile |
A file whose bearer token is reread before every request. |
certfile |
A combined X.509 certificate-chain and private-key PEM file. |
If no credential option is given, vosfs falls back to exactly one of the
environment variables VOSFS_TOKEN, VOSFS_TOKEN_FILE, or VOSFS_CERT_FILE.
Providing any explicit credential option ignores all of these variables. With
no credential at all, access is anonymous. endpoint_url must use https
whenever a credential is configured.
Serialization
A literal token is kept for pickle and for to_json() / to_dict().
to_json(include_password=False) omits it. Prefer tokenfile or an
environment source when the filesystem may be serialized.
fs = fsspec.filesystem(
"vos",
endpoint_url="https://staging.canfar.net/arc",
tokenfile="/run/secrets/vos-token",
)
For a CADC proxy certificate, pass an absolute path to the combined certificate-chain and private-key PEM file, or set the equivalent environment variable before constructing the filesystem:
fs = fsspec.filesystem(
"vos",
endpoint_url="https://staging.canfar.net/arc",
certfile="/absolute/path/to/cadcproxy.pem",
)
Do not pass both forms: an explicit credential option ignores all credential
environment variables. Paths beginning with ~ are not expanded; use an
absolute path.
Reading and writing¶
Paths are literal. /project/100%41 names a file containing %41, not
/project/100A. Spaces, ?, and # also remain part of the filename.
vosfs encodes these characters in node and transfer URIs.
Partial cat_file / cat_ranges reads send HTTP Range when the bounds allow.
If the negotiated byte endpoint returns a validated 206, only those bytes are
kept. If it returns 200/204 (Cavern ignores Range), vosfs treats the
body as a whole object and slices locally. Staged open still downloads the
whole object into a disk-backed temporary file. Writes issue one whole PUT.
with fs.open("/project/data.csv", "rb") as handle:
header = handle.readline()
fs.pipe_file("/project/notes.txt", b"hello")
fs.get("/project/data.csv", "local.csv")
fs.put("local.csv", "/project/copy.csv")
Download a complete remote tree with the same public method:
Recursive get is client-derived. vosfs traverses the remote
ContainerNode tree client-side, materializes every container as a local
directory (including empty containers), and downloads each DataNode through
the normal whole-object read path.
Coordinated put and pipe operations create any required remote
ContainerNode parents before writing descendant bytes. A recursive put
also reproduces the local directory tree, including empty directories:
fs.put("local-results", "/project/archive/results", recursive=True)
fs.pipe(
{
"/project/generated/a.txt": b"a",
"/project/generated/b.txt": b"b",
}
)
Each destination file still uses one negotiated whole-object PUT; parent
creation is scoped to the coordinated operation and does not change the
single-file put_file or pipe_file hooks. Cancelling coordinated put or
pipe stops later batch items and waits for started writes to close. A
dispatched PUT remains uncertain, so vosfs invalidates cached destination
and parent state without attempting rollback.
open("rb") downloads once into a disk-backed temporary file and then provides
local read/readinto/readline/iteration/tell/seek. Pass block_size
or cache_type to open a ranged file instead: that open negotiates one byte
endpoint and reads it with Range. A 206 is accepted only when its interval
and byte count match the request. A 200 stages that response and later reads
of the same open stay local. An unknown byte length also uses staging; a link's
metadata size does not describe the bytes of its target. open("wb") and
open("w") disk-stage writes and upload once only after a successful close or
clean context exit. A write-block exception or local close failure before PUT
uploads nothing. Once PUT begins, failure is uncertain and may have truncated
the destination.
Same-service moves can use a server job when available. If the operation is
unsupported, vosfs copies through the client and verifies the destination
before deleting the source. Cancelling or timing out a server job requests its
abort; an uncertain outcome does not start another transfer. Client copies
also work with max_connections=1 by staging the download before uploading.
Scientific Python stack¶
Because vosfs registers the vos protocol, any fsspec-aware tool can read and
write vos:// URLs directly. Pass the endpoint (and at most one credential) as
storage_options, or construct a VOSpaceFileSystem and hand it to the tool.
storage_options mirrors the constructor
Every keyword accepted by VOSpaceFileSystem — endpoint_url, token,
tokenfile, certfile, timeouts — is valid inside storage_options.
Omit the credential to resolve it from VOSFS_TOKEN, VOSFS_TOKEN_FILE, or
VOSFS_CERT_FILE in the environment.
import pandas as pd
storage_options = {"endpoint_url": "https://staging.canfar.net/arc"}
frame = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
frame.to_csv("vos://project/data.csv", index=False, storage_options=storage_options)
restored = pd.read_csv("vos://project/data.csv", storage_options=storage_options)
Save and load through an fsspec file object: .npy and .npz round-trip,
and delimited text is read with np.loadtxt. Any NumPy reader or writer
that accepts a file object works the same way.
import fsspec
import numpy as np
storage_options = {"endpoint_url": "https://staging.canfar.net/arc"}
array = np.arange(12, dtype="int64").reshape(3, 4)
with fsspec.open("vos://project/a.npy", "wb", **storage_options) as handle:
np.save(handle, array)
with fsspec.open("vos://project/a.npy", "rb", **storage_options) as handle:
restored = np.load(handle)
Zarr v3 reads and writes each chunk as a whole object. Use an asynchronous filesystem for the store (requires Python 3.11+).
import numpy as np
import zarr
from vosfs import VOSpaceFileSystem
fs = VOSpaceFileSystem(
endpoint_url="https://staging.canfar.net/arc",
asynchronous=True,
)
store = zarr.storage.FsspecStore(fs, path="/project/array.zarr")
root = zarr.open_group(store=store, mode="w")
data = root.create_array("data", shape=(10,), dtype="int32")
data[:] = np.arange(10, dtype="int32")
reopened = zarr.open_group(store=store, mode="r")
assert reopened["data"][3:6].tolist() == [3, 4, 5]
Wrap the filesystem in a PyFileSystem and address paths without the
vos:// prefix.
import pyarrow as pa
import pyarrow.parquet as pq
from pyarrow.fs import FSSpecHandler, PyFileSystem
from vosfs import VOSpaceFileSystem
fs = VOSpaceFileSystem(endpoint_url="https://staging.canfar.net/arc")
pa_fs = PyFileSystem(FSSpecHandler(fs))
table = pa.table({"a": [1, 2, 3], "b": ["x", "y", "z"]})
pq.write_table(table, "/project/data.parquet", filesystem=pa_fs)
restored = pq.read_table("/project/data.parquet", filesystem=pa_fs)
Whole-object staged open
The default open still downloads whole objects. Pass block_size or
cache_type for ranged reads of one open file. Formats that rely on
random access work by staging locally. Partial cat_file /
cat_ranges may use HTTP Range when the byte endpoint returns
206. For Dask over staged files, pass blocksize=None so each file
is a single partition.
Capability summary¶
Every public behavior carries exactly one classification.
Normative source
The tables below are a reader-friendly digest. The full, authoritative capability matrix lives in the capability contract; where the two differ, the contract wins.
Supported¶
Native operations and the client-derived behaviors composed from them.
-
Inspect
info·ls·exists·isfile·isdir·size·modified -
Traverse
Client-derived from
ls:walk·find·glob·du·tree -
Read
cat·cat_file·cat_ranges·head·tail·read_block·get·get_fileWhole-object transfer; ranges are sliced client-side.
-
Write
pipe·pipe_file·put·put_fileOne whole
PUTper file. -
Open
open("rb" / "r" / "wb" / "w" / "xb" / "x")Backed by a disk-staged temporary file.
-
Directories
mkdir·makedirs -
Delete
rm·rmdir·rm_fileEmpty-check or leaves-first client deletion.
-
Copy
cp·copy·cp_file -
Move
mv·move·renameDataNode/ContainerNode: non-atomic copy/recreate then delete, no overwrite.
LinkNode: unsupported; raises
NotImplementedErrorbefore mutation. -
Identity & metadata
touch(truncate=True)·checksum·ukey -
Serialization
pickle·to_json -
Cache wrappers
simplecache::vos://·filecache::vos://
Unsupported¶
These raise NotImplementedError before any remote mutation, or are absent entirely
The filesystem never silently degrades — an unsupported call fails fast.
| Operation / feature | Why |
|---|---|
blockcache:: / cached:: wrappers |
Not claimed; use simplecache:: / filecache:: |
Append, + mode, offset / atomic / resumable / multipart writes |
One whole PUT per file |
touch(truncate=False) |
Would require a partial update |
rm(..., maxdepth=...) |
Bounded-depth deletion is not modeled |
created timestamps, open_async |
Not provided by the profile |
| Server-side copy, move, search, sort, pagination | Absent from the OpenCADC profile |
| Public property, permission, and link-creation APIs | Out of v0.3.0 scope |
| FUSE mounting | Not supported |
Errors¶
Failures map to the closest standard Python exception. Anything without a
precise match is raised as the single public vosfs.VOSpaceError.
Looking up a failure you actually hit?
Troubleshooting is organized by symptom rather than by exception class.
| Exception | Raised when |
|---|---|
ValueError |
Invalid input — a malformed path, bad option, or oversized/ill-formed XML |
PermissionError |
Authentication or authorization was refused |
FileNotFoundError |
A referenced node or its parent does not exist |
FileExistsError |
The target already exists and the operation will not overwrite |
NotImplementedError |
The operation is unsupported, or the service advertises no matching binding |
OSError (errno.ENOSPC) |
A storage quota is exhausted |
BlockingIOError |
The node is locked |
TimeoutError |
An HTTP request exceeded its timeout |
ConnectionError |
The underlying connection failed |
vosfs.VOSpaceError |
Every remaining OpenCADC, integrity, HTTP, and partial-completion failure |
vosfs.VOSpaceError
A subclass of OSError. It carries the HTTP status, a symbolic fault code,
retry guidance, and — for partial failures — the completed and failed paths.
Redaction and retries
Credentials and pre-authorized URL tokens are redacted from every exception
and log. vosfs never retries automatically; callers own higher-level retry
policy.
Logging¶
vosfs reports transfer progress through the standard-library logging
module under the vosfs logger. It installs only a NullHandler, so nothing
is printed unless your application configures logging.
INFO: one record per completed file operation, for exampledownloaded /data/a.fits -> /tmp/a.fits (2880 bytes, 0.214 s). Outcomes aredownloaded,uploaded,copied,staged(anopen("rb")download),removed, andmoved. Each record also carries the structured attributesvosfs_outcome,vosfs_source,vosfs_destination,vosfs_bytes, andvosfs_secondswhere they apply.DEBUG: the negotiated byte endpoint and security method, Range fallbacks, and MD5 comparisons after uploads.
Logged URLs never include a query string or fragment, and pre-authorized tokens are redacted.
import logging
logging.basicConfig(level=logging.INFO)
logging.getLogger("vosfs").setLevel(logging.DEBUG)
Closing¶
Use fs.invalidate_cache(path) to discard cached listings and metadata indexes
for that path, its descendants, and its parent. Omit path to clear all cached
directory metadata.
aclose() (async) and close() (sync) release every HTTP client, evict the
instance from fsspec's cache, and make later I/O fail as closed. Both calls are
idempotent.
A live filesystem belongs to the process that constructed it. Do not use an
inherited instance after fork(); reconstruct it in the child or worker from
pickle, fsspec JSON, or the original storage options. Reconstruction creates
fresh event-loop, lock, HTTP-client, service-binding, and cache state. Dask
tokenization depends only on the serialized constructor options, not that live
runtime state.
Close a filesystem deterministically when its work is finished: