SPB Git forge

spb/wp15_uqo

Public MIT
3commits 1branches 1releases
7.6 MBsize
maindefault branch
2 h agolast push
TeX 53.6% Python 46.2%
2.5 KB · 75 lines python
Raw Blame History
1"""Raw-file manifest: one row per monthly file, machine-readable (CLAUDE.md §4.2)."""23from __future__ import annotations45import json6from dataclasses import asdict, dataclass, field7from pathlib import Path8910@dataclass11class FileRecord:12    dataset_id: str13    dataset_revision: str14    remote_path: str15    local_path: str16    year: int17    month: int18    expected_bytes: int | None = None19    downloaded_bytes: int | None = None20    sha256: str | None = None21    lfs_sha256: str | None = None22    status: str = "pending"  # pending | downloading | complete | failed23    download_started_at: str | None = None24    download_completed_at: str | None = None25    row_count: int | None = None26    column_names: list[str] = field(default_factory=list)27    column_types: list[str] = field(default_factory=list)28    min_timestamp: str | None = None29    max_timestamp: str | None = None30    unique_symbol_count: int | None = None31    duplicate_key_count: int | None = None32    null_count_by_column: dict = field(default_factory=dict)33    audit_version: str = "0.1"34    code_commit: str | None = None353637class Manifest:38    """JSON-backed manifest with atomic writes; convertible to parquet/csv."""3940    def __init__(self, path: Path):41        self.path = path42        self.records: dict[str, FileRecord] = {}43        if path.exists():44            data = json.loads(path.read_text())45            for r in data["files"]:46                self.records[r["remote_path"]] = FileRecord(**r)4748    def upsert(self, rec: FileRecord) -> None:49        self.records[rec.remote_path] = rec5051    def save(self) -> None:52        tmp = self.path.with_suffix(".tmp")53        tmp.write_text(json.dumps(54            {"files": [asdict(r) for r in sorted(self.records.values(),55                                                 key=lambda x: x.remote_path)]},56            indent=1))57        tmp.replace(self.path)5859    def export_tabular(self) -> None:60        import polars as pl61        rows = []62        for r in self.records.values():63            d = asdict(r)64            # JSON-encode nested fields: empty structs are not writable to Parquet65            d["column_names"] = "|".join(d["column_names"])66            d["column_types"] = "|".join(d["column_types"])67            d["null_count_by_column"] = json.dumps(d["null_count_by_column"])68            rows.append(d)69        df = pl.DataFrame(rows)70        df.write_parquet(self.path.with_suffix(".parquet"))71        df.write_csv(self.path.with_suffix(".csv"))7273    def pending(self) -> list[FileRecord]:74        return [r for r in self.records.values() if r.status != "complete"]75