Praneshrajan15's picture
Deploy DataForge playground API
791c076 verified
Raw
History Blame Contribute Delete
7.4 kB
"""Small string-preserving table primitives for DataForge core paths.
The CLI hot path should not need pandas just to profile or repair a CSV.
This module provides the narrow DataFrame-like surface that DataForge's
detectors, repairers, and verifier actually need.
"""
from __future__ import annotations
import csv
import io
from collections.abc import Iterable, Iterator, Sequence
from pathlib import Path
from typing import Any, Protocol, cast, overload
class TableLike(Protocol):
"""Protocol for the tabular surface consumed by DataForge core logic."""
@property
def columns(self) -> Any: ...
@property
def index(self) -> Any: ...
@property
def at(self) -> Any: ...
def __getitem__(self, key: str) -> Any: ...
def copy(self, deep: bool = True) -> Any: ...
def to_csv(
self,
buffer: io.StringIO,
*,
index: bool = False,
lineterminator: str = "\n",
) -> None: ...
class ColumnView(Sequence[str]):
"""Read-only column view with the small API repairers expect."""
def __init__(self, values: Sequence[str]) -> None:
self._values = values
def __iter__(self) -> Iterator[str]:
return iter(self._values)
def __len__(self) -> int:
return len(self._values)
@overload
def __getitem__(self, index: int) -> str: ...
@overload
def __getitem__(self, index: slice) -> Sequence[str]: ...
def __getitem__(self, index: int | slice) -> str | Sequence[str]:
return self._values[index]
def tolist(self) -> list[str]:
"""Return a list copy, matching pandas Series enough for detectors."""
return list(self._values)
class _AtIndexer:
"""``table.at[row, column]`` getter/setter compatibility shim."""
def __init__(self, table: Table) -> None:
self._table = table
def __getitem__(self, key: tuple[int, str]) -> str:
row, column = key
return self._table.cell(row, column)
def __setitem__(self, key: tuple[int, str], value: object) -> None:
row, column = key
self._table.set_cell(row, column, value)
class Table:
"""In-memory CSV table with string-preserving cells."""
def __init__(self, columns: Sequence[str], rows: Iterable[dict[str, object]]) -> None:
self._columns = [str(column) for column in columns]
self._rows: list[dict[str, str]] = [
{
column: "" if row.get(column) is None else str(row.get(column, ""))
for column in self._columns
}
for row in rows
]
self.at = _AtIndexer(self)
@property
def columns(self) -> list[str]:
"""Return column names in CSV order."""
return list(self._columns)
@property
def index(self) -> range:
"""Return zero-based row positions."""
return range(len(self._rows))
@property
def empty(self) -> bool:
"""Return whether the table has no rows."""
return not self._rows
@overload
def __getitem__(self, key: str) -> ColumnView: ...
@overload
def __getitem__(self, key: list[str]) -> Table: ...
@overload
def __getitem__(self, key: tuple[str, ...]) -> Table: ...
def __getitem__(self, key: str | list[str] | tuple[str, ...]) -> ColumnView | Table:
if isinstance(key, str):
if key not in self._columns:
raise KeyError(key)
return ColumnView([row.get(key, "") for row in self._rows])
columns = [str(column) for column in key]
for column in columns:
if column not in self._columns:
raise KeyError(column)
return Table(
columns, ({column: row.get(column, "") for column in columns} for row in self._rows)
)
def __len__(self) -> int:
return len(self._rows)
def copy(self, deep: bool = True) -> Table:
"""Return an independent table copy."""
del deep
return Table(self._columns, (dict(row) for row in self._rows))
def cell(self, row: int, column: str) -> str:
"""Return a cell value."""
if column not in self._columns:
raise KeyError(column)
return self._rows[row].get(column, "")
def set_cell(self, row: int, column: str, value: object) -> None:
"""Set a cell value after validating the column."""
if column not in self._columns:
raise KeyError(column)
self._rows[row][column] = "" if value is None else str(value)
def iter_records(self, columns: Sequence[str] | None = None) -> Iterator[dict[str, str]]:
"""Yield row dictionaries in table order."""
selected = self._columns if columns is None else [str(column) for column in columns]
for row in self._rows:
yield {column: row.get(column, "") for column in selected}
def to_dict(self, orient: str = "records") -> list[dict[str, str]]:
"""Return records in the pandas-compatible orientation used by DataForge."""
if orient != "records":
raise ValueError("Only orient='records' is supported.")
return list(self.iter_records())
def to_csv(
self, buffer: io.StringIO, *, index: bool = False, lineterminator: str = "\n"
) -> None:
"""Write the table as CSV to a text buffer."""
if index:
raise ValueError("Table.to_csv does not support index=True.")
writer = csv.DictWriter(buffer, fieldnames=self._columns, lineterminator=lineterminator)
writer.writeheader()
for row in self._rows:
writer.writerow({column: row.get(column, "") for column in self._columns})
def read_csv(path: Path) -> Table:
"""Read a CSV as a string-preserving ``Table``."""
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
columns = list(reader.fieldnames or [])
return Table(columns, reader)
def table_to_csv_bytes(table: TableLike) -> bytes:
"""Serialize a table-like object to UTF-8 CSV bytes."""
output = io.StringIO()
if isinstance(table, Table):
table.to_csv(output, index=False, lineterminator="\n")
else:
# pandas-compatible fallback for tests and optional integrations.
table.to_csv(output, index=False, lineterminator="\n")
return output.getvalue().encode("utf-8")
def column_names(table: TableLike) -> list[str]:
"""Return table column names as strings."""
return [str(column) for column in table.columns]
def row_count(table: TableLike) -> int:
"""Return the number of rows in a table-like object."""
return len(table.index)
def column_values(table: TableLike, column: str) -> list[Any]:
"""Return all values for one column."""
values = table[column]
if hasattr(values, "tolist"):
return list(values.tolist())
return list(values)
def cell_value(table: TableLike, row: int, column: str) -> str:
"""Return a cell value as a string."""
return str(table.at[row, column])
def set_cell_value(table: TableLike, row: int, column: str, value: object) -> None:
"""Set a cell value on a table-like object."""
table.at[row, column] = value
def copy_table(table: TableLike) -> TableLike:
"""Return a deep copy of a table-like object."""
copied = table.copy(deep=True)
return cast(TableLike, copied)