Skip to content

Objects, files, and JSON

The URI scheme selects the backend. The examples use s3://. Local paths, file://, and gs:// support the same operations. HTTP has fewer operations.

Backend support

Operation Local, S3, GCS HTTP/HTTPS
Read, download, check existence Yes Yes
Write, create, upload Yes No
List, count, delete Yes No
Locate Yes No

Unsupported operations raise ondio.UnsupportedOperationError. Windowed FLAC reads over HTTP also need a server that honors range requests; see FLAC over HTTP.

The base install supports local files and S3. Install ondio[gcs] for GCS and ondio[http] for HTTP. S3 uses the credentials and configuration available to boto3. GCS uses the credentials and configuration available to the Google Cloud Storage client. Configure those credentials before accessing a private bucket. To pick an AWS profile or another client setting per call, see Backend options. To build a backend once and use it for many calls, see Reusing a backend.

Bytes in memory

read returns the whole object as bytes, and write stores bytes at a URI. Neither touches the local filesystem.

import ondio

data = ondio.read("s3://my-bucket/audio/rec.flac")
ondio.write("s3://my-bucket/results/example.txt", b"done")

Byte ranges

read_range reads the bytes from start_byte to end_byte, both included. end_byte=None reads to the end. A range past the end returns the bytes that exist, possibly none.

head = ondio.read_range("s3://my-bucket/audio/rec.flac", 0, 3)
assert head == b"fLaC"

The result is the stored bytes, not a playable file. For a time window of a FLAC file, use read_flac.

JSON

write_json serializes a Python value and writes it; read_json reads and parses one. No local file is involved.

ondio.write_json("s3://my-bucket/results/summary.json", {"detections": 118})
summary = ondio.read_json("s3://my-bucket/results/summary.json")

Files on disk

download streams an object to a local path, creating parent directories. upload streams a local file to a URI without reading it into memory, replacing any object there. The URI always comes first, the local path second. A failed download leaves no partial file: the data goes to a temporary file beside the local path, which replaces the local path only when the download is complete. A file that was already at the local path stays as it was.

ondio.download("gs://my-bucket/audio/rec.flac", "data/rec.flac")
ondio.upload("s3://my-bucket/audio/rec.flac", "data/rec.flac")

With overwrite=False, upload writes the file only if no object exists at the URI, as create writes bytes, and raises ObjectExistsError otherwise. It works on S3, GCS and local disk. On S3 it is one conditional PUT, so the file can be at most 5 GiB; a larger file raises UnsupportedOperationError before anything is sent. As with create, on S3 and GCS ObjectExistsError can name the object that the same call wrote, when the client retried after a lost response.

try:
    ondio.upload("s3://my-bucket/runs/1/rec.flac", "data/rec.flac", overwrite=False)
except ondio.ObjectExistsError:
    ...  # another writer got there first

On local disk, upload writes to a temporary file beside the target and moves it into place, so a reader never sees a partly written file. If the target is a link, the file it points to is replaced and the link stays.

download_files downloads many objects into one directory at once. Each file is named by its basename, and the returned local paths follow the input order. If two URIs have the same basename, the call raises ValueError before downloading.

uris = ondio.list_files("s3://my-bucket/audio/", required_ext="flac")
local = ondio.download_files(uris, "results/", max_workers=8)

upload_files goes the other way: it uploads every file under a local folder to a prefix, keeping the relative paths, and returns the URIs sorted by relative path. Hidden files are uploaded too, and empty folders make no objects. A link to a file is uploaded with the content it points to; a link to a folder is not entered. A folder that cannot be read raises PermissionError, and an entry that is not a file, such as a broken link, raises FileNotFoundError, both before anything is uploaded. overwrite=False works as it does for upload. The first failure stops the call; files that already finished stay uploaded.

uris = ondio.upload_files("s3://my-bucket/runs/2", "results/", max_workers=8)

Create only if absent

create writes bytes only if no object exists at the URI, and a reader never sees a partial object.

try:
    ondio.create("s3://my-bucket/runs/1/result.json", b"{}")
except ondio.ObjectExistsError:
    ...  # another writer got there first

create works on S3 (a conditional PUT with If-None-Match: *), on GCS (a conditional upload with if_generation_match=0), and on local disk (a temporary file linked into place). On HTTP, HttpBackend raises UnsupportedOperationError. Missing credentials raise AuthError: no request was sent. A connection failure or timeout raises NetworkError with the message "could not confirm create". If create fails for any reason other than ObjectExistsError or AuthError, the object may or may not exist; read it to find out. On S3 and GCS, ObjectExistsError can name an object that the same call wrote, when the client retried after a lost response; compare the bytes if you need to know who wrote it.

Where a URI points

locate gives the store and the path in it that ondio reads, without touching storage.

ondio.locate("s3://my-bucket/runs/1/out")
# Location(store='s3://my-bucket', path='runs/1/out')

Listing and counting

list_files returns the full URIs under a folder path, sorted. required_ext and required_prefix filter by filename, and they compose with max_items. object_count counts with the same filters, without listing.

uris = ondio.list_files("s3://my-bucket/results/")
flacs = ondio.list_files("s3://my-bucket/audio/", required_ext="flac")
chunks = ondio.list_files("s3://my-bucket/audio/", required_prefix="chunk_", max_items=100)
n_flacs = ondio.object_count("s3://my-bucket/audio/", required_ext="flac")

Listing prefixes have folder semantics on every backend: a path names the object at that exact location plus everything under it as a folder, never sibling objects that merely share leading characters — s3://my-bucket/results/run-1 matches results/run-1/… but not results/run-10/…. To match objects by partial filename (e.g. everything starting with chunk_), use required_prefix as above.

A trailing slash narrows the path to folder contents only, excluding an object stored at the path itself. This matters only where both exist:

ondio.list_files("s3://my-bucket/results/run-1")   # the run-1 object, if any, plus run-1/…
ondio.list_files("s3://my-bucket/results/run-1/")  # only run-1/… — never the run-1 object

object_count(prefix) always equals len(list_files(prefix)) given the same filters, on every backend — folder placeholder objects (the empty dir/ keys some consoles create) are counted by neither.

Checking and deleting

exists says whether an object exists. delete removes it, and deleting a missing object does nothing.

if ondio.exists("s3://my-bucket/tmp/scratch.json"):
    ondio.delete("s3://my-bucket/tmp/scratch.json")

Errors

Every storage failure raises a type of ondio.OndioError. An error from boto3, google-cloud-storage or requests is translated, and the original exception is its __cause__.

Error Meaning
ObjectNotFoundError No object at the URI
AuthError Missing, wrong or expired credentials, or access refused
NetworkError No full answer from the store: the connection failed or closed, the response stopped in the middle, or it timed out. The request can be retried
UnsupportedOperationError The backend cannot do this, e.g. write over HTTP
ObjectExistsError create found an object at the URI
OndioError Any other storage failure

A bad request raises ValueError, not OndioError: an inverted byte range, a FLAC window that is empty, a malformed URI, or an S3 request that botocore refuses before sending it, such as one with an empty key. So code that falls back to another source can catch OndioError for "the fetch failed" and let ValueError show a bug.

try:
    data = ondio.read("s3://my-bucket/audio/missing.flac")
except ondio.ObjectNotFoundError:
    data = None

A missing extra (for example ondio[gcs]) raises ImportError.

Backend options

Keyword arguments go to the backend's constructor: boto3.Session for s3://, google.cloud.storage.Client for gs://. For example, to pick an AWS profile:

ondio.list_files("s3://my-bucket/audio/", profile_name="dse")

For http(s)://, the options go to HttpBackend; see Private HTTPS files.

Reusing a backend

Each call builds a new backend from the URI and its options. To use one client and its connections for many calls, build the backend once and pass it as backend=. Every public function takes it. With backend=, other options raise ValueError: give them to the backend instead.

from ondio.backends import AwsBackend

s3 = AwsBackend(region_name="us-east-1")
header = ondio.extract_flac_header("s3://my-bucket/audio/rec.flac", backend=s3)
samples, rate = ondio.read_flac("s3://my-bucket/audio/rec.flac", 10.0, 15.0, backend=s3)

AwsBackend(client=...) and GcsBackend(client=...) use a client that you made. No other option can be given with client.

from google.cloud import storage
from ondio.backends import GcsBackend

gcs = GcsBackend(client=storage.Client(project="my-project"))
ondio.read("gs://my-bucket/audio/rec.flac", backend=gcs)

download_files and upload_files give the one backend to all their threads. S3 clients and HttpBackend can be shared that way. A google-cloud-storage client does not promise it; give download_files and upload_files no GCS backend if that matters to you.

ondio does not check that the backend matches the URI scheme. A wrong backend fails at its first call.

Private HTTPS files

HttpBackend takes these options, as keywords:

Option Default Use
headers none Headers sent with every request
auth none Anything requests takes as auth, e.g. ("user", "password")
cookies none Cookies sent with every request
verify True False skips the TLS certificate check; a path names a CA bundle
timeout (10, 60) Seconds: (connect, read), one number, or None for no limit
allow_redirects True If False, a 3xx answer raises OndioError

A bearer token is a header. When a redirect goes to another host, ondio sends it no Authorization, no cookies, and none of the headers you gave. No ondio error message holds an option's value.

from ondio.backends import HttpBackend

web = HttpBackend(headers={"Authorization": "Bearer <token>"}, timeout=(5, 30))
samples, rate = ondio.read_flac("https://example.org/rec.flac", 0.0, 5.0, backend=web)

The same options also work per call, without a backend: ondio.read(url, headers={...}). A Range header is refused with ValueError: use read_range.

The read timeout limits each wait for data, not the whole request. A server that keeps sending data slowly is not stopped by it.

Public buckets

To read a public S3 or GCS bucket with no credentials, pass anonymous=True:

ondio.read("s3://a-public-bucket/rec.flac", anonymous=True)
ondio.list_files("gs://a-public-bucket/audio/", anonymous=True)

On S3 the requests are unsigned, and only endpoint_url and region_name can be given with it. No profile is used, also not one that AWS_PROFILE names. On GCS the client has no credentials and no project, and only endpoint_url can be given with it. Other options raise ValueError. A bucket that is not public then raises AuthError, so a caller can try with credentials first and retry with anonymous=True on AuthError. For any other client, pass it as client=, for example AwsBackend(client=my_s3_client).

Tests with a fake backend

backend= also takes a test double. A backend is any object with the methods of StorageBackend, each taking the URI first. This one serves s3:// URIs from a local directory:

from pathlib import Path

from ondio.backends import LocalBackend


class S3OnDisk:
    def __init__(self, root):
        self._root = Path(root)
        self._local = LocalBackend()

    def __getattr__(self, name):
        method = getattr(self._local, name)
        return lambda uri, *args, **kwargs: method(
            str(self._root / uri.removeprefix("s3://")), *args, **kwargs
        )


Path("fake/my-bucket").mkdir(parents=True)
Path("fake/my-bucket/a.txt").write_bytes(b"hello")
assert ondio.read("s3://my-bucket/a.txt", backend=S3OnDisk("fake")) == b"hello"

StorageBackend is part of the public API: a change to its methods is a breaking change.

Endpoints

endpoint_url= sets the S3 or GCS endpoint, for example an S3 store that is not AWS. To make another process (a worker, a batch job) use the same endpoint, get_environment gives the environment variables to set there; that process then calls ondio with no options:

ondio.read("s3://my-bucket/runs/1/out", endpoint_url="http://127.0.0.1:9000")
env = ondio.get_environment("s3://my-bucket/runs/1", endpoint_url="http://127.0.0.1:9000")
# {"AWS_ENDPOINT_URL_S3": "http://127.0.0.1:9000"}: add it to the worker's environment

An option that no variable carries (profile_name, region_name, or endpoint_url on GCS) raises UnsupportedOperationError, rather than being dropped. The other process must not set AWS_IGNORE_CONFIGURED_ENDPOINT_URLS, which makes S3 ignore the endpoint variable.