Read and write bytes through yggdryl's one positional handle (IOBase / Holder) on memory, local files, Arrow filesystems, S3 / Google Cloud Storage / Azure Blob, HTTP(S) URLs, page caches and ZIP archives, with gzip/zlib/zstd codings and charsets; talk HTTP like requests (http.Session, get/post, send_all fan-outs, resumable streams, HTTP/2 and HTTP/3, an in-process test Server). Use when opening a path or URL, reading a range or streaming chunks (read_range_bytes / readRangeBytes, pstream_bytes / pstreamBytes), cursors, listing or globbing a folder (ls, glob, rglob), clear/remove, open/close scopes, compress_into / decompressInto, gzip.dumps / zstd.loads, Charset / charset.decode, digests or read_scalar on a handle, S3Options / S3File, AWS credentials (aws::Session, profiles, SSO, assume role) or a MinIO / S3-compatible endpoint, Buffered caches, Counted call budgets. Covers Rust, Python and Node.js.
Resources
1Install
npx skillscat add platob/yggdryl/yggdryl-storage Install via the SkillsCat registry.
Storage: handles, bytes, codings, charsets
Every storage backend is one positional IOBase handle; Holder is the Rust
enum over all of them and the bindings expose it as one IOBase class. A
caller writes the same calls whatever it holds: pread/pwrite are the
primitives and every whole read, stream, digest, coding and record surface
derives from them. Hold one mental model: a call is a round trip - one
syscall, one object-store request, one lock per wrapper - so the fastest code
is the one that makes the fewest calls, and construction makes none.
Records on a handle (read_arrow_reader, overwrite/append/merge,RecordOptions, partitions) are yggdryl-records; JSON/YAML/TOML/XML codecs
are yggdryl-documents; parsing a URL, a glob or a Hive path is yggdryl-uri.
Install and cross-language conventions are in yggdryl.
Choose the door
| Task | Rust | Python | JavaScript |
|---|---|---|---|
| handle from a path, role decided later | Holder::local(path)? |
IOBase(path) (composes coding + media, see below) |
new IOBase(path), IOBase.from(value) |
| handle from a URL (scheme picks backend) | Holder::from_url(&url, [("media_type", "...")])? |
IOBase("s3://b/k"), IOBase(Url(...)) |
new IOBase('s3://b/k') |
| in-memory bytes | Buffer::new(), Buffer::from_bytes(vec), Holder::buffer(b) |
IOBase.from_bytes(data) |
IOBase.fromBytes(buf) |
| declare what bytes are | Buffer::with_media_type(mt), set_media_type(mt) |
h.media_type = "text/csv" |
h.mediaType = 'text/csv' |
| commit to a role, stored bytes | LocalFile::new(p)?, LocalFolder::new(p)?, LocalPath::new(p)? |
LocalFile(p), LocalFolder(p), LocalPath(p) |
n/a: one IOBase class |
| well-known roots | LocalFolder::temporary()? / home()? / config()? |
LocalFolder.temporary() / home() / config() |
n/a |
| whole read / write | read_all_bytes()?, write_all_bytes(b)? |
read_bytes(), read_text(), write_bytes(b), write_text(s) |
readBytes(), readText(), writeBytes(b), writeText(s) |
| ranged read | read_range_bytes(offset, len)? |
read_range_bytes(offset, len), read_range(o, n, cls=str) |
readRangeBytes(offset, len), readRange(o, n, { text: true }) |
| positional write, append | pwrite(o, b)?, append_bytes(b)? -> offset |
pwrite(o, b), append_bytes(b), append(str_or_bytes) |
pwrite(o, b), appendBytes(b), append(strOrBytes) |
| bounded chunks | pstream_bytes(position, batch_size)? |
pstream_bytes(position=0, batch_size=65536) |
pstreamBytes(position?, batchSize?) |
| cursor | cursor(), cursor_at(n) + IOCursor (tell, seek_to, read_next, write_next, stream_bytes), std::io::Read/Write/Seek |
cursor(pos): read, readinto, write, seek(o, whence), tell, stream_bytes |
cursor(pos): read, write, seek, tell, position, streamBytes |
std::io adapters |
reader_at(o), writer_at(o) |
the cursor is file-like | n/a |
| size, kind, existence | size(), kind(), is_container(), kind().is_known() |
size, kind, exists(), is_dir(), is_file() |
size, kind, exists(), isDir(), isFile() |
| child, parent | child_by_path("a/b.bin")?, parent() |
h / "a/b.bin", joinpath(...), parent |
joinpath('a/b.bin'), parent |
| list, glob | ls(recursive, include_private), glob(pattern, include_private)? |
ls(recursive=False), iterdir(), glob(p), rglob(p) |
ls(recursive?), iterdir(), glob(p), rglob(p), [...h] |
| make a folder | LocalPath::as_directory()?.create()?, truncate(0)? on a folder |
mkdir() (returns the folder role) |
mkdir() |
| empty / delete | clear()?, remove(recursive)? |
clear(), remove(recursive=False) |
clear(), remove(recursive?) |
| media type, stored coding | media_type(), codec() |
media_type, codec (None = identity) |
mediaType, codec (null = identity) |
| decoded view of coded bytes | Coded::wrap(h, Codec::Gzip), Coded::infer(h), gzip::Gzip::new(h), holder.into_coded() |
IOBase("x.log.gz"), LocalPath(p).into_coded(codec=None, level=None) |
n/a: byte methods address stored bytes |
| move bytes adding/removing a coding | compress_into(&mut t, codec)?, decompress_into(&mut t)?, copy_into(&mut t)? |
compress_into(t, codec=None, level=None), decompress_into(t), copy_into(t) |
compressInto(t, codec?, level?), decompressInto(t), copyInto(t) |
| whole-buffer codec | gzip::dump/load, zlib::dump_raw/load_raw, zstd::dump_with_level(b, Level::BEST), Codec::Zstd.dump(b) |
gzip.dumps(b, level=None), gzip.loads(b), zlib.dumps_raw, zstd.dumps |
gzip.dumps(b, level?), gzip.loads(b), zlib.dumpsRaw, zstd.dumps |
| streaming codec | gzip::reader(r), gzip::writer(w) + finish()?, Codec::reader/writer |
n/a (use a coded handle) | n/a |
| charset decode / encode | Charset::Cp1252.decode(b)?, .encode(s)?, .decode_lossy(b), .transcribe(b) |
charset.decode(name, b), encode, decode_lossy |
charset.decode(name, b), encode, decodeLossy |
| byte-order mark, names | Charset::from_bom(b), Charset::from_str(s)?, as_str() |
charset.from_bom(b), bom(name), canonical_name(name) |
charset.fromBom(b), bom(name), canonicalName(name) |
| whole resource in a charset | Transcoded::new(h, Charset::Cp1252), Transcoded::infer(h) |
media type ;charset= (record reads); charset.decode(name, h.read_bytes()) |
same as Python |
| digest of a handle | read_digest(DigestAlgorithm::Xxh3)?, read_range_digest(o, n, alg)? |
read_digest("xxh3-64"), read_range_digest(o, n) |
readDigest('xxh3-64'), readRangeDigest(o, n) |
| structured value | read_scalar(Some(&field))?, write_scalar(&v)? |
read_scalar(field, cls=Scalar), write_scalar(v) |
readScalar(field), readScalar({ field, scalar: true }), writeScalar(v) |
| scope (cache metadata) | open()? ... close()? |
with IOBase(p) as h: |
h.open() ... h.close() |
| page cache | h.buffered(BufferedOptions::default()) |
h.buffered(page_size=, max_bytes=, ttl=) (spends h) |
h.buffered({ pageSize, maxBytes, ttlMs }) (returns h) |
| count calls | Counted::new(h), calls().get(Call::Pread), counts() |
Rust only | Rust only |
object store (s3 feature) |
s3::file(url)?, s3::file_with(url, S3Options)?, s3::file_at(Provider::Aws, bucket, key)? |
IOBase("gs://b/k"), S3File(url), S3File(bucket, key, provider="s3", options={...}) |
new IOBase('az://c@acct.blob.core.windows.net/k') |
HTTP resource (http feature) |
Session::new().get(url)?.send()?, http::get(url)?, Holder::from_url(&url, props)? (a leaf) |
http.Session(base).get(path), http.get(url), IOBase(url) |
new http.Session(base).get(path), http.get(url), new IOBase(url) |
| many HTTP requests | session.send_all(requests, Some(n)) (lazy, ordered) |
session.send_all(items, concurrency=n) |
session.sendAll(items, n) |
| HTTP version | HttpOptions::with_http_version(Some(HttpVersion::Http2)) (http2, http3 features) |
http.Session(base, http_version=2) |
new http.Session(base, { httpVersion: 2 }) |
| paginate a REST API | request.with_pagination(Pagination::from_str("url:next")?).pages(), session.pages(request) |
session.pages(url, pagination="url:next"), Request(method, url, session=session).pages(pagination=...) |
session.pages(url, { pagination: 'url:next' }), new Request(m, url).withSession(s).withPagination('url:next').pages() |
| an origin for tests, in process | Server::bind("127.0.0.1:0")? + respond, route, mount, inject(Fault) |
with http.Server.bind() as server: |
http.Server.bind() ... server.shutdown() |
| foreign Arrow filesystem | FsFile::from_path(Arc<dyn FileSystem>, path, uri)? |
IOBase.from_fs(pyarrow_fs, path, uri=None), IOBase.from_uri(uri, options=) |
IOBase.fromFs(handler, path, uri?) |
| ZIP archive | zip::mount(holder), zip::from_url(&url)?, ZipArchive::new(h).mount() |
Rust only | Rust only |
| from an open file | n/a | IOBase(open_file) (path), IOBase(io.BytesIO(b)) (content) |
n/a |
Rules for fast, correct use
- Count calls, not bytes.
read_all_bytesis one call whatever the size;
a footer isread_range_bytes(size - 8, 8)- one rangedGET, not the
object;sizeitself is oneHEADon a closed store handle and free on a
listed handle, insideopen(), or afterS3File::with_known_size(n).
Slice later needs out of a read you already hold. - Construction touches nothing. Building a handle, a child
(child_by_path,/,joinpath), a media type or a partition costs zero
calls on every backend, S3/GCS/Azure included. - EAFP - never guard. A read of something absent is empty (
b"", size0), a write creates the resource and every missing parent,removeof
something absent succeeds.exists()/is_dir()/mkdir()before an
operation is a wasted round trip and a race. - Stream bounded chunks.
pstream_byteshands out 64 KiB owned chunks
and never asks forsize; digests (read_digest) stream too. Reach forread_all_bytesonly when the whole value is the answer. - One scan, one stream on a coded handle. A
preadat a compressed
offset rebuilds a decoder from the start: sixteen ranged reads of a gzip
value cost sixteen decodes. Drain onepstream_bytes, oropen()it
(caches the decoded value), or wrap it inbuffered. - Open a scope for metadata-heavy work.
opencaches the descriptor and
mapping (local), the object's metadata (stores:sizeis aHEADclosed,
free open), a Parquet footer, a decoded value;closepublishes and
releases. Outside a scope each call fetches fresh - right for a resource
another writer changes. - The name declares the coding.
trades.json.gzis baseapplication/json
plus codinggzip:media_typenames the decoded representation,codec
the stored coding. In-memory bytes are sniffed; declare a type the bytes
cannot prove (CSV) withwith_media_type/ themedia_typesetter. - Know which bytes a handle presents. Python
IOBase(name)composes what
the name declares (Text(Gzip(LocalPath))) and reads decoded bytes; the
role classes (LocalPath,LocalFile,S3File,FsPath) address stored
bytes. JavaScript byte methods always address stored bytes; the coding is
applied byreadScalar/writeScalar, the record surface andcompressInto/decompressInto. RustHolder::local,Holder::from_url
and theLocal*roles address stored bytes;holder.into_coded()(orinto_declared_media(), which also puts the record encoding on top)
composes what the name declares - the Rust equivalent of PythonIOBase(name). - Wrappers compose over a handle, never inside it.
Buffered<Coded<_>>
caches decoded pages,Coded<Buffered<_>>the encoded transport; wrapping
twice reconfigures the one layer. Python conversions (buffered,into_coded) consume the handle they took. - Text crosses the charset boundary once. Decode at intake - a
Transcodedhandle, a media type's;charset=, orCharset::decode- and
work in UTF-8 after. No reader takes a charset argument.iso-8859-1is
neverwindows-1252; bareutf-16is refused (sayutf-16le/utf-16be). - Listings are lazy, sorted, and free to stop. Building a listing costs
nothing and stopping early stops the walk: taking the first entries reads
one directory level (all its names, sorted - never the tree) locally, or
one 1000-entry page on a store. A recursive walk skips.git,.venv,.DS_Storeunlessinclude_private. A glob descends its fixed prefix
(year=2024/**/*.parquet) rather than listing and filtering; an object
listing states every size, so a listed object is never re-asked. - Join nested children in one string.
child_by_path("sub/inner.bin")
creates the parents on write; joining segment by segment needs each
intermediate to already be a container. - A local file is memory-mapped. Another process truncating it raises
SIGBUS;copy_intoaBufferwhen the file may change underneath.flush/closepublish the logical length;append_bytesandwrite_all_bytesalready end with a flush and publish on return, on
every backend (a remote object written, the mapped file's growth slack
trimmed) - only barepwritestages without publishing. - Object stores state their request count. Ranged read: one
GET;
whole read/drain/digest: oneGET; whole write: onePUT; listing: one
request per 1000 entries; remove: oneDELETE, no probe.with_known_size
skips theHEAD;open()beforebufferedon a remote handle. - Credentials resolve lazily, explicit wins. Unset knobs come from the
URL, the environment (AWS_,GOOGLE_,AZURE_,YGGDRYL_), the store's
files, then defaults; the AWS identity is botocore's chain throughaws::Session.with_environment(false)seals everything but explicit
values - seereferences/backends.md. - HTTP is a handle and a client. An
http/httpsURL is a leaf: a
whole read is oneGET, a range one rangedGET,sizeoneHEAD
(none insideopen()). A body read whole or streamed resumes a cut
transfer from its cursor (Range+If-Range), for a successful uncodedGETonly. Retries touch only what cannot act twice: an idempotent
method, or a request no connection took; aPOSTanswered503is
handed back. Fan out withsend_allon one session - one pool, answers
in order, nothing held past the requests in flight - never a thread pool
ofgets. HTTP/2 and HTTP/3 are negotiated (ALPNh2, then HTTP/3 onceAlt-Svcadvertises it) and multiplex one connection per origin; a plainhttp://origin speaksh2conly when asked, and a proxied request
HTTP/1.1. Proxies, CA bundles and.netrccome from the environment as
curl andrequestsread them. - Pagination is one ladder, seven spellings.
Pagination::Auto(the
default) tries theLinkheader, then a few common next-page headers,
then a next URL or cursor in the body, in a fixed order; name one
explicitly withlink,header:<name>,url:<path>,cursor:<path>:<parameter>,offset:<parameter>:<size>orpage:<parameter>:<start>..pages()walks aRequestthat has not
been sent -session.pages(request)/request.pages()in Rust,session.pages(url, pagination=...)/request.pages(pagination=...)
in Python,session.pages(url, { pagination })/request.withPagination(...).pages()in JavaScript - never an
already-sentResponse: Python's and JavaScript'ssession.get(url)
sends immediately and answers one with no.pages(); Rust'ssession.get(url)alone is the unsentRequestitself, only.send()'s answer has none. Every page lays out as one Arrow batch
throughPages::into_arrow_reader,.intoArrowReader, or Python's.read_arrow().
Pitfalls
| Wrong | Right |
|---|---|
if not h.exists(): h.mkdir() then write |
write; parents are created on the first write |
reading the whole value to slice off a footer (read_all_bytes / read_bytes()[-8:]) |
Rust h.read_range_bytes(h.size() - 8, 8)?, Python h.read_range_bytes(h.size - 8, 8), JS h.readRangeBytes(h.size - 8, 8) |
a loop of read_range_bytes over a .gz handle |
one pstream_bytes drain, or open()/buffered first |
Python IOBase("x.gz").read_bytes() expecting gzip bytes |
it is decoded; LocalPath("x.gz").read_bytes() is stored |
JS new IOBase('x.gz').writeText(s) expecting a gzip file |
writes plain bytes under a .gz name; plain.compressInto(gz) or gz.writeBytes(gzip.dumps(b)) |
Python cached = h.buffered(); h.read_bytes() |
h is spent ("consumed by a conversion"); use cached |
compress_into(IOBase("x.gz")) in Python |
the target must present stored bytes: LocalPath("x.gz") |
compress_into(memory_target) with no codec |
a nameless target declares nothing: pass codec="zstd" |
read_text() on windows-1252 bytes |
it is strict UTF-8: charset.decode("windows-1252", h.read_bytes()), or declare text/plain;charset=windows-1252 for record reads |
charset.decode("latin1", ...) for Windows 0x80 = € |
windows-1252 (cp1252); ISO 8859-1 maps 0x80 to U+0080 |
folder.remove(); assert not folder.is_dir() |
is_dir()/isDir() keeps answering the cached kind (exists() is accurate); check the parent's listing |
JS root.joinpath('a', 'b.bin') when a does not exist |
root.joinpath('a/b.bin') |
treating read_range_bytes(past_end, n) as an error |
it answers what exists (possibly empty); check the length |
Python IOBase.from_uri("s3://...") expecting the native S3 client |
from_uri binds pyarrow.fs.S3FileSystem; IOBase("s3://...")/S3File is the native store |
a str of document content passed to IOBase(...) |
a str is a path; content is IOBase.from_bytes(b) or IOBase(io.BytesIO(b)) |
s3://my.bucket.com/key |
a first part ending .com/.io/.net is a host; use s3::file_at(Provider::Aws, bucket, key) / S3File(bucket, key, provider="s3") |
logging bound_uri |
it may carry credentials; log masked_uri |
a thread pool calling session.get(url) per URL |
session.send_all(urls, concurrency): one pool, ordered, lazy, the interpreter released |
expecting a POST retried after a 503 or a reset |
a non-idempotent request is sent once unless no connection took it; retry it yourself when it is safe |
Client(...).session(http_version=2) on a client built without it |
refused by name: the pool's knobs are the client's - Client({"http_version": "2"}).session(...) |
http_version=3 against a plain http:// origin |
QUIC needs TLS: it answers as 2 (h2c); HTTP/3 is an https origin |
session.get(url).content on a large download |
get(url, stream=True) and iter_content(n) / Rust stream()?: the body stays on the wire and resumes |
Language references
references/rust.md- read when writing Rust (Holder,Buffer,Coded,Transcoded,Counted,s3,zip).references/python.md- read when writing Python (IOBase, role classes,yggdryl.gzip,yggdryl.charset).references/javascript.md- read when writing Node.js (IOBase,gzip/zlib/zstd/charsetnamespaces, handler protocol).references/backends.md- configuration and cost tables for Local, Filesystems, S3 / GCS / Azure, HTTP, Buffered and ZIP.
Deeper
- Handles, bytes, values, call counts, every backend: https://platob.github.io/yggdryl/holder/
- Laziness and kinds: https://platob.github.io/yggdryl/holder/#laziness-and-kinds
- Media type and codings: https://platob.github.io/yggdryl/holder/#media-type-and-codings
- Object stores: https://platob.github.io/yggdryl/holder/#object-stores
- HTTP, HTTP/2 and HTTP/3: https://platob.github.io/yggdryl/holder/#http
- Pagination and
Pages: https://platob.github.io/yggdryl/holder/#pages - ZIP: https://platob.github.io/yggdryl/holder/#zip
- Compression (gzip, zlib, zstd): https://platob.github.io/yggdryl/media/#compression
- Charsets: https://platob.github.io/yggdryl/media/#charsets
- Sibling skills:
yggdryl-records(rows on a handle, partitions),yggdryl-uri
(URLs, globs, Hive paths),yggdryl-documents(JSON/YAML/TOML/XML),yggdryl-hashing(digest values).