from typing import Optional import logging import requests from requests.auth import HTTPDigestAuth from requests.adapters import HTTPAdapter, Retry from requests.exceptions import HTTPError from django.conf import settings logger = logging.getLogger(__name__) TIDBCLOUD_API_BASE = "https://api.tidbcloud.com" SERVERLESS_API_BASE = "https://serverless.tidbapi.com" class TiDBCloudServerlessClient: def __init__(self, public_key: Optional[str] = None, private_key: Optional[str] = None): public_key = public_key or settings.TIDB_CLOUD_PUBLIC_KEY private_key = private_key or settings.TIDB_CLOUD_PRIVATE_KEY self.request_session = requests.Session() self.request_session.auth = HTTPDigestAuth(public_key, private_key) retries = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) self.request_session.mount("https://", HTTPAdapter(max_retries=retries)) def create_branch(self, cluster_id: str, new_branch_name: str) -> str: url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/branches" data = {"displayName": new_branch_name} response = self.request_session.post(url, json=data) if response.status_code != 200: raise HTTPError( f"Failed to create branch {new_branch_name}, " f"status code: {response.status_code}, response: {response.text}" ) return response.json()["branchId"] def delete_branch(self, cluster_id: str, branch_id: str) -> None: url = ( f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/branches/{branch_id}" ) response = self.request_session.delete(url) if response.status_code != 200: raise HTTPError( f"Failed to delete branch {branch_id}, " f"status code: {response.status_code}, response: {response.text}" ) def get_branch( self, cluster_id: str, branch_id: str, view: str = "BASIC" ) -> Optional[dict]: if view not in ["BASIC", "FULL"]: raise ValueError("Invalid view, must be BASIC or FULL") url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/branches/{branch_id}?view={view}" response = self.request_session.get(url) if response.status_code == 404: return None if response.status_code != 200: raise HTTPError( f"Failed to get branch {branch_id}, " f"status code: {response.status_code}, response: {response.text}" ) return response.json() def list_branches( self, cluster_id: str, page_size: int = 50, page_token: str = "" ) -> list[dict]: url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/branches" params = { "pageSize": page_size, } if page_token: params["pageToken"] = page_token response = self.request_session.get(url, params=params) if response.status_code != 200: raise HTTPError( f"Failed to list branches, " f"status code: {response.status_code}, response: {response.text}" ) return response.json() def create_admin_user_for_branch( self, project_id: str, cluster_id: str, branch_name: str ) -> dict: url = f"{TIDBCLOUD_API_BASE}/api/internal/projects/{project_id}/clusters/{cluster_id}/branches/{branch_name}/users" data = {"type": "Admin"} response = self.request_session.post(url, json=data) if response.status_code != 200: raise HTTPError( f"Failed to create admin user for branch {branch_name}, " f"status code: {response.status_code}, response: {response.text}" ) return response.json() def create_export_task(self, cluster_id: str, task_detail: dict) -> dict: url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/exports" response = self.request_session.post(url, json=task_detail) if response.status_code != 200: raise HTTPError( f"Failed to create export task, " f"status code: {response.status_code}, response: {response.text}" ) return response.json() def list_export_tasks(self, cluster_id: str, page_size: int = 50, page_token: str = "") -> list[dict]: url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/exports" params = { "pageSize": page_size, } if page_token: params["pageToken"] = page_token response = self.request_session.get(url, params=params) if response.status_code != 200: raise HTTPError( f"Failed to list export tasks, " f"status code: {response.status_code}, response: {response.text}" ) return response.json() def delete_export_task(self, cluster_id: str, task_id: str) -> None: url = f"{SERVERLESS_API_BASE}/v1beta1/clusters/{cluster_id}/exports/{task_id}" response = self.request_session.delete(url) if response.status_code != 200: raise HTTPError( f"Failed to delete export task {task_id}, " f"status code: {response.status_code}, response: {response.text}" ) return response.json()