Newer
Older
TIASDataHub / src / tiasdata / check.py
"""目録とストアの点検(`tiasdata check`.SPEC_03「CLI」).

問題を見つけても止めずに全体を数え,件数だけを返す(error-handling.md「データの問題は止める」の例外).
対応表(`identity.sqlite`)は開かない.
"""

import sqlite3
from dataclasses import dataclass
from pathlib import Path

from tiasdata import catalog, store


@dataclass(frozen=True)
class CheckResult:
    """点検の結果(件数).

    Attributes:
        catalog_files: 目録のファイルの数(ハッシュを照合した数).
        hash_ok: ストアのファイルの中身が SHA-256 と一致した数.
        hash_mismatch: ストアのファイルの中身が SHA-256 と一致しない数.
        missing_in_store: 目録にあってストアに無いファイルの数.
        store_files: ストアの正式な名前のファイルの数.
        not_in_catalog: ストアにあって目録に無いファイルの数(拡張子が目録と違うものを含む).
        writable: 読み取り専用が外れたストアのファイルの数.
        temp_files: 書きかけの一時ファイルの数(取り込みの中断で残ったもの).
        unexpected_in_store: ストアの想定外のファイル・フォルダの数.
        dangling_annotations: 参照先が無い所見の数.
        broken_dataset_members: 凍結した版の構成のうち参照先が欠けている行の数.
    """

    catalog_files: int
    hash_ok: int
    hash_mismatch: int
    missing_in_store: int
    store_files: int
    not_in_catalog: int
    writable: int
    temp_files: int
    unexpected_in_store: int
    dangling_annotations: int
    broken_dataset_members: int

    @property
    def problems(self) -> int:
        """問題の合計(0 なら問題なし)."""
        return (
            self.hash_mismatch
            + self.missing_in_store
            + self.not_in_catalog
            + self.writable
            + self.temp_files
            + self.unexpected_in_store
            + self.dangling_annotations
            + self.broken_dataset_members
        )


def run_check(conn: sqlite3.Connection, store_dir: Path) -> CheckResult:
    """目録とストアを点検する.

    Args:
        conn: 目録への接続(スキーマ最新).
        store_dir: ストアのフォルダ.

    Returns:
        点検の結果(件数).

    Raises:
        ValueError: ストアのフォルダが無い.
    """
    scan = store.scan_store(store_dir)
    catalog_files = catalog.list_asset_files(conn)

    statuses = [store.verify_stored(store_dir, sha256, ext) for sha256, ext in catalog_files]
    registered = set(catalog_files)
    return CheckResult(
        catalog_files=len(catalog_files),
        hash_ok=statuses.count(store.StoredFileStatus.OK),
        hash_mismatch=statuses.count(store.StoredFileStatus.MISMATCH),
        missing_in_store=statuses.count(store.StoredFileStatus.MISSING),
        store_files=len(scan.entries),
        not_in_catalog=sum(
            1 for entry in scan.entries if (entry.sha256, entry.ext) not in registered
        ),
        writable=sum(1 for entry in scan.entries if entry.writable),
        temp_files=scan.temp_files,
        unexpected_in_store=scan.unexpected,
        dangling_annotations=catalog.count_dangling_annotations(conn),
        broken_dataset_members=catalog.count_broken_dataset_members(conn),
    )