globusfs
An fsspec filesystem for Globus collections.
Status: working. Verified against three live collections — including ALCF Eagle (
alcf#dtn_eagle, 747 project directories), wherels,glob,info,open()with mid-file seek, and sparse ranged reads all work against production Lustre. Writes (PUT/DELETE) round-trip on Globus Tutorial Collection 1. Anonymous pyarrow column projection works on a public collection — one column of sixty, a few KB instead of 360 KB — while that collection was intermittently returning backend-fault 404s.
import globusfs
# Browser login once; tokens persist to ~/.globusfs/tokens.json
fs = globusfs.filesystem("<collection-uuid>")
fs.ls("/")
with fs.open("data/file.parquet", "rb") as f:
...
Public collections need no credentials:
import fsspec, pyarrow.parquet as pq
fs = fsspec.filesystem(
"globus",
collection_id="isaac",
https_url="https://g-05a4b6.2d513.8443.data.globus.org",
)
with fs.open("isaac/ability/ALL_2007-01.parquet", "rb") as f:
# Reads only the bytes this column needs, over HTTP range requests.
table = pq.ParquetFile(f).read(columns=["author"])
Why
Globus is how large scientific datasets actually move between facilities,
but there is no fsspec backend for it — so pyarrow, pandas, dask, and
grain can't read a Globus collection the way they read s3:// or
gs://. This fills that gap with one backend that serves all of them.
How it works
Globus Connect Server exposes two services, and this needs both:
| Concern | Service | Why |
|---|---|---|
| Reading bytes | HTTPS collection endpoint | Serves full HTTP range semantics: 206, Content-Range, mid-file seeks, multipart |
| Listing / metadata | Transfer API | The HTTPS interface has no directory listings |
The read path subclasses fsspec's HTTPFileSystem, which already
speaks exactly the range dialect GCS serves.
Server quirks this works around
Verified against a live collection:
- Backend flakiness surfaces as a
404. GCS load-balances across GridFTP backends; a failing one returnsENDPOINT_ERROR/GCS Manager Internal Errorrendered as HTTP 404 — byte-identical in status to a genuinely missing file, and sticky for the life of a connection. Observed failure rates on the public test collection swung from 0/20 to 20/20 within minutes, hitting files, directories, and the collection root alike. Retries need a fresh connection, and the only way to tell a transient error from a real miss is to parse the body. A client that treats 404 as "absent" will report healthy data as missing. - Suffix ranges (
bytes=-8) return416, which is how parquet readers typically seek to the footer. Becauseinfo()knows the true size, readers can use absolute offsets instead. HEADis unusable. A HEAD 404 carries no body — and the body is the only thing distinguishing a backend fault from a real miss. So HEAD results are permanently ambiguous. Size and existence come from the Transfer API, or from a rangedGET(which does return a body and carries the total inContent-Range).- The HTTPS interface has no directory listings at all — hence the Transfer API for metadata.
Known ALCF collections
Resolved via endpoint_search; ALCF's docs list names, not UUIDs.
| Collection | UUID | Type |
|---|---|---|
alcf#dtn_eagle |
05d2c76a-e867-4f67-aa57-76edeb0beda0 |
mapped |
alcf#dtn_flare |
f39a7a0f-5bfc-46ce-9615-ba9f8592814f |
mapped |
alcf#dtn_grand |
3caddd4a-bb35-4c3d-9101-d9a0ad7f3a30 |
mapped |
| Globus Tutorials on ALCF Eagle | a6f165fa-aee2-4fe5-95f3-97429c28bf82 |
guest, public |
Display names work too — globusfs.filesystem("alcf#dtn_eagle") resolves
the name to its UUID before building any scopes. (Passing a name to
login() directly requires an authenticated client to resolve it; UUIDs
never need a lookup.)
Eagle's collection root is already /eagle/projects, so paths are
project-relative: fs.ls("/datascience"), not /eagle/projects/datascience.
Expect roughly 2 s per metadata or read operation through the DTN — fine for sparse reads and exploration, not for per-record access in a training loop. See the note on training workloads below.
Credentials
Token acquisition is pluggable, because it varies more than anything else: a public collection needs nothing, a portal already has a token, an interactive user needs a browser.
| Provider | Use |
|---|---|
AnonymousCredentials |
Public collections (default) |
StaticToken |
A token you already hold |
CallableToken |
Fetch on demand — the pickle-safe option |
AppCredentials |
Wraps globus_sdk UserApp/ClientApp |
Two constraints worth knowing, both from the globus-sdk docs:
GlobusAppis not thread-safe, but fsspec shares one filesystem across threads.AppCredentialsserializes every call through a lock.- fsspec pickles filesystems to worker processes. A live token in the
constructor args would be copied into every worker payload, so
StaticTokenrefuses to pickle; useCallableTokenreading from the environment or shared storage so workers re-read rather than receive.
Also set request_refresh_tokens=True — it defaults to False, and
without it a long run dies when the access token expires mid-epoch.
Workers and clusters
A filesystem from globusfs.filesystem() is picklable, so it can be sent
to dataloader workers or a dask cluster. Workers reload tokens from
disk rather than receiving them, so on a cluster token_path must be
reachable from every node:
fs = globusfs.filesystem(COLLECTION, token_path="/eagle/myproject/.globus-tokens.json")
Two consequences worth knowing. A worker that finds no usable tokens fails immediately instead of blocking on a browser prompt nobody can answer — log in once on the head node first. And no bearer token appears in the pickled bytes, so the credential stays where you put it.
A note on training workloads
This is built for remote and sparse reads: column projection, exploration, data not yet staged. For distributed training, staging with Globus Transfer to node-local scratch beats per-record HTTPS on every axis — no per-record latency, no token expiry mid-epoch, and it works with formats like ArrayRecord whose readers do their own seeking.
Using it with grain
Grain wants a RandomAccessDataSource — __len__ plus
__getitem__, thread-safe, picklable, and __repr__ for checkpointing.
globusfs supplies the bytes; a small source supplies the indexing. This
one indexes parquet row groups:
import fsspec
import pyarrow.parquet as pq
import grain
class ParquetSource:
def __init__(self, path, **so):
self._path, self._so = path, so
self._fs = None
with self._open() as f:
self._n = pq.ParquetFile(f).metadata.num_row_groups
def _open(self):
# Built lazily so the source stays picklable for grain workers.
if self._fs is None:
self._fs = fsspec.filesystem("globus", **self._so)
return self._fs.open(self._path, "rb")
def __len__(self):
return self._n
def __getitem__(self, i):
with self._open() as f:
return pq.ParquetFile(f).read_row_group(int(i), columns=["author"])
def __getstate__(self):
return {**self.__dict__, "_fs": None} # workers rebuild it
def __repr__(self):
return f"ParquetSource({self._path!r}, n={self._n})"
src = ParquetSource(
"isaac/ability/ALL_2007-01.parquet",
collection_id="isaac",
https_url="https://g-05a4b6.2d513.8443.data.globus.org",
)
ds = grain.MapDataset.source(src).shuffle(seed=10)
Index row groups rather than individual records. Each __getitem__ is at
least one HTTP round-trip, so per-record indexing over a wide-area link
means one round-trip per record — see the note on training workloads.
Note also that ArrayRecordDataSource will not work over globus://:
its reader is C++ and does its own file seeks, so it cannot route through
a Python filesystem. Formats with Python-level readers (parquet, or your
own offset index) are the ones that work.
Pairing with Globus Streaming
Globus Streaming solves a different problem, and the two compose rather than compete.
globusfs |
Globus Streaming | |
|---|---|---|
| Abstraction | files with byte offsets | TCP socket between two processes |
| Addressing | globus://<uuid>/path |
<host>:<port> contact string |
| Random access | yes — seek, ranged reads |
no; ordered byte stream |
| Data at rest | reads stored files | no file access at all |
| Mechanism | HTTPS + Transfer API | LD_PRELOAD socket interception |
| Platform | any Python 3.10+ | Linux x86_64, Python 3.12+ |
Streaming intercepts only connection establishment — bind(),
connect(), getaddrinfo — so unmodified binaries tunnel through
firewalls with no per-byte overhead, but there is no file to open and no
offset to seek to. It cannot back an fsspec filesystem. Conversely
globusfs pays an HTTP round-trip per read and cannot see a live feed.
The natural split is streaming writes it, globusfs reads it back:
an instrument ships frames to a facility during an experiment, and
analysis addresses the resulting files afterward.
During the experiment — on the instrument host, pipe the detector into a socket that Globus tunnels to the facility:
# One-time: create a tunnel at https://app.globus.org/streams
globus-streams environment initialize --globus-contact dtn.alcf.anl.gov:8888 "$TUNNEL_ID"
# Frames leave the instrument through an ordinary TCP socket.
globus-streams-launch.sh "$TUNNEL_ID" ./detector-stream --host dtn.alcf.anl.gov --port 8888
On the facility side a listener accepts the stream and writes it to the collection's filesystem — plain files on Lustre, nothing Globus-specific:
globus-streams environment initialize --listener-contact-string 10.0.2.164:8888 "$TUNNEL_ID"
globus-streams-launch.sh "$TUNNEL_ID" ./ingest --out /eagle/myproject/run042/
Afterwards — the frames are now stored files, so globusfs
addresses them from anywhere, no tunnel and no login on the instrument:
import globusfs
fs = globusfs.filesystem("05d2c76a-e867-4f67-aa57-76edeb0beda0") # alcf#dtn_eagle
frames = fs.glob("/myproject/run042/*.h5")
print(f"{len(frames)} frames landed")
# Read only what you need: headers first, before pulling any bulk data.
for path in frames[:10]:
with fs.open(path, "rb") as f:
magic = f.read(8)
f.seek(fs.info(path)["size"] - 4)
footer = f.read(4)
print(path, magic, footer)
Why the handoff is clean: streaming's job ends once bytes are on disk,
and that is exactly where globusfs starts. Neither knows about the
other.
Two caveats. Streaming authenticates every leg of the route but leaves end-to-end encryption to your application. And for the analysis pass, if you are reading every byte of every frame repeatedly — training rather than inspection — stage with Globus Transfer instead, per the note above.
Tests
Characterization tests hit a real public collection and are marked
network:
pytest # everything
pytest -m "not network" # offline only
License
MIT
Metadata
Release files for globusfs 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| globusfs-0.2.1.tar.gz | 134.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| globusfs-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 160.4 kB
Release files / globusfs-0.2.1.tar.gz
| Download URL | globusfs-0.2.1.tar.gz |
|---|---|
| Size | 134.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
82b9972a727b495f1fe9d48b738266f3ee3872168d81185abc184c5cf0d30af8
|
|
BLAKE2b-256 checksum How to use checksums |
00d8406de31714f05d7411ed17306eb0638b387a081113109f2b622bbe8e90f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logRelease files / globusfs-0.2.1-py3-none-any.whl
| Download URL | globusfs-0.2.1-py3-none-any.whl |
|---|---|
| Size | 25.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
900a8c6aea3db73024e0560d8ba4d65a81edf74cfcd87eb791c268f6fe0b860e
|
|
BLAKE2b-256 checksum How to use checksums |
c93db719e034e48ea18d9615bcf422afe4ecd85f1ed9a90cbd945bacd86487c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency log