# -*- coding: utf-8 -*- """数据总线命名与分区模板化 —— 换行业只改配置,内核零改动(PRD 5.2 / Issue #4)。 模板(template)是行业复制的唯一粒度,所有外部资源命名均由 「模板名 + 点位维度」推导: - Kafka topic :`{topic_prefix}.{device_id}.points`(与 edge-gateway 上行一致); 分区 = hash(device_id) % num_partitions,保证单设备分区内有序; - TDengine :超级表 `{tpl}_points`、每测点子表 `{tpl}_pt_{point_id}`; - PostgreSQL :schema `tpl_{tpl}`,表名 `{schema}.{table}`; - MinIO :桶 `{template}-artifacts`,对象键 `features/{model_id}/{...}`。 命名清洗规则: - Kafka topic / MinIO 桶:小写 + 保留 `-` / `_` / `.` 之外替换为 `_`; - SQL 标识符(TDengine / PostgreSQL):额外把 `-` 替换为 `_`,避免引号转义。 """ from __future__ import annotations import hashlib import re from typing import List, Optional # 保留字符集(Kafka topic / MinIO 桶名均允许小写字母、数字、- _ .) _KEEP = re.compile(r"[^a-z0-9_.-]+") # SQL 标识符额外排除 `-`(TDengine / PostgreSQL 不加引号时不允许) _SQL_KEEP = re.compile(r"[^a-z0-9_.]+") def sanitize(name: str) -> str: """通用命名清洗:小写 + 非安全字符替换为 `_`。""" s = (name or "").strip().lower() s = _KEEP.sub("_", s) s = s.strip("._") or "tpl" return s def sanitize_sql(name: str) -> str: """SQL 标识符清洗:`-` 与其余非安全字符替换为 `_`。""" s = sanitize(name) s = _SQL_KEEP.sub("_", s) return s.strip("._") or "tpl" def _sql_str(value: str) -> str: """SQL 字符串字面量转义(单引号加倍)。""" return "'" + str(value).replace("'", "''") + "'" class TemplateNaming: """按模板推导全部外部资源命名(Kafka / TDengine / PostgreSQL / MinIO)。""" def __init__( self, template: str, topic_prefix: Optional[str] = None, num_partitions: int = 12, bucket_suffix: str = "artifacts", ): self.template = sanitize(template) # SQL 标识符用下划线形态(避免 `-` 需加引号) self.tpl_sql = sanitize_sql(self.template) self.topic_prefix = sanitize(topic_prefix) if topic_prefix else self.template self.num_partitions = max(1, int(num_partitions)) self.bucket_suffix = sanitize_sql(bucket_suffix) # ------------------------------------------------------------------ # Kafka:topic 命名 / 分区策略(子任务 #28) # ------------------------------------------------------------------ def topic(self, device_id: str) -> str: """上行 topic:`{topic_prefix}.{device_id}.points`(对齐 edge-gateway)。""" return f"{self.topic_prefix}.{sanitize(device_id)}.points" def partition(self, device_id: str, num_partitions: Optional[int] = None) -> int: """分区策略:按 device_id 一致性哈希 → 单设备分区内严格有序。""" n = num_partitions or self.num_partitions digest = hashlib.md5(sanitize(device_id).encode("utf-8")).hexdigest() return int(digest[:8], 16) % n def partitions(self, device_ids: List[str], num_partitions: Optional[int] = None) -> dict: """设备 → 分区映射(模板配置台预览用)。""" n = num_partitions or self.num_partitions return {d: self.partition(d, n) for d in device_ids} # ------------------------------------------------------------------ # TDengine:超级表 + 每测点子表(子任务 #29) # ------------------------------------------------------------------ def stable(self) -> str: """时序超级表:`{tpl}_points`。""" return f"{self.tpl_sql}_points" def subtable(self, point_id: str) -> str: """测点子表:`{tpl}_pt_{point_id}`(点位维度,字典驱动自动生成)。""" return f"{self.tpl_sql}_pt_{sanitize_sql(point_id)}" # ------------------------------------------------------------------ # PostgreSQL:schema 与表(子任务 #30) # ------------------------------------------------------------------ def pg_schema(self) -> str: """关系 schema:`tpl_{tpl}`。""" return f"tpl_{self.tpl_sql}" def pg_table(self, table: str) -> str: """`{schema}.{table}` 限定名。""" return f"{self.pg_schema()}.{sanitize_sql(table)}" # ------------------------------------------------------------------ # MinIO:对象桶与对象键(子任务 #31) # ------------------------------------------------------------------ def bucket(self) -> str: """对象存储桶:`{template}-artifacts`(S3 桶名允许 `-`)。""" return f"{self.template}-{self.bucket_suffix}" def snapshot_key(self, model_id: str, date: str, seq: int) -> str: """特征快照对象键:`features/{model_id}/{date}/{seq:06d}.jsonl`。""" return f"features/{sanitize_sql(model_id)}/{date}/{int(seq):06d}.jsonl" def model_artifact_key(self, model_id: str, version: str) -> str: """模型 artifact 对象键:`models/{model_id}/{version}/model.bin`。""" return f"models/{sanitize_sql(model_id)}/{sanitize_sql(version)}/model.bin" # ------------------------------------------------------------------ def _sql_str(self, value: str) -> str: return _sql_str(value)