feat(scripts): 支持动态指标和维度参数配置

为 build_studio_urls.py 添加指标和维度的可配置支持,包括:
- 新增 metrics.json 和 dimensions.json 映射文件加载
- 支持中文/英文别名映射及指标代码透传
- 列别名扩展以识别"指标"和"维度"列
- 空值时自动回退到 CONFIG 默认值
This commit is contained in:
2026-08-24 15:28:48 +08:00
parent ba00651fa9
commit b8bd749f43
16 changed files with 996 additions and 36 deletions

View File

@@ -18,7 +18,7 @@ import math
import pandas as pd
import pytest
from conftest import COUNTRIES_JSON
from conftest import COUNTRIES_JSON, DIMENSIONS_JSON, METRICS_JSON
# 文档化锚点2026-06-15 = 1781506800000见 docs/adr/0001脚本内 ANCHOR_MS
ANCHOR_MS = 1781506800000
@@ -183,8 +183,121 @@ class TestLoadCountryMap:
# ---------------------------------------------------------------------------
# parse_countries国家列解析
# load_metric_map / parse_metric指标映射与解析
# ---------------------------------------------------------------------------
class TestLoadMetricMap:
def test_missing_file_returns_empty_and_warns(self, url_builder, tmp_path, capsys):
path = tmp_path / "not_exists.json"
assert url_builder.load_metric_map(str(path)) == {}
assert "未找到指标映射文件" in capsys.readouterr().err
def test_bom_file_loads(self, url_builder, tmp_path):
p = tmp_path / "m.json"
p.write_bytes('{"订阅净增长": "subscribers_net_change"}'.encode("utf-8-sig"))
assert url_builder.load_metric_map(str(p))["订阅净增长"] == "SUBSCRIBERS_NET_CHANGE"
def test_known_code_passthrough_added(self, url_builder, tmp_path):
p = tmp_path / "m.json"
p.write_text("{}", encoding="utf-8")
m = url_builder.load_metric_map(str(p))
assert m["subscribers_net_change"] == "SUBSCRIBERS_NET_CHANGE"
assert m["total_estimated_earnings"] == "TOTAL_ESTIMATED_EARNINGS"
def test_real_metrics_json(self, real_metrics):
assert real_metrics["订阅净增长"] == "SUBSCRIBERS_NET_CHANGE"
assert real_metrics["观看时长"] == "EXTERNAL_WATCH_TIME"
assert real_metrics["预计收益"] == "TOTAL_ESTIMATED_EARNINGS"
# 全量:估算的合作伙伴收入 + 新增「税前收益/税前收入」别名
assert real_metrics["估算的合作伙伴收入"] == "TOTAL_ESTIMATED_EARNINGS"
assert real_metrics["税前收益"] == "TOTAL_ESTIMATED_EARNINGS"
assert real_metrics["税前收入"] == "TOTAL_ESTIMATED_EARNINGS"
# 覆盖足够多的指标(>50 条)
assert len(real_metrics) > 50
class TestParseMetric:
@pytest.mark.parametrize("empty", [None, "", float("nan")])
def test_blank_returns_none(self, url_builder, empty):
assert url_builder.parse_metric(empty, {}) is None
def test_known_code_passthrough_case_insensitive(self, url_builder):
assert url_builder.parse_metric("SUBSCRIBERS_NET_CHANGE", {}) == "SUBSCRIBERS_NET_CHANGE"
assert url_builder.parse_metric("subscribers_net_change", {}) == "SUBSCRIBERS_NET_CHANGE"
def test_chinese_name_mapped(self, url_builder):
assert url_builder.parse_metric("订阅净增长", {"订阅净增长": "SUBSCRIBERS_NET_CHANGE"}) \
== "SUBSCRIBERS_NET_CHANGE"
def test_english_synonym_mapped(self, url_builder):
assert url_builder.parse_metric("watch_time", {"watch_time": "EXTERNAL_WATCH_TIME"}) \
== "EXTERNAL_WATCH_TIME"
def test_quotes_stripped(self, url_builder):
assert url_builder.parse_metric("'订阅净增长'", {"订阅净增长": "SUBSCRIBERS_NET_CHANGE"}) \
== "SUBSCRIBERS_NET_CHANGE"
def test_unknown_raises(self, url_builder):
with pytest.raises(ValueError, match="未识别的指标"):
url_builder.parse_metric("神秘指标", {})
def test_real_map_common(self, url_builder, real_metrics):
assert url_builder.parse_metric("平均观看时长", real_metrics) == "AVERAGE_WATCH_TIME"
assert url_builder.parse_metric("收益", real_metrics) == "TOTAL_ESTIMATED_EARNINGS"
def test_estimated_partner_revenue_aliases(self, url_builder, real_metrics):
assert url_builder.parse_metric("估算的合作伙伴收入", real_metrics) == "TOTAL_ESTIMATED_EARNINGS"
assert url_builder.parse_metric("税前收益", real_metrics) == "TOTAL_ESTIMATED_EARNINGS"
assert url_builder.parse_metric("税前收入", real_metrics) == "TOTAL_ESTIMATED_EARNINGS"
# ---------------------------------------------------------------------------
# load_dimension_map / parse_dimension维度映射与解析
# ---------------------------------------------------------------------------
class TestLoadDimensionMap:
def test_missing_file_returns_empty_and_warns(self, url_builder, tmp_path, capsys):
path = tmp_path / "not_exists.json"
assert url_builder.load_dimension_map(str(path)) == {}
assert "未找到维度映射文件" in capsys.readouterr().err
def test_known_code_passthrough_added(self, url_builder, tmp_path):
p = tmp_path / "d.json"
p.write_text("{}", encoding="utf-8")
d = url_builder.load_dimension_map(str(p))
assert d["user"] == "USER"
def test_real_dimensions_json(self, real_dimensions):
assert real_dimensions["内容"] == "VIDEO"
assert real_dimensions["地理位置"] == "COUNTRY"
assert real_dimensions["频道"] == "USER"
assert len(real_dimensions) >= 30
class TestParseDimension:
@pytest.mark.parametrize("empty", [None, "", float("nan")])
def test_blank_returns_none(self, url_builder, empty):
assert url_builder.parse_dimension(empty, {}) is None
def test_known_code_passthrough_case_insensitive(self, url_builder):
# 仅 CONFIG 默认维度USER在无映射时也能透传
assert url_builder.parse_dimension("USER", {}) == "USER"
assert url_builder.parse_dimension("user", {}) == "USER"
def test_chinese_name_mapped(self, url_builder):
assert url_builder.parse_dimension("地理位置", {"地理位置": "COUNTRY"}) == "COUNTRY"
def test_quotes_stripped(self, url_builder):
assert url_builder.parse_dimension("'内容'", {"内容": "VIDEO"}) == "VIDEO"
def test_unknown_raises(self, url_builder):
with pytest.raises(ValueError, match="未识别的维度"):
url_builder.parse_dimension("神秘维度", {})
def test_real_map_common(self, url_builder, real_dimensions):
assert url_builder.parse_dimension("内容", real_dimensions) == "VIDEO"
assert url_builder.parse_dimension("设备类型", real_dimensions) == "DEVICE_PLATFORM_TYPE"
class TestParseCountries:
def test_none_returns_empty(self, url_builder):
assert url_builder.parse_countries(None, {}) == []
@@ -319,6 +432,62 @@ class TestBuildUrl:
for m in url_builder.CONFIG["t_metrics"]:
assert "t_metrics=%s" % m in url
# ---- 指标(每行可选)----
def test_metric_blank_uses_config_default(self, url_builder):
"""指标列留空:主指标与排序字段回退 CONFIG 默认(向后兼容)。"""
url, status, _ = url_builder.build_url(make_row(), {})
assert status == "ok"
assert "metric=%s" % url_builder.CONFIG["metric"] in url
assert "o_column=%s" % url_builder.CONFIG["o_column"] in url
def test_metric_chinese_sets_metric_and_o_column(self, url_builder, real_metrics):
"""指标列选中"预计收益"metric 与 o_column 都跟随所选指标。"""
url, status, _ = url_builder.build_url(
make_row(metric="预计收益"), {}, real_metrics)
assert status == "ok"
assert "metric=TOTAL_ESTIMATED_EARNINGS" in url
assert "o_column=TOTAL_ESTIMATED_EARNINGS" in url
def test_metric_code_passthrough(self, url_builder):
"""指标列填已知代码:原样透传。"""
url, status, _ = url_builder.build_url(
make_row(metric="EXTERNAL_VIEWS"), {})
assert status == "ok"
assert "metric=EXTERNAL_VIEWS" in url
assert "o_column=EXTERNAL_VIEWS" in url
def test_metric_unknown_raises(self, url_builder):
with pytest.raises(ValueError, match="未识别的指标"):
url_builder.build_url(make_row(metric="神秘指标"), {}, {})
def test_metric_case_insensitive_code(self, url_builder):
url, status, _ = url_builder.build_url(make_row(metric="external_views"), {})
assert status == "ok"
assert "metric=EXTERNAL_VIEWS" in url
# ---- 维度(每行可选)----
def test_dimension_blank_uses_config_default(self, url_builder):
"""维度列留空:回退 CONFIG 默认(向后兼容)。"""
url, status, _ = url_builder.build_url(make_row(), {})
assert status == "ok"
assert "dimension=%s" % url_builder.CONFIG["dimension"] in url
def test_dimension_chinese_sets_dimension(self, url_builder, real_dimensions):
url, status, _ = url_builder.build_url(
make_row(dimension="地理位置"), {}, dimension_map=real_dimensions)
assert status == "ok"
assert "dimension=COUNTRY" in url
def test_dimension_code_passthrough(self, url_builder, real_dimensions):
url, status, _ = url_builder.build_url(
make_row(dimension="video"), {}, dimension_map=real_dimensions)
assert status == "ok"
assert "dimension=VIDEO" in url
def test_dimension_unknown_raises(self, url_builder):
with pytest.raises(ValueError, match="未识别的维度"):
url_builder.build_url(make_row(dimension="神秘维度"), {}, {})
def test_no_country_omits_country_params(self, url_builder):
url, _, _ = url_builder.build_url(make_row(), {})
assert "ur_dimensions" not in url