client.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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 的 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. FROM loghubods.dwd_topic_decode_result_di
  130. WHERE dt = '{dt}'
  131. AND vid IN ({in_list})
  132. """
  133. results.extend(self.execute_sql(sql))
  134. return results
  135. def fetch_real_rov_vov_7d(
  136. self,
  137. dt_left: str,
  138. dt_right: str,
  139. limit: int = 1000,
  140. ) -> list[dict[str, Any]]:
  141. """拉取近 N 日人工/自动 AGC 的 rov_diff / vov_diff(相对全局基线)。"""
  142. sql = f"""
  143. WITH base AS (
  144. SELECT
  145. 特征维度,
  146. 特征值,
  147. 特征值_寻找词,
  148. 供给类型,
  149. 当日分发曝光pv,
  150. 当日分发回流uv,
  151. 当日分发拉回曝光pv
  152. FROM loghubods.dwd_video_produce_plan_stat_hour
  153. WHERE dt BETWEEN '{dt_left}' AND '{dt_right}'
  154. AND 供给类型 IN ('人工AGC', '自动AGC')
  155. AND 特征值 NOT IN ('-', '', 'null')
  156. ),
  157. grouped AS (
  158. SELECT
  159. 1 AS row_order,
  160. 特征维度,
  161. 特征值,
  162. 特征值_寻找词,
  163. 供给类型,
  164. SUM(当日分发曝光pv) AS exp,
  165. SUM(当日分发回流uv) AS return_uv,
  166. SUM(当日分发拉回曝光pv) AS new_exp
  167. FROM base
  168. GROUP BY
  169. 特征维度,
  170. 特征值,
  171. 特征值_寻找词,
  172. 供给类型
  173. ),
  174. result_rows AS (
  175. SELECT
  176. 0 AS row_order,
  177. '全局SUM' AS 特征维度,
  178. '全局SUM' AS 特征值,
  179. '全局SUM' AS 特征值_寻找词,
  180. '全局SUM' AS 供给类型,
  181. SUM(exp) AS exp,
  182. SUM(return_uv) AS return_uv,
  183. SUM(new_exp) AS new_exp,
  184. COALESCE(ROUND(SUM(return_uv) / NULLIF(SUM(exp), 0), 4), 0) AS rov,
  185. COALESCE(ROUND(SUM(new_exp) / NULLIF(SUM(exp), 0), 4), 0) AS vov,
  186. 0 AS rov_diff,
  187. 0 AS vov_diff
  188. FROM grouped
  189. UNION ALL
  190. SELECT
  191. row_order,
  192. 特征维度,
  193. 特征值,
  194. 特征值_寻找词,
  195. 供给类型,
  196. exp,
  197. return_uv,
  198. new_exp,
  199. COALESCE(ROUND(return_uv / NULLIF(exp, 0), 4), 0) AS rov,
  200. COALESCE(ROUND(new_exp / NULLIF(exp, 0), 4), 0) AS vov,
  201. COALESCE(
  202. ROUND(
  203. (return_uv / NULLIF(exp, 0))
  204. / NULLIF(SUM(return_uv) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  205. - 1,
  206. 4
  207. ),
  208. 0
  209. ) AS rov_diff,
  210. COALESCE(
  211. ROUND(
  212. (new_exp / NULLIF(exp, 0))
  213. / NULLIF(SUM(new_exp) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  214. - 1,
  215. 4
  216. ),
  217. 0
  218. ) AS vov_diff
  219. FROM grouped
  220. )
  221. SELECT
  222. 特征维度,
  223. 特征值,
  224. 特征值_寻找词,
  225. 供给类型,
  226. exp,
  227. return_uv AS `return`,
  228. new_exp,
  229. rov,
  230. vov,
  231. rov_diff,
  232. vov_diff
  233. FROM result_rows
  234. ORDER BY
  235. row_order,
  236. exp DESC
  237. LIMIT {int(limit)}
  238. """
  239. return self.execute_sql(sql)
  240. @lru_cache
  241. def get_odps_client() -> ODPSClient:
  242. settings = get_infra_settings()
  243. return ODPSClient(
  244. access_id=settings.odps_access_id,
  245. access_key=settings.odps_access_key,
  246. project=settings.odps_project,
  247. endpoint=settings.odps_endpoint,
  248. )