client.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. from __future__ import annotations
  2. import logging
  3. from functools import lru_cache
  4. from typing import Any
  5. from supply_infra.config import get_infra_settings
  6. logger = logging.getLogger(__name__)
  7. class ODPSClient:
  8. """ODPS / MaxCompute 查询客户端封装。"""
  9. def __init__(
  10. self,
  11. access_id: str,
  12. access_key: str,
  13. project: str,
  14. endpoint: str,
  15. ) -> None:
  16. self.access_id = access_id
  17. self.access_key = access_key
  18. self.project = project
  19. self.endpoint = endpoint
  20. self._client: Any = None
  21. def _get_client(self) -> Any:
  22. if self._client is None:
  23. try:
  24. from odps import ODPS
  25. except ImportError as e:
  26. raise ImportError(
  27. "pyodps 未安装,请执行: pip install pyodps"
  28. ) from e
  29. self._client = ODPS(
  30. self.access_id,
  31. self.access_key,
  32. project=self.project,
  33. endpoint=self.endpoint,
  34. )
  35. return self._client
  36. def execute_sql(self, sql: str) -> list[dict[str, Any]]:
  37. """执行 SQL 并返回字典列表。"""
  38. client = self._get_client()
  39. logger.info("ODPS executing SQL: %s", sql[:200])
  40. instance = client.execute_sql(sql)
  41. with instance.open_reader() as reader:
  42. columns = [col.name for col in reader._schema.columns] # type: ignore[attr-defined]
  43. return [dict(zip(columns, row.values)) for row in reader]
  44. return []
  45. def fetch_pattern_mining_elements(self, bizdate: str) -> list[dict[str, Any]]:
  46. """拉取 pattern_mining_element 元素(name, category_id)。"""
  47. sql = f"""
  48. SELECT t1.name
  49. ,t1.category_id
  50. FROM loghubods.pattern_mining_element t1
  51. LEFT JOIN loghubods.post t2
  52. ON t1.post_id = t2.post_id
  53. AND t2.dt = '{bizdate}'
  54. WHERE t1.dt = '{bizdate}'
  55. AND t1.execution_id = 401
  56. AND t1.source_table = 'post_decode_topic_point_element'
  57. AND t1.name is NOT NULL
  58. AND t1.category_id IS NOT NULL
  59. AND t1.element_type = '实质'
  60. AND t2.platform = 'piaoquan'
  61. GROUP BY t1.name
  62. ,t1.category_id
  63. """
  64. return self.execute_sql(sql)
  65. def fetch_pattern_mining_categories(self, bizdate: str) -> list[dict[str, Any]]:
  66. """拉取 public_pattern_mining_category 分类。"""
  67. sql = f"""
  68. SELECT id,name,description,level,parent_id
  69. FROM loghubods.public_pattern_mining_category
  70. WHERE dt = '{bizdate}'
  71. AND execution_id = 401
  72. AND source_type = '实质'
  73. """
  74. return self.execute_sql(sql)
  75. def fetch_multi_demand_pool(self, bizdate: str) -> list[dict[str, Any]]:
  76. """拉取 dwd_multi_demand_pool_di 策略需求天级数据(video_list 仅取前 10 个)。"""
  77. sql = f"""
  78. SELECT strategy
  79. ,demand_id
  80. ,demand_name
  81. ,weight
  82. ,`type`
  83. ,video_count
  84. ,SLICE(video_list, 1, 10) AS video_list
  85. ,extend
  86. FROM loghubods.dwd_multi_demand_pool_di
  87. WHERE dt = '{bizdate}'
  88. """
  89. return self.execute_sql(sql)
  90. def count_multi_demand_pool(self, bizdate: str) -> int:
  91. """统计 dwd_multi_demand_pool_di 分区去重后行数(strategy + demand_id)。"""
  92. sql = f"""
  93. SELECT COUNT(1) AS cnt
  94. FROM (
  95. SELECT strategy
  96. ,demand_id
  97. FROM loghubods.dwd_multi_demand_pool_di
  98. WHERE dt = '{bizdate}'
  99. GROUP BY strategy
  100. ,demand_id
  101. ) t
  102. """
  103. rows = self.execute_sql(sql)
  104. if not rows:
  105. return 0
  106. return int(rows[0].get("cnt") or 0)
  107. def fetch_topic_decode_results(
  108. self,
  109. dt: str,
  110. vids: list[str],
  111. batch_size: int = 100,
  112. ) -> list[dict[str, Any]]:
  113. """按 vid 批量拉取 dwd_topic_decode_result_di 的 vid、url1、url2、decode_result。"""
  114. # 保序去重
  115. unique_vids = list(
  116. dict.fromkeys(
  117. str(v).strip() for v in vids if v is not None and str(v).strip()
  118. )
  119. )
  120. if not unique_vids:
  121. return []
  122. results: list[dict[str, Any]] = []
  123. for i in range(0, len(unique_vids), batch_size):
  124. batch = unique_vids[i : i + batch_size]
  125. in_list = ",".join("'" + v.replace("'", "''") + "'" for v in batch)
  126. sql = f"""
  127. SELECT vid
  128. ,decode_result
  129. ,url1
  130. ,url2
  131. FROM loghubods.dwd_topic_decode_result_di
  132. WHERE dt = '{dt}'
  133. AND vid IN ({in_list})
  134. """
  135. results.extend(self.execute_sql(sql))
  136. return results
  137. def fetch_real_rov_vov_7d(
  138. self,
  139. dt_left: str,
  140. dt_right: str,
  141. limit: int = 1000,
  142. ) -> list[dict[str, Any]]:
  143. """拉取近 N 日人工/自动 AGC 的 rov_diff / vov_diff(相对全局基线)。"""
  144. sql = f"""
  145. WITH base AS (
  146. SELECT
  147. 特征维度,
  148. 特征值,
  149. 特征值_寻找词,
  150. 供给类型,
  151. 当日分发曝光pv,
  152. 当日分发回流uv,
  153. 当日分发拉回曝光pv
  154. FROM loghubods.dwd_video_produce_plan_stat_hour
  155. WHERE dt BETWEEN '{dt_left}' AND '{dt_right}'
  156. AND 供给类型 IN ('人工AGC', '自动AGC')
  157. AND 特征值 NOT IN ('-', '', 'null')
  158. ),
  159. grouped AS (
  160. SELECT
  161. 1 AS row_order,
  162. 特征维度,
  163. 特征值,
  164. 特征值_寻找词,
  165. 供给类型,
  166. SUM(当日分发曝光pv) AS exp,
  167. SUM(当日分发回流uv) AS return_uv,
  168. SUM(当日分发拉回曝光pv) AS new_exp
  169. FROM base
  170. GROUP BY
  171. 特征维度,
  172. 特征值,
  173. 特征值_寻找词,
  174. 供给类型
  175. ),
  176. result_rows AS (
  177. SELECT
  178. 0 AS row_order,
  179. '全局SUM' AS 特征维度,
  180. '全局SUM' AS 特征值,
  181. '全局SUM' AS 特征值_寻找词,
  182. '全局SUM' AS 供给类型,
  183. SUM(exp) AS exp,
  184. SUM(return_uv) AS return_uv,
  185. SUM(new_exp) AS new_exp,
  186. COALESCE(ROUND(SUM(return_uv) / NULLIF(SUM(exp), 0), 4), 0) AS rov,
  187. COALESCE(ROUND(SUM(new_exp) / NULLIF(SUM(exp), 0), 4), 0) AS vov,
  188. 0 AS rov_diff,
  189. 0 AS vov_diff
  190. FROM grouped
  191. UNION ALL
  192. SELECT
  193. row_order,
  194. 特征维度,
  195. 特征值,
  196. 特征值_寻找词,
  197. 供给类型,
  198. exp,
  199. return_uv,
  200. new_exp,
  201. COALESCE(ROUND(return_uv / NULLIF(exp, 0), 4), 0) AS rov,
  202. COALESCE(ROUND(new_exp / NULLIF(exp, 0), 4), 0) AS vov,
  203. COALESCE(
  204. ROUND(
  205. (return_uv / NULLIF(exp, 0))
  206. / NULLIF(SUM(return_uv) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  207. - 1,
  208. 4
  209. ),
  210. 0
  211. ) AS rov_diff,
  212. COALESCE(
  213. ROUND(
  214. (new_exp / NULLIF(exp, 0))
  215. / NULLIF(SUM(new_exp) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  216. - 1,
  217. 4
  218. ),
  219. 0
  220. ) AS vov_diff
  221. FROM grouped
  222. )
  223. SELECT
  224. 特征维度,
  225. 特征值,
  226. 特征值_寻找词,
  227. 供给类型,
  228. exp,
  229. return_uv AS `return`,
  230. new_exp,
  231. rov,
  232. vov,
  233. rov_diff,
  234. vov_diff
  235. FROM result_rows
  236. ORDER BY
  237. row_order,
  238. exp DESC
  239. LIMIT {int(limit)}
  240. """
  241. return self.execute_sql(sql)
  242. @lru_cache
  243. def get_odps_client() -> ODPSClient:
  244. settings = get_infra_settings()
  245. return ODPSClient(
  246. access_id=settings.odps_access_id,
  247. access_key=settings.odps_access_key,
  248. project=settings.odps_project,
  249. endpoint=settings.odps_endpoint,
  250. )