feat(scripts): 支持动态指标和维度参数配置
为 build_studio_urls.py 添加指标和维度的可配置支持,包括: - 新增 metrics.json 和 dimensions.json 映射文件加载 - 支持中文/英文别名映射及指标代码透传 - 列别名扩展以识别"指标"和"维度"列 - 空值时自动回退到 CONFIG 默认值
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user