Chronaxis用户手册English
本页目录

chronaxis.core.declarations

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""在读取数据之前检查来源、信号和派生名称声明,生成不可变的声明绑定。23输入为来源定位、时基、请求范围、信号定义和同步证据;输出DeclarationBindings4及其SHA-256身份。函数只检查配置关系,不证明文件、DBC字段或实际观测存在,5也不创建虚拟样本。静态表达式检查可使用这些声明;实际解释器仍需包含内存数据6的SignalBindings。本模块只依赖标准库和核心模型,没有文件操作、样本计算、7全局注册或执行计划缓存。8"""910# 冻结声明并为规范身份计算稳定摘要。11from dataclasses import dataclass, field12from hashlib import sha25613# 复用已验契约与同步证据,不放宽实际绑定校验。14from .binding import SignalDefinition, SynchronizationEvidence, _identity15from .unit_conversion import binding_version16from .unit_registry import REGISTRY_VERSION17from .lookup import LookupTablePlan, validate_lookups18# 时基和范围已经独立验证,不借空SessionSpec制造实际来源。19from .models import Interval, TimebaseSpec20from .types import TimeType21from .validation import require_identifier, require_text22from ..timebase.conversion import require_int64_ns232425@dataclass(frozen=True)26class DeclaredSource:27    """保存一个尚未读取的来源定位,以及其声明的固定时间映射。2829    input_identity包含规范配置中的路径顺序、格式和处理配置标识,不是文件内容30    哈希。offset_ns是整数纳秒偏移;模型不会估计漂移。字段实际存在性由来源执行31    接口另行检查,不能凭声明认定数据已读取。全部字段不可变。32    """3334    name: str  # 当前计划来源名。35    timebase: TimebaseSpec  # 原始来源坐标。36    input_identity: str  # 规范声明身份,与实际数据身份分别保存。37    offset_ns: int = 0  # 显式固定偏移,不自动估计漂移。3839    def __post_init__(self) -> None:40        """校验声明结构,保持声明身份与实际内容证据保持分开。4142        参数: 无;读取name/timebase/input_identity/offset_ns。43        返回: None,冻结声明。44        异常: TypeError/ValueError/TimeOverflowError为配置错误。45        """46        # 所有名称遵守统一安全规则。47        require_identifier(self.name, 'source.name')48        # 规范身份必须明确提供,不凭文件名猜测。49        require_text(self.input_identity, 'source.input_identity')50        # offset必须保持定宽整数。51        require_int64_ns(self.offset_ns)52        # 时基不得使用未校验字典。53        if not isinstance(self.timebase, TimebaseSpec):54            # 静态接口也不接无类型占位。55            raise TypeError('声明来源需要TimebaseSpec')565758@dataclass(frozen=True)59class DeclarationBindings:60    """冻结一个计划的来源、信号名称和同步关系,供执行前检查使用。6162    signals与derived_names共用名称空间。identity只证明配置声明;来源路径、63    处理配置、转换或有效期变化会改变它。模型不验证文件内容、实际列或DBC64    元数据,不能代替执行数据绑定。同步检查只针对当前节点实际使用的来源。65    """6667    session_id: str  # 当前会话声明。68    timebase: TimebaseSpec  # 计划分析坐标。69    domain: Interval | None  # 用户请求范围;None声明整文件无时间筛选,不表示无观测。70    sources: tuple[DeclaredSource, ...]  # 保留声明原顺序。71    signals: tuple[SignalDefinition, ...]  # 已校验单信号契约。72    derived_names: tuple[str, ...] = ()  # signals统一名称空间。73    synchronization: tuple[SynchronizationEvidence, ...] = ()  # 显式核实依据。74    config_version: int | None = field(default=None, kw_only=True)  # 缺省继承信号;空声明可指定版本。75    extensions: object = field(default=None, kw_only=True, repr=False)  # 仅接收宿主传入的不可变注册。76    lookups: tuple[LookupTablePlan, ...] = field(default=(), kw_only=True)  # v4命名表,独立于信号来源。77    temporal_inputs: tuple = field(default=(), kw_only=True)  # 离散序列对应的原采集时基。78    identity: str = field(init=False)  # 配置身份,不是执行绑定SHA。7980    def __post_init__(self) -> None:81        """检查名称、来源和显式同步关系,并生成声明身份。8283        参数: 无;输入均为已经校验的不可变声明模型。84        返回: None;不读取任何样本、文件或DBC。85        异常: TypeError/ValueError为结构错误、名称冲突、来源错误或同步关系冲突。86        """87        # 会话名采用同一标识符规则。88        require_identifier(self.session_id, 'dataset.session')89        # 容器不得在构造后被调用者改写。90        if (not isinstance(self.timebase, TimebaseSpec)91                or self.domain is not None and not isinstance(self.domain, Interval)92                or any(type(items) is not tuple for items in (self.sources, self.signals, self.derived_names, self.synchronization))):93            # 不冻结任意迭代器或未知对象。94            raise TypeError('声明绑定需要typed时基/范围与tuple容器')95        # 单项必须由对应模型校验,不使用duck typing。96        if any(not isinstance(item, DeclaredSource) for item in self.sources) or any(not isinstance(item, SignalDefinition) for item in self.signals):97            # 静态编译也不接受伪造类名。98            raise TypeError('声明绑定需要DeclaredSource/SignalDefinition')99        # 定义版本必须一致;没有信号时仍允许明确指定新版本。100        object.__setattr__(self, "config_version", binding_version(self.signals, self.config_version))101        validate_lookups(self.lookups, self.config_version)102        if self.extensions is not None:103            from ..extensions import ExtensionRegistry104            if type(self.extensions) is not ExtensionRegistry or self.config_version != 4:105                raise ValueError('extensions需要版本4及宿主ExtensionRegistry')106        # 来源表只属于当前计划。107        sources = {item.name: item for item in self.sources}108        # 重复来源不能字典覆盖。109        if len(sources) != len(self.sources):110            # 没有自动选取第一个来源的回退。111            raise ValueError('source.name重复')112        # 所有原信号和派生名称必须唯一。113        names = set()114        # 原字段只检查已声明来源,不声称实际存在。115        for definition in self.signals:116            # 完整单信号契约已经校验。117            if definition.source not in sources or definition.name in names:118                # 定位声明中实际冲突的信号。119                raise ValueError(f'{definition.name}来源不存在或别名重复')120            # 防止派生与当前信号重名。121            names.add(definition.name)122        # 派生名称不以点号或函数名绕过白名单。123        for name in self.derived_names:124            # 与实际执行绑定使用同一规则。125            require_identifier(name, 'derived.name')126            # 声明整个名称空间,未引用也不能隐藏冲突。127            if name in names:128                # 保留用户名称,不加后缀。129                raise ValueError(f'signals/derived名称冲突:{name}')130            # 登记下一个冲突校验点。131            names.add(name)132        # 同步声明必须唯一且指向当前计划。133        evidenced = set()134        # 不从机器时钟或两文件首点为0推断同步。135        for evidence in self.synchronization:136            # 正确类型和目标是声明有效性的必要条件。137            if not isinstance(evidence, SynchronizationEvidence):138                # 不以文字字符串冒充typed同步说明。139                raise TypeError('同步需要SynchronizationEvidence')140            # 重复/未知来源及不同分析时基都拒绝。141            if evidence.source not in sources or evidence.source in evidenced or evidence.analysis_timebase != self.timebase:142                # 不静默选一个映射。143                raise ValueError('同步来源重复/未知或目标时基不一致')144            # 保留已核实来源声明集合。145            evidenced.add(evidence.source)146        # 静态身份显式使用独立版本前缀,不与真实绑定共用证明。147        # 已有显式Interval仍使用原v1字节;无筛选声明以独立v2前缀和None端点表示。148        version = 'declaration-v1' if self.domain is not None else 'declaration-v2-no-time-request'149        payload = (version, self.session_id, self.timebase.timebase_id, self.timebase.time_type.value, self.timebase.origin,150                   None if self.domain is None else self.domain.start_ns,151                   None if self.domain is None else self.domain.end_ns,152                   tuple((item.name, item.timebase.timebase_id, item.timebase.time_type.value, item.timebase.origin, item.offset_ns, item.input_identity) for item in self.sources),153                   tuple(item.identity() for item in self.signals), self.derived_names,154                   tuple((item.source, item.evidence) for item in self.synchronization))155        # 当前规则版本和单位目录均属于身份。156        payload += (self.config_version, REGISTRY_VERSION)157        if self.extensions is not None:158            payload += ('host-extensions-v1', self.extensions.identity)159        if self.lookups:160            payload += ('lookup-table-v1', tuple(t.identity for t in self.lookups))161        if self.temporal_inputs:162            payload += ('temporal-inputs-v1', self.temporal_inputs)163        # 所有身份材料是已校验结构,不调用外部对象repr。164        object.__setattr__(self, 'identity', sha256(_identity(payload).encode('utf-8')).hexdigest())165166    def resolve(self, name: str) -> SignalDefinition:167        """定位当前声明中的唯一信号别名。168169        参数: name简单别名。170        返回: 当前计划SignalDefinition。171        异常: ValueError为名称非法或未知。172        """173        # 不把semantic点号转换成别名。174        require_identifier(name, 'signal alias')175        # 只查当前声明集合。176        for definition in self.signals:177            # 保留大小写敏感身份。178            if definition.name == name:179                # 不继承上个任务的输入。180                return definition181        # 未知信号不能通过空数据隐式创建。182        raise ValueError(f'未知信号别名:{name}')183184    def require_synchronized(self, names: tuple[str, ...]) -> None:185        """按实际联合依赖检查声明的公共时间基准。186187        参数: names为原信号简单别名tuple。188        返回: None,单来源或公共起点/显式同步依据齐备时通过。189        异常: TypeError/ValueError为容器、别名或同步缺项。190        """191        # 与实际绑定保持相同的依赖接口。192        if type(names) is not tuple:193            # 不接受可变或无限依赖列表。194            raise TypeError('联合依赖names必须为tuple')195        # 单来源多个信号只产生一个同步成员。196        if self.temporal_inputs:197            inputs = dict(self.temporal_inputs)198            names = tuple(alias for name in names for alias in inputs.get(name,(name,)))199        used = {self.resolve(name).source for name in names}200        # 独立来源可在自己坐标中校验。201        if len(used) <= 1:202            # 未使用来源不参与当前声明的证据检查。203            return204        # 构造已校验所有证据的来源/目标关系。205        evidenced = {item.source for item in self.synchronization}206        # 联合计算只检查实际参与来源。207        for source in self.sources:208            # 未参与或已有显式固定映射的来源无需重复声明公共起点。209            if source.name not in used or source.name in evidenced:210                # 不自动估计其他来源的漂移。211                continue212            # absolute坐标已统一为UTC;relative坐标须有相同的完整起点声明。213            same = source.timebase.time_type is self.timebase.time_type and (source.timebase.time_type is TimeType.ABSOLUTE or source.timebase == self.timebase)214            # 数值offset本身不能证明同步。215            if not same:216                # 提供具体缺项位置供配置修正。217                raise ValueError(f'联合依赖缺少同步依据:{source.name};需要公共起点或固定offset核实证据')218
Chronaxis 用户手册 · 离线阅读