odps_client.py 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. from __future__ import annotations
  2. import asyncio
  3. import logging
  4. from dataclasses import dataclass
  5. from typing import Any
  6. import pandas as pd
  7. from odps import ODPS
  8. from .config import Settings
  9. from .models import QueryResult
  10. from .sql_guard import SQLGuard, TableRef
  11. logger = logging.getLogger(__name__)
  12. @dataclass(frozen=True)
  13. class TableMetadata:
  14. partitions: dict[str, list[str]]
  15. class ODPSClient:
  16. def __init__(self, settings: Settings) -> None:
  17. kwargs: dict[str, Any] = {
  18. "access_id": settings.odps_access_id,
  19. "secret_access_key": settings.odps_access_key,
  20. "project": settings.odps_project,
  21. "endpoint": settings.odps_endpoint,
  22. }
  23. if settings.odps_tunnel_endpoint:
  24. kwargs["tunnel_endpoint"] = settings.odps_tunnel_endpoint
  25. self._client = ODPS(**kwargs)
  26. self.project = settings.odps_project
  27. self.timeout = settings.query_timeout_seconds
  28. self.max_rows = settings.query_max_rows
  29. async def partition_metadata(self, refs: list[TableRef]) -> TableMetadata:
  30. return await asyncio.to_thread(self._partition_metadata_sync, refs)
  31. def _partition_metadata_sync(self, refs: list[TableRef]) -> TableMetadata:
  32. result: dict[str, list[str]] = {}
  33. for ref in refs:
  34. full_name = f"{ref.project}.{ref.name}" if ref.project else ref.name
  35. table = self._client.get_table(full_name)
  36. result[full_name] = [column.name for column in table.schema.partitions]
  37. return TableMetadata(result)
  38. async def execute(self, sql: str) -> QueryResult:
  39. return await asyncio.to_thread(self._execute_sync, sql)
  40. def _execute_sync(self, sql: str) -> QueryResult:
  41. instance = self._client.run_sql(sql)
  42. instance_id = str(instance.id)
  43. logger.info("ODPS query started instance_id=%s", instance_id)
  44. try:
  45. instance.wait_for_success(timeout=self.timeout)
  46. except Exception:
  47. try:
  48. instance.stop()
  49. except Exception:
  50. logger.warning("Failed to stop ODPS instance %s", instance_id, exc_info=True)
  51. raise
  52. with instance.open_reader() as reader:
  53. frame = reader.to_pandas(start=0, count=self.max_rows + 1)
  54. truncated = len(frame.index) > self.max_rows
  55. if truncated:
  56. frame = frame.iloc[: self.max_rows].copy()
  57. if not isinstance(frame, pd.DataFrame):
  58. frame = pd.DataFrame(frame)
  59. logger.info("ODPS query finished instance_id=%s rows=%d truncated=%s", instance_id, len(frame), truncated)
  60. return QueryResult(frame, instance_id, truncated)
  61. async def validate_for_odps(sql: str, guard: SQLGuard, odps: ODPSClient) -> list[TableRef]:
  62. refs = guard.validate(sql)
  63. metadata = await odps.partition_metadata(refs)
  64. guard.validate_partition_predicates(sql, metadata.partitions)
  65. return refs