File-format routing¶
omniload reads each file format through the best available path rather than one generic reader. This page explains how that routing works and why, so the per-format pages (BSON, CBOR, MessagePack, XML, YAML) can stay focused on how to use each format. It is background reading: you do not need any of it to load a file, but it explains the behaviour you will see around extended types, corrupt files, and which optional package a format needs.
Which path reads which format¶
Format |
Read path |
Why |
|---|---|---|
CSV, JSONL, Parquet |
Native Polars / pyarrow readers |
Fast, columnar, already well tested. |
BSON |
Dedicated in-tree codec |
Needs extended-type normalization the generic decoders do not do. |
CBOR |
Whole-file decode with |
Whole-file format; a direct decode surfaces corruption instead of hiding it. |
MessagePack |
Streaming |
No better native path; streamed record-by-record. |
XML |
Whole-file parse with a hardened |
iterabledata’s XML parser resolves entities (an XXE risk) and can’t be locked down through its API; a direct, safe parse is a security requirement, not just a speed choice. |
YAML |
Whole-file decode with |
iterabledata’s YAML wrapper is eager and swallows parse errors; a direct safe-load surfaces corruption and rejects code-execution tags. |
The routing policy is deliberately conservative: fill gaps, don’t replace working paths. A
format goes through iterabledata only where that is genuinely the better path; where its
handling would be unsafe or would hide errors (XML entity resolution, YAML’s error-swallowing
wrapper), omniload owns the decode instead and rides only the shared harness (the reader
plumbing, chunking, missing-decoder hint, seekable spool). New long-tail formats are added
incrementally behind the optional iterable extra, without touching the CSV / JSONL / Parquet
paths.
Two read mechanisms¶
Formats that go through the long-tail machinery are read one of two ways, chosen per format.
Streaming through the iterabledata bridge¶
MessagePack (and future streaming formats) are read through iterabledata, which exposes a
uniform per-format class that yields record dicts from a file object. omniload feeds it the
source’s own already-authenticated fsspec handle, so a file on S3, GCS, Azure or SFTP
keeps flowing through the source’s existing credentials. iterabledata’s own cloud-storage /
credential layer is never touched, so there is no second authentication surface to configure.
Records are pulled in batches until the reader signals end-of-file, and flushed per file so a multi-file glob never drops a partial final chunk. iterabledata rewinds the handle on construction, which fails on a non-seekable stream (a pipe, some compressed or SFTP handles); such a stream is spooled into memory first, while a seekable handle streams straight through.
Whole-file decode¶
Some formats are whole-file rather than streaming, and iterabledata’s wrapper for them wraps
its decode in a broad except that yields nothing on a bad payload, meaning a truncated or
corrupt file would load as zero rows with no error (silent data loss). For those, omniload
decodes the bytes with the format’s own library directly (CBOR via cbor2, YAML via
yaml.safe_load_all, XML via a hardened lxml parse) so decode errors propagate instead of
vanishing. This is why these formats need only their own decoder and not iterabledata itself,
even though all ship together in the iterable extra.
For XML, going direct is also a safety requirement, not just an error-surfacing one:
iterabledata’s parser resolves entities with no way to turn it off through its API, so an
untrusted file could trigger an XXE read (&xxe; pointing at file:///etc/passwd or a URL) or
an entity-expansion bomb. omniload’s lxml parse disables entity resolution, DTD loading and
network access, so those inputs are neutralized: an external entity is simply dropped from the
row, and an expansion bomb never materializes. For YAML, omniload uses the safe loader, so
a tag that would construct an arbitrary Python object (!!python/object/...) is rejected rather
than executed.
Reader hints¶
XML has no single natural “row” the way a JSON array does, so its reader needs one piece of
per-file information: which repeated element is a row. That arrives through the URI’s
reader-hint channel as #tagname=<row-tag> (e.g.
file://products.xml#tagname=product), the first real consumer of that channel. The hint is
threaded from the source URI down to the decoder; a hint the reader doesn’t declare is dropped,
and a missing-but-required one (XML without #tagname) raises a clear error rather than failing
obscurely.
Extended-type normalization¶
Binary formats carry types JSON does not (raw bytes, timestamps, tagged values), and the
decoders hand some of those back as native Python objects that a text or Parquet loader cannot
serialize. omniload normalizes rows to portable values before handing data to the loader:
bytesbecomes a base64-encoded string (portable across text loaders and Parquet alike). This covers a MessagePack / CBOR binary value and a YAML!!binary.A MessagePack
Timestampextension becomes a UTC datetime.An unknown CBOR tag becomes a plain
{"tag": ..., "value": ...}object rather than crashing the load.A YAML
!!setbecomes a list. XML needs no such normalization: its values are all strings or nested objects/lists, which every loader handles.
Some values are made portable by the decoder itself rather than by omniload: cbor2 decodes
the standard CBOR tags (datetime, big integers, decimals) into native Python types directly.
Those load into Parquet and SQL destinations, but a native decimal cannot be serialized to a
JSONL file destination, so use a Parquet or SQL destination for data that carries decimals.
Nested maps and arrays are handled recursively. The exact per-format mapping is on each
format’s own page under “Extended-type handling”.
Integrity and truncation¶
The read mechanism determines how a damaged file behaves, and it is worth knowing which guarantee you get:
Whole-file decode (CBOR, XML, YAML) raises on a corrupt or malformed file rather than loading partial data. CBOR additionally must be a single top-level value; files that concatenate several top-level objects are read only up to the first, a decoder limitation that cannot be detected at read time. XML additionally rejects an entity-expansion bomb and a mismatched encoding declaration.
Streaming formats (MessagePack) carry no length prefix, so a truncated tail reads as a clean end-of-file: the partial trailing record, and anything after a mid-stream corruption, are dropped silently. Validate file integrity upstream if partial loads would be a problem.
The iterable extra¶
Long-tail format support ships in the optional iterable extra (also part of full):
pip install 'omniload[iterable]'
It installs iterabledata plus the shipped tranche’s decoders (cbor2, lxml, msgpack,
pyyaml). If a file in one of these formats is loaded without the extra installed, omniload
fails with a clear error naming the exact pip install to run, rather than a bare ImportError.