ursa.store.backends._obstore

obstore-backed ObjectStore implementation.

One class wraps any obstore handle (S3Store for R2, LocalStore, future GCSStore/AzureStore). Responsibilities:

  • Translate obstore types into Ursa types (ObjectMeta).

  • Translate obstore exceptions into Ursa errors.

  • Validate extra_metadata against S3/R2 user-metadata constraints before any network call so callers see typed errors rather than opaque obstore tracebacks.

  • Emit OTel spans on each public op (low-cardinality attrs only — never the caller-supplied key).

The wrapper does no path composition: prefix scoping lives in the obstore handle (S3Store(prefix=...), LocalStore(prefix=...)). This keeps raw_obstore() correct for Lance/Zarr backends that bypass the wrapper.

Module Contents

Classes

ObstoreBackend

ObjectStore implementation over an obstore handle.

_OpenReader

Forward-only streaming reader returned by ObjectStore.open().

_NoSeekStreamReader

Adapt an obstore byte-chunk iterator into a forward-only BinaryIO.

Functions

_trace

Wrap a public method with an OTel span.

_validate_extra_metadata

Validate extra_metadata against S3/R2 user-metadata constraints.

_build_attributes

Compose obstore Attributes (a dict) from the wrapper’s split args.

_meta_from_obstore

Translate obstore’s ObjectMeta TypedDict into ours.

_translate_errors

Map obstore exceptions to Ursa errors.

Data

API

ursa.store.backends._obstore.__all__

[‘ObstoreBackend’]

ursa.store.backends._obstore._TRACER

‘get_tracer(…)’

ursa.store.backends._obstore._F

‘TypeVar(…)’

ursa.store.backends._obstore._METADATA_KEY_RE: Final

‘compile(…)’

ursa.store.backends._obstore._METADATA_TOTAL_BYTES_LIMIT: Final

2048

ursa.store.backends._obstore._RESERVED_ATTRIBUTES: Final

‘frozenset(…)’

ursa.store.backends._obstore._trace(op: str) Callable[[ursa.store.backends._obstore._F], ursa.store.backends._obstore._F][source]

Wrap a public method with an OTel span.

Span attributes are intentionally low-cardinality: ursa.store.role, ursa.store.backend, ursa.store.op. Keys are not included — they’re unbounded-cardinality and don’t belong in span tags.

ursa.store.backends._obstore._validate_extra_metadata(extra: collections.abc.Mapping[str, str] | None) None[source]

Validate extra_metadata against S3/R2 user-metadata constraints.

Raises InvalidMetadataError (with a specific reason) if any key fails the charset rule or if the combined header size exceeds 2 KiB.

ursa.store.backends._obstore._build_attributes(*, content_type: str | None, sha256: str | None, extra_metadata: collections.abc.Mapping[str, str] | None) dict[str, str] | None[source]

Compose obstore Attributes (a dict) from the wrapper’s split args.

ursa.store.backends._obstore._meta_from_obstore(raw: collections.abc.Mapping[str, Any], *, strip_prefix: str = '', sha256: str | None = None) ursa.store.base.ObjectMeta[source]

Translate obstore’s ObjectMeta TypedDict into ours.

path (obstore) -> key (ours, prefix-stripped). last_modified is normalized to UTC. sha256 defaults to None because obstore’s list/head do not surface user metadata; callers that want sha256 from a read must use get and inspect the result’s attributes.

ursa.store.backends._obstore._translate_errors() collections.abc.Iterator[None][source]

Map obstore exceptions to Ursa errors.

class ursa.store.backends._obstore.ObstoreBackend(inner: obstore.store.ObjectStore, *, role: str, backend: str, prefix: str = '', supports_attributes: bool = True, supports_etag_match: bool = True, lance_uri: str = '', lance_storage_options: collections.abc.Mapping[str, str] | None = None)[source]

ObjectStore implementation over an obstore handle.

The handle is constructed already-prefix-scoped by the per-backend factories (backends/r2.py, backends/local.py); this class does no path composition.

Initialization

supports_attributes / supports_etag_match are backend capability flags. obstore’s LocalStore raises NotImplementedError when attributes or UpdateVersion modes are passed; those backends opt out by setting these to False, and put raises a typed InvalidMetadataError with a clear message rather than letting the obstore error leak through.

prefix is the same value pushed into the obstore handle. The wrapper records it to strip from result paths because obstore’s S3Store returns prefixed paths from head (while LocalStore returns them relative). Stripping here makes the contract consistent.

_strip_prefix(path: str) str[source]
get(key: str) bytes[source]
get_range(key: str, *, start: int, length: int) bytes[source]
open(key: str) contextlib.AbstractContextManager[BinaryIO][source]
put(key: str, data: bytes | typing.BinaryIO, *, content_type: str | None = None, sha256: str | None = None, extra_metadata: collections.abc.Mapping[str, str] | None = None, if_none_match: typing.Literal[*] | None = None, if_match: str | None = None) ursa.store.base.ObjectMeta[source]
copy(src: str, dst: str, *, overwrite: bool = False) ursa.store.base.ObjectMeta[source]
head(key: str) ursa.store.base.ObjectMeta[source]
exists(key: str) bool[source]
list(prefix: str = '') collections.abc.Iterator[ursa.store.base.ObjectMeta][source]
list_prefixes(prefix: str = '') collections.abc.Iterator[str][source]
delete(key: str) None[source]
raw_obstore() obstore.store.ObjectStore[source]
lance_connection() tuple[str, dict[str, str]][source]
class ursa.store.backends._obstore._OpenReader(inner: obstore.store.ObjectStore, key: str)[source]

Bases: contextlib.AbstractContextManager[typing.BinaryIO]

Forward-only streaming reader returned by ObjectStore.open().

Implemented over obstore.get(...).stream() rather than obstore.open_reader because the latter has a known prefix-handling bug on S3Store: when the store is constructed with prefix=, the reader double-applies the prefix at request time. get().stream() is consistent across backends.

Initialization

__enter__() BinaryIO[source]
__exit__(*exc: object) None[source]
class ursa.store.backends._obstore._NoSeekStreamReader(chunks: Any)[source]

Bases: io.RawIOBase

Adapt an obstore byte-chunk iterator into a forward-only BinaryIO.

Explicitly seekable() == False; seek() raises so callers can’t silently get wrong bytes.

Initialization

Initialize self. See help(type(self)) for accurate signature.

readable() bool[source]
seekable() bool[source]
seek(offset: int, whence: int = 0) int[source]
read(size: int = -1) bytes[source]
_pull_chunk() None[source]
readinto(buffer: Any) int[source]