Chronaxis用户手册English
本页目录

chronaxis.planning.config_groups

源码基线:8904c2f744951857659abd40aaa3535d57faf53c

1"""把当前配置的就地分组送入既有解析器,不改变时间或数值规则。23输入为JSON配置副本,输出独立副本。reader集中读取参数,conversion集中换算,4interval跟随实际消费者,scope对象明确引用规则或窗口。仅复制配置,不读输入、5不推断设备同步、不生成逐帧追溯。反向函数用于配置工具输出当前唯一公共形式。6"""7from copy import deepcopy8from functools import wraps9import re10from .diagnostic_collection import usable_node, collect_node_error, current_collection11from .checks import object_at, array_at, text_at, timezone_at, PlanValidationError1213CONVERSION = ('input_unit', 'scale', 'offset')14READER_FLAT = ('encoding', 'delimiter', 'missing_values', 'decode_scope', 'input_check')15NODES = ('rules', 'windows', 'calculations', 'sequences', 'transforms')161718def public_errors(function):19    """将内部字段错误还原到用户声明位置。参数: 计划构建方法。返回: 包装方法。异常: 原PlanValidationError。2021    不改变错误代码和原因,不读取或保存输入;结构阶段对旧名的拒绝保留旧名位置。22    """23    @wraps(function)24    def checked(cls, config, *args, **kwargs):25        """调用原解析器,仅在错误退出时修正已归组字段的路径。"""26        try:27            return function(cls, config, *args, **kwargs)28        except PlanValidationError as error:29            combined = error30            for error in combined.diagnostics:31                if error.code == 'unknown_field' or type(config) is not dict:32                    continue33                path = error.path34                path = re.sub(r'(\$\.sources\[\d+\])\.(?:parser|reader_options)(?=\.|$)', r'\1.reader', path)35                path = re.sub(r'(\$\.sources\[\d+\])\.(encoding|delimiter|missing_values|decode_scope|input_check)(?=\.|$)', r'\1.reader.\2', path)36                path = re.sub(r'(\$\.signals\[\d+\](?:\.signals\[\d+\])?)\.(input_unit|scale|offset)(?=\.|$)', r'\1.conversion.\2', path)37                path = re.sub(r'(\.time)\.origin(?=\.|$)', r'\1.clock', path)38                path = re.sub(r'(\.resampling)\.origin(?=\.|$)', r'\1.start', path)39                path = path.replace('$.outputs.can_frames.timezone_offset_minutes', '$.outputs.can_frames.timezone')40                path = re.sub(r'(\$\.outputs\.(?:can_frames|signal_csv))\.window(?=\.|$)', r'\1.scope.window', path)41                match = re.match(r'^\$\.intervals\.overrides\.([^\.]+)(.*)$', path)42                if match:43                    for section in NODES:44                        nodes = config.get(section, [])45                        if type(nodes) is not list: continue46                        for index, node in enumerate(nodes):47                            if type(node) is dict and node.get('name') == match[1] and 'interval' in node:48                                path = f'$.{section}[{index}].interval' + match[2]49                error.path = path50                error.refresh_text()51            combined.path = combined.diagnostics[0].path52            combined.refresh_text()53            raise54    return checked555657def _leaves(signals, path='$.signals'):58    """遍历叶信号及声明路径。参数: 数组。返回: (dict,path)。异常: 错误数组/对象。"""59    for index, item in enumerate(array_at(signals, path)):60        child_path = f'{path}[{index}]'61        if type(item) is not dict:62            raise PlanValidationError(child_path, '信号声明必须为对象')63        if 'signals' in item:64            yield from _leaves(item['signals'], child_path + '.signals')65        else:66            yield item, child_path676869def normalize_configuration(config):70    """解析当前公共分组。参数: 已验证JSON字典。返回: 独立内部字典。异常: 公共结构错误。7172    只接受当前拼写,旧扁平参数明确拒绝;内部parser/reader_options等仅供既有73    解析器消费,不作为第二套用户配置入口。调用应在预设展开之前。74    """75    root = deepcopy(config)76    for index, source in enumerate(array_at(root['sources'], '$.sources')):77        path = f'$.sources[{index}]'78        if not usable_node(path, source):79            continue80        try:81            path = f'$.sources[{index}]'82            if type(source) is not dict:83                raise PlanValidationError(path, '来源必须为对象')84            for key in ('parser', 'reader_options', *READER_FLAT):85                if key in source:86                    raise PlanValidationError(path + '.' + key, '读取参数请放在reader对象内', 'unknown_field')87            if 'reader' in source:88                reader = dict(object_at(source.pop('reader'), path + '.reader', (),89                    (*READER_FLAT, 'decode_policy', 'version', 'layout', 'time_format', 'id_base',90                     'length_base', 'channel_origin', 'payload_style', 'rollover_min_drop')))91                # 信号表读取参数和CAN解码参数分别交给各自的原校验器。92                for key in ('decode_scope', 'input_check'):93                    if key in reader: source[key] = reader.pop(key)94                if 'decode_policy' in reader:95                    source['reader_options'] = {'decode_policy': reader.pop('decode_policy')}96                if source.get('source_type') == 'signal_csv':97                    source.update(reader)98                elif reader:99                    source['parser'] = reader100            clock = source.get('time')101            if type(clock) is dict and 'origin' in clock:102                raise PlanValidationError(path + '.time.origin', '来源时间基准名称请使用clock', 'unknown_field')103            if type(clock) is dict and 'clock' in clock:104                clock['origin'] = clock.pop('clock')105            if type(clock) is dict and 'timezone_offset_minutes' in clock:106                raise PlanValidationError(path + '.time.timezone_offset_minutes',107                    '时区统一使用timezone,例如+08:00', 'unknown_field')108        except PlanValidationError as error:109            collect_node_error(error, path, source)110    for signal, path in _leaves(root['signals']):111        if not usable_node(path, signal):112            continue113        try:114            for key in CONVERSION:115                if key in signal:116                    raise PlanValidationError(path + '.' + key, '信号换算请放在conversion对象内', 'unknown_field')117            if 'conversion' in signal:118                signal.update(object_at(signal.pop('conversion'), path + '.conversion', (), CONVERSION))119        except PlanValidationError as error:120            collect_node_error(error, path, signal)121    intervals = dict(object_at(root.get('intervals', {}), '$.intervals', (), ('default',)))122    overrides = {}123    for section in NODES:124        for index, node in enumerate(array_at(root.get(section, []), '$.' + section)):125            path = f'$.{section}[{index}]'126            if type(node) is not dict:127                raise PlanValidationError(path, '节点必须为对象')128            if 'interval' in node:129                name = text_at(node.get('name'), path + '.name', identifier=True)130                if name in overrides:131                    raise PlanValidationError(path + '.name', '间隔消费者名称重复')132                # 原间隔解析器随后检查模式、驱动、正周期和消费者是否适用。133                overrides[name] = node.pop('interval')134            if section == 'calculations' and 'scope' in node:135                scope = node['scope']136                if scope == 'session':137                    continue138                scope = object_at(scope, path + '.scope', (), ('rule', 'window'))139                if len(scope) != 1:140                    raise PlanValidationError(path + '.scope', 'scope需要且只能引用一个rule或window')141                key = next(iter(scope))142                target = text_at(scope[key], path + '.scope.' + key, identifier=True)143                node['scope'] = ('event:' if key == 'rule' else 'segment:') + target144    if overrides: intervals['overrides'] = overrides145    if intervals or 'intervals' in root: root['intervals'] = intervals146    for kind in ('signal_csv', 'can_frames'):147        outputs = root.get('outputs', {})148        output = outputs.get(kind) if type(outputs) is dict else None149        if type(output) is not dict: continue150        path = '$.outputs.' + kind151        if kind == 'can_frames':152            if 'timezone_offset_minutes' in output:153                raise PlanValidationError(path + '.timezone_offset_minutes', '时区请使用timezone,例如+08:00', 'unknown_field')154            if 'timezone' in output:155                zone = timezone_at(output.pop('timezone'), path + '.timezone')156                offset = None if zone is None else zone.utcoffset(None)157                if zone is not None and offset is None:158                    raise PlanValidationError(path + '.timezone', '原帧导出需要UTC或固定时区,例如+08:00')159                output['timezone_offset_minutes'] = None if offset is None else int(offset.total_seconds() // 60)160        grid = output.get('resampling')161        if type(grid) is dict:162            if 'origin' in grid:163                raise PlanValidationError(path + '.resampling.origin', '采样网格起点请使用start', 'unknown_field')164            if 'start' in grid: grid['origin'] = grid.pop('start')165        if 'window' in output:166            raise PlanValidationError(path + '.window', '窗口引用请写scope: {window: 名称}', 'unknown_field')167        scope = output.get('scope', 'session')168        if scope == 'session': continue169        scope = object_at(scope, path + '.scope', (), ('window', 'windows'))170        if len(scope) != 1:171            raise PlanValidationError(path + '.scope', '导出scope需要一个window引用或windows: all')172        output['scope'] = 'segments'173        if 'window' in scope:174            output['window'] = text_at(scope['window'], path + '.scope.window', identifier=True)175        elif kind != 'can_frames' or scope['windows'] != 'all':176            raise PlanValidationError(path + '.scope.windows', '仅原帧导出支持windows: all')177    return root178179180def public_configuration(config):181    """把生成器内部结构转成当前公共配置。参数: 配置字典。返回: 独立字典。异常: 结构错误。182183    不读取文件,不改变自由metadata或业务值;已使用新分组的内容原样保留。184    用于预设展开和resolve,把内部字段还原为当前公开配置结构。185    """186    root = deepcopy(config)187    for source in root.get('sources', []):188        reader = dict(source.get('reader', {}))189        for key in ('parser', 'reader_options'):190            reader.update(source.pop(key, {}))191        for key in READER_FLAT:192            if key in source: reader[key] = source.pop(key)193        if reader: source['reader'] = reader194        clock = source.get('time', {})195        if 'origin' in clock: clock['clock'] = clock.pop('origin')196        if 'timezone_offset_minutes' in clock:197            minutes = clock.pop('timezone_offset_minutes')198            if type(minutes) is not int or not -1439 <= minutes <= 1439:199                raise PlanValidationError('$.sources.time.timezone', '固定时区分钟数无效')200            clock['timezone'] = ('+' if minutes >= 0 else '-') + f'{abs(minutes)//60:02}:{abs(minutes)%60:02}'201    for signal, _ in _leaves(root.get('signals', [])):202        conversion = dict(signal.get('conversion', {}))203        for key in CONVERSION:204            if key in signal: conversion[key] = signal.pop(key)205        if conversion: signal['conversion'] = conversion206    intervals = root.get('intervals', {})207    overrides = intervals.pop('overrides', {}) if type(intervals) is dict else {}208    for section in NODES:209        for node in root.get(section, []):210            if node.get('name') in overrides:211                node['interval'] = overrides.pop(node['name'])212            scope = node.get('scope')213            if section == 'calculations' and type(scope) is str and ':' in scope:214                kind, target = scope.split(':', 1)215                if kind in ('event', 'segment'):216                    node['scope'] = {'rule' if kind == 'event' else 'window': target}217    # 无对应节点不能丢弃声明,让原错误继续可见而不是静默删配置。218    if overrides: intervals['overrides'] = overrides219    for kind in ('signal_csv', 'can_frames'):220        outputs = root.get('outputs', {})221        output = outputs.get(kind) if type(outputs) is dict else None222        if type(output) is not dict: continue223        if kind == 'can_frames' and 'timezone_offset_minutes' in output:224            minutes = output.pop('timezone_offset_minutes')225            output['timezone'] = None if minutes is None else ('+' if minutes >= 0 else '-') + f'{abs(minutes)//60:02}:{abs(minutes)%60:02}'226        grid = output.get('resampling')227        if type(grid) is dict and 'origin' in grid: grid['start'] = grid.pop('origin')228        if output.get('scope') != 'segments': continue229        window = output.pop('window', None)230        output['scope'] = {'windows': 'all'} if window is None else {'window': window}231    return root232
Chronaxis 用户手册 · 离线阅读