Files
wehub-resource-sync 6b7e6b44f1
Python Build and Type Check / python-ci (ubuntu-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
gh-pages / build (push) Has been cancelled
Python Publish (pypi) / Upload release to PyPI (push) Has been cancelled
Spellcheck / spellcheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:31 +08:00

151 lines
5.2 KiB
Python

# Copyright (C) 2025 Microsoft
# Licensed under the MIT License
"""A Parquet-based implementation of the Table abstraction with simulated streaming."""
from __future__ import annotations
import inspect
from io import BytesIO
from typing import TYPE_CHECKING, Any, cast
import pandas as pd
from graphrag_storage.tables.table import RowTransformer, Table
if TYPE_CHECKING:
from collections.abc import AsyncIterator
from graphrag_storage.storage import Storage
def _identity(row: dict[str, Any]) -> Any:
"""Return row unchanged (default transformer)."""
return row
def _apply_transformer(transformer: RowTransformer, row: dict[str, Any]) -> Any:
"""Apply transformer to row, handling both callables and classes.
If transformer is a class (e.g., Pydantic model), calls it with **row.
Otherwise calls it with row as positional argument.
"""
if inspect.isclass(transformer):
return transformer(**row)
return transformer(row)
class ParquetTable(Table):
"""Simulated streaming interface for Parquet tables.
Parquet format doesn't support true row-by-row streaming, so this
implementation simulates streaming via:
- Read: Loads DataFrame, yields rows via iterrows()
- Write: Accumulates rows in memory, writes all at once on close()
This provides API compatibility with CSVTable while maintaining
Parquet's performance characteristics for bulk operations.
"""
def __init__(
self,
storage: Storage,
table_name: str,
transformer: RowTransformer | None = None,
truncate: bool = True,
):
"""Initialize with storage backend and table name.
Args:
storage: Storage instance (File, Blob, or Cosmos)
table_name: Name of the table (e.g., "documents")
transformer: Optional callable to transform each row before
yielding. Receives a dict, returns a transformed dict.
Defaults to identity (no transformation).
truncate: If True (default), overwrite file on close.
If False, append to existing file.
"""
self._storage = storage
self._table_name = table_name
self._file_key = f"{table_name}.parquet"
self._transformer = transformer or _identity
self._truncate = truncate
self._df: pd.DataFrame | None = None
self._write_rows: list[dict[str, Any]] = []
def __aiter__(self) -> AsyncIterator[Any]:
"""Iterate through rows one at a time.
Loads the entire DataFrame on first iteration, then yields rows
one at a time with the transformer applied.
Yields
------
Any:
Each row as dict or transformed type (e.g., Pydantic model).
"""
return self._aiter_impl()
async def _aiter_impl(self) -> AsyncIterator[Any]:
"""Implement async iteration over rows."""
if self._df is None:
if await self._storage.has(self._file_key):
data = await self._storage.get(self._file_key, as_bytes=True)
self._df = pd.read_parquet(BytesIO(data))
else:
self._df = pd.DataFrame()
for _, row in self._df.iterrows():
row_dict = cast("dict[str, Any]", row.to_dict())
yield _apply_transformer(self._transformer, row_dict)
async def length(self) -> int:
"""Return the number of rows in the table."""
if self._df is None:
if await self._storage.has(self._file_key):
data = await self._storage.get(self._file_key, as_bytes=True)
self._df = pd.read_parquet(BytesIO(data))
else:
return 0
return len(self._df)
async def has(self, row_id: str) -> bool:
"""Check if row with given ID exists."""
async for row in self:
if isinstance(row, dict):
if row.get("id") == row_id:
return True
elif getattr(row, "id", None) == row_id:
return True
return False
async def write(self, row: dict[str, Any]) -> None:
"""Accumulate a single row for later batch write.
Rows are stored in memory and written to Parquet format
when close() is called.
Args
----
row: Dictionary representing a single row to write.
"""
self._write_rows.append(row)
async def close(self) -> None:
"""Flush accumulated rows to Parquet file and release resources.
Converts all accumulated rows to a DataFrame and writes
to storage as a Parquet file. If truncate=False and file exists,
appends to existing data.
"""
if self._write_rows:
new_df = pd.DataFrame(self._write_rows)
if not self._truncate and await self._storage.has(self._file_key):
existing_data = await self._storage.get(self._file_key, as_bytes=True)
existing_df = pd.read_parquet(BytesIO(existing_data))
new_df = pd.concat([existing_df, new_df], ignore_index=True)
await self._storage.set(self._file_key, new_df.to_parquet())
self._write_rows = []
self._df = None