Readers for IDAT v3, BPM (locus entries v4-v8), CSV manifest, EGT (data blocks 5-9) and GTC v3-v5 (plus a layout-preserving GTC writer); sample-sheet validation (IN-001..IN-014), IDAT discovery and pairing (IN-020..IN-022), message catalogue and exit codes (CLI-014/016), JSON Lines logging (CLI-007/008), atomic writes (CLI-011), run manifest, and `simurg inspect`. Validation (docs/validation/P1.md): GTC, BPM and EGT readers agree exactly with Illumina BeadArrayFiles on GSA v4 / GDA v1 files; GTCs round-trip byte-identically; BPM and CSV manifests are consistent. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01814GKVzZVL3FuF3DeiZXhv
43 lines
1.2 KiB
Python
43 lines
1.2 KiB
Python
"""File-type sniffing for ``simurg inspect`` (spec CLI-020)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gzip
|
|
from pathlib import Path
|
|
|
|
KINDS = ("idat", "bpm", "egt", "gtc", "csv_manifest", "samplesheet", "vcf", "unknown")
|
|
|
|
|
|
def _head(path: Path, n: int = 4096) -> bytes:
|
|
with path.open("rb") as fh:
|
|
b = fh.read(n)
|
|
if b[:2] == b"\x1f\x8b":
|
|
try:
|
|
with gzip.open(path, "rb") as gz:
|
|
return gz.read(n)
|
|
except OSError:
|
|
return b
|
|
return b
|
|
|
|
|
|
def detect_kind(path: str | Path) -> str:
|
|
p = Path(path)
|
|
head = _head(p)
|
|
name = p.name.lower()
|
|
if head[:4] == b"IDAT":
|
|
return "idat"
|
|
if head[:3] == b"BPM":
|
|
return "bpm"
|
|
if head[:3] == b"gtc":
|
|
return "gtc"
|
|
if head.startswith(b"##fileformat=VCF"):
|
|
return "vcf"
|
|
if name.endswith(".egt") or (len(head) >= 5 and head[:4] == b"\x03\x00\x00\x00"):
|
|
return "egt"
|
|
text = head.decode("utf-8", errors="replace").lower()
|
|
if "[assay]" in text or ("[heading]" in text and "descriptor file name" in text):
|
|
return "csv_manifest"
|
|
if "sentrixbarcode_a" in text or "beadchipname" in text or "[data]" in text:
|
|
return "samplesheet"
|
|
return "unknown"
|