From 518fbe2d8d5f1068a4297f53d8ee15dad7bb05ab Mon Sep 17 00:00:00 2001 From: Rob Reeves Date: Wed, 8 Apr 2026 23:00:29 +0000 Subject: [PATCH] [DataLoader] Re-add ArrivalOrder API and batch_size support via li-pyiceberg Re-introduce the ArrivalOrder scan order and batch_size parameter that were removed in #504. The original removal was necessary because the fork dependency (sumedhsakdeo/iceberg-python) could not pass ELR. Now that li-pyiceberg 0.11.3 includes the ArrivalOrder API from upstream (apache/iceberg-python#3046), we can restore the functionality using an approved registry dependency. --- integrations/python/dataloader/pyproject.toml | 2 +- .../src/openhouse/dataloader/data_loader.py | 7 + .../openhouse/dataloader/data_loader_split.py | 12 +- .../dataloader/tests/integration_tests.py | 11 +- .../dataloader/tests/test_arrival_order.py | 137 ++++++++++++++++++ .../dataloader/tests/test_data_loader.py | 27 ++++ .../tests/test_data_loader_split.py | 46 ++++++ integrations/python/dataloader/uv.lock | 34 ++--- 8 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 integrations/python/dataloader/tests/test_arrival_order.py diff --git a/integrations/python/dataloader/pyproject.toml b/integrations/python/dataloader/pyproject.toml index 2fa65ec1a..31314c4b5 100644 --- a/integrations/python/dataloader/pyproject.toml +++ b/integrations/python/dataloader/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.12" license = {text = "BSD-2-Clause"} keywords = ["openhouse", "data-loader", "lakehouse", "iceberg", "datafusion"] -dependencies = ["datafusion==51.0.0", "li-pyiceberg==0.11.2", "requests>=2.31.0", "sqlglot>=29.0.0", "tenacity>=8.0.0"] +dependencies = ["datafusion==51.0.0", "li-pyiceberg==0.11.3", "requests>=2.31.0", "sqlglot>=29.0.0", "tenacity>=8.0.0"] [[tool.uv.index]] url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/simple/" diff --git a/integrations/python/dataloader/src/openhouse/dataloader/data_loader.py b/integrations/python/dataloader/src/openhouse/dataloader/data_loader.py index f2118d30b..062bdbd55 100644 --- a/integrations/python/dataloader/src/openhouse/dataloader/data_loader.py +++ b/integrations/python/dataloader/src/openhouse/dataloader/data_loader.py @@ -114,6 +114,7 @@ def __init__( filters: Filter | None = None, context: DataLoaderContext | None = None, max_attempts: int = 3, + batch_size: int | None = None, ): """ Args: @@ -126,6 +127,10 @@ def __init__( filters: Row filter expression, defaults to always_true() (all rows) context: Data loader context max_attempts: Total number of attempts including the initial try (default 3) + batch_size: Maximum number of rows per RecordBatch yielded by each split. + Passed to PyArrow's Scanner which produces batches of at most this many + rows. Smaller values reduce peak memory but increase per-batch overhead. + None uses the PyArrow default (~131K rows). """ if branch is not None and branch.strip() == "": raise ValueError("branch must not be empty or whitespace") @@ -138,6 +143,7 @@ def __init__( self._filters = filters if filters is not None else always_true() self._context = context or DataLoaderContext() self._max_attempts = max_attempts + self._batch_size = batch_size if self._context.jvm_config is not None and self._context.jvm_config.planner_args is not None: apply_libhdfs_opts(self._context.jvm_config.planner_args) @@ -260,4 +266,5 @@ def __iter__(self) -> Iterator[DataLoaderSplit]: scan_context=scan_context, transform_sql=optimized_sql, udf_registry=self._context.udf_registry, + batch_size=self._batch_size, ) diff --git a/integrations/python/dataloader/src/openhouse/dataloader/data_loader_split.py b/integrations/python/dataloader/src/openhouse/dataloader/data_loader_split.py index 38331bbe4..59649536e 100644 --- a/integrations/python/dataloader/src/openhouse/dataloader/data_loader_split.py +++ b/integrations/python/dataloader/src/openhouse/dataloader/data_loader_split.py @@ -7,7 +7,7 @@ from datafusion.context import SessionContext from pyarrow import RecordBatch from pyiceberg.io.pyarrow import ArrowScan -from pyiceberg.table import FileScanTask +from pyiceberg.table import ArrivalOrder, FileScanTask from openhouse.dataloader._jvm import apply_libhdfs_opts from openhouse.dataloader._table_scan_context import TableScanContext @@ -53,11 +53,13 @@ def __init__( scan_context: TableScanContext, transform_sql: str | None = None, udf_registry: UDFRegistry | None = None, + batch_size: int | None = None, ): self._file_scan_task = file_scan_task self._scan_context = scan_context self._transform_sql = transform_sql self._udf_registry = udf_registry or NoOpRegistry() + self._batch_size = batch_size @property def id(self) -> str: @@ -76,7 +78,8 @@ def __iter__(self) -> Iterator[RecordBatch]: """Reads the file scan task and yields Arrow RecordBatches. Uses PyIceberg's ArrowScan to handle format dispatch, schema resolution, - delete files, and partition spec lookups. + delete files, and partition spec lookups. The number of batches loaded + into memory at once is bounded to prevent using too much memory at once. """ ctx = self._scan_context if ctx.worker_jvm_args is not None: @@ -88,7 +91,10 @@ def __iter__(self) -> Iterator[RecordBatch]: row_filter=ctx.row_filter, ) - batches = arrow_scan.to_record_batches([self._file_scan_task]) + batches = arrow_scan.to_record_batches( + [self._file_scan_task], + order=ArrivalOrder(concurrent_streams=1, batch_size=self._batch_size), + ) if self._transform_sql is None: yield from batches diff --git a/integrations/python/dataloader/tests/integration_tests.py b/integrations/python/dataloader/tests/integration_tests.py index 3ccfe4f48..538e4dd62 100644 --- a/integrations/python/dataloader/tests/integration_tests.py +++ b/integrations/python/dataloader/tests/integration_tests.py @@ -228,8 +228,13 @@ def read_token() -> str: snap1 = OpenHouseDataLoader(catalog=catalog, database=DATABASE_ID, table=TABLE_ID).snapshot_id assert snap1 is not None - # 4. Read all data - result = _read_all(OpenHouseDataLoader(catalog=catalog, database=DATABASE_ID, table=TABLE_ID)) + # 4. Read all data with batch_size and verify batch count + loader = OpenHouseDataLoader(catalog=catalog, database=DATABASE_ID, table=TABLE_ID, batch_size=2) + batches = [batch for split in loader for batch in split] + assert len(batches) == 2, f"Expected 2 batches (3 rows, batch_size=2), got {len(batches)}" + for batch in batches: + assert batch.num_rows <= 2 + result = pa.concat_tables([pa.Table.from_batches([b]) for b in batches]).sort_by(COL_ID) finally: os.dup2(saved_stdout, 1) os.close(saved_stdout) @@ -240,7 +245,7 @@ def read_token() -> str: assert result.column(COL_ID).to_pylist() == [1, 2, 3] assert result.column(COL_NAME).to_pylist() == ["alice", "bob", "charlie"] assert result.column(COL_SCORE).to_pylist() == [1.1, 2.2, 3.3] - print(f"PASS: read all {result.num_rows} rows") + print(f"PASS: read all {result.num_rows} rows in {len(batches)} batches (batch_size=2)") # 5a. Row filter loader = OpenHouseDataLoader(catalog=catalog, database=DATABASE_ID, table=TABLE_ID, filters=col(COL_ID) > 1) diff --git a/integrations/python/dataloader/tests/test_arrival_order.py b/integrations/python/dataloader/tests/test_arrival_order.py new file mode 100644 index 000000000..21e3870f2 --- /dev/null +++ b/integrations/python/dataloader/tests/test_arrival_order.py @@ -0,0 +1,137 @@ +"""Tests verifying the ArrivalOrder API from pyiceberg PR #3046 is available and functional. + +These tests confirm that the openhouse dataloader can access the new ScanOrder class hierarchy +added upstream (apache/iceberg-python#3046) and that ArrowScan.to_record_batches accepts the +order parameter. +""" + +import os + +import pyarrow as pa +import pyarrow.parquet as pq +import pytest +from pyiceberg.expressions import AlwaysTrue +from pyiceberg.io import load_file_io +from pyiceberg.io.pyarrow import ArrowScan +from pyiceberg.manifest import DataFile, FileFormat +from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC +from pyiceberg.schema import Schema +from pyiceberg.table import ArrivalOrder, FileScanTask, ScanOrder, TaskOrder +from pyiceberg.table.metadata import new_table_metadata +from pyiceberg.table.sorting import UNSORTED_SORT_ORDER +from pyiceberg.types import LongType, NestedField, StringType + +_SCHEMA = Schema( + NestedField(field_id=1, name="id", field_type=LongType(), required=False), + NestedField(field_id=2, name="name", field_type=StringType(), required=False), +) + + +def _write_parquet(tmp_path: object, table: pa.Table) -> str: + """Write a parquet file with Iceberg field IDs and return its path.""" + file_path = str(tmp_path / "test.parquet") # type: ignore[operator] + fields = [field.with_metadata({b"PARQUET:field_id": str(i + 1).encode()}) for i, field in enumerate(table.schema)] + pq.write_table(table.cast(pa.schema(fields)), file_path) + return file_path + + +def _make_arrow_scan(tmp_path: object, file_path: str) -> ArrowScan: + metadata = new_table_metadata( + schema=_SCHEMA, + partition_spec=UNPARTITIONED_PARTITION_SPEC, + sort_order=UNSORTED_SORT_ORDER, + location=str(tmp_path), + properties={}, + ) + return ArrowScan( + table_metadata=metadata, + io=load_file_io(properties={}, location=file_path), + projected_schema=_SCHEMA, + row_filter=AlwaysTrue(), + ) + + +def _make_file_scan_task(file_path: str, table: pa.Table) -> FileScanTask: + data_file = DataFile.from_args( + file_path=file_path, + file_format=FileFormat.PARQUET, + record_count=table.num_rows, + file_size_in_bytes=os.path.getsize(file_path), + ) + data_file._spec_id = 0 + return FileScanTask(data_file=data_file) + + +def _sample_table() -> pa.Table: + return pa.table( + { + "id": pa.array([1, 2, 3], type=pa.int64()), + "name": pa.array(["alice", "bob", "charlie"], type=pa.string()), + } + ) + + +class TestScanOrderImports: + """Verify the ScanOrder class hierarchy is importable from pyiceberg.table.""" + + def test_scan_order_base_class_exists(self) -> None: + assert ScanOrder is not None + + def test_task_order_is_scan_order(self) -> None: + assert issubclass(TaskOrder, ScanOrder) + + def test_arrival_order_is_scan_order(self) -> None: + assert issubclass(ArrivalOrder, ScanOrder) + + def test_arrival_order_default_params(self) -> None: + ao = ArrivalOrder() + assert ao.concurrent_streams == 8 + assert ao.batch_size is None + assert ao.max_buffered_batches == 16 + + def test_arrival_order_custom_params(self) -> None: + ao = ArrivalOrder(concurrent_streams=4, batch_size=32768, max_buffered_batches=8) + assert ao.concurrent_streams == 4 + assert ao.batch_size == 32768 + assert ao.max_buffered_batches == 8 + + def test_arrival_order_rejects_invalid_concurrent_streams(self) -> None: + with pytest.raises(ValueError, match="concurrent_streams"): + ArrivalOrder(concurrent_streams=0) + + def test_arrival_order_rejects_invalid_max_buffered_batches(self) -> None: + with pytest.raises(ValueError, match="max_buffered_batches"): + ArrivalOrder(max_buffered_batches=0) + + +class TestToRecordBatchesOrder: + """Verify ArrowScan.to_record_batches accepts the order parameter and returns correct data.""" + + def test_default_order_returns_all_rows(self, tmp_path: object) -> None: + """Default (TaskOrder) still works — backward compatible.""" + table = _sample_table() + file_path = _write_parquet(tmp_path, table) + arrow_scan = _make_arrow_scan(tmp_path, file_path) + task = _make_file_scan_task(file_path, table) + batches = list(arrow_scan.to_record_batches([task])) + result = pa.Table.from_batches(batches).sort_by("id") + assert result.column("id").to_pylist() == [1, 2, 3] + + def test_explicit_task_order_returns_all_rows(self, tmp_path: object) -> None: + table = _sample_table() + file_path = _write_parquet(tmp_path, table) + arrow_scan = _make_arrow_scan(tmp_path, file_path) + task = _make_file_scan_task(file_path, table) + batches = list(arrow_scan.to_record_batches([task], order=TaskOrder())) + result = pa.Table.from_batches(batches).sort_by("id") + assert result.column("id").to_pylist() == [1, 2, 3] + + def test_arrival_order_returns_all_rows(self, tmp_path: object) -> None: + table = _sample_table() + file_path = _write_parquet(tmp_path, table) + arrow_scan = _make_arrow_scan(tmp_path, file_path) + task = _make_file_scan_task(file_path, table) + batches = list(arrow_scan.to_record_batches([task], order=ArrivalOrder(concurrent_streams=2))) + result = pa.Table.from_batches(batches).sort_by("id") + assert result.column("id").to_pylist() == [1, 2, 3] + assert result.column("name").to_pylist() == ["alice", "bob", "charlie"] diff --git a/integrations/python/dataloader/tests/test_data_loader.py b/integrations/python/dataloader/tests/test_data_loader.py index 4f07ecced..8816ab1c0 100644 --- a/integrations/python/dataloader/tests/test_data_loader.py +++ b/integrations/python/dataloader/tests/test_data_loader.py @@ -567,6 +567,33 @@ def fake_scan(**kwargs): assert branch_splits[0]._file_scan_task.file.file_path == "branch.parquet" +# --- batch_size tests --- + + +def test_batch_size_forwarded_to_splits(tmp_path): + """batch_size is correctly passed through to each DataLoaderSplit.""" + catalog = _make_real_catalog(tmp_path) + + loader = OpenHouseDataLoader(catalog=catalog, database="db", table="tbl", batch_size=32768) + splits = list(loader) + + assert len(splits) >= 1 + for split in splits: + assert split._batch_size == 32768 + + +def test_batch_size_default_is_none(tmp_path): + """Omitting batch_size defaults to None in each split.""" + catalog = _make_real_catalog(tmp_path) + + loader = OpenHouseDataLoader(catalog=catalog, database="db", table="tbl") + splits = list(loader) + + assert len(splits) >= 1 + for split in splits: + assert split._batch_size is None + + # --- Predicate pushdown with transformer tests --- diff --git a/integrations/python/dataloader/tests/test_data_loader_split.py b/integrations/python/dataloader/tests/test_data_loader_split.py index 306eb3f84..afdea471d 100644 --- a/integrations/python/dataloader/tests/test_data_loader_split.py +++ b/integrations/python/dataloader/tests/test_data_loader_split.py @@ -43,6 +43,7 @@ def _create_test_split( transform_sql: str | None = None, table_id: TableIdentifier = _DEFAULT_TABLE_ID, udf_registry: UDFRegistry | None = None, + batch_size: int | None = None, ) -> DataLoaderSplit: """Create a DataLoaderSplit for testing by writing data to disk. @@ -103,6 +104,7 @@ def _create_test_split( scan_context=scan_context, transform_sql=transform_sql, udf_registry=udf_registry, + batch_size=batch_size, ) @@ -422,3 +424,47 @@ def test_worker_jvm_args_sets_libhdfs_opts(tmp_path, monkeypatch): list(split) assert os.environ[LIBHDFS_OPTS_ENV] == "-Xmx512m" + + +# --- batch_size tests --- + +_BATCH_SCHEMA = Schema( + NestedField(field_id=1, name="id", field_type=LongType(), required=False), +) + + +def _make_table(num_rows: int) -> pa.Table: + return pa.table({"id": pa.array(list(range(num_rows)), type=pa.int64())}) + + +def test_split_batch_size_limits_rows_per_batch(tmp_path): + """When batch_size is set, each RecordBatch has at most that many rows.""" + table = _make_table(100) + split = _create_test_split(tmp_path, table, FileFormat.PARQUET, _BATCH_SCHEMA, batch_size=10) + + batches = list(split) + + assert len(batches) >= 2, "Expected multiple batches with batch_size=10 and 100 rows" + for batch in batches: + assert batch.num_rows <= 10 + assert sum(b.num_rows for b in batches) == 100 + + +def test_split_batch_size_none_returns_all_rows(tmp_path): + """Default batch_size (None) returns all data correctly.""" + table = _make_table(50) + split = _create_test_split(tmp_path, table, FileFormat.PARQUET, _BATCH_SCHEMA) + + result = pa.Table.from_batches(list(split)) + assert result.num_rows == 50 + assert sorted(result.column("id").to_pylist()) == list(range(50)) + + +def test_split_batch_size_preserves_data(tmp_path): + """batch_size controls chunking but all data is preserved.""" + table = _make_table(25) + split = _create_test_split(tmp_path, table, FileFormat.PARQUET, _BATCH_SCHEMA, batch_size=7) + + result = pa.Table.from_batches(list(split)) + assert result.num_rows == 25 + assert sorted(result.column("id").to_pylist()) == list(range(25)) diff --git a/integrations/python/dataloader/uv.lock b/integrations/python/dataloader/uv.lock index cb01c81a9..1f2eca409 100644 --- a/integrations/python/dataloader/uv.lock +++ b/integrations/python/dataloader/uv.lock @@ -304,7 +304,7 @@ wheels = [ [[package]] name = "li-pyiceberg" -version = "0.11.2" +version = "0.11.3" source = { registry = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/simple/" } dependencies = [ { name = "cachetools" }, @@ -320,22 +320,22 @@ dependencies = [ { name = "tenacity" }, { name = "zstandard" }, ] -sdist = { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2.tar.gz", hash = "sha256:6d73600d862c097143edaebd0480c491a6b682f0ff5e82412318b3c49727ddf4" } +sdist = { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3.tar.gz", hash = "sha256:13558096c793ecd64eaeee5440440d406184b2693761a0c4b189a24453a26f4b" } wheels = [ - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b9cd3983816a7f6ea5db59e76369dded31594ee108f4dec097800aa3175612d" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e1e5544c1218af47c8ac60bc64a374c07d839678fd9ec9303b13ca0a79540e6" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f43f331134943d3a73b2a6e7fdd3582ac437e4757a1a47f94ea104b8b5321e9b" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d672d5d5eeb321e287cf40b8b787448a24b6ce9a8f7ec34604cb3a40dcf3c9e7" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5c2182c7b99e7e53b1e4604f75fa6b4783c8dd48cd5936fdadfc4de84c654ce" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9cfa4c517923e7c4b1886642813ef3ec9fe5fe771869e1fcafee7920d38561f" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:4e1fc561cb4953694de5600de7bf6fb084e3e7aad58775ee7f057a737f58d0a0" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbd2e2aa77c34700685bc532440f6112383a887766af77d89a7b3fa521755934" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c637b7d9f1376229b3a06a8b3b3a6f826043be721212107114b3e54f14c1629" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdcfc4fe6587173b5501c0d02a2d912ad9522282677892015b54fb80986dd72a" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:546025112098048774eab4ae056c248c7d00daed632258c8b4565a405d098c03" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e695d2d1532f1525b8963e0703a516fff9a19e392c13157daedb1e875faf633e" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d20cb2636e0ef3af1a6407aa728944f0e54b6c6dbe916ddde667999957b37af" }, - { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.2/li_pyiceberg-0.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:7cc8c418b0ddfafe4b8b4c4100f520f9a63794aecd8be74e3cbb651e4cb55af1" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:448af31363788d888fa436fb3d9913cb46c7e01409d6e41c6f878b8565da57ec" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:831f85d6d1abe221df33d1e2a1d8a5bb17821f03ca6d8ddbd2932527498fb1c7" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:567f4976eee9425b04324a09419d0daa28535d4ae5408f58575fb80a1564fc85" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b09979c347d81fba3fa7de83918979fbd406ffa8ec90ec825394415aa4063ac1" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33a649304ab689bddbb72d69ee9f2e063c75c5bb5e311941c72b88117296f27a" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ced2768668d1b178b4120e460bb5cbe524833933fe82a989253cf89ddd1abebe" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:9baee52cc5a0ebd129efe98046edacca44b5f4bf286c04874756177907332e35" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e503e918487f268a36fae60d23901dda89c3295c5dfa1ed3e5ee9fed0dc883bb" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de30e53dea782549445063d66d9b308555bdcbc65a24aa01d65dde667663b2de" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0d5f8e9fbf8c17508097520fed8e1572b92c48bd4c510a226355f173df9d31b" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7a5aaf3b95e7cdba86a1f494f787abaaaa69d56ba9abd130edafe4442de6679" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bf722ad6a68139f3a2a878de661419377d22e5c2d50edeae94c2d2ece3aba2f7" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4faef4e74d5fdda80fad796deb2f79f8e6beef798d0311ee0928c955d14832cd" }, + { url = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/li-pyiceberg/0.11.3/li_pyiceberg-0.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:98719cd3f6802cf1d2b19da929bb2befc254c28c24597df1d6dada806d1ff6b4" }, ] [[package]] @@ -607,7 +607,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "datafusion", specifier = "==51.0.0" }, - { name = "li-pyiceberg", specifier = "==0.11.2", index = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/simple/" }, + { name = "li-pyiceberg", specifier = "==0.11.3", index = "https://linkedin.jfrog.io/artifactory/api/pypi/openhouse-pypi/simple/" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "requests", specifier = ">=2.31.0" },