Files
PlantAI-dev/apps/cli/autofill_common.py
T
2026-08-03 18:42:47 +08:00

141 lines
6.0 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any
from gfa_core.config_templates import CONFIG_HEADERS
FASTQ_SUFFIX_RE = re.compile(r"\.(fastq|fq)(\.gz)?$", re.I)
CONFIG_COLUMNS = CONFIG_HEADERS
CONFIG_REVIEW_COLUMNS = [*CONFIG_HEADERS, "confidence", "notes"]
def now_tag() -> str:
return datetime.now().strftime("%Y%m%d_%H%M%S_%f")
def clean_text(value: Any) -> str:
if value is None:
return ""
text = str(value).strip()
return "" if text.lower() in {"nan", "none", "null", "undefined"} else text
def parse_key_values(text: str) -> dict[str, str]:
data: dict[str, str] = {}
if not text:
return data
patterns = {
"species_code": r"(?:species_code|物种代码|代码)\s*(?:是|为|=|:|:)?\s*([A-Za-z][A-Za-z0-9_-]{0,12})",
"species_name": r"(?:species_name|拉丁名|物种名|物种名称)\s*(?:是|为|=|:|:)?\s*([A-Za-z][A-Za-z ._-]{2,80})",
"gene_family": r"(?:gene_family|基因家族|家族)\s*(?:是|为|=|:|:)?\s*([A-Za-z0-9_.-]{2,30})",
"target": r"(?:target|目标物种)\s*(?:是|为|=|:|:)?\s*([A-Za-z][A-Za-z0-9_-]{0,12})",
"control": r"(?:control|对照组|ck)\s*(?:是|为|=|:|:)?\s*([A-Za-z0-9_.-]+)",
"treatment": r"(?:treatment|stress|处理|处理方式|实验处理)\s*(?:是|为|=|:|:)?\s*([A-Za-z0-9\u4e00-\u9fff _.+-]{1,40})",
}
for key, pattern in patterns.items():
match = re.search(pattern, text, re.I)
if match:
data[key] = match.group(1).strip(" ,,;;。")
if "treatment" not in data:
keyword_patterns = [
("salt stress", ["盐胁迫", "盐处理", "salt stress", "salinity", "nacl"]),
("drought stress", ["干旱胁迫", "干旱", "drought"]),
("heat stress", ["热胁迫", "高温胁迫", "heat stress", "heat"]),
("cold stress", ["冷胁迫", "低温胁迫", "cold stress", "cold"]),
("cadmium stress", ["镉胁迫", "镉处理", "cadmium", "cd stress"]),
("pathogen stress", ["病原", "侵染", "pathogen", "infection"]),
]
lower = text.lower()
for label, markers in keyword_patterns:
if any(marker.lower() in lower for marker in markers):
data["treatment"] = label
break
return data
def infer_species_code(name: str, hint: str = "") -> tuple[str, str]:
lower = name.lower()
known = [
("Lc", ["litchi_chinensis", "litchi-chinensis", "litchi.chinensis", "litchi", "lychee", "lchinesis", "lchinensis", "lc."]),
("Dl", ["dimocarpus_longan", "dimocarpus-longan", "dimocarpus.longan", "dimocarpus", "longan", "dl."]),
("Os", ["oryza_sativa", "oryza-sativa", "oryza.sativa", "oryza", "irgsp", "osa", "os."]),
("At", ["arabidopsis_thaliana", "arabidopsis-thaliana", "arabidopsis.thaliana", "arabidopsis", "thaliana", "tair", "ath", "at."]),
("Zm", ["zea_mays", "zea-mays", "zea.mays", "zea", "maize", "mays", "zma", "zm."]),
("Sl", ["solanum_lycopersicum", "solanum-lycopersicum", "solanum.lycopersicum", "solanum", "tomato", "sly", "sl."]),
("Cp", ["carica_papaya", "carica-papaya", "carica.papaya", "cpapaya", "papaya", "cpap", "cp."]),
("Ft", ["fagopyrum_tataricum", "fagopyrum-tataricum", "fagopyrum.tataricum", "fagopyrum", "tataricum", "buckwheat", "ft."]),
]
for code, markers in known:
if any(marker in lower for marker in markers):
return code, "high"
match = re.match(r"([A-Z][a-zA-Z0-9]{1,5})[._-]", name)
if match:
return match.group(1), "high"
if hint:
return hint, "medium"
return "", "low"
def infer_file_type(path: Path) -> tuple[str, str, str]:
name = path.name.lower()
if FASTQ_SUFFIX_RE.search(name):
return "fastq", "high", "RNA-seq FASTQ"
if "promoter" in name or "upstream" in name or "up2000" in name:
return "", "low", "Promoter FASTA is not a config.xlsx genome/cds/pep/gff input"
if name.endswith((".gff", ".gff3", ".gtf", ".gff.gz", ".gff3.gz", ".gtf.gz")):
return "gff", "high", ""
if any(x in name for x in ["cds", "cdna"]) and name.endswith((".fa", ".fasta", ".fna", ".ffn", ".cds", ".fa.gz", ".fasta.gz", ".fna.gz", ".ffn.gz", ".cds.gz")):
return "cds", "high", ""
if any(x in name for x in ["pep", "protein", ".faa"]) and name.endswith((".fa", ".fasta", ".faa", ".pep", ".fa.gz", ".fasta.gz", ".faa.gz", ".pep.gz")):
return "pep", "high", ""
if any(x in name for x in ["genome", "genomic", "dna", "toplevel", "chr", "chromosome"]) and name.endswith((".fa", ".fasta", ".fna", ".fa.gz", ".fasta.gz", ".fna.gz")):
return "genome", "medium", ""
if name.endswith((".fa", ".fasta", ".fna", ".faa", ".fa.gz", ".fasta.gz", ".fna.gz", ".faa.gz")):
return "pep", "low", "FASTA type is ambiguous; confirm pep/cds/genome"
return "", "low", "Unrecognized file type"
def scan_files(source_dir: Path, recursive: bool = True) -> list[Path]:
if not source_dir.exists():
return []
iterator = source_dir.rglob("*") if recursive else source_dir.iterdir()
skip_parts = {
"uploaded_configs",
"logs",
"cache",
"__pycache__",
"final",
"genome_prepare",
"reproducibility",
}
out = []
for path in iterator:
if not path.is_file():
continue
if any(part in skip_parts for part in path.parts):
continue
if path.name.startswith("."):
continue
out.append(path)
return sorted(out)
def backup_if_exists(path: Path, archive_dir: Path, label: str) -> str:
if not path.exists():
return ""
archive_dir.mkdir(parents=True, exist_ok=True)
backup = archive_dir / f"{now_tag()}_previous_{label}"
shutil.copy2(path, backup)
return str(backup)
def write_json(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")