chronaxis.sources.csv_spec
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""声明独立已解码信号表的格式,并保存每个CSV样本的位置。23SignalCSVSpec接收有序路径、时间列、时间单位、时区、编码、分隔符和4缺失标记,输出冻结配置;构造仅检查声明,不打开文件或根据后缀猜格式。5CSVSourceRef在通用来源引用上增加文件路径、记录末物理行和原始单元格6文本,供实际读取后追溯。时间量化诊断单独保留,不从文件名推断观测范围。7"""89# encoding合法性可以静态检查,不读取输入表内容。10import codecs11# 冻结配置与继承来源引用,不创建可变全局状态。12from dataclasses import dataclass13# 绝对日期没有自身时区偏移时需要明确tzinfo,不能采用机器当前时区。14from datetime import tzinfo15# 文件路径只作声明,不查询文件状态、不打开文件,也不重新排序。16from pathlib import Path17# 每样本引用沿用已验收的会话/来源/字段校验。18from ..core.models import SourceRef, TimebaseSpec19# 时间类型区别于表示单位,不能互相猜测。20from ..core.types import TimeType21# 字段保留原文,来源名是简单标识。22from ..core.validation import require_identifier, require_text23# offset遵守精确纳秒范围。24from ..timebase.conversion import require_int64_ns252627@dataclass(frozen=True)28class SignalCSVSpec:29 """保存已解码信号表的格式和时钟声明,保留paths的给定顺序。3031 name标识来源,paths指定连续输入文件;timebase给出相对公共起点或绝对32 时钟,time_column与unit解释时间单元格,offset_ns是显式纳秒偏移。33 timezone只用于缺少自身时区的绝对日期。encoding、sep和missing_values34 明确表格解析规则;声明检查不会核实文件存在性或实际表头。3536 实际适配器把每行每个请求列当作一次观测。缺失文本精确匹配后转None,37 表示明确缺失更新;旧Filter输出中的空白未更新含义须另按旧来源契约接入。38 """3940 name: str # 当前计划来源名。41 paths: tuple[Path, ...] # 明确连续文件顺序,不扫描目录自动排序。42 timebase: TimebaseSpec # relative公共起点/absolute明确声明。43 time_column: str # 原时间列名,必须在每个表头存在。44 unit: str # s/ms/us/ns或absolute的ISO8601。45 offset_ns: int = 0 # 显式固定偏移,不能证明同步。46 timezone: tzinfo | None = None # ISO无offset时明确采用的时区。47 encoding: str = 'utf-8-sig' # 默认接受普通UTF-8及文件开头BOM;显式编码仍优先。48 delimiter: str = ',' # 单字符分隔符,不自动sniff。49 missing_values: tuple[str, ...] = ('',) # 精确匹配原cell,不strip标记。5051 def __post_init__(self) -> None:52 """检查表格格式与时间声明,保持路径和缺失标记的原顺序。5354 参数:55 无。使用name、paths、timebase、time_column、unit、offset_ns、56 timezone、encoding、sep和missing_values字段。57 返回:58 None。只检查冻结实例,不读取表头或数据。59 异常:60 TypeError: 路径、时区或声明结构类型错误。61 ValueError: 时间单位、名称、分隔符或缺失标记不合法。62 LookupError: 编码名称不受本机编码注册表支持。63 OverflowError: offset_ns超出整数纳秒范围。64 """65 # 来源名必须是计划内安全别名。66 require_identifier(self.name, 'csv.name')67 # paths必须明确非空,不能接受字符串作为可迭代字符路径。68 if not isinstance(self.paths, tuple) or not self.paths or any(not isinstance(path, Path) for path in self.paths):69 # 不从目录猜测会话/文件顺序。70 raise TypeError('csv.paths需要非空tuple[Path]')71 # 必须明确时基/表示单位,ISO只用于absolute。72 if not isinstance(self.timebase, TimebaseSpec) or self.unit not in ('s', 'ms', 'us', 'ns', 'ISO8601') or self.unit == 'ISO8601' and self.timebase.time_type is not TimeType.ABSOLUTE:73 # 原始报文时间策略不在此通用表接口猜测。74 raise ValueError('csv时基/时间单位错误')75 # 原列名完整保留,不自动改名。76 require_text(self.time_column, 'csv.time_column')77 # 合法偏移也不会自动证明与另一来源已同步。78 require_int64_ns(self.offset_ns, 'csv.offset_ns')79 # 时区仅接受已经解析的显式tzinfo。80 if self.timezone is not None and not isinstance(self.timezone, tzinfo):81 # 配置解析层负责把地区时区名称或固定偏移文本转换为tzinfo。82 raise TypeError('csv.timezone需要tzinfo')83 # 编码名称是明确配置,查询注册表不会读取CSV文件。84 require_text(self.encoding, 'csv.encoding')85 # 未知编码在执行前报告,不尝试自动检测。86 codecs.lookup(self.encoding)87 # csv标准reader只接一个非换行分隔符。88 if type(self.delimiter) is not str or len(self.delimiter) != 1 or self.delimiter in '\r\n':89 # TSV可显式用tab,任意多字符分隔拒绝。90 raise ValueError('csv.delimiter需要单个非换行字符')91 # 标记可为空字符串但必须原生str、唯一冻结tuple。92 if not isinstance(self.missing_values, tuple) or any(type(item) is not str for item in self.missing_values) or len(set(self.missing_values)) != len(self.missing_values):93 # 不把未知格式猜为无效0。94 raise ValueError('csv.missing_values需要唯一字符串tuple')959697@dataclass(frozen=True)98class CSVSourceRef(SourceRef):99 """保留单个已读取样本的文件位置和原始文本。100101 继承的session_id、source_name和field确定会话、来源及原字段。102 path指出文件,line_number是CSV记录结束的物理行,time_text和value_text103 保留原单元格文本;time_quantized标记时间是否发生纳秒量化。104 引用不保存sequence,稳定原行序号由SignalSeries的sequence列另行保存。105 引号内跨多行的记录以最后物理行为位置,不能把它解释成记录首行。106 """107108 path: str # 实际读取路径原文。109 line_number: int # csv reader给出的记录末物理行,1开始。110 time_text: str # 原时间cell文本。111 value_text: str # 原字段cell文本,解析异常诊断不丢原文。112 time_quantized: bool # 时间转换曾按纳秒half-even量化。113114 def __post_init__(self) -> None:115 """检查继承的来源身份和CSV附加定位,不访问路径。116117 参数:118 无。读取来源身份、path、line_number、time_text、value_text和time_quantized。119 返回:120 None。文本保持原样,缺失单元格允许value_text为空字符串。121 异常:122 TypeError或ValueError: 来源名称、字段、路径、正整数行号或附加字段类型错误。123 副作用:124 无文件访问;引用构造本身不能证明path已经成功打开。125 """126 # 基类校验会话/来源/原字段,不能省略而只留下行号。127 super().__post_init__()128 # 路径文本必须非空;是否成功打开由实际读取适配器另行核实。129 require_text(self.path, 'csv_ref.path')130 # 物理行号明确正数,不接受True或0。131 if type(self.line_number) is not int or self.line_number <= 0 or type(self.time_text) is not str or type(self.value_text) is not str or type(self.time_quantized) is not bool:132 # 缺失cell可以value_text为空,不因此丢来源。133 raise ValueError('CSVSourceRef行号/文本/量化类型错误')134