client.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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_global_categories(self, bizdate: str) -> list[dict[str, Any]]:
  46. """拉取 global_category 中仍有效的“实质”分类树。"""
  47. sql = f"""
  48. SELECT stable_id,name,description,level,parent_stable_id
  49. FROM loghubods.global_category
  50. WHERE dt = '{bizdate}'
  51. AND retired_at_execution_id IS NULL
  52. AND source_type = '实质'
  53. ORDER BY level
  54. """
  55. return self.execute_sql(sql)
  56. def fetch_multi_demand_pool(self, bizdate: str) -> list[dict[str, Any]]:
  57. """拉取 dwd_multi_demand_pool_di 策略需求天级数据(video_list 仅取前 10 个)。"""
  58. sql = f"""
  59. SELECT strategy
  60. ,demand_id
  61. ,demand_name
  62. ,weight
  63. ,`type`
  64. ,video_count
  65. ,SLICE(video_list, 1, 10) AS video_list
  66. ,extend
  67. FROM loghubods.dwd_multi_demand_pool_di
  68. WHERE dt = '{bizdate}'
  69. """
  70. return self.execute_sql(sql)
  71. def count_multi_demand_pool(self, bizdate: str) -> int:
  72. """统计 dwd_multi_demand_pool_di 分区去重后行数(strategy + demand_id)。"""
  73. sql = f"""
  74. SELECT COUNT(1) AS cnt
  75. FROM (
  76. SELECT strategy
  77. ,demand_id
  78. FROM loghubods.dwd_multi_demand_pool_di
  79. WHERE dt = '{bizdate}'
  80. GROUP BY strategy
  81. ,demand_id
  82. ) t
  83. """
  84. rows = self.execute_sql(sql)
  85. if not rows:
  86. return 0
  87. return int(rows[0].get("cnt") or 0)
  88. def fetch_topic_decode_results(
  89. self,
  90. dt: str,
  91. vids: list[str],
  92. batch_size: int = 100,
  93. ) -> list[dict[str, Any]]:
  94. """按 vid 批量拉取 dwd_topic_decode_result_di 的 vid、url1、url2、decode_result。"""
  95. # 保序去重
  96. unique_vids = list(
  97. dict.fromkeys(
  98. str(v).strip() for v in vids if v is not None and str(v).strip()
  99. )
  100. )
  101. if not unique_vids:
  102. return []
  103. results: list[dict[str, Any]] = []
  104. for i in range(0, len(unique_vids), batch_size):
  105. batch = unique_vids[i : i + batch_size]
  106. in_list = ",".join("'" + v.replace("'", "''") + "'" for v in batch)
  107. sql = f"""
  108. SELECT vid
  109. ,decode_result
  110. ,url1
  111. ,url2
  112. FROM loghubods.dwd_topic_decode_result_di
  113. WHERE dt = '{dt}'
  114. AND vid IN ({in_list})
  115. """
  116. results.extend(self.execute_sql(sql))
  117. return results
  118. def fetch_real_rov_vov_7d(
  119. self,
  120. dt_left: str,
  121. dt_right: str,
  122. limit: int = 1000,
  123. ) -> list[dict[str, Any]]:
  124. """拉取近 N 日人工/自动 AGC 的 rov_diff / vov_diff(相对全局基线)。"""
  125. sql = f"""
  126. WITH base AS (
  127. SELECT
  128. 特征维度,
  129. 特征值,
  130. 特征值_寻找词,
  131. 供给类型,
  132. 当日分发曝光pv,
  133. 当日分发回流uv,
  134. 当日分发拉回曝光pv
  135. FROM loghubods.dwd_video_produce_plan_stat_hour
  136. WHERE dt BETWEEN '{dt_left}' AND '{dt_right}'
  137. AND 供给类型 IN ('人工AGC', '自动AGC')
  138. AND 特征值 NOT IN ('-', '', 'null')
  139. ),
  140. grouped AS (
  141. SELECT
  142. 1 AS row_order,
  143. 特征维度,
  144. 特征值,
  145. 特征值_寻找词,
  146. 供给类型,
  147. SUM(当日分发曝光pv) AS exp,
  148. SUM(当日分发回流uv) AS return_uv,
  149. SUM(当日分发拉回曝光pv) AS new_exp
  150. FROM base
  151. GROUP BY
  152. 特征维度,
  153. 特征值,
  154. 特征值_寻找词,
  155. 供给类型
  156. ),
  157. result_rows AS (
  158. SELECT
  159. 0 AS row_order,
  160. '全局SUM' AS 特征维度,
  161. '全局SUM' AS 特征值,
  162. '全局SUM' AS 特征值_寻找词,
  163. '全局SUM' AS 供给类型,
  164. SUM(exp) AS exp,
  165. SUM(return_uv) AS return_uv,
  166. SUM(new_exp) AS new_exp,
  167. COALESCE(ROUND(SUM(return_uv) / NULLIF(SUM(exp), 0), 4), 0) AS rov,
  168. COALESCE(ROUND(SUM(new_exp) / NULLIF(SUM(exp), 0), 4), 0) AS vov,
  169. 0 AS rov_diff,
  170. 0 AS vov_diff
  171. FROM grouped
  172. UNION ALL
  173. SELECT
  174. row_order,
  175. 特征维度,
  176. 特征值,
  177. 特征值_寻找词,
  178. 供给类型,
  179. exp,
  180. return_uv,
  181. new_exp,
  182. COALESCE(ROUND(return_uv / NULLIF(exp, 0), 4), 0) AS rov,
  183. COALESCE(ROUND(new_exp / NULLIF(exp, 0), 4), 0) AS vov,
  184. COALESCE(
  185. ROUND(
  186. (return_uv / NULLIF(exp, 0))
  187. / NULLIF(SUM(return_uv) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  188. - 1,
  189. 4
  190. ),
  191. 0
  192. ) AS rov_diff,
  193. COALESCE(
  194. ROUND(
  195. (new_exp / NULLIF(exp, 0))
  196. / NULLIF(SUM(new_exp) OVER () / NULLIF(SUM(exp) OVER (), 0), 0)
  197. - 1,
  198. 4
  199. ),
  200. 0
  201. ) AS vov_diff
  202. FROM grouped
  203. )
  204. SELECT
  205. 特征维度,
  206. 特征值,
  207. 特征值_寻找词,
  208. 供给类型,
  209. exp,
  210. return_uv AS `return`,
  211. new_exp,
  212. rov,
  213. vov,
  214. rov_diff,
  215. vov_diff
  216. FROM result_rows
  217. ORDER BY
  218. row_order,
  219. exp DESC
  220. LIMIT {int(limit)}
  221. """
  222. return self.execute_sql(sql)
  223. @lru_cache
  224. def get_odps_client() -> ODPSClient:
  225. settings = get_infra_settings()
  226. return ODPSClient(
  227. access_id=settings.odps_access_id,
  228. access_key=settings.odps_access_key,
  229. project=settings.odps_project,
  230. endpoint=settings.odps_endpoint,
  231. )