Projects / MVR-Data

Python Binding

The Python Binding chapter includes:

The mvr_data package is a thin pybind11 layer over the C++20 reference implementation. It does not shell out or reimplement format behavior in Python. Public classes and methods keep the C++ names, while Arrow values are presented as native PyArrow objects.

Create the build environment

The repository includes a reproducible Conda environment with Python 3.12, GCC/G++ through cxx-compiler, CMake, Ninja, pybind11, scikit-build-core, PyArrow 24, and pytest:

git clone https://github.com/WittenYeh/MVR-Data.git
cd MVR-Data
conda env create -f environment.yml
conda activate mvr-data

The package metadata supports Python 3.10 or newer and PyArrow 14 or newer. The checked-in environment pins the versions used for the project's clean build and compatibility tests, including a modern C++ runtime for the native module.

Build and install

Build a platform-specific wheel from the checkout and install it into the active environment:

python -m pip install . --no-build-isolation

The first build fetches the pinned Apache Arrow, nlohmann/json, and LibRHash sources through CMake and compiles them locally. --no-build-isolation reuses the compiler-facing pybind11 and scikit-build-core packages from the Conda environment. PyArrow remains a runtime dependency.

For an in-tree development build with both test suites enabled:

cmake -S . -B build -G Ninja \
  -DMVR_DATA_BUILD_PYTHON=ON \
  -DBUILD_TESTING=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure
PYTHONPATH="$PWD/build/python" python -m pytest -q tests/test_python_bindings.py

Same-named public surface

Importing mvr_data exposes PackageKind, TableRole, TableInfo, VectorConfig, Manifest, Schema, DataReader, and Checksum. Each corresponds directly to the C++ type with that name, and methods such as Manifest.load, DataReader.open, DataReader.get_batched_scanner, DataReader.read_table, Checksum.refresh, and Checksum.verify retain their C++ method names.

The source-linked Schema API, Manifest API, Reader API, and Integrity API define the shared behavioral contracts. Python signatures and return annotations are kept in the package's inline type stubs.

Python accepts str, os.PathLike[str], and pathlib.Path wherever the C++ API accepts std::filesystem::path. C++ std::optional results become either their value or None, table shard vectors become list[str], and Manifest extensions become ordinary nested Python dictionaries and values.

Arrow interoperability

The binding maps Arrow values without a Python-side schema model:

C++ type Python type
arrow::DataType pyarrow.DataType
arrow::Schema pyarrow.Schema
arrow::RecordBatchReader pyarrow.RecordBatchReader
arrow::Table pyarrow.Table

Schemas and dtypes cross the extension boundary through the Arrow C Data PyCapsule protocol. Batch readers use the Arrow C Stream protocol. This avoids serialization between the statically linked C++ core and the PyArrow runtime, and allows Arrow buffers to be shared without a serialization copy.

PyArrow types can also be passed back into the native API:

import pyarrow as pa

import mvr_data

dtype = mvr_data.Schema.parse_vector_dtype("float32")
assert dtype == pa.float32()

schema = mvr_data.Schema.make_embedded_object_schema(
    dimension=128,
    dtype=pa.float32(),
)
assert isinstance(schema, pa.Schema)

Stream or materialize a table

Open a package with the same factory and table-role names used in C++:

from pathlib import Path

import mvr_data

package = Path("/data/example")
mvr_data.Checksum.verify(package)

reader = mvr_data.DataReader.open(package)
base_role = mvr_data.TableRole.base()

for batch in reader.get_batched_scanner(base_role):
    consume(batch)

get_batched_scanner returns a pyarrow.RecordBatchReader. It preserves Manifest shard order and keeps the C++ reader's lazy, one-shard-at-a-time I/O behavior. Errors discovered while advancing the stream are raised by the PyArrow reader.

Use read_table only when the complete table fits in memory:

base = reader.read_table(mvr_data.TableRole.base())
print(base.schema)
print(base.num_rows)

The result is a pyarrow.Table, so downstream PyArrow operations work without an MVR-Data-specific adapter.

Read typed Manifest metadata

Manifest accessors use the same names as their C++ counterparts:

manifest = reader.manifest()

print(manifest.data_name(), manifest.data_version())
print(manifest.table_info(mvr_data.TableRole.query()).shards)

if manifest.package_kind() == mvr_data.PackageKind.embedded:
    vector = manifest.vector_config()
    assert vector is not None
    print(vector.dimension, vector.dtype, vector.scoring)

TableInfo.schema and VectorConfig.dtype are PyArrow values. Optional description, license, source, vector configuration, and extensions accessors return None when their Manifest fields are absent.

Python exceptions

Non-OK Arrow statuses become familiar Python exception categories:

Arrow status Python exception
Invalid ValueError
I/O error OSError
Type error TypeError
Key error KeyError
Index error IndexError
Capacity error OverflowError
Out of memory MemoryError
Already exists FileExistsError
Not implemented NotImplementedError
Other failures RuntimeError

Potentially blocking Manifest, package, shard, checksum, and filesystem operations release the Python GIL while the C++ work runs. Error messages retain the original Arrow status text for diagnosis.

Static typing

The wheel includes mvr_data/__init__.pyi and a py.typed marker. Type checkers therefore see concrete PyArrow return types, optional Manifest fields, path-like inputs, and the same-named class methods without separate stub installation.

The current package remains read-only. A package writer, CLI, and full semantic validator are outside this binding's public surface.