ByteRangeNDSource¶
A ProxyNDSource that serves the chunks – and the single blocks – of a
Blosc2 frame it can read byte ranges of, instead of transferring the whole
container. It knows the frame format and nothing about where the frame lives:
subclasses supply read_range(offset, size) and nothing else.
FsspecNDSource reads through fsspec, and C2Array reads over HTTP
ranges from a Caterva2 server. For other sources, see ProxyNDSource
and ProxySource.
- class blosc2.ByteRangeNDSource(urlpath: str, max_concurrency: int = 8, traffic: Traffic | None = None)[source]¶
A Proxy source that serves parts of a remote Blosc2 frame.
The frame stays where it is: only its header, its chunk offsets, and what a slice actually touches ever cross the network.
A chunk large enough to be worth taking apart is read block by block –
chunk_layout()fetches the offsets of its blocks,block_plan()turns the wanted ones into as few range reads as they fit in – so a slice landing in a corner of a multi-megabyte chunk costs a few kilobytes. Small chunks, memcpyed ones and run-length ones come whole, since there is nothing to save there;wants_blocks()decides which is which without reading anything.Everything above is the Blosc2 frame format and nothing else, so a subclass only has to say how to read bytes:
read_range()is the one abstract method, and the transport behind it decides nothing about the rest. FsspecNDSource reads them with fsspec, and C2Array reads them over HTTP ranges from a Caterva2 server, carrying its auth cookie.A subclass sets its transport up first and then calls this constructor, which reads the frame’s header through it – one small read, and everything an open decides: that this is a frame at all, and one holding an NDArray. Where the chunks are waits for the first one anything asks about, so a Proxy over a cache that already holds the slice wanted opens the source without ever fetching its index. It may also set a
stamp, anything that names the exact bytes it reads, so that Proxy can tell a cache built from other bytes.Contiguous frames carrying a
b2ndmetalayer only, which is whatblosc2.asarray()and friends write to a single file. Sparse frames and.b2dstores are directories, and cannot be read this way.- Parameters:
urlpath¶ (str) – Where the frame is, for error messages and for the caller to read back.
max_concurrency¶ (int, optional) – How many fetches the enclosing Proxy may run at once. Every chunk or block costs one range request, so against an object store a slice is almost entirely round-trip latency and overlapping the requests is the whole win. Defaults to 8, the same figure
Proxy.afetch()uses for remote sources. Pass 1 for a protocol with no latency to hide, where the thread pool costs about 10 microseconds per chunk and saves nothing.
- Attributes:
Methods
aget_chunk(nchunk)Same as
get_chunk(), but without blocking the caller's event loop.block_plan(nchunk, nblocks)The range reads that cover nblocks, near-adjacent ones merged.
chunk_layout(nchunk)Read where the blocks of a chunk are: its header, bstarts and extents.
chunk_layouts(nchunks)chunk_layout()for several chunks, in as few requests as they fit.get_chunk(nchunk)Return the compressed chunk in
self.Forget where the chunks and blocks are, so the next read looks again.
read_range(offset, size)The bytes at [offset, offset + size) of the frame.
read_ranges(spans)The bytes of every
(offset, size)in spans, in that order.wants_blocks(nchunk, nwanted[, wave, nruns])Whether fetching nwanted blocks of a chunk beats fetching all of it.
Which chunks of the frame hold content, as a boolean per chunk.
- async aget_chunk(nchunk: int) bytes[source]¶
Same as
get_chunk(), but without blocking the caller’s event loop.This is what makes
Proxy.afetch()worth using against an object store, where a slice spanning many chunks is nearly all round-trip latency.The fetch goes to a worker thread rather than being awaited directly. Awaiting an async filesystem’s coroutine looks like the obvious thing to do and does not work: fsspec drives those on a private event loop of its own, so a client created there and awaited here raises “got Future attached to a different loop” (seen with s3fs). Its blocking API is the supported way in, and it hands off to that same private loop, so the thread parks on a queue rather than on a socket.
- block_plan(nchunk: int, nblocks: Sequence[int]) list[tuple[int, int, tuple]][source]¶
The range reads that cover nblocks, near-adjacent ones merged.
Each is
(offset, size, members)in frame coordinates, where members says which block each piece of the answer is, as(nblock, offset within the read, size).
- chunk_layout(nchunk: int) tuple[bytes, ndarray, ndarray] | None[source]¶
Read where the blocks of a chunk are: its header, bstarts and extents.
One range read, of a size known in advance since every chunk holds the same number of blocks. None for a chunk this cannot take apart, which the header is what says:
a chunk of a single block is its own block, and a memcpyed one stores its blocks raw with no
bstartsat all;a chunk that is a run of one value is its header and that value, with no blocks in the file: blosc2.full writes those at a real offset, unlike the runs of zeros the frame keeps in the offsets themselves;
a chunk compressed against a codec dictionary keeps it between
bstartsand the streams, and _splice_chunk would drop it while leaving the flag that promises it;a variable-length-block chunk does not use the zero-length stream that stands in for a block _splice_chunk does not have;
a chunk without the extended header keeps its
bstartssomewhere else entirely, so reading them at byte 32 would be reading data.
Each of those is then fetched whole, at the cost of the one header read that found out.
- chunk_layouts(nchunks: Sequence[int]) list[source]¶
chunk_layout()for several chunks, in as few requests as they fit.One request each unless the transport takes several ranges at once, and none at all for a chunk already read: a fetch asks for layouts only where blocks are missing, but the same chunk comes up again as a slice fills it in.
- get_chunk(nchunk: int) bytes[source]¶
Return the compressed chunk in
self.- Parameters:
nchunk¶ (int) – The unidimensional index of the chunk to retrieve.
- Returns:
out – The compressed chunk.
- Return type:
bytes object
- invalidate_index() None[source]¶
Forget where the chunks and blocks are, so the next read looks again.
The frame’s offsets move whenever it is written to: a chunk written into a slot that held no content is appended past the old offsets block, which the new one is then written after. Chunks already placed keep their offsets – that is what makes an append-only fill cheap to read alongside – but the index as a whole has to be read again to see the slot that was filled, and the header with it, since the frame’s length and its payload extent are what the offsets are found through.
Nothing is read here: the next lookup pays for it, so a writer that never reads back spends no request on this at all.
Only for a handle that writes, or that follows a frame someone else is writing. A frame that nobody mutates never needs this.
- abstractmethod read_range(offset: int, size: int) bytes[source]¶
The bytes at [offset, offset + size) of the frame.
The whole of the transport: everything else here is the frame format. Fewer bytes may come back only at the end of the frame; anything else is an error, since the caller has no way to ask for the rest. Must be safe to call from several threads at once, which is what lets Proxy overlap the fetches of one slice.
- read_ranges(spans: Sequence[tuple[int, int]]) list[bytes][source]¶
The bytes of every
(offset, size)in spans, in that order.One request each, unless a transport that can carry several ranges in one overrides this and raises
max_rangesto say how many. Nothing else has to change for it: this is the only method a batching transport needs, and everything that reads bytes goes through it.
- wants_blocks(nchunk: int, nwanted: int, wave: Mapping[int, int] | None = None, nruns: int | None = None) bool[source]¶
Whether fetching nwanted blocks of a chunk beats fetching all of it.
Answered without reading anything, so a chunk that says no costs exactly what it costs today: the number of blocks a slice touches is geometry, and an upper bound on the chunk’s compressed size is already in hand from the frame’s offsets. See the thresholds at the top of this module.
wave is the whole fetch this chunk belongs to,
{nchunk: nwanted}, which a transport that batches ranges is asked with; see_wave_saves()for what it is used for and why. nruns is how many ranges those blocks will coalesce into, which is what they cost where every range is its own request.
- written_chunks() ndarray[source]¶
Which chunks of the frame hold content, as a boolean per chunk.
False only for a chunk that was never written: a frame keeps those in their offset rather than in the file, tagged as uninitialized, which is what blosc2.uninit fills an array with. Everything else is True, a run of zeros included – a writer that stored an all-zero chunk stored something, and the tag says so, which is the whole reason to pre-size an array with uninit rather than with zeros.
One range read of the frame’s offsets, and none at all once they have been read: this is the same index every chunk read goes through. So the progress of an array being filled is legible from the bytes a reader already fetches, without asking the server anything about it.
- property blocks: tuple¶
The block shape of the source.
- property chunks: tuple¶
The chunk shape of the source.
- property dtype: dtype¶
The dtype of the source.
- max_ranges = 1¶
How many ranges one request of this transport may carry.
One means one request each, which is all any object store offers. A server answering
multipart/byterangestakes more – seeread_ranges()– and then a slice costs a couple of requests rather than a couple per chunk it touches.
- serves_blocks = True¶
That blocks are worth asking this source for, which reading a frame is.
The frame is there to be read in pieces – that is what an open of one settles – so a Proxy over it goes straight to the block path. A source that only sometimes serves blocks (C2Array, whose server may compute the dataset rather than store it) overrides this.
- property shape: tuple¶
The shape of the source.
- wants_wave = True¶
That
wants_blocks()takes the wave, and wants to be given it.The implementation here does, and weighs a shared round trip against what the whole fetch skips. A subclass that overrides wants_blocks with a two-argument one sets this back to False, and is then called with two.
When a range read is refused¶
A transport that reads byte ranges may be answered with something other than the
bytes asked for: a server that now streams the dataset, a server that is too
busy to serve it, a body that cannot be taken apart. Those raise
blosc2.proxy_source.NotRanged, which a Proxy catches for itself –
whatever the fetch is still missing comes as whole chunks – and which a caller
reading ranges directly can catch by name.
- class blosc2.proxy_source.NotRanged(message: str, status: int | None = None)[source]¶
A transport that reads byte ranges answered with something other than one.
Raised out of
ByteRangeNDSource.read_range()and its neighbours when the answer is not the bytes that were asked for: an HTTP 200 carrying the whole dataset, a busy server, a body that cannot be taken apart. It is not fatal to a fetch –Proxy.fetch()catches it and asks for the chunks it wanted whole, which every source can serve – so what it costs is the block granularity, not the data.A ValueError, which is what a source that cannot be read in ranges raised before this had a type of its own.
- Attributes:
- args
transientWhether asking again could be answered differently.
Methods
add_note(object, /)Exception.add_note(note) -- add a note to the exception
with_traceback(object, /)Exception.with_traceback(tb) -- set self.__traceback__ to tb and return self.
- class blosc2.proxy_source.PartsMissing(message: str, status: int | None = None)[source]¶
A multi-range answer did not carry all the bytes that were asked for.
A
NotRangedfor the caller that only wants to know the read failed, and its own type for the transport, which answers it by asking for one range at a time rather than by giving up on ranges.- Attributes:
- args
transientWhether asking again could be answered differently.
Methods
add_note(object, /)Exception.add_note(note) -- add a note to the exception
with_traceback(object, /)Exception.with_traceback(tb) -- set self.__traceback__ to tb and return self.