Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

170 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
旧版分类迁移的值编译和条件树基础工具。
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Final
from app.domain.classification.vocabulary import classification_field_options
from app.schemas.category import (
ClassificationCondition,
ClassificationConditionGroup,
ClassificationConditionNode,
ClassificationFieldDefinition,
ClassificationMediaType,
)
_TMDB_SOURCE: Final[str] = "themoviedb"
_EXTENSION_PREFIX: Final[str] = f"extensions.{_TMDB_SOURCE}."
_LEGACY_FIELD_PRESENTATION: Final[dict[str, tuple[str, str]]] = {
"genre_ids": ("风格(旧规则)", "media.genre_keys"),
"origin_country": ("原产国家/地区(旧规则)", "media.countries"),
}
@dataclass(frozen=True, slots=True)
class _LegacyToken:
"""
保留旧字段展开后的同向值集合及其排除语义。
:param negative: 是否排除这些值;False 表示包含条件
:param values: 旧配置单个条件中的候选值,保持解析后的顺序
"""
negative: bool
values: tuple[str, ...]
def _legacy_field_definition(
field_id: str,
media_types: list[ClassificationMediaType],
) -> ClassificationFieldDefinition:
"""
构造不会出现在新规则选择器中的旧 TMDB 字段说明。
"""
field_name = field_id.removeprefix(_EXTENSION_PREFIX)
presentation = _LEGACY_FIELD_PRESENTATION.get(field_name)
label = presentation[0] if presentation else f"TMDB {field_name}"
replacement_field = presentation[1] if presentation else None
replacement_hint = f";新增条件请使用{presentation[0].replace('(旧规则)', '')}" if presentation else ""
return ClassificationFieldDefinition(
id=field_id,
label=label,
group="旧规则",
description=(f"从旧分类配置迁移,保留原有匹配方式{replacement_hint}"),
value_type="string_list",
operators=["contains_any", "contains_none", "exists", "not_exists"],
media_types=media_types,
source_support={_TMDB_SOURCE: "extension"},
options=classification_field_options("media.countries") if field_name == "origin_country" else [],
selectable=False,
replacement_field=replacement_field,
)
def _parse_legacy_tokens(value: str) -> tuple[tuple[_LegacyToken, ...], bool]:
"""
沿用旧范围展开语义,并合并同向枚举,避免值数量膨胀为叶子数量。
"""
raw_tokens = [item for item in value.split(",") if item]
values_by_sign: dict[bool, list[str]] = {}
requires_exists = not raw_tokens
for raw_token in raw_tokens:
expanded = _expand_legacy_token(raw_token)
if not expanded:
requires_exists = True
continue
for expanded_value in expanded:
negative = expanded_value.startswith("!")
plain_value = expanded_value[1:] if negative else expanded_value
values_by_sign.setdefault(negative, []).append(plain_value)
return tuple(_LegacyToken(negative, tuple(values)) for negative, values in values_by_sign.items()), requires_exists
def _expand_legacy_token(value: str) -> tuple[str, ...]:
"""
复现旧代码对数字闭区间和非数字连字符端点的展开。
"""
if "-" not in value:
return (value,)
value_begin, value_end = value.split("-", 1)
prefix = ""
if value_begin.startswith("!"):
prefix = "!"
value_begin = value_begin[1:]
if value_begin.isdigit() and value_end.isdigit():
return tuple(f"{prefix}{item}" for item in range(int(value_begin), int(value_end) + 1))
return (f"{prefix}{value_begin}", f"{prefix}{value_end}")
def _legacy_list_condition(
field_id: str,
tokens: Sequence[_LegacyToken],
requires_exists: bool,
) -> ClassificationConditionNode:
"""
把旧列表成员条件编译为正项 OR、负项逐组排除的条件树。
"""
positives = [
ClassificationCondition(
field=field_id,
operator="contains_any",
value=list(token.values),
)
for token in tokens
if not token.negative
]
negatives = [
ClassificationCondition(
field=field_id,
operator="contains_none",
value=list(token.values),
)
for token in tokens
if token.negative
]
nodes: list[ClassificationConditionNode] = []
if positives:
nodes.append(_any_or_single(positives))
nodes.extend(negatives)
if not nodes and requires_exists:
return ClassificationCondition(field=field_id, operator="exists")
return _all_or_single(nodes)
def _all_or_single(
nodes: Sequence[ClassificationConditionNode],
) -> ClassificationConditionNode:
"""
合并相邻 all 组并避免为单节点额外增加条件树深度。
"""
flattened: list[ClassificationConditionNode] = []
for node in nodes:
if isinstance(node, ClassificationConditionGroup) and node.all is not None:
flattened.extend(node.all)
else:
flattened.append(node)
if len(flattened) == 1:
return flattened[0]
return ClassificationConditionGroup(all=flattened)
def _any_or_single(
nodes: Sequence[ClassificationConditionNode],
) -> ClassificationConditionNode:
"""
合并相邻 any 组并避免为单节点额外增加条件树深度。
"""
flattened: list[ClassificationConditionNode] = []
for node in nodes:
if isinstance(node, ClassificationConditionGroup) and node.any is not None:
flattened.extend(node.any)
else:
flattened.append(node)
if len(flattened) == 1:
return flattened[0]
return ClassificationConditionGroup(any=flattened)