Files
2026-07-09 14:32:16 +08:00

152 lines
5.6 KiB
Python

import time
import jwt
import httpx
from .exceptions import KLingAPIError
from enum import Enum
DEFAULT_GET_RETRY_COUNT = 3
DEFAULT_GET_RETRY_DELAY = 1.0
DEFAULT_CONNECT_RETRY_COUNT = 2
def _preview_payload(value, max_string_length=80):
if isinstance(value, str):
if len(value) > max_string_length:
return value[:max_string_length] + "...[truncated]"
return value
if isinstance(value, list):
return [_preview_payload(item, max_string_length) for item in value]
if isinstance(value, dict):
return {key: _preview_payload(item, max_string_length) for key, item in value.items()}
return value
def _raise_for_status(resp: httpx.Response) -> None:
if resp.status_code != 200 or 'data' not in resp.json():
raise KLingAPIError.from_response(resp)
class ApiLocation(Enum):
CHINA = "https://api-beijing.klingai.com"
GLOBAL = "https://api-singapore.klingai.com"
class Client:
__token = None
__client = None
def __init__(self, access_key, secret_key, in_china=True, timeout=30, poll_interval=1.0, ttl=1800, api_key=None):
super().__init__()
self._access_key = access_key
self._secret_key = secret_key
self._api_key = str(api_key or "").strip() or None
self._timeout = timeout
self._create_time = None
self._ttl = ttl
self._area = ApiLocation.CHINA if in_china else ApiLocation.GLOBAL
self.poll_interval = poll_interval
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
self.close()
return False
def request(self, method: str, path: str, **kwargs) -> dict:
method_upper = str(method or "").upper()
if "json" in kwargs:
import json as _json
_preview = _preview_payload(kwargs["json"])
print(f"[KLING DEBUG] {method} {path} payload keys={list(kwargs['json'].keys())}")
print(f"[KLING DEBUG] payload preview: {_json.dumps(_preview, default=str)[:500]}")
retry_count = DEFAULT_GET_RETRY_COUNT if method_upper == "GET" else DEFAULT_CONNECT_RETRY_COUNT
for attempt in range(1, retry_count + 1):
try:
resp = self._client.request(method, path, **kwargs)
break
except httpx.ConnectError as exc:
if attempt < retry_count:
print(
f"[KLING DEBUG] retrying {method_upper} {path} after connect error "
f"({attempt}/{retry_count}): {exc}"
)
self.close()
time.sleep(DEFAULT_GET_RETRY_DELAY)
continue
raise ConnectionError(
f"Kling API request failed while connecting for {method} {path}: {exc}"
) from exc
except httpx.TimeoutException as exc:
if method_upper == "GET" and attempt < retry_count:
print(
f"[KLING DEBUG] retrying {method_upper} {path} after timeout "
f"({attempt}/{retry_count}): {exc}"
)
self.close()
time.sleep(DEFAULT_GET_RETRY_DELAY)
continue
raise TimeoutError(
f"Kling API request timed out after {self._timeout}s while waiting for {method} {path}. "
"Try increasing the client request_timeout, especially for advanced element creation "
"with multiple reference images."
) from exc
except httpx.TransportError as exc:
if method_upper == "GET" and attempt < retry_count:
print(
f"[KLING DEBUG] retrying {method_upper} {path} after transport error "
f"({attempt}/{retry_count}): {exc}"
)
self.close()
time.sleep(DEFAULT_GET_RETRY_DELAY)
continue
raise ConnectionError(
f"Kling API request failed during transport for {method} {path}: {exc}"
) from exc
_raise_for_status(resp)
return resp.json()
@property
def _is_expired(self):
if self._api_key:
return False
if self._create_time is None:
return True
return time.time() - self._create_time > self._ttl
@property
def _token(self):
self._create_time = time.time()
headers = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"iss": self._access_key,
"exp": int(self._create_time) + self._ttl,
"nbf": int(self._create_time) - 5
}
self.__token = jwt.encode(payload, self._secret_key, headers=headers)
print(f'create token: {self._create_time}')
return self.__token
@property
def _client(self) -> httpx.Client:
if self.__client is None or self._is_expired:
base_url = str(self._area.value)
bearer_token = self._api_key or self._token
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {bearer_token}"
}
timeout = httpx.Timeout(connect=10.0, read=self._timeout, write=self._timeout, pool=self._timeout)
self.__client = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
return self.__client
def close(self) -> None:
if self.__client is not None:
self.__client.close()
self.__client = None