Chronaxis用户手册English
本页目录

chronaxis.core.windows

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""声明事件窗口和片段结果,校验归属并保存请求、实际边界与原活动关联。23输入是WindowSpec、已完成Event元组及真实观测Interval;独立后端负责扩展、4分组和裁剪,再由本模块包装Segment。片段合并不重写Event或填平条件未知。5核心执行模型独立于上层JSON WindowPlan,由执行层显式转换,避免核心导入配置/I/O。6本模块只验证协议、归类和生成稳定ID;不计算窗口数值、不读取旧日志或调用writer。7输入与返回均为完整内存模型;资源预算和原始报文导出由上层负责。8"""910# 冻结结构保留边界和身份,标准库hash只生成结果标识。11from dataclasses import dataclass12from hashlib import sha25613import json14from math import isclose, isfinite15# 所有时间与来源位置为typed模型。16from .models import Interval, SourceRef17from .temporal import Event18from .types import QualityFlag as Q19from .validation import require_identifier, require_text20from ..timebase.conversion import require_int64_ns212223@dataclass(frozen=True)24class WindowSpec:25    """声明围绕同一规则事件的前后范围及明确合并政策。2627    anchor为point或activity;pre/post为非负int64纳秒。point至少一项正。28    exit_delay仅用于activity且post=0,根据真实活动间隔分组,不能按pre重叠扩大。29    实例不保存执行状态,不依赖上层配置或旧模式的隐式参数解释。30    """3132    window_id: str  # 当前窗口唯一简单标识。33    event_rule_id: str  # 已完成事件的规则标识。34    anchor: str  # point或activity,以onset/点时刻为锚。35    pre_ns: int = 0  # 非负前扩展,不使用confirmed代替onset。36    post_ns: int = 0  # 非负后扩展,exit_delay模式必须0。37    merge: str = 'none'  # none/overlap/overlap_and_adjacent/exit_delay。38    exit_delay_ns: int | None = None  # 只用于同名合并模式,不能重复附加post。3940    def __post_init__(self):41        """检查窗口声明,不计算边界或创建运行状态。4243        参数: 无;读取冻结字段。44        返回: None。45        异常: TypeError/ValueError/OverflowError为标识、参数或时间协议错误。46        """47        # 名称与真实引用分开,不能用自由路径冒充规则标识。48        require_identifier(self.window_id, 'window_id')49        require_identifier(self.event_rule_id, 'event_rule_id')50        # 精确非负纳秒,不接受bool、float或非法模式。51        object.__setattr__(self, 'pre_ns', require_int64_ns(self.pre_ns, 'pre_ns'))52        object.__setattr__(self, 'post_ns', require_int64_ns(self.post_ns, 'post_ns'))53        if self.pre_ns < 0 or self.post_ns < 0 or self.anchor not in ('point', 'activity') or self.merge not in ('none', 'overlap', 'overlap_and_adjacent', 'exit_delay'):54            raise ValueError('窗口锚点、扩展或合并政策错误')55        # 点本身没有持续长度,必须明确扩展出窗口。56        if self.anchor == 'point' and self.pre_ns == self.post_ns == 0:57            raise ValueError('point窗口至少一个扩展为正')58        # single只有退出延迟扩展,不能再附加同值post。59        if self.merge == 'exit_delay':60            object.__setattr__(self, 'exit_delay_ns', require_int64_ns(self.exit_delay_ns, 'exit_delay_ns'))61            if self.anchor != 'activity' or self.post_ns != 0 or self.exit_delay_ns < 0:62                raise ValueError('exit_delay要求activity、post=0及非负delay')63        elif self.exit_delay_ns is not None:64            raise ValueError('exit_delay_ns仅用于exit_delay合并')656667@dataclass(frozen=True)68class Segment:69    """保存一个片段的请求与实际范围,并关联全部原事件。7071    requested永不因EOF裁剪重写;actual为真实交集或None。覆盖为实际/请求时长。72    status为complete、partial或empty;event_statuses保留closed/timeout/interrupted/truncated/point。73    parent_event_ids、质量和字段来源不可变;合并不改变原事件数量或统计scope。74    """7576    segment_id: str  # 内容SHA,区别于旧整数秒文件名。77    dataset_id: str  # 当前数据集归属。78    session_id: str  # 当前真实会话,不跨会话合并。79    window: WindowSpec  # 原窗口声明,保留合并政策。80    requested: Interval  # 完整请求半开范围。81    actual: Interval | None  # 裁剪后实际范围,无交集为None。82    requested_coverage: float  # 两后端独立计算的时长比。83    status: str  # complete/partial/empty,不冒充文件发布完成。84    parent_event_ids: tuple[str, ...]  # 全部原活动/点事件稳定关联。85    event_statuses: tuple[str, ...]  # 同长原事件结束状态,UNKNOWN中断不改closed。86    quality: Q  # 全部原事件的结束质量及依赖诊断。87    source_refs: tuple[SourceRef, ...]  # 当前所有依赖原字段引用。8889    def __post_init__(self):90        """检查片段归属、范围、覆盖及原事件引用的一致性。9192        参数: 无;读取全部冻结字段。93        返回: None,不补范围或静默改覆盖。94        异常: TypeError/ValueError/OverflowError为非法结果模型。95        """96        # 稳定文本与简单会话标识各自校验。97        require_text(self.segment_id, 'segment_id')98        require_identifier(self.dataset_id, 'dataset_id')99        require_identifier(self.session_id, 'session_id')100        if not isinstance(self.window, WindowSpec) or not isinstance(self.requested, Interval) or self.requested.duration_ns() == 0:101            raise TypeError('Segment需WindowSpec及正请求Interval')102        # 交集必须在原请求内,不能外推或接受自由对象。103        if self.actual is not None and (not isinstance(self.actual, Interval) or self.actual.start_ns < self.requested.start_ns or self.actual.end_ns > self.requested.end_ns):104            raise ValueError('Segment实际范围必须是请求子区间')105        actual_duration = 0 if self.actual is None else self.actual.duration_ns()106        # 完整/部分/空范围由真实边界判定,不仅靠浮点比值。107        expected_status = 'complete' if self.actual == self.requested else 'partial' if actual_duration else 'empty'108        if self.status != expected_status or type(self.requested_coverage) not in (int, float) or not isfinite(self.requested_coverage) or not 0 <= self.requested_coverage <= 1:109            raise ValueError('Segment状态或覆盖错误')110        # 独立float64比值只容许相对舍入差;绝对容差为0,极小正覆盖不能伪报为0。111        if not isclose(self.requested_coverage, actual_duration / self.requested.duration_ns(), rel_tol=1e-15, abs_tol=0.):112            raise ValueError('Segment覆盖与时间事实不一致')113        # 每个原事件都有唯一ID与保留的结束状态。114        if not isinstance(self.parent_event_ids, tuple) or not self.parent_event_ids or len(set(self.parent_event_ids)) != len(self.parent_event_ids):115            raise ValueError('Segment需非空唯一parent_event_ids')116        for identity in self.parent_event_ids:117            require_text(identity, 'parent_event_id')118        if not isinstance(self.event_statuses, tuple) or len(self.event_statuses) != len(self.parent_event_ids) or any(status not in ('closed', 'timeout', 'interrupted', 'truncated', 'point') for status in self.event_statuses):119            raise ValueError('Segment事件状态需同长完整tuple')120        if not isinstance(self.quality, Q) or not isinstance(self.source_refs, tuple) or any(not isinstance(ref, SourceRef) for ref in self.source_refs):121            raise TypeError('Segment质量与来源需typed模型')122123124def require_windows(events, spec, observation):125    """验证已完成事件的排序、规则/会话归属和真实观测范围。126127    参数: events为已排序Event tuple;spec为WindowSpec;observation为真实Interval。128    返回: None,只校验协议,不扩展或合并窗口。129    异常: TypeError/ValueError为类型、重复、回退、归属或锚点不匹配。130    """131    # 上层负责取得真实观测,不以请求domain补齐EOF。132    if not isinstance(events, tuple) or not isinstance(spec, WindowSpec) or not isinstance(observation, Interval) or any(not isinstance(event, Event) for event in events):133        raise TypeError('窗口执行需Event tuple/WindowSpec/Interval')134    identities = set()135    previous = None136    for event in events:137        # 同一窗口仅使用同一规则/数据集/会话;不重新排序以掩盖回退。138        if event.rule.rule_id != spec.event_rule_id or event.event_type != spec.anchor or event.event_id in identities:139            raise ValueError('窗口事件规则、锚点或唯一性不匹配')140        # start_only的固定结束与抑制时长是同一声明,不能额外合并或改变post。141        if event.rule.rule_type == 'start_only' and (spec.merge != 'none' or spec.post_ns != event.rule.window_duration_ns):142            raise ValueError('start_only窗口需merge=none且post等于固定时长')143        if previous is not None and (event.start_ns < previous.start_ns or (event.rule.dataset_id, event.rule.session_id) != (previous.rule.dataset_id, previous.rule.session_id)):144            raise ValueError('窗口事件不能回退或跨会话合并')145        # 相同持续规则的真实活动不能重叠;点同刻不同规则已被上面拒绝。146        if previous is not None and spec.anchor == 'activity' and event.start_ns < previous.end_ns:147            raise ValueError('同一规则真实活动不能重叠')148        if event.start_ns < observation.start_ns or (event.start_ns if event.end_ns is None else event.end_ns) > observation.end_ns:149            raise ValueError('事件事实超出实际观测范围')150        identities.add(event.event_id)151        previous = event152153154@dataclass(frozen=True)155class CompactSegment:156    """仅保实际业务范围和父事件关联,不输出重复请求边界、覆盖比例或来源副本。"""157    segment_id: str158    dataset_id: str159    session_id: str160    window_id: str161    actual: Interval | None162    status: str163    parent_event_ids: tuple[str,...]164    quality: Q165    coverage: float | None = None  # 仅下游明确设置覆盖门槛时计算,默认不保存。166167168def require_resolved_windows(events,bounds,compact):169    """检查预先按选定周期取得的实际边界,未提供时沿旧实际扩展。170171    参数: 原事件、可选同长(start,end,left_clipped,right_clipped)tuple、精简开关。172    返回: None。异常: 类型/长度/边界错误。副作用: 不构造另一时间轴。173    """174    if type(compact) is not bool:175        raise TypeError('compact需要bool')176    if bounds is None:177        return178    if not compact or type(bounds) is not tuple or len(bounds)!=len(events):179        raise ValueError('已解析边界仅用于精简窗口且需同长tuple')180    for item in bounds:181        if type(item) is not tuple or len(item)!=4 or any(type(v) is not bool for v in item[2:]):182            raise TypeError('窗口边界需起止与两侧裁剪标识')183        Interval(item[0],item[1])184185186def make_segment(spec, requested, actual, coverage, parents, *, compact=False, clipped=False):187    """包装独立后端的边界/覆盖事实,保留全部原活动元数据。188189    参数: spec原窗口;requested/actual已算边界;coverage独立时长比;parents非空Event tuple;190        compact仅构造精简业务输出,clipped说明选定周期已在输入首尾裁剪。191    返回: Segment,稳定ID包含窗口语义、原事件引用与请求/实际边界。192    异常: 输出模型不满足约束时传播检查异常。193    副作用: 无;不计算扩展、分组或窗口覆盖,不修改Event。194    """195    # 原活动ID与状态保持顺序;元数据归并不涉及数值或旧扫描。196    identities = tuple(event.event_id for event in parents)197    if compact:198        quality=Q.NONE199        for event in parents:200            quality|=event.quality | (event.dependency_quality & Q.ESTIMATED)201        if actual is not None and not actual.duration_ns():202            actual=None203        status='empty' if actual is None else 'partial' if clipped or actual!=requested else 'complete'204        payload=('window-v2',spec.window_id,identities,None if actual is None else (actual.start_ns,actual.end_ns),status)205        identity=sha256(json.dumps(payload,separators=(',',':')).encode('utf-8')).hexdigest()206        return CompactSegment(identity,parents[0].rule.dataset_id,parents[0].rule.session_id,207                              spec.window_id,actual,status,identities,quality)208    statuses = tuple(event.status for event in parents)209    quality = Q.NONE210    refs = []211    for event in parents:212        quality |= event.quality | event.dependency_quality213        for ref in event.source_refs:214            if ref not in refs:215                refs.append(ref)216    # 稳定规范JSON生成ID;裁剪范围和配置变化对应不同片段身份。217    payload = ('window-v1', spec.window_id, spec.event_rule_id, spec.anchor, spec.pre_ns, spec.post_ns,218               spec.merge, spec.exit_delay_ns, identities, requested.start_ns, requested.end_ns,219               None if actual is None else (actual.start_ns, actual.end_ns))220    identity = sha256(json.dumps(payload, separators=(',', ':')).encode('utf-8')).hexdigest()221    # 分类只是输出政策,不复用任一后端的扩展或分组算法。222    status = 'complete' if actual == requested else 'partial' if actual is not None and actual.duration_ns() else 'empty'223    return Segment(identity, parents[0].rule.dataset_id, parents[0].rule.session_id, spec, requested, actual,224                   coverage, status, identities, statuses, quality, tuple(refs))225
Chronaxis 用户手册 · 离线阅读