from __future__ import annotations import logging from functools import lru_cache from typing import Any from supply_infra.config import get_infra_settings logger = logging.getLogger(__name__) class ODPSClient: """ODPS / MaxCompute 查询客户端封装。""" def __init__( self, access_id: str, access_key: str, project: str, endpoint: str, ) -> None: self.access_id = access_id self.access_key = access_key self.project = project self.endpoint = endpoint self._client: Any = None def _get_client(self) -> Any: if self._client is None: try: from odps import ODPS except ImportError as e: raise ImportError( "pyodps 未安装,请执行: pip install pyodps" ) from e self._client = ODPS( self.access_id, self.access_key, project=self.project, endpoint=self.endpoint, ) return self._client def execute_sql(self, sql: str) -> list[dict[str, Any]]: """执行 SQL 并返回字典列表。""" client = self._get_client() logger.info("ODPS executing SQL: %s", sql[:200]) instance = client.execute_sql(sql) with instance.open_reader() as reader: columns = [col.name for col in reader._schema.columns] # type: ignore[attr-defined] return [dict(zip(columns, row.values)) for row in reader] return [] def fetch_pattern_mining_elements(self, bizdate: str) -> list[dict[str, Any]]: """拉取 pattern_mining_element 元素(name, category_id)。""" sql = f""" SELECT t1.name ,t1.category_id FROM loghubods.pattern_mining_element t1 LEFT JOIN loghubods.post t2 ON t1.post_id = t2.post_id AND t2.dt = '{bizdate}' WHERE t1.dt = '{bizdate}' AND t1.execution_id = 401 AND t1.source_table = 'post_decode_topic_point_element' AND t1.name is NOT NULL AND t1.category_id IS NOT NULL AND t1.element_type = '实质' AND t2.platform = 'piaoquan' GROUP BY t1.name ,t1.category_id """ return self.execute_sql(sql) def fetch_pattern_mining_categories(self, bizdate: str) -> list[dict[str, Any]]: """拉取 public_pattern_mining_category 分类。""" sql = f""" SELECT id,name,description,level,parent_id FROM loghubods.public_pattern_mining_category WHERE dt = '{bizdate}' AND execution_id = 401 AND source_type = '实质' """ return self.execute_sql(sql) def fetch_multi_demand_pool(self, bizdate: str) -> list[dict[str, Any]]: """拉取 dwd_multi_demand_pool_di 策略需求天级数据(video_list 仅取前 10 个)。""" sql = f""" SELECT strategy ,demand_id ,demand_name ,weight ,`type` ,video_count ,SLICE(video_list, 1, 10) AS video_list ,extend FROM loghubods.dwd_multi_demand_pool_di WHERE dt = '{bizdate}' """ return self.execute_sql(sql) def count_multi_demand_pool(self, bizdate: str) -> int: """统计 dwd_multi_demand_pool_di 分区去重后行数(strategy + demand_id)。""" sql = f""" SELECT COUNT(1) AS cnt FROM ( SELECT strategy ,demand_id FROM loghubods.dwd_multi_demand_pool_di WHERE dt = '{bizdate}' GROUP BY strategy ,demand_id ) t """ rows = self.execute_sql(sql) if not rows: return 0 return int(rows[0].get("cnt") or 0) def fetch_topic_decode_results( self, dt: str, vids: list[str], batch_size: int = 100, ) -> list[dict[str, Any]]: """按 vid 批量拉取 dwd_topic_decode_result_di 的 vid、url1、url2、decode_result。""" # 保序去重 unique_vids = list( dict.fromkeys( str(v).strip() for v in vids if v is not None and str(v).strip() ) ) if not unique_vids: return [] results: list[dict[str, Any]] = [] for i in range(0, len(unique_vids), batch_size): batch = unique_vids[i : i + batch_size] in_list = ",".join("'" + v.replace("'", "''") + "'" for v in batch) sql = f""" SELECT vid ,decode_result ,url1 ,url2 FROM loghubods.dwd_topic_decode_result_di WHERE dt = '{dt}' AND vid IN ({in_list}) """ results.extend(self.execute_sql(sql)) return results def fetch_real_rov_vov_7d( self, dt_left: str, dt_right: str, limit: int = 1000, ) -> list[dict[str, Any]]: """拉取近 N 日人工/自动 AGC 的 rov_diff / vov_diff(相对全局基线)。""" sql = f""" WITH base AS ( SELECT 特征维度, 特征值, 特征值_寻找词, 供给类型, 当日分发曝光pv, 当日分发回流uv, 当日分发拉回曝光pv FROM loghubods.dwd_video_produce_plan_stat_hour WHERE dt BETWEEN '{dt_left}' AND '{dt_right}' AND 供给类型 IN ('人工AGC', '自动AGC') AND 特征值 NOT IN ('-', '', 'null') ), grouped AS ( SELECT 1 AS row_order, 特征维度, 特征值, 特征值_寻找词, 供给类型, SUM(当日分发曝光pv) AS exp, SUM(当日分发回流uv) AS return_uv, SUM(当日分发拉回曝光pv) AS new_exp FROM base GROUP BY 特征维度, 特征值, 特征值_寻找词, 供给类型 ), result_rows AS ( SELECT 0 AS row_order, '全局SUM' AS 特征维度, '全局SUM' AS 特征值, '全局SUM' AS 特征值_寻找词, '全局SUM' AS 供给类型, SUM(exp) AS exp, SUM(return_uv) AS return_uv, SUM(new_exp) AS new_exp, COALESCE(ROUND(SUM(return_uv) / NULLIF(SUM(exp), 0), 4), 0) AS rov, COALESCE(ROUND(SUM(new_exp) / NULLIF(SUM(exp), 0), 4), 0) AS vov, 0 AS rov_diff, 0 AS vov_diff FROM grouped UNION ALL SELECT row_order, 特征维度, 特征值, 特征值_寻找词, 供给类型, exp, return_uv, new_exp, COALESCE(ROUND(return_uv / NULLIF(exp, 0), 4), 0) AS rov, COALESCE(ROUND(new_exp / NULLIF(exp, 0), 4), 0) AS vov, COALESCE( ROUND( (return_uv / NULLIF(exp, 0)) / NULLIF(SUM(return_uv) OVER () / NULLIF(SUM(exp) OVER (), 0), 0) - 1, 4 ), 0 ) AS rov_diff, COALESCE( ROUND( (new_exp / NULLIF(exp, 0)) / NULLIF(SUM(new_exp) OVER () / NULLIF(SUM(exp) OVER (), 0), 0) - 1, 4 ), 0 ) AS vov_diff FROM grouped ) SELECT 特征维度, 特征值, 特征值_寻找词, 供给类型, exp, return_uv AS `return`, new_exp, rov, vov, rov_diff, vov_diff FROM result_rows ORDER BY row_order, exp DESC LIMIT {int(limit)} """ return self.execute_sql(sql) @lru_cache def get_odps_client() -> ODPSClient: settings = get_infra_settings() return ODPSClient( access_id=settings.odps_access_id, access_key=settings.odps_access_key, project=settings.odps_project, endpoint=settings.odps_endpoint, )