"""CSV utilities tailored for Alibaba Cloud billing exports.""" from __future__ import annotations import csv from pathlib import Path from typing import Dict, Iterable, List class CSVHandler: """Read and merge billing CSV files while preserving deterministic ordering.""" fieldnames: List[str] = [ "date", "account_id", "account_name", "financial_unit", "product_code", "product", "product_detail", "business_type", "region", "availability_zone", "cost", "list_price", "discount_amount", "coupon_deduction", "currency", ] def load_rows(self, path: Path) -> List[Dict[str, str]]: """Return all billing rows from ``path`` or an empty list when missing.""" if not path.exists(): return [] with path.open("r", encoding="utf-8") as handle: reader = csv.DictReader(handle) return [row for row in reader] def write_rows(self, path: Path, rows: Iterable[Dict[str, str]]) -> None: """Persist billing rows to ``path`` in canonical field order.""" path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=self.fieldnames) writer.writeheader() for row in rows: writer.writerow({field: row.get(field, "") for field in self.fieldnames}) def merge_rows( self, existing: Iterable[Dict[str, str]], new_rows: Iterable[Dict[str, str]], ) -> List[Dict[str, str]]: """Merge two iterables of rows, de-duplicating by a deterministic key.""" merged: Dict[str, Dict[str, str]] = {} for source in (existing, new_rows): for row in source: key = self._row_identity(row) merged[key] = row.copy() # Return rows sorted to keep git diffs stable: date → product → region. return sorted( merged.values(), key=lambda row: ( row.get("date", ""), row.get("account_id", ""), row.get("product", ""), row.get("product_detail", ""), row.get("region", ""), ), ) @staticmethod def _row_identity(row: Dict[str, str]) -> str: """Generate a stable identity key for a billing row.""" parts = [ row.get("date", ""), row.get("account_id", ""), row.get("product_code", ""), row.get("product_detail", ""), row.get("region", ""), row.get("availability_zone", ""), ] return "|".join(parts)